简历上传,pdf简历下载

This commit is contained in:
2026-04-03 17:52:07 +08:00
parent 821a950df2
commit a39835be01
25 changed files with 2076 additions and 301 deletions
+227 -82
View File
@@ -1,17 +1,27 @@
<template>
<div class="home-page">
<!-- SEO: 语义化 header -->
<header class="home-nav">
<div class="home-nav__inner">
<div class="home-nav__logo">
<span class="home-nav__logo-text">Offer派</span>
</div>
<button class="home-nav__btn" @click="router.push('/jobs')">免费领取 offer</button>
</div>
</header>
<!-- Hero Section -->
<!-- Hero 主视觉区包含顶部导航 -->
<section class="home-hero">
<!-- 底层背景色块 -->
<div class="home-hero__orb home-hero__orb--top"></div>
<div class="home-hero__orb home-hero__orb--bottom"></div>
<!-- 顶部导航栏 -->
<header class="home-nav mt32">
<div class="home-nav__inner">
<div class="home-nav__logo">
<!-- 导航栏Logo图片 -->
<img src="@/assets/images/logo.png" alt="Offer派" class="home-nav__logo-img" />
</div>
<div class="dflex-end aliite-c">
<!-- 未登录时显示登陆和立即加入按钮 -->
<div v-if="!store.state.isAuthenticated" class="fs14 color-1 mr40 fw600 cursor-po" @click="handleLoginClick">登陆</div>
<button v-if="!store.state.isAuthenticated" class="home-nav__btn" @click="router.push('/jobs')">立即加入</button>
<!-- 已登录时显示进入平台按钮 -->
<button v-else class="home-nav__btn" @click="router.push('/jobs')">进入平台</button>
</div>
</div>
</header>
<div class="home-hero__inner">
<div class="home-hero__left">
<h1 class="home-hero__title">Offer派<br/>收offer就是快!</h1>
@@ -84,8 +94,8 @@
<div class="showcase-stat__label">岗位总数</div>
</div>
<div class="showcase-stat">
<div class="showcase-stat__num"><span class="accent">3120</span>今日新增</div>
<div class="showcase-stat__label">今日新</div>
<div class="showcase-stat__num"><span class="accent">3120</span>个岗位</div>
<div class="showcase-stat__label">今日</div>
</div>
</div>
<div class="home-jobs-showcase__scroll">
@@ -134,6 +144,14 @@
<!-- Feature 2: 一键自动网申 -->
<section class="home-feature home-feature--reverse">
<div class="home-feature__inner">
<div class="home-feature__text">
<h2>校招一键<br/>自动网申</h2>
<p>每日向数百岗位一键投递覆盖各大企业网申系统告别重复填写节省 80% 的宝贵时间</p>
<button class="home-feature__btn" @click="router.push('/jobs')">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M3 10l2-8h10l2 8-7 7-7-7z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
<span>开启自动网申</span>
</button>
</div>
<div class="home-feature__visual home-feature__visual--apply">
<div class="feature-apply-card">
<div class="feature-apply-card__header">
@@ -162,14 +180,7 @@
</div>
</div>
</div>
<div class="home-feature__text">
<h2>一键<br/>自动网申</h2>
<p>每日向数百岗位一键投递覆盖各大企业网申系统告别重复填写节省 80% 的宝贵时间</p>
<button class="home-feature__btn" @click="router.push('/jobs')">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M3 10l2-8h10l2 8-7 7-7-7z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
<span>开启自动网申</span>
</button>
</div>
</div>
</section>
@@ -178,7 +189,7 @@
<div class="home-feature__inner">
<div class="home-feature__text">
<h2>岗位定制简历</h2>
<p>6秒内生成针对特定岗位优化的专业简历通过ATS系统突出你的核心优势</p>
<p>10秒内生成针对特定岗位优化的专业简历通过ATS系统突出你的核心优势</p>
<button class="home-feature__btn" @click="router.push('/jobs')">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M4 2h8l4 4v12H4V2z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M12 2v4h4" stroke="currentColor" stroke-width="2"/><path d="M4 11h8" stroke="currentColor" stroke-width="2"/></svg>
<span>优化我的简历</span>
@@ -186,19 +197,22 @@
</div>
<div class="home-feature__visual home-feature__visual--resume">
<div class="feature-resume-card">
<div class="feature-resume-card__doc">
<div class="doc-header">
<div class="doc-avatar"></div>
<div class="doc-lines">
<div class="doc-line doc-line--half"></div>
<div class="doc-line doc-line--third"></div>
<div class="feature-resume-card__doc--border">
<div class="feature-resume-card__doc">
<div class="doc-header">
<div class="doc-avatar"></div>
<div class="doc-lines">
<div class="doc-line doc-line--half"></div>
<div class="doc-line doc-line--third"></div>
</div>
</div>
<div class="doc-bars">
<div class="doc-bar doc-bar--1"></div>
<div class="doc-bar doc-bar--2"></div>
<div class="doc-bar doc-bar--3"></div>
</div>
</div>
<div class="doc-bars">
<div class="doc-bar doc-bar--1"></div>
<div class="doc-bar doc-bar--2"></div>
<div class="doc-bar doc-bar--3"></div>
</div>
</div>
<div class="feature-resume-card__badge">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><path d="M3 2h7l3 3v8H3V2z" stroke="#4FC2C9" stroke-width="1.2"/><path d="M6 7h3" stroke="#4FC2C9" stroke-width="1.2"/></svg>
@@ -212,6 +226,14 @@
<!-- Feature 4: 内推人脉 -->
<section class="home-feature home-feature--reverse">
<div class="home-feature__inner">
<div class="home-feature__text">
<h2>名企内推<br/>人脉直通</h2>
<p>实时获取名企最新内推信息自动填写网申内推码简历更快到达HR</p>
<button class="home-feature__btn" @click="router.push('/jobs')">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M3 3h10l4 4v10H3V3z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M6 12l2 2 4-4" stroke="currentColor" stroke-width="2"/></svg>
<span>立即投递</span>
</button>
</div>
<div class="home-feature__visual home-feature__visual--referral">
<div class="referral-grid">
<div class="referral-card" v-for="(ref, i) in referralCards" :key="i" :class="{ 'referral-card--offset': i % 2 === 1 }">
@@ -222,14 +244,7 @@
</div>
</div>
</div>
<div class="home-feature__text">
<h2>内推<br/>人脉直通</h2>
<p>实时获取名企最新内推信息自动填写网申内推码简历更快到达HR</p>
<button class="home-feature__btn" @click="router.push('/jobs')">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M3 3h10l4 4v10H3V3z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M6 12l2 2 4-4" stroke="currentColor" stroke-width="2"/></svg>
<span>立即投递</span>
</button>
</div>
</div>
</section>
@@ -260,50 +275,63 @@
</div>
</section>
<!-- Testimonials Section -->
<!-- 用户评价区 Testimonials Section -->
<section class="home-testimonials">
<div class="home-testimonials__header">
<h2>万千毕业生的信赖之选</h2>
<p>REAL VOICES FROM THE COMMUNITY</p>
</div>
<div class="home-testimonials__founder">
<div class="home-testimonials__founder-img">👩💼</div>
<blockquote>"很多大学生还在用传统方式找工作,海投、反复改简历,效率很低。Offer派利用AI技术让整个校招流程更丝滑,节省了80%的繁琐过程。"</blockquote>
<cite><span class="cite-name">创始人</span> <span class="cite-role">Offer派</span></cite>
</div>
<div class="home-testimonials__scroll">
<div class="testimonial-card" v-for="(t, i) in testimonials" :key="i">
<svg class="testimonial-card__quote" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M6 6h14v36" stroke="rgba(82,202,209,0.2)" stroke-width="2"/><path d="M28 6h14v36" stroke="rgba(82,202,209,0.2)" stroke-width="2"/></svg>
<p class="testimonial-card__text">"{{ t.text }}"</p>
<div class="testimonial-card__author">
<div class="testimonial-card__avatar"></div>
<div>
<p class="testimonial-card__name">{{ t.name }}</p>
<p class="testimonial-card__school">{{ t.school }}</p>
<!-- 底层背景色块 -->
<div class="home-testimonials__orb home-testimonials__orb--left"></div>
<div class="home-testimonials__orb home-testimonials__orb--right"></div>
<!-- 内容层白色容器 -->
<div class="home-testimonials__container">
<!-- 标题区 -->
<div class="home-testimonials__header">
<h2>万千毕业生的信赖之选</h2>
<p>REAL VOICES FROM THE COMMUNITY</p>
</div>
<!-- 评价卡片横向排列 -->
<div class="home-testimonials__cards">
<div class="testimonial-card" v-for="(t, i) in testimonials" :key="i">
<!-- 引号装饰 SVG -->
<svg class="testimonial-card__quote" width="48" height="48" viewBox="0 0 48 48" fill="none">
<path d="M6 6h14v36" stroke="rgba(82,202,209,0.2)" stroke-width="2"/>
<path d="M28 6h14v36" stroke="rgba(82,202,209,0.2)" stroke-width="2"/>
</svg>
<p class="testimonial-card__text">"{{ t.text }}"</p>
<div class="testimonial-card__author">
<img class="testimonial-card__avatar" :src="avatarImg" alt="用户头像" />
<div class="testimonial-card__info">
<p class="testimonial-card__name">{{ t.name }}</p>
<p class="testimonial-card__school">{{ t.school }}</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Job Search Section -->
<section class="home-job-search">
<div class="home-job-search__inner">
<h3>一键找到最适合你的工作</h3>
<div class="home-job-search__filters">
<div class="filter-select">
<span>行业</span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2"/></svg>
<!-- 创始人引言卡片可切换 -->
<div class="home-testimonials__founder">
<!-- 装饰圆环 SVG -->
<svg class="home-testimonials__founder-decor" width="455" height="455" viewBox="0 0 455 455" fill="none">
<circle cx="227.5" cy="227.5" r="189.5" stroke="#fff" stroke-width="20" opacity="0.08"/>
<circle cx="227.5" cy="227.5" r="80" stroke="#fff" stroke-width="20" opacity="0.08"/>
</svg>
<div class="home-testimonials__founder-content">
<img class="home-testimonials__founder-img" :src="avatarImg" alt="创始人头像" />
<div class="home-testimonials__founder-text">
<blockquote>"{{ founderQuotes[founderIndex].text }}"</blockquote>
<cite>
<span class="cite-name">{{ founderQuotes[founderIndex].name }}</span>
<span class="cite-role">{{ founderQuotes[founderIndex].role }}</span>
</cite>
</div>
</div>
<div class="filter-select">
<span>地区</span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2"/></svg>
</div>
<div class="filter-select">
<span>岗位</span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2"/></svg>
</div>
<button class="filter-btn" @click="router.push('/jobs')">搜索职位</button>
</div>
<!-- 分页小圆点 -->
<div class="home-testimonials__dots">
<span
v-for="(_, i) in founderQuotes"
:key="i"
class="home-testimonials__dot"
:class="{ 'home-testimonials__dot--active': founderIndex === i }"
@click="founderIndex = i"
></span>
</div>
</div>
</section>
@@ -348,12 +376,52 @@
</div>
</section>
<!-- Job Search Section -->
<section class="home-job-search">
<div class="home-job-search__inner">
<h3>一键找到最适合你的工作</h3>
<div class="home-job-search__filters">
<!-- 行业选择器 -->
<IndustrySelector
:industryIds="homeIndustryIds"
:level="2"
:maxSelect="3"
:allowParentSelect="false"
:triggerStyle="filterTriggerStyle"
:displayStyle="filterDisplayStyle"
@update:industryIds="onHomeIndustryChange"
/>
<!-- 地区选择器 -->
<RegionSelector
:regionCodes="homeRegionCodes"
:level="2"
:maxSelect="3"
:triggerStyle="filterTriggerStyle"
:displayStyle="filterDisplayStyle"
@update:regionCodes="onHomeRegionChange"
/>
<!-- 岗位选择器 -->
<JobCategorySelector
:categoryIds="homeCategoryIds"
:level="3"
:maxSelect="3"
:allowParentSelect="false"
:triggerStyle="filterTriggerStyle"
:displayStyle="filterDisplayStyle"
@update:categoryIds="onHomeCategoryChange"
/>
<button class="filter-btn" @click="goSearchJobs">搜索职位</button>
</div>
</div>
</section>
<!-- Footer -->
<footer class="home-footer">
<div class="home-footer__inner">
<div class="home-footer__col">
<div class="home-footer__logo">
<span class="home-footer__logo-text">Offer派</span>
<!-- 网站Logo图片 -->
<img src="@/assets/images/logo.png" alt="Offer派" class="home-footer__logo-img" />
</div>
<p class="home-footer__slogan">大学生AI求职平台</p>
</div>
@@ -391,9 +459,13 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import avatarImg from '@/assets/images/home/avatar-temporary.png'
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
import RegionSelector from '@/components/tools/RegionSelector.vue'
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
/** 路由实例 */
const router = useRouter()
@@ -403,6 +475,15 @@ const store = useStore()
/** 当前展开的 FAQ 索引,-1 表示全部收起 */
const faqOpen = ref(-1)
/** 创始人引言当前索引 — 点击小圆点可切换 */
const founderIndex = ref(0)
/** 创始人引言数据 — 支持多条切换 */
const founderQuotes = [
{ text: '很多大学生还在用传统方式找工作,海投、反复改简历,效率很低。Offer派利用AI技术让整个校招流程更丝滑,节省了80%的繁琐过程。', name: 'Stella', role: 'Offer派创始人' },
{ text: '我们的目标是让每一位大学生都能高效地找到心仪的工作,AI技术正在改变校招的游戏规则。', name: 'Stella', role: 'Offer派创始人' },
]
/** 岗位滚动展示数据 — 模拟最新发布的校招岗位 */
const tickerJobs = [
{ company: '字节跳动', time: '15分钟前', title: '前端开发工程师' },
@@ -436,10 +517,74 @@ const faqs = [
{ q: '支持哪些企业?', a: '目前已覆盖互联网、金融、制造、快消等行业的数千家企业,包括字节跳动、腾讯、阿里巴巴、华为等头部企业。' },
]
// ==================== 底部筛选区:行业/地区/岗位选择器 ====================
/** 首页选中的行业 ID 数组 — 读写 store.jobIntention */
const homeIndustryIds = computed<number[]>(() => store.state.jobIntention.industryIds || [])
/** 首页选中的地区编码数组 — 读写 store.jobIntention */
const homeRegionCodes = computed<string[]>(() => store.state.jobIntention.regionCodes || [])
/** 首页选中的岗位 ID 数组 — 读写 store.jobIntention */
const homeCategoryIds = computed<number[]>(() => store.state.jobIntention.categoryIds || [])
/** 选择器触发按钮样式 — 匹配首页 .filter-select 的外观 */
const filterTriggerStyle = {
width: '2.35rem',
height: '0.48rem',
'border-radius': '0.24rem',
border: '1px solid #e5e7eb',
padding: '0 0.2rem',
background: '#fff',
'justify-content': 'space-between',
}
/** 选择器显示文字样式 — 匹配首页 .filter-select 内文字 */
const filterDisplayStyle = {
'font-size': '0.16rem',
'line-height': '0.24rem',
color: 'rgba(0, 0, 0, 0.45)',
'max-width': '1.8rem',
}
/** 行业选择变更回调 — 保存到 store */
function onHomeIndustryChange(ids: number[]) {
store.dispatch('saveJobIntention', {
...store.state.jobIntention,
industryIds: ids,
})
}
/** 地区选择变更回调 — 保存到 store */
function onHomeRegionChange(codes: string[]) {
store.dispatch('saveJobIntention', {
...store.state.jobIntention,
regionCodes: codes,
})
}
/** 岗位选择变更回调 — 保存到 store */
function onHomeCategoryChange(ids: number[]) {
store.dispatch('saveJobIntention', {
...store.state.jobIntention,
categoryIds: ids,
})
}
/** 点击搜索职位 — 跳转到 Jobs 页面 */
function goSearchJobs() {
router.push('/jobs')
}
onMounted(() => {
// 加载公共工具数据(行业分类、岗位分类、地区分类等)
store.dispatch('loadCommonData')
// 触发预渲染事件 — 配合 vite.config.ts 中 PrerenderPlugin 的 renderAfterDocumentEvent 设置
document.dispatchEvent(new Event('prerender-trigger'))
})
/** 点击登陆按钮 — 打开登录弹窗,登录成功后跳转到 jobs 页面 */
function handleLoginClick() {
store.dispatch('openLogin', '/jobs')
}
</script>
+3 -1
View File
@@ -100,7 +100,7 @@
</div>
<!-- 职位列表 -->
<div ref="jobListRef" class="jobs-page__list pr5" :style="restoring ? { visibility: 'hidden' } : {}" @scroll="onListScroll">
<div ref="jobListRef" v-loading="loading" class="jobs-page__list pr5" :style="restoring ? { visibility: 'hidden' } : {}" @scroll="onListScroll">
<div
v-for="(job, index) in jobList"
:key="index"
@@ -232,6 +232,8 @@
</div>
</div>
</div>
<!-- 暂无数据提示 -->
<div v-if="!loading && jobList.length === 0" class="jobs-page__empty">暂无数据</div>
<!-- 加载更多提示 -->
<div v-if="loadingMore" class="jobs-page__loading-more">加载中...</div>
<div v-else-if="noMore && jobList.length > 0" class="jobs-page__loading-more">没有更多了</div>
+104 -37
View File
@@ -77,15 +77,22 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import SideNav from '@/components/SideNav.vue'
import { uploadResume } from '@/utils/aiRequest'
import { fetchResumeList, type ResumeListItem } from '@/api/resume'
const router = useRouter()
// ==================== 头像颜色池 ====================
/** 头像背景色列表,按索引循环取色 */
const avatarColors = ['#1A1A2E', '#4FC2C9', '#F2994A', '#6C5CE7', '#E17055', '#00B894', '#FDCB6E']
// ==================== 类型定义 ====================
/** 简历列表项类型 */
/** 简历列表项(前端展示用) */
interface ResumeItem {
id: string
name: string
@@ -97,41 +104,68 @@ interface ResumeItem {
createdAt: string
}
// ==================== 数据(模拟数据,后续对接接口) ====================
// ==================== 工具方法 ====================
/**
* 将 Instant 时间戳转为友好的相对时间文案
* @param instant 后端返回的 Instant 对象
*/
function formatTime(instant?: { seconds?: number; nanos?: number }): string {
if (!instant?.seconds) return '-'
const date = new Date(instant.seconds * 1000)
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[]>([
{
id: '1',
name: '李华_产品经理',
avatarLetter: 'D',
avatarColor: '#1A1A2E',
isDefault: true,
targetJob: '产品经理',
updatedAt: '1个月前',
createdAt: '1个月前',
},
{
id: '2',
name: '李华_产品运营',
avatarLetter: 'C',
avatarColor: '#4FC2C9',
isDefault: false,
targetJob: '产品运营',
updatedAt: '1个月前',
createdAt: '1个月前',
},
{
id: '3',
name: '李华_产品经理',
avatarLetter: '?',
avatarColor: '#BFBFBF',
isDefault: false,
targetJob: '',
updatedAt: '1个月前',
createdAt: '1个月前',
},
])
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()
})
// ==================== 页面状态 ====================
@@ -154,9 +188,42 @@ function handleAction(action: string, id: string) {
console.log(action, id)
}
/** 上传简历 */
/** 上传简历 — 弹出文件选择,选择后调用 AI 接口上传 */
function handleUpload() {
console.log('上传简历')
const input = document.createElement('input')
input.type = 'file'
// 限定 pdf 和 word 格式
input.accept = '.pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document'
input.onchange = async () => {
const file = input.files?.[0]
if (!file) return
// 全屏加载提示,AI 接口响应较慢
const loading = ElLoading.service({
lock: true,
text: '简历解析中,请耐心等待…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
try {
const res = await uploadResume(file)
if (res.code === 0) {
// 上传成功,刷新列表并跳转详情页
loadResumeList()
goDetail(String(res.data.resumeId))
} else {
ElMessage.error(res.msg || '上传失败')
}
} catch {
ElMessage.error('上传失败,请稍后重试')
} finally {
loading.close()
}
}
input.click()
}
/** 跳转到简历详情页 */
+634 -81
View File
@@ -2,6 +2,15 @@
<div class="resume-detail dflex">
<SideNav />
<div class="resume-detail__content">
<!-- 个人资料编辑抽屉 -->
<ProfileEditDrawer
v-model="showEditDrawer"
:module="editModule"
:initial-data="editInitialData"
:use-education-text-value="true"
@save="handleSaveEdit"
/>
<!-- 顶部标题 -->
<h2 class="resume-detail__page-title">我的简历</h2>
@@ -13,7 +22,7 @@
<path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
<span class="resume-detail__tab-name">{{ resume.name }}</span>
<span class="resume-detail__tab-name">{{ resumeMain.resumeName || '未命名简历' }}</span>
</div>
<div class="resume-detail__toolbar-right">
<button class="resume-detail__tool-btn" @click="handleFeedback">问题反馈</button>
@@ -41,23 +50,23 @@
<!-- 简历评分区域 -->
<div class="resume-detail__score-bar">
<div class="resume-detail__score-left">
<span class="resume-detail__score-avatar" :style="{ background: resume.avatarColor }">
{{ resume.avatarLetter }}
<span class="resume-detail__score-avatar">
{{ getAvatarLetter() }}
</span>
<span class="resume-detail__score-badge">良好</span>
<button class="resume-detail__score-link" @click="handleViewReport">查看评估报告 ></button>
</div>
<div class="resume-detail__score-right">
<div class="resume-detail__score-item">
<span class="resume-detail__score-num">{{ resume.urgentCount }}</span>
<span class="resume-detail__score-num">0</span>
<span class="resume-detail__score-label">紧急修复项</span>
</div>
<div class="resume-detail__score-item">
<span class="resume-detail__score-num">{{ resume.severeCount }}</span>
<span class="resume-detail__score-num">0</span>
<span class="resume-detail__score-label">严重问题</span>
</div>
<div class="resume-detail__score-item">
<span class="resume-detail__score-num">{{ resume.optionalCount }}</span>
<span class="resume-detail__score-num">0</span>
<span class="resume-detail__score-label">可选修复项</span>
</div>
<button class="resume-detail__diagnose-btn" @click="handleDiagnose">重新诊断</button>
@@ -70,72 +79,327 @@
<div class="resume-detail__card">
<div class="resume-detail__card-header">
<div>
<h3 class="resume-detail__user-name">{{ resume.realName }}</h3>
<p class="resume-detail__user-title">{{ resume.jobTitle }}</p>
</div>
<div class="resume-detail__card-actions">
<button class="resume-detail__card-btn resume-detail__card-btn--outline" @click="handleUrgentFix">紧急修复项</button>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handlePolish">修复</button>
<h3 class="resume-detail__user-name">{{ resumeMain.name || '未填写姓名' }}</h3>
<p class="resume-detail__user-title">{{ resumeMain.targetPosition || '' }}</p>
</div>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('info')" aria-label="编辑个人信息">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div class="resume-detail__contact">
<span class="resume-detail__contact-item">
<!-- 邮箱 -->
<span v-if="resumeMain.email" class="resume-detail__contact-item">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
<rect x="2" y="3" width="12" height="10" rx="1.5" stroke="currentColor" stroke-width="1.2"/>
<path d="M2 5.5l6 4 6-4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
</svg>
{{ resume.email }}
{{ resumeMain.email }}
</span>
<span class="resume-detail__contact-item">
<!-- 手机号 -->
<span v-if="resumeMain.mobileNumber" class="resume-detail__contact-item">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
<path d="M4 2h2.5l1.5 3-1.5 1.5a8 8 0 003 3L11 8l3 1.5V12a2 2 0 01-2 2C6.5 14 2 9.5 2 4a2 2 0 012-2z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
{{ resume.phone }}
{{ resumeMain.mobileNumber }}
</span>
<span class="resume-detail__contact-item">
<!-- 所在城市 -->
<span v-if="resumeMain.city" class="resume-detail__contact-item">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
<circle cx="8" cy="6.5" r="2.5" stroke="currentColor" stroke-width="1.2"/>
<path d="M8 14s-5-4-5-7.5a5 5 0 0110 0C13 10 8 14 8 14z" stroke="currentColor" stroke-width="1.2"/>
</svg>
{{ resume.location }}
{{ resumeMain.city }}
</span>
<!-- 微信号 -->
<span v-if="resumeMain.wechatNumber" class="resume-detail__contact-item">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
<path d="M2 8a6 6 0 1112 0 6 6 0 01-12 0z" stroke="currentColor" stroke-width="1.2"/>
<path d="M5.5 7.5c.5-1 1.5-1.5 2.5-1.5s2 .5 2.5 1.5M6 10s.8 1 2 1 2-1 2-1" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
</svg>
{{ resumeMain.wechatNumber }}
</span>
</div>
<!-- 问题操作区 -->
<div v-if="cardIssueConfig.personalInfo.show" class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getCardIssueTypes('personalInfo')" :key="t.value"
class="resume-detail__issue-type-btn"
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.personalInfo.activeType === t.value }"
@click="cardIssueConfig.personalInfo.activeType = t.value"
>{{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('personalInfo')">修复</button>
</div>
</div>
<!-- 个人概述 -->
<div v-if="resumeMain.summary" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">个人概述</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('summary')" aria-label="编辑个人概述">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<p class="resume-detail__summary-text">{{ resumeMain.summary }}</p>
<!-- 问题操作区 -->
<div v-if="cardIssueConfig.summary.show" class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getCardIssueTypes('summary')" :key="t.value"
class="resume-detail__issue-type-btn"
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.summary.activeType === t.value }"
@click="cardIssueConfig.summary.activeType = t.value"
>{{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('summary')">修复</button>
</div>
</div>
<!-- 作品集链接 -->
<div v-if="resumeMain.portfolioUrl" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">作品集</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('portfolio')" aria-label="编辑作品集">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<a :href="resumeMain.portfolioUrl" target="_blank" rel="noopener noreferrer" class="resume-detail__portfolio-link">
{{ resumeMain.portfolioUrl }}
</a>
<!-- 问题操作区 -->
<div v-if="cardIssueConfig.portfolio.show" class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getCardIssueTypes('portfolio')" :key="t.value"
class="resume-detail__issue-type-btn"
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.portfolio.activeType === t.value }"
@click="cardIssueConfig.portfolio.activeType = t.value"
>{{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('portfolio')">修复</button>
</div>
</div>
<!-- 教育背景 -->
<div class="resume-detail__card">
<div v-if="educationList.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">教育背景</h3>
<div class="resume-detail__card-actions">
<button class="resume-detail__card-btn resume-detail__card-btn--outline">紧急修复项</button>
<button class="resume-detail__card-btn resume-detail__card-btn--dark">修复</button>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('education')" aria-label="编辑教育背景">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div v-for="edu in educationList" :key="edu.id" class="resume-detail__edu-item">
<div class="resume-detail__edu-degree">{{ edu.degree }} · {{ edu.major }}</div>
<div class="resume-detail__edu-meta">{{ edu.school }} · {{ edu.studyType }} · {{ edu.startDate }} - {{ edu.endDate }}</div>
<div v-if="edu.description?.length" class="resume-detail__desc-list">
<p v-for="p in edu.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
</div>
</div>
<div v-for="(edu, i) in resume.education" :key="i" class="resume-detail__edu-item">
<div class="resume-detail__edu-degree">{{ edu.degree }}</div>
<div class="resume-detail__edu-meta">{{ edu.school }} · {{ edu.period }}</div>
<div v-if="edu.gpa" class="resume-detail__edu-meta">GPA: {{ edu.gpa }}</div>
<!-- 问题操作区 -->
<div v-if="cardIssueConfig.education.show" class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getCardIssueTypes('education')" :key="t.value"
class="resume-detail__issue-type-btn"
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.education.activeType === t.value }"
@click="cardIssueConfig.education.activeType = t.value"
>{{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('education')">修复</button>
</div>
</div>
<!-- 工作经 -->
<div class="resume-detail__card">
<h3 class="resume-detail__section-title">工作经验</h3>
<div v-for="(exp, i) in resume.experience" :key="i" class="resume-detail__exp-item">
<div class="resume-detail__exp-title">{{ exp.title }}</div>
<div class="resume-detail__exp-meta">{{ exp.company }} · {{ exp.period }}</div>
<p class="resume-detail__exp-desc">{{ exp.description }}</p>
<!-- 工作经 -->
<div v-if="workList.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">工作经历</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('work')" aria-label="编辑工作经历">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div v-for="exp in workList" :key="exp.id" class="resume-detail__exp-item">
<div class="resume-detail__exp-title dflex">
<span>{{ exp.companyName }}</span>
<span v-if="exp.startDate||exp.endDate" class="color-5 fw400 fs12 pt5">{{ exp.startDate }} <span v-if="exp.startDate&&exp.endDate"> - </span> {{ exp.endDate }}</span>
</div>
<div class="resume-detail__exp-meta">{{ exp.position }} </div>
<ul v-if="exp.description?.length" class="resume-detail__desc-list pl16">
<li v-for="p in exp.description" :key="p.id" class="resume-detail__desc-item fs12 color-5">{{ p.text }}</li>
</ul>
</div>
<!-- 问题操作区 -->
<div v-if="cardIssueConfig.work.show" class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getCardIssueTypes('work')" :key="t.value"
class="resume-detail__issue-type-btn"
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.work.activeType === t.value }"
@click="cardIssueConfig.work.activeType = t.value"
>{{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('work')">修复</button>
</div>
</div>
<!-- 实习经历 -->
<div v-if="internshipList.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">实习经历</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('internship')" aria-label="编辑实习经历">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div v-for="intern in internshipList" :key="intern.id" class="resume-detail__exp-item">
<div class="resume-detail__exp-title dflex">
<span>{{ intern.position }}</span>
<span v-if="intern.startDate||intern.endDate" class="color-5 fw400 fs12 pt5">{{ intern.startDate }} <span v-if="intern.startDate&&intern.endDate"> - </span> {{ intern.endDate }}</span>
</div>
<div class="resume-detail__exp-meta">{{ intern.companyName }}</div>
<div v-if="intern.description?.length" class="resume-detail__desc-list">
<p v-for="p in intern.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
</div>
</div>
<!-- 问题操作区 -->
<div v-if="cardIssueConfig.internship.show" class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getCardIssueTypes('internship')" :key="t.value"
class="resume-detail__issue-type-btn"
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.internship.activeType === t.value }"
@click="cardIssueConfig.internship.activeType = t.value"
>{{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('internship')">修复</button>
</div>
</div>
<!-- 项目经历 -->
<div v-if="projectList.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">项目经历</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('project')" aria-label="编辑项目经历">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div v-for="proj in projectList" :key="proj.id" class="resume-detail__exp-item">
<div class="resume-detail__exp-title dflex">
<span>{{ proj.projectName }}</span>
<span v-if="proj.startDate||proj.endDate" class="color-5 fw400 fs12 pt5">{{ proj.startDate }} <span v-if="proj.startDate&&proj.endDate"> - </span> {{ proj.endDate }}</span>
</div>
<div class="resume-detail__exp-meta">{{ proj.companyName }} · {{ proj.role }}</div>
<div v-if="proj.description?.length" class="resume-detail__desc-list">
<p v-for="p in proj.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
</div>
</div>
<!-- 问题操作区 -->
<div v-if="cardIssueConfig.project.show" class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getCardIssueTypes('project')" :key="t.value"
class="resume-detail__issue-type-btn"
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.project.activeType === t.value }"
@click="cardIssueConfig.project.activeType = t.value"
>{{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('project')">修复</button>
</div>
</div>
<!-- 竞赛经历 -->
<div v-if="competitionList.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">竞赛经历</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('competition')" aria-label="编辑竞赛经历">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div v-for="comp in competitionList" :key="comp.id" class="resume-detail__exp-item">
<div class="resume-detail__exp-title dflex">
<span>{{ comp.competitionName }}</span>
<span v-if="comp.awardDate" class="color-5 fw400 fs12 pt5">{{ comp.awardDate }}</span>
</div>
<div class="resume-detail__exp-meta">{{ comp.award }}</div>
<div v-if="comp.description?.length" class="resume-detail__desc-list">
<p v-for="p in comp.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
</div>
</div>
<!-- 问题操作区 -->
<div v-if="cardIssueConfig.competition.show" class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getCardIssueTypes('competition')" :key="t.value"
class="resume-detail__issue-type-btn"
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.competition.activeType === t.value }"
@click="cardIssueConfig.competition.activeType = t.value"
>{{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('competition')">修复</button>
</div>
</div>
<!-- 技能 -->
<div class="resume-detail__card">
<h3 class="resume-detail__section-title">技能</h3>
<div v-if="resumeMain.skills?.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">技能</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('skills')" aria-label="编辑技能">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div class="resume-detail__skills">
<span v-for="(skill, i) in resume.skills" :key="i" class="resume-detail__skill-tag">
<span v-for="(skill, i) in resumeMain.skills" :key="i" class="resume-detail__skill-tag">
{{ skill }}
</span>
</div>
<!-- 问题操作区 -->
<div v-if="cardIssueConfig.skills.show" class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getCardIssueTypes('skills')" :key="t.value"
class="resume-detail__issue-type-btn"
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.skills.activeType === t.value }"
@click="cardIssueConfig.skills.activeType = t.value"
>{{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('skills')">修复</button>
</div>
</div>
<!-- 证书 -->
<div v-if="resumeMain.certificates?.length" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">证书</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('certificate')" aria-label="编辑证书">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div class="resume-detail__skills">
<span v-for="(cert, i) in resumeMain.certificates" :key="i" class="resume-detail__skill-tag">
{{ cert }}
</span>
</div>
<!-- 问题操作区 -->
<div v-if="cardIssueConfig.certificates.show" class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getCardIssueTypes('certificates')" :key="t.value"
class="resume-detail__issue-type-btn"
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.certificates.activeType === t.value }"
@click="cardIssueConfig.certificates.activeType = t.value"
>{{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('certificates')">修复</button>
</div>
</div>
</div>
</div>
@@ -143,60 +407,149 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import SideNav from '@/components/SideNav.vue'
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
import {
fetchResumeMain,
fetchResumeEducation,
fetchResumeWork,
fetchResumeInternship,
fetchResumeProject,
fetchResumeCompetition,
saveResumeMain,
saveResumeEducation,
saveResumeWork,
saveResumeInternship,
saveResumeProject,
saveResumeCompetition,
type ResumeMainData,
type ResumeEducation,
type ResumeWork,
type ResumeInternship,
type ResumeProject,
type ResumeCompetition,
} from '@/api/resume'
// ==================== 问题类型相关 ====================
/** 问题类型枚举值 */
type IssueType = 'urgent' | 'optimize' | 'expression'
/** 问题类型选项列表 */
const issueTypes: { label: string; value: IssueType }[] = [
{ label: '紧急修复项', value: 'urgent' },
{ label: '重点优化项', value: 'optimize' },
{ label: '表达提升', value: 'expression' },
]
/** 单个卡片的问题配置 */
interface CardIssueItem {
/** 是否显示问题操作区 */
show: boolean
/** 当前选中的问题类型 */
activeType: IssueType
/** 该模块拥有的问题类型列表(只显示这些) */
types: IssueType[]
}
/** 所有卡片模块的 key */
type CardKey = 'personalInfo' | 'summary' | 'portfolio' | 'education' | 'work' | 'internship' | 'project' | 'competition' | 'skills' | 'certificates'
/** 各模块问题操作区的显示与选中状态配置,每个模块只包含自己实际存在的问题类型 */
const cardIssueConfig = reactive<Record<CardKey, CardIssueItem>>({
personalInfo: { show: true, activeType: 'urgent', types: ['urgent'] },
summary: { show: true, activeType: 'expression', types: ['expression'] },
portfolio: { show: true, activeType: 'optimize', types: ['optimize'] },
education: { show: true, activeType: 'urgent', types: ['urgent'] },
work: { show: true, activeType: 'optimize', types: ['optimize'] },
internship: { show: true, activeType: 'optimize', types: ['optimize'] },
project: { show: true, activeType: 'expression', types: ['expression'] },
competition: { show: true, activeType: 'urgent', types: ['urgent'] },
skills: { show: true, activeType: 'optimize', types: ['optimize'] },
certificates: { show: true, activeType: 'urgent', types: ['urgent'] },
})
/** 根据模块 key 获取该模块可用的问题类型选项 */
function getCardIssueTypes(cardKey: CardKey) {
return issueTypes.filter(t => cardIssueConfig[cardKey].types.includes(t.value))
}
// ==================== 路由相关 ====================
const router = useRouter()
const route = useRoute()
/** 当前简历 ID,从路由参数中获取 */
/** 当前简历 ID,从路由参数中获取(保持字符串,避免大整数精度丢失) */
const resumeId = route.params.id as string
// ==================== 简历数据(模拟数据,后续对接接口) ====================
// ==================== 数据 ====================
const resume = ref({
id: resumeId,
name: '李华_产品经理',
avatarLetter: 'B',
avatarColor: '#1A1A2E',
realName: '李华',
jobTitle: '数据产品经理',
email: '[email]',
phone: '+1 (555) 123-4567',
location: '北京',
/** 紧急修复项数量 */
urgentCount: 8,
/** 严重问题数量 */
severeCount: 0,
/** 可选修复项数量 */
optionalCount: 1,
/** 教育背景 */
education: [
{ degree: '计算机科学学士', school: '斯坦福大学', period: '2018 - 2022', gpa: '3.8/4.0' },
{ degree: '高中毕业', school: '加州理工学院附属中学', period: '2014 - 2018', gpa: '' },
],
/** 工作经验 */
experience: [
{
title: '高级软件工程师',
company: 'Google',
period: '2022 - 至今',
description: '负责开发和维护大规模分布式系统,优化系统性能和可靠性。',
},
{
title: '软件工程师实习生',
company: 'Microsoft',
period: '2021 - 2022',
description: '参与云服务平台的开发,协助团队完成多个核心功能模块。',
},
],
/** 技能标签 */
skills: ['JavaScript', 'Python', 'React', 'Node.js', 'SQL', 'AWS', 'Docker', 'Kubernetes'],
/** 简历主表数据 */
const resumeMain = ref<ResumeMainData>({})
/** 教育经历列表 */
const educationList = ref<ResumeEducation[]>([])
/** 工作经历列表 */
const workList = ref<ResumeWork[]>([])
/** 实习经历列表 */
const internshipList = ref<ResumeInternship[]>([])
/** 项目经历列表 */
const projectList = ref<ResumeProject[]>([])
/** 竞赛经历列表 */
const competitionList = ref<ResumeCompetition[]>([])
// ==================== 数据加载 ====================
/** 加载所有简历详情数据 */
async function loadResumeDetail() {
const [mainRes, eduRes, workRes, internRes, projRes, compRes] = await Promise.all([
fetchResumeMain(resumeId),
fetchResumeEducation(resumeId),
fetchResumeWork(resumeId),
fetchResumeInternship(resumeId),
fetchResumeProject(resumeId),
fetchResumeCompetition(resumeId),
])
if (mainRes.code === '0' && mainRes.data) {
resumeMain.value = mainRes.data
}
if (eduRes.code === '0' && eduRes.data) {
educationList.value = eduRes.data
}
if (workRes.code === '0' && workRes.data) {
workList.value = workRes.data
}
if (internRes.code === '0' && internRes.data) {
internshipList.value = internRes.data
}
if (projRes.code === '0' && projRes.data) {
projectList.value = projRes.data
}
if (compRes.code === '0' && compRes.data) {
competitionList.value = compRes.data
}
}
// ==================== 生命周期 ====================
onMounted(() => {
loadResumeDetail()
})
// ==================== 计算属性 ====================
/** 头像首字母 */
function getAvatarLetter(): string {
return resumeMain.value.name?.charAt(0) || resumeMain.value.resumeName?.charAt(0) || '?'
}
// ==================== 事件处理 ====================
/** 返回简历列表页 */
@@ -222,9 +575,209 @@ function handleViewReport() { console.log('查看评估报告') }
/** 重新诊断简历 */
function handleDiagnose() { console.log('重新诊断') }
/** 紧急修复项 */
function handleUrgentFix() { console.log('紧急修复项') }
/** 修复指定模块的问题,传入模块 key */
function handleFixIssue(cardKey: CardKey) {
const config = cardIssueConfig[cardKey]
console.log(`修复模块: ${cardKey}, 问题类型: ${config.activeType}`)
}
/** 修复简历问题 */
function handlePolish() { console.log('修复') }
// ==================== 编辑抽屉相关 ====================
/** 编辑抽屉的显示状态 */
const showEditDrawer = ref(false)
/** 当前编辑的模块名称 */
const editModule = ref('info')
/** 当前编辑模块的初始数据 */
const editInitialData = ref<Record<string, any>>({})
/** 打开编辑抽屉 — 根据模块名设置初始数据 */
function openEditDrawer(section: string) {
editModule.value = section
if (section === 'info') {
editInitialData.value = {
name: resumeMain.value.name || '',
email: resumeMain.value.email || '',
phone: resumeMain.value.mobileNumber || '',
location: '',
wechat: resumeMain.value.wechatNumber || '',
}
} else if (section === 'summary') {
editInitialData.value = {
summary: resumeMain.value.summary || '',
}
} else if (section === 'education') {
editInitialData.value = {
education: educationList.value.map(edu => ({
school: edu.school || '',
major: edu.major || '',
studyType: edu.studyType || '全日制',
degree: edu.degree || '本科',
startDate: edu.startDate || '',
endDate: edu.endDate || '',
description: (edu.description || []).map(d => ({ ...d })),
})),
}
} else if (section === 'work') {
editInitialData.value = {
works: workList.value.map(w => ({
companyName: w.companyName || '',
position: w.position || '',
startDate: w.startDate || '',
endDate: w.endDate || '',
description: (w.description || []).map(d => ({ ...d })),
})),
}
} else if (section === 'internship') {
editInitialData.value = {
internships: internshipList.value.map(i => ({
companyName: i.companyName || '',
position: i.position || '',
startDate: i.startDate || '',
endDate: i.endDate || '',
description: (i.description || []).map(d => ({ ...d })),
})),
}
} else if (section === 'project') {
editInitialData.value = {
projects: projectList.value.map(p => ({
projectName: p.projectName || '',
companyName: p.companyName || '',
role: p.role || '',
startDate: p.startDate || '',
endDate: p.endDate || '',
description: (p.description || []).map(d => ({ ...d })),
})),
}
} else if (section === 'competition') {
editInitialData.value = {
competitions: competitionList.value.map(c => ({
competitionName: c.competitionName || '',
award: c.award || '',
awardDate: c.awardDate || '',
description: (c.description || []).map(d => ({ ...d })),
})),
}
} else if (section === 'portfolio') {
editInitialData.value = {
portfolioUrl: resumeMain.value.portfolioUrl || '',
}
} else if (section === 'skills') {
editInitialData.value = {
skills: [...(resumeMain.value.skills || [])],
}
} else if (section === 'certificate') {
editInitialData.value = {
certificates: [...(resumeMain.value.certificates || [])],
}
}
showEditDrawer.value = true
}
/** 保存编辑数据 — 将抽屉返回的数据调用对应接口持久化,并更新本地数据 */
async function handleSaveEdit(data: Record<string, any>) {
try {
if (editModule.value === 'info') {
// 保存基本信息到简历主表
await saveResumeMain({
resumeId,
name: data.name,
email: data.email,
mobileNumber: data.phone,
city: data.locationName || data.location || '',
wechatNumber: data.wechat,
})
resumeMain.value.name = data.name
resumeMain.value.email = data.email
resumeMain.value.mobileNumber = data.phone
resumeMain.value.city = data.locationName || data.location || ''
resumeMain.value.wechatNumber = data.wechat
} else if (editModule.value === 'summary') {
// 保存个人概述到简历主表
await saveResumeMain({
resumeId,
summary: data.summary,
})
resumeMain.value.summary = data.summary
} else if (editModule.value === 'education') {
// 保存教育经历
await saveResumeEducation(resumeId, data.education.map((edu: any) => ({
school: edu.school,
major: edu.major,
degree: edu.degree,
studyType: edu.studyType,
startDate: edu.startDate,
endDate: edu.endDate,
description: edu.description,
})))
educationList.value = data.education.map((edu: any) => ({ ...edu, description: edu.description.map((d: any) => ({ ...d })) }))
} else if (editModule.value === 'work') {
// 保存工作经历
await saveResumeWork(resumeId, data.works.map((w: any) => ({
companyName: w.companyName,
position: w.position,
startDate: w.startDate,
endDate: w.endDate,
description: w.description,
})))
workList.value = data.works.map((w: any) => ({ ...w, description: w.description.map((d: any) => ({ ...d })) }))
} else if (editModule.value === 'internship') {
// 保存实习经历
await saveResumeInternship(resumeId, data.internships.map((i: any) => ({
companyName: i.companyName,
position: i.position,
startDate: i.startDate,
endDate: i.endDate,
description: i.description,
})))
internshipList.value = data.internships.map((i: any) => ({ ...i, description: i.description.map((d: any) => ({ ...d })) }))
} else if (editModule.value === 'project') {
// 保存项目经历
await saveResumeProject(resumeId, data.projects.map((p: any) => ({
projectName: p.projectName,
companyName: p.companyName,
role: p.role,
startDate: p.startDate,
endDate: p.endDate,
description: p.description,
})))
projectList.value = data.projects.map((p: any) => ({ ...p, description: p.description.map((d: any) => ({ ...d })) }))
} else if (editModule.value === 'competition') {
// 保存竞赛经历
await saveResumeCompetition(resumeId, data.competitions.map((c: any) => ({
competitionName: c.competitionName,
award: c.award,
awardDate: c.awardDate,
description: c.description,
})))
competitionList.value = data.competitions.map((c: any) => ({ ...c, description: c.description.map((d: any) => ({ ...d })) }))
} else if (editModule.value === 'portfolio') {
// 保存作品集链接到简历主表
await saveResumeMain({
resumeId,
portfolioUrl: data.portfolioUrl,
})
resumeMain.value.portfolioUrl = data.portfolioUrl
} else if (editModule.value === 'skills') {
// 保存技能到简历主表
await saveResumeMain({
resumeId,
skills: [...data.skills],
})
resumeMain.value.skills = [...data.skills]
} else if (editModule.value === 'certificate') {
// 保存证书到简历主表
await saveResumeMain({
resumeId,
certificates: [...data.certificates],
})
resumeMain.value.certificates = [...data.certificates]
}
} catch {
console.error('[ResumeDetail] 保存失败')
}
}
</script>