335 lines
10 KiB
Vue
335 lines
10 KiB
Vue
<template>
|
||
<div class="resume-page dflex">
|
||
<SideNav />
|
||
<div class="resume-page__content">
|
||
<!-- 页面标题 + 上传按钮 -->
|
||
<div class="resume-page__header">
|
||
<h2 class="resume-page__title">我的简历</h2>
|
||
<button class="resume-page__upload-btn" @click="handleUpload">
|
||
<svg viewBox="0 0 16 16" fill="none" class="resume-page__upload-icon">
|
||
<path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||
</svg>
|
||
上传简历
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 简历表格 -->
|
||
<div class="resume-page__table-wrap">
|
||
<!-- 空状态提示 -->
|
||
<div v-if="resumeList.length === 0" class="resume-page__empty">
|
||
<p>暂无简历,请点击上方按钮上传简历</p>
|
||
</div>
|
||
<table v-else class="resume-page__table">
|
||
<thead>
|
||
<tr>
|
||
<th class="resume-page__th">简历</th>
|
||
<th class="resume-page__th">目标岗位</th>
|
||
<th class="resume-page__th">最近修改</th>
|
||
<th class="resume-page__th resume-page__th--action"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr
|
||
v-for="item in resumeList"
|
||
:key="item.id"
|
||
class="resume-page__row"
|
||
@click="goDetail(item.id)"
|
||
>
|
||
<td class="resume-page__td">
|
||
<div class="resume-page__name-cell">
|
||
<!-- <span class="resume-page__avatar" :style="{ background: item.avatarColor }">-->
|
||
<!-- {{ item.avatarLetter }}-->
|
||
<!-- </span>-->
|
||
<span class="resume-page__name">{{ item.name }}</span>
|
||
<span v-if="item.isDefault" class="resume-page__default-tag">默认</span>
|
||
<!-- 编辑名称岗位图标 -->
|
||
<button
|
||
class="resume-page__edit-btn"
|
||
aria-label="编辑名称岗位"
|
||
@click.stop="handleEditName(item)"
|
||
>
|
||
<el-icon class="resume-page__edit-icon"><EditPen /></el-icon>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
<td class="resume-page__td">{{ item.targetJob }}</td>
|
||
<td class="resume-page__td">{{ item.updatedAt }}</td>
|
||
<!-- 操作按钮列 -->
|
||
<td class="resume-page__td resume-page__td--action">
|
||
<div class="resume-page__actions">
|
||
<button
|
||
v-if="!item.isDefault"
|
||
class="resume-page__action-btn"
|
||
@click.stop="handleSetDefault(item.id)"
|
||
>设为默认</button>
|
||
<button
|
||
class="resume-page__action-btn"
|
||
@click.stop="handleExport(item)"
|
||
>导出</button>
|
||
<button
|
||
class="resume-page__action-btn resume-page__action-btn--delete"
|
||
@click.stop="handleDelete(item.id, item.isDefault)"
|
||
>删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<!-- 编辑简历名称弹窗 -->
|
||
<ResumeEditNameDialog
|
||
v-model="editNameVisible"
|
||
:resume-id="editResumeId"
|
||
:resume-name="editResumeName"
|
||
:target-position="editTargetPosition"
|
||
@saved="loadResumeList"
|
||
/>
|
||
|
||
<!-- 导出简历弹窗组件 -->
|
||
<ResumeExportDialog
|
||
v-model="exportDialogVisible"
|
||
:resume-id="exportResumeId"
|
||
:resume-name="exportResumeName"
|
||
/>
|
||
|
||
<!-- 上传简历弹窗组件 -->
|
||
<ResumeUploadDialog
|
||
v-model="uploadDialogVisible"
|
||
@confirm="handleUploadConfirm"
|
||
/>
|
||
<!-- 全屏加载遮罩 -->
|
||
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { EditPen } from '@element-plus/icons-vue'
|
||
import SideNav from '@/components/SideNav.vue'
|
||
import ResumeEditNameDialog from '@/components/ResumeEditNameDialog.vue'
|
||
import ResumeExportDialog from '@/components/ResumeExportDialog.vue'
|
||
import ResumeUploadDialog from '@/components/ResumeUploadDialog.vue'
|
||
import { uploadResume } from '@/utils/aiRequest'
|
||
import {
|
||
fetchResumeList, deleteResume, setDefaultResume, type ResumeListItem,
|
||
} from '@/api/resume'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
// 命令式调用的组件,按需引入插件不会自动加载其样式,需手动引入
|
||
import 'element-plus/es/components/message-box/style/css'
|
||
import FullscreenLoading from '@/components/FullscreenLoading.vue'
|
||
|
||
const router = useRouter()
|
||
|
||
// ==================== 全屏加载状态 ====================
|
||
/** 全屏加载是否显示 */
|
||
const fullLoading = ref(false)
|
||
/** 全屏加载文案 */
|
||
const fullLoadingText = ref('')
|
||
|
||
// ==================== 头像颜色池 ====================
|
||
|
||
/** 头像背景色列表,按索引循环取色 */
|
||
const avatarColors = ['#1A1A2E', '#4FC2C9', '#F2994A', '#6C5CE7', '#E17055', '#00B894', '#FDCB6E']
|
||
|
||
// ==================== 类型定义 ====================
|
||
|
||
/** 简历列表项(前端展示用) */
|
||
interface ResumeItem {
|
||
id: string
|
||
name: string
|
||
avatarLetter: string
|
||
avatarColor: string
|
||
isDefault: boolean
|
||
targetJob: string
|
||
updatedAt: string
|
||
createdAt: string
|
||
}
|
||
|
||
// ==================== 工具方法 ====================
|
||
|
||
/**
|
||
* 将毫秒时间戳转为友好的相对时间文案
|
||
* @param timestamp 后端返回的毫秒级时间戳
|
||
*/
|
||
function formatTime(timestamp?: number): string {
|
||
if (!timestamp) return '-'
|
||
const date = new Date(timestamp)
|
||
const now = Date.now()
|
||
const diff = now - date.getTime()
|
||
const minutes = Math.floor(diff / 60000)
|
||
if (minutes < 1) return '刚刚'
|
||
if (minutes < 60) return `${minutes}分钟前`
|
||
const hours = Math.floor(minutes / 60)
|
||
if (hours < 24) return `${hours}小时前`
|
||
const days = Math.floor(hours / 24)
|
||
if (days < 30) return `${days}天前`
|
||
const months = Math.floor(days / 30)
|
||
if (months < 12) return `${months}个月前`
|
||
return `${Math.floor(months / 12)}年前`
|
||
}
|
||
|
||
/**
|
||
* 将后端简历数据转为前端展示结构
|
||
*/
|
||
function mapResumeItem(item: ResumeListItem, index: number): ResumeItem {
|
||
const name = item.resumeName || '未命名简历'
|
||
return {
|
||
id: String(item.id ?? ''),
|
||
name,
|
||
avatarLetter: name.charAt(0) || '?',
|
||
avatarColor: avatarColors[index % avatarColors.length],
|
||
isDefault: item.isDefault === 1,
|
||
targetJob: item.targetPosition || '',
|
||
updatedAt: formatTime(item.updateTime),
|
||
createdAt: formatTime(item.createTime),
|
||
}
|
||
}
|
||
|
||
// ==================== 数据 ====================
|
||
|
||
/** 简历列表 */
|
||
const resumeList = ref<ResumeItem[]>([])
|
||
|
||
/** 加载简历列表 */
|
||
async function loadResumeList() {
|
||
try {
|
||
const res = await fetchResumeList()
|
||
if (res.code === '0' && res.data) {
|
||
resumeList.value = res.data.map(mapResumeItem)
|
||
}
|
||
} catch (e) {
|
||
console.error('获取简历列表失败', e)
|
||
}
|
||
}
|
||
|
||
// ==================== 生命周期 ====================
|
||
|
||
onMounted(() => {
|
||
loadResumeList()
|
||
})
|
||
|
||
// ==================== 页面状态 ====================
|
||
|
||
// ==================== 编辑简历名称弹窗状态 ====================
|
||
|
||
/** 编辑弹窗是否可见 */
|
||
const editNameVisible = ref(false)
|
||
/** 当前编辑的简历 ID */
|
||
const editResumeId = ref('')
|
||
/** 当前编辑的简历名称 */
|
||
const editResumeName = ref('')
|
||
/** 当前编辑的目标岗位 */
|
||
const editTargetPosition = ref('')
|
||
|
||
// ==================== 导出简历弹窗状态 ====================
|
||
|
||
/** 导出弹窗是否可见 */
|
||
const exportDialogVisible = ref(false)
|
||
/** 当前导出的简历 ID */
|
||
const exportResumeId = ref('')
|
||
/** 当前导出的简历名称(用于文件名) */
|
||
const exportResumeName = ref('')
|
||
|
||
// ==================== 上传简历弹窗状态 ====================
|
||
|
||
/** 上传弹窗是否可见 */
|
||
const uploadDialogVisible = ref(false)
|
||
|
||
// ==================== 事件处理 ====================
|
||
|
||
/** 点击编辑图标 — 打开编辑名称岗位弹窗 */
|
||
function handleEditName(item: ResumeItem) {
|
||
editResumeId.value = item.id
|
||
editResumeName.value = item.name
|
||
editTargetPosition.value = item.targetJob
|
||
editNameVisible.value = true
|
||
}
|
||
|
||
/** 设为默认简历 */
|
||
async function handleSetDefault(id: string) {
|
||
try {
|
||
const res = await setDefaultResume(id)
|
||
if (res.code === '0') {
|
||
ElMessage.success('已设为默认简历')
|
||
loadResumeList()
|
||
} else {
|
||
ElMessage.error(res.msg || '设置失败')
|
||
}
|
||
} catch {
|
||
ElMessage.error('设置失败,请稍后重试')
|
||
}
|
||
}
|
||
|
||
/** 导出简历 */
|
||
function handleExport(item: ResumeItem) {
|
||
exportResumeId.value = item.id
|
||
exportResumeName.value = item.name || '简历'
|
||
exportDialogVisible.value = true
|
||
}
|
||
|
||
/** 删除简历 */
|
||
async function handleDelete(id: string, isDefault: boolean) {
|
||
try {
|
||
await ElMessageBox.confirm('确定要删除这份简历吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
|
||
if(resumeList.value.length<=1||isDefault){
|
||
ElMessage.warning('默认简历不可删除')
|
||
return
|
||
}
|
||
|
||
const res = await deleteResume(id)
|
||
if (res.code === '0') {
|
||
ElMessage.success('删除成功')
|
||
loadResumeList()
|
||
} else {
|
||
ElMessage.error(res.msg || '删除失败')
|
||
}
|
||
} catch (error) {
|
||
if (error !== 'cancel') {
|
||
ElMessage.error('删除失败,请稍后重试')
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 上传简历 — 打开上传弹窗 */
|
||
function handleUpload() {
|
||
uploadDialogVisible.value = true
|
||
}
|
||
|
||
/** 上传弹窗确认回调 — 接收 File 对象,调用接口上传 */
|
||
async function handleUploadConfirm(file: File) {
|
||
// 全屏加载提示,AI 接口响应较慢
|
||
fullLoadingText.value = '简历解析中,请耐心等待…'
|
||
fullLoading.value = true
|
||
|
||
try {
|
||
const res = await uploadResume(file)
|
||
if (res.code === 0) {
|
||
// 上传成功,刷新列表
|
||
loadResumeList()
|
||
// 等待让后端异步处理数据,再关闭加载动画并跳转详情页
|
||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||
fullLoading.value = false
|
||
goDetail(res.data.resumeId)
|
||
} else {
|
||
fullLoading.value = false
|
||
ElMessage.error(res.msg || '上传失败')
|
||
}
|
||
} catch {
|
||
fullLoading.value = false
|
||
ElMessage.error('上传失败,请稍后重试')
|
||
}
|
||
}
|
||
|
||
/** 跳转到简历详情页 */
|
||
function goDetail(id: string) {
|
||
router.push(`/resume/${id}`)
|
||
}
|
||
</script>
|