Files
offerpai_web/src/views/Resume.vue
T

335 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>