仓库初始化+岗位相关页面
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
<template>
|
||||
<!-- 求职助手页面 — 准备阶段 -->
|
||||
<div class="agent-page dflex">
|
||||
<!-- 左侧导航栏 -->
|
||||
<SideNav />
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="agent-page__content">
|
||||
<!-- 个人资料编辑抽屉 -->
|
||||
<ProfileEditDrawer
|
||||
v-model="showEditDrawer"
|
||||
:module="editModule"
|
||||
:initial-data="editInitialData"
|
||||
@save="handleSaveEdit"
|
||||
/>
|
||||
|
||||
<!-- 顶部步骤导航条 -->
|
||||
<div class="agent-page__steps">
|
||||
<template v-for="(step, index) in steps" :key="index">
|
||||
<!-- 单个步骤项 -->
|
||||
<div
|
||||
class="agent-page__step"
|
||||
:class="{ 'agent-page__step--active': currentStep === index + 1 }"
|
||||
>
|
||||
<span class="agent-page__step-number">{{ index + 1 }}</span>
|
||||
<span class="agent-page__step-label">{{ step }}</span>
|
||||
</div>
|
||||
<!-- 步骤间分隔箭头 -->
|
||||
<span v-if="index < steps.length - 1" class="agent-page__step-arrow">
|
||||
<svg viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容区域 — 左右两栏布局 -->
|
||||
<div class="agent-page__main">
|
||||
<!-- ========== 第1步:确认个人资料 ========== -->
|
||||
<template v-if="currentStep === 1">
|
||||
<!-- 左侧:说明引导卡片 -->
|
||||
<div class="agent-page__left">
|
||||
<div class="agent-page__intro-card">
|
||||
<!-- 图标 + 标题 -->
|
||||
<div class="agent-page__intro-header">
|
||||
<div class="agent-page__intro-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 12c2.7 0 5-2.3 5-5s-2.3-5-5-5-5 2.3-5 5 2.3 5 5 5zm0 2c-3.3 0-10 1.7-10 5v2h20v-2c0-3.3-6.7-5-10-5z" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="agent-page__intro-title">在开始之前请先确认你的个人资料是否无误</h2>
|
||||
</div>
|
||||
|
||||
<!-- 说明文字 -->
|
||||
<p class="agent-page__intro-desc">
|
||||
请仔细对你的资料评估(包括个人信息、教育背景、工作履历及技能),确保资料齐全,让我能顺利为你开启求职之旅。
|
||||
</p>
|
||||
|
||||
<!-- 导入个人资料入口 -->
|
||||
<div class="agent-page__import-row">
|
||||
<div class="agent-page__import-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="agent-page__import-text">李华的个人资料</span>
|
||||
</div>
|
||||
|
||||
<!-- 确认并进入按钮 -->
|
||||
<button class="agent-page__confirm-btn" @click="handleNext">
|
||||
确认并进入
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:个人档案预览 -->
|
||||
<div class="agent-page__right">
|
||||
<div class="agent-page__profile-wrapper">
|
||||
<!-- 个人档案标题 -->
|
||||
<div class="agent-page__profile-title">个人档案</div>
|
||||
<!-- 引用 ProfilePageContent 组件 -->
|
||||
<ProfilePageContent
|
||||
:profile="profile"
|
||||
@edit="handleEdit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
import ProfilePageContent from '@/components/ProfilePageContent.vue'
|
||||
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
|
||||
|
||||
// ==================== 步骤导航数据 ====================
|
||||
|
||||
/** 求职助手准备阶段的5个步骤名称 */
|
||||
const steps = [
|
||||
'确认个人资料',
|
||||
'确认目标',
|
||||
'竞争力评估',
|
||||
'开启自动申请',
|
||||
'配置求职助手',
|
||||
]
|
||||
|
||||
/** 当前激活的步骤序号(1-5) */
|
||||
const currentStep = ref(1)
|
||||
|
||||
// ==================== 编辑抽屉状态 ====================
|
||||
|
||||
/** 编辑抽屉是否显示 */
|
||||
const showEditDrawer = ref(false)
|
||||
|
||||
/** 当前编辑的模块名称(info / education / work 等) */
|
||||
const editModule = ref('info')
|
||||
|
||||
/** 当前编辑模块的初始数据 */
|
||||
const editInitialData = ref<Record<string, any>>({})
|
||||
|
||||
// ==================== 个人档案数据 ====================
|
||||
|
||||
// ==================== 模拟数据 ====================
|
||||
const profile = ref({
|
||||
name: '李华',
|
||||
phone: '13600008888',
|
||||
email: '[email]',
|
||||
idNumber: '510201420040328',
|
||||
location: '北京',
|
||||
wechat: '15100001232',
|
||||
/** 教育经历 — 对应数据库 bg_user_profile_education */
|
||||
education: [
|
||||
{
|
||||
/** 学校名称 */
|
||||
school: '华南科技大学',
|
||||
/** 专业 */
|
||||
major: '会计学/工商管理',
|
||||
/** 学历类型(0=全日制 1=非全日制) */
|
||||
studyType: 0,
|
||||
/** 学历(1=大专 2=本科 3=硕士 4=博士) */
|
||||
degree: 2,
|
||||
/** 入学年份 */
|
||||
startYear: 2017,
|
||||
/** 毕业年份 */
|
||||
endYear: 2021,
|
||||
/** 描述段落,格式:[{id, text}] */
|
||||
description: [
|
||||
{ id: 'e1d1', text: 'GPA 3.8/4.0,主要课程包括金融工程、风险管理、量化投资等。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
school: '北京理工大学',
|
||||
major: '金融学/应用经济学',
|
||||
studyType: 0,
|
||||
degree: 3,
|
||||
startYear: 2021,
|
||||
endYear: 2024,
|
||||
description: [
|
||||
{ id: 'e2d1', text: 'GPA 3.7/4.0,研究方向为金融科技与量化交易。' },
|
||||
],
|
||||
},
|
||||
],
|
||||
/** 工作经历 — 对应数据库 bg_user_profile_work */
|
||||
works: [
|
||||
{
|
||||
/** 公司名称 */
|
||||
companyName: '华泰证券',
|
||||
/** 职位 */
|
||||
position: '投资经理',
|
||||
/** 开始时间 */
|
||||
startDate: '2024-07-01',
|
||||
/** 结束时间 */
|
||||
endDate: '',
|
||||
/** 描述段落,格式:[{id, text}] */
|
||||
description: [
|
||||
{ id: 'w1d1', text: '负责新能源行业投资研究,独立完成多份行业深度研究报告,为投资决策提供数据支持。' },
|
||||
{ id: 'w1d2', text: '管理投资组合,跟踪市场动态,定期输出投资策略建议和风险评估报告。' },
|
||||
],
|
||||
},
|
||||
],
|
||||
internships: [
|
||||
{
|
||||
/** 公司名称 */
|
||||
companyName: '中信证券',
|
||||
/** 职位 */
|
||||
position: '投资分析师实习生',
|
||||
/** 开始时间 */
|
||||
startDate: '2022-06-01',
|
||||
/** 结束时间 */
|
||||
endDate: '2022-09-30',
|
||||
/** 描述段落,格式:[{id, text}] */
|
||||
description: [
|
||||
{ id: 'i1d1', text: '参与编制行业研究报告,协助产品经理进行市场分析,完成多项行业深度研究报告。' },
|
||||
{ id: 'i1d2', text: '参与公司新产品的市场调研,独立完成3份产品市场调研报告,为新产品策略提供数据支持。' },
|
||||
{ id: 'i1d3', text: '参与团队投资研究项目的数据清洗工作,运用Python进行数据分析和可视化。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
companyName: '招商证券',
|
||||
position: '研究部实习生',
|
||||
startDate: '2022-01-01',
|
||||
endDate: '2022-05-31',
|
||||
description: [
|
||||
{ id: 'i2d1', text: '协助分析师研究TMT行业趋势,参与公司研究报告撰写,深入了解行业发展动态。' },
|
||||
{ id: 'i2d2', text: '参与公司研究部门的数据整理,负责维护和更新行业数据库及金融模型数据。' },
|
||||
{ id: 'i2d3', text: '参与量化模型的搭建与优化测试,加深了对研究分析、定量分析的理解和应用能力。' },
|
||||
],
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
/** 项目名称 */
|
||||
projectName: '中石化项目',
|
||||
/** 所属公司 */
|
||||
companyName: '中石化集团',
|
||||
/** 担任角色 */
|
||||
role: '数据分析师',
|
||||
/** 开始时间 */
|
||||
startDate: '2023-04-01',
|
||||
/** 结束时间 */
|
||||
endDate: '2023-09-30',
|
||||
/** 描述段落,格式:[{id, text}] */
|
||||
description: [
|
||||
{ id: 'p1d1', text: '参与石化行业数据分析研究,搭建行业数据监控系统,完善数据可视化展示方案。' },
|
||||
{ id: 'p1d2', text: '负责行业关键指标监测,数据驱动业务决策,协助完成3份产品研究报告。' },
|
||||
{ id: 'p1d3', text: '运用Python进行数据分析与可视化,加深了对行业研究、数据分析的理解。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
projectName: '综合运营',
|
||||
companyName: '',
|
||||
role: 'TMTV运营实习',
|
||||
startDate: '2022-07-01',
|
||||
endDate: '2022-12-31',
|
||||
description: [
|
||||
{ id: 'p2d1', text: '分析市场数据并进行TMT行业深度分析,参与产品运营策略制定,完善运营数据分析体系。' },
|
||||
{ id: 'p2d2', text: '负责公司新媒体平台内容策划,负责维护和更新行业数据及金融模型数据。' },
|
||||
{ id: 'p2d3', text: '参与运营团队的数据分析和报告撰写,进行了深度分析,深刻认识到行业趋势的重要性。' },
|
||||
],
|
||||
},
|
||||
],
|
||||
skills: ['CET 6', 'Photoshop', 'Python'],
|
||||
/** 竞赛经历 — 对应数据库 bg_user_profile_competition */
|
||||
competitions: [
|
||||
{
|
||||
/** 竞赛名称 */
|
||||
competitionName: '全国大学生创新创业大赛',
|
||||
/** 获奖情况 */
|
||||
award: '全国二等奖',
|
||||
/** 获奖时间 */
|
||||
awardDate: '2023.07.12',
|
||||
/** 描述段落,格式:[{id, text}] */
|
||||
description: [
|
||||
{ id: 'c1d1', text: '负责中央财经大学TMT产业研究,参与研究报告撰写,负责数据分析与模型搭建。参与公司新媒体平台内容策划,与公司媒体部门协作完成多个品牌营销方案。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
competitionName: '商业分析大赛',
|
||||
award: '全国一等奖',
|
||||
awardDate: '2023.07.12',
|
||||
description: [
|
||||
{ id: 'c2d1', text: '' },
|
||||
],
|
||||
},
|
||||
],
|
||||
certificates: ['CFA', 'CMA'],
|
||||
})
|
||||
|
||||
// ==================== 事件处理方法 ====================
|
||||
|
||||
/** 打开编辑抽屉 — 根据模块名提取对应的初始数据传给抽屉 */
|
||||
function handleEdit(section: string) {
|
||||
editModule.value = section
|
||||
|
||||
if (section === 'info') {
|
||||
// 基本信息
|
||||
editInitialData.value = {
|
||||
name: profile.value.name,
|
||||
email: profile.value.email,
|
||||
phone: profile.value.phone,
|
||||
location: profile.value.location,
|
||||
wechat: profile.value.wechat,
|
||||
}
|
||||
} else if (section === 'education') {
|
||||
// 教育经历 — 深拷贝 description 避免引用污染
|
||||
editInitialData.value = {
|
||||
education: profile.value.education.map(edu => ({
|
||||
school: edu.school,
|
||||
major: edu.major,
|
||||
studyType: edu.studyType,
|
||||
degree: edu.degree,
|
||||
startYear: edu.startYear,
|
||||
endYear: edu.endYear,
|
||||
description: edu.description.map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'work') {
|
||||
// 工作经历
|
||||
editInitialData.value = {
|
||||
works: profile.value.works.map(exp => ({
|
||||
companyName: exp.companyName,
|
||||
position: exp.position,
|
||||
startDate: exp.startDate,
|
||||
endDate: exp.endDate,
|
||||
description: exp.description.map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'internship') {
|
||||
// 实习经历
|
||||
editInitialData.value = {
|
||||
internships: (profile.value.internships || []).map(exp => ({
|
||||
companyName: exp.companyName,
|
||||
position: exp.position,
|
||||
startDate: exp.startDate,
|
||||
endDate: exp.endDate,
|
||||
description: exp.description.map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'project') {
|
||||
// 项目经历
|
||||
editInitialData.value = {
|
||||
projects: (profile.value.projects || []).map(proj => ({
|
||||
projectName: proj.projectName,
|
||||
companyName: proj.companyName,
|
||||
role: proj.role,
|
||||
startDate: proj.startDate,
|
||||
endDate: proj.endDate,
|
||||
description: proj.description.map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'competition') {
|
||||
// 竞赛经历
|
||||
editInitialData.value = {
|
||||
competitions: profile.value.competitions.map(comp => ({
|
||||
competitionName: comp.competitionName,
|
||||
award: comp.award,
|
||||
awardDate: comp.awardDate,
|
||||
description: comp.description.map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'skills') {
|
||||
// 技能
|
||||
editInitialData.value = {
|
||||
skills: [...profile.value.skills],
|
||||
}
|
||||
} else if (section === 'certificate') {
|
||||
// 证书
|
||||
editInitialData.value = {
|
||||
certificates: [...(profile.value.certificates || [])],
|
||||
}
|
||||
} else {
|
||||
editInitialData.value = {}
|
||||
}
|
||||
|
||||
showEditDrawer.value = true
|
||||
}
|
||||
|
||||
/** 保存编辑数据 — 将抽屉返回的数据按模块回写到 profile */
|
||||
function handleSaveEdit(data: Record<string, any>) {
|
||||
if (editModule.value === 'info') {
|
||||
// 基本信息 — 直接合并
|
||||
Object.assign(profile.value, data)
|
||||
} else if (editModule.value === 'education') {
|
||||
// 教育经历
|
||||
profile.value.education = data.education.map((edu: any) => ({
|
||||
school: edu.school,
|
||||
major: edu.major,
|
||||
studyType: edu.studyType,
|
||||
degree: edu.degree,
|
||||
startYear: edu.startYear,
|
||||
endYear: edu.endYear,
|
||||
description: edu.description.map((d: any) => ({ ...d })),
|
||||
}))
|
||||
} else if (editModule.value === 'work') {
|
||||
// 工作经历
|
||||
profile.value.works = data.works.map((work: any) => ({
|
||||
companyName: work.companyName,
|
||||
position: work.position,
|
||||
startDate: work.startDate,
|
||||
endDate: work.endDate,
|
||||
description: work.description.map((d: any) => ({ ...d })),
|
||||
}))
|
||||
} else if (editModule.value === 'internship') {
|
||||
// 实习经历
|
||||
profile.value.internships = data.internships.map((intern: any) => ({
|
||||
companyName: intern.companyName,
|
||||
position: intern.position,
|
||||
startDate: intern.startDate,
|
||||
endDate: intern.endDate,
|
||||
description: intern.description.map((d: any) => ({ ...d })),
|
||||
}))
|
||||
} else if (editModule.value === 'project') {
|
||||
// 项目经历
|
||||
profile.value.projects = data.projects.map((proj: any) => ({
|
||||
projectName: proj.projectName,
|
||||
companyName: proj.companyName,
|
||||
role: proj.role,
|
||||
startDate: proj.startDate,
|
||||
endDate: proj.endDate,
|
||||
description: proj.description.map((d: any) => ({ ...d })),
|
||||
}))
|
||||
} else if (editModule.value === 'competition') {
|
||||
// 竞赛经历
|
||||
profile.value.competitions = data.competitions.map((comp: any) => ({
|
||||
competitionName: comp.competitionName,
|
||||
award: comp.award,
|
||||
awardDate: comp.awardDate,
|
||||
description: comp.description.map((d: any) => ({ ...d })),
|
||||
}))
|
||||
} else if (editModule.value === 'skills') {
|
||||
// 技能
|
||||
profile.value.skills = [...data.skills]
|
||||
} else if (editModule.value === 'certificate') {
|
||||
// 证书
|
||||
profile.value.certificates = [...data.certificates]
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理"确认并进入"按钮点击 — 进入下一步 */
|
||||
function handleNext() {
|
||||
if (currentStep.value < steps.length) {
|
||||
currentStep.value++
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '../assets/styles/pages/agent';
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<h1>首页</h1>
|
||||
<el-button type="primary" size="large" @click="router.push('/jobs')">浏览职位</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 进入首页时加载公共工具数据(行业分类等)
|
||||
store.dispatch('loadCommonData')
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,546 @@
|
||||
<template>
|
||||
<div class="job-detail dflex">
|
||||
<SideNav />
|
||||
<div class="job-detail__content">
|
||||
<!-- 页面标题 + Tab 切换 -->
|
||||
<JobPageHeader :activeTab="''" />
|
||||
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="job-detail__toolbar">
|
||||
<button class="job-detail__close-btn" @click="goBack" aria-label="关闭">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__close-icon">
|
||||
<path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="job-detail__toolbar-right">
|
||||
<button class="job-detail__tool-btn" aria-label="编辑" @click="openDislikeDialog">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__tool-icon">
|
||||
<path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="jobs-page__action-icon-btn" :class="{ 'job-detail__tool-btn--liked': job.isFavorite }" aria-label="收藏">
|
||||
<svg viewBox="0 0 16 16" :fill="job.isFavorite ? 'currentColor' : 'none'" class="job-detail__tool-icon">
|
||||
<path d="M8 13.7l-1.1-1C3.6 9.8 1.5 7.9 1.5 5.7 1.5 3.9 2.9 2.5 4.7 2.5c1 0 2 .5 2.6 1.2h1.4c.6-.7 1.6-1.2 2.6-1.2 1.8 0 3.2 1.4 3.2 3.2 0 2.2-2.1 4.1-5.4 6.9L8 13.7z" stroke="currentColor" stroke-width="1"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="job-detail__apply-btn" @click="handleApply">去投递</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域(可滚动) -->
|
||||
<div class="job-detail__body">
|
||||
<!-- 导航 Tab:岗位详情 / 公司概况 -->
|
||||
<div class="job-detail__nav-tabs">
|
||||
<div
|
||||
class="job-detail__nav-tab"
|
||||
:class="{ 'job-detail__nav-tab--active': activeSection === 'job' }"
|
||||
@click="activeSection = 'job'"
|
||||
>岗位详情</div>
|
||||
<div
|
||||
class="job-detail__nav-tab"
|
||||
:class="{ 'job-detail__nav-tab--active': activeSection === 'company' }"
|
||||
@click="scrollToCompany"
|
||||
>公司概况</div>
|
||||
<div class="job-detail__nav-tab-right">
|
||||
<span class="job-detail__link-btn" @click="handleFeedback">问题反馈</span>
|
||||
<span class="job-detail__link-btn" @click="handleReport">原链接</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 岗位详情内容 -->
|
||||
<template v-if="activeSection === 'job'">
|
||||
<!-- 公司 & 职位头部 -->
|
||||
<div class="job-detail__card">
|
||||
<div class="job-detail__card-top">
|
||||
<div class="job-detail__card-left">
|
||||
<div class="job-detail__company-row">
|
||||
<div class="job-detail__company-icon">
|
||||
<img v-if="job.companyLogoUrl" :src="job.companyLogoUrl" :alt="job.company" class="job-detail__company-logo-img" />
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" class="job-detail__company-svg">
|
||||
<rect x="3" y="7" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M7 7V5a2 2 0 012-2h6a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M3 13h18" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="job-detail__company-name">{{ job.company }}</span>
|
||||
</div>
|
||||
<h3 class="job-detail__job-title">{{ job.title }}</h3>
|
||||
<div class="job-detail__job-meta">
|
||||
<span class="job-detail__meta-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-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>
|
||||
{{ job.location }}
|
||||
</span>
|
||||
<span class="job-detail__meta-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
|
||||
<rect x="2" y="3" width="12" height="11" rx="1.5" stroke="currentColor" stroke-width="1.2"/>
|
||||
<path d="M2 6.5h12" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
{{ job.experience }}
|
||||
</span>
|
||||
<span class="job-detail__meta-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
|
||||
<rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" stroke-width="1.2"/>
|
||||
<path d="M5 8h6M8 5v6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
{{ job.type }}
|
||||
</span>
|
||||
<span v-if="job.education" class="job-detail__meta-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
|
||||
<path d="M8 2L1 6l7 4 7-4-7-4z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<path d="M3 7.5v4l5 3 5-3v-4" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{{ job.education }}
|
||||
</span>
|
||||
<span v-if="job.salary" class="job-detail__meta-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
|
||||
<path d="M8 1v14M4 4h8M3 8h10M5 12h6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
{{ job.salary }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 匹配度环 -->
|
||||
<div class="job-detail__match-area">
|
||||
<div class="job-detail__match-ring">
|
||||
<svg viewBox="0 0 80 80" class="job-detail__ring-svg">
|
||||
<circle cx="40" cy="40" r="34" stroke-width="5" stroke="#E8E8E8" fill="none" opacity="0.3"/>
|
||||
<circle cx="40" cy="40" r="34" stroke-width="5" fill="none"
|
||||
stroke="#4FC2C9"
|
||||
stroke-linecap="round"
|
||||
:stroke-dasharray="2 * Math.PI * 34"
|
||||
:stroke-dashoffset="2 * Math.PI * 34 * (1 - job.matchScore / 100)"
|
||||
transform="rotate(-90 40 40)"
|
||||
/>
|
||||
</svg>
|
||||
<div class="job-detail__match-score">{{ job.matchScore }}%</div>
|
||||
</div>
|
||||
<div class="job-detail__match-label">岗位匹配值</div>
|
||||
<div class="job-detail__match-details">
|
||||
<div class="job-detail__match-item" v-for="m in matchItems" :key="m.label">
|
||||
<div class="job-detail__match-mini-ring">
|
||||
<svg viewBox="0 0 40 40" class="job-detail__mini-ring-svg">
|
||||
<circle cx="20" cy="20" r="16" stroke-width="3" stroke="#E8E8E8" fill="none" opacity="0.3"/>
|
||||
<circle cx="20" cy="20" r="16" stroke-width="3" fill="none"
|
||||
stroke="#BFBFBF"
|
||||
stroke-linecap="round"
|
||||
:stroke-dasharray="2 * Math.PI * 16"
|
||||
:stroke-dashoffset="2 * Math.PI * 16 * (1 - m.score / 100)"
|
||||
transform="rotate(-90 20 20)"
|
||||
/>
|
||||
</svg>
|
||||
<span class="job-detail__mini-score">{{ m.score }}%</span>
|
||||
</div>
|
||||
<span class="job-detail__match-item-label">{{ m.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 优化简历提示 -->
|
||||
<div class="job-detail__optimize-bar">
|
||||
<span>优化简历大幅提升面试成功率!</span>
|
||||
<button class="job-detail__optimize-btn" @click="handleGenerateResume">生成岗位专属简历</button>
|
||||
</div>
|
||||
|
||||
<!-- 岗位描述 -->
|
||||
<div class="job-detail__card">
|
||||
<p class="job-detail__desc-text">{{ job.companyInfo.summary }}</p>
|
||||
<div class="job-detail__tag-list">
|
||||
<span v-for="tag in job.tags" :key="tag" class="job-detail__tag">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 岗位职责 -->
|
||||
<div class="job-detail__card">
|
||||
<h3 class="job-detail__section-title">岗位职责</h3>
|
||||
<ol class="job-detail__list">
|
||||
<li v-for="(item, i) in job.responsibilities" :key="i">{{ item }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- 任职要求 -->
|
||||
<div class="job-detail__card">
|
||||
<div class="job-detail__section-header">
|
||||
<h3 class="job-detail__section-title">任职要求</h3>
|
||||
<span class="job-detail__skill-hint">
|
||||
<svg viewBox="0 0 14 14" fill="none" class="job-detail__hint-icon">
|
||||
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1"/>
|
||||
<path d="M7 6.5V10" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="4.5" r="0.5" fill="currentColor"/>
|
||||
</svg>
|
||||
查看您的技能与岗位要求的匹配情况
|
||||
</span>
|
||||
</div>
|
||||
<div class="job-detail__skill-tags">
|
||||
<span
|
||||
v-for="skill in job.requiredSkills"
|
||||
:key="skill.name"
|
||||
class="job-detail__skill-tag cursor-po"
|
||||
:class="{ 'job-detail__skill-tag--matched': skill.matched }"
|
||||
@click="skill.matched = !skill.matched"
|
||||
>
|
||||
{{ skill.name }}
|
||||
<svg v-if="skill.matched" viewBox="0 0 12 12" fill="none" class="job-detail__skill-close">
|
||||
<path d="M9 3L3 9M3 3l6 6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<ol class="job-detail__list">
|
||||
<li v-for="(item, i) in job.requirements" :key="i">{{ item }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- 加分项 -->
|
||||
<div class="job-detail__card">
|
||||
<h3 class="job-detail__section-title">加分项</h3>
|
||||
<p class="job-detail__desc-text">{{ job.bonus }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 公司概况 -->
|
||||
<div ref="companySectionRef" class="job-detail__card">
|
||||
<h3 class="job-detail__section-title">公司概况</h3>
|
||||
<div class="job-detail__company-info">
|
||||
<div class="job-detail__company-info-left">
|
||||
<div class="job-detail__company-header">
|
||||
<div class="job-detail__company-logo">
|
||||
<img v-if="job.companyLogoUrl" :src="job.companyLogoUrl" :alt="job.company" class="job-detail__company-logo-img" />
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" class="job-detail__company-svg">
|
||||
<rect x="3" y="7" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M7 7V5a2 2 0 012-2h6a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="job-detail__company-info-name">{{ job.company }}</span>
|
||||
</div>
|
||||
<p class="job-detail__company-desc">{{ job.companyInfo.description }}</p>
|
||||
</div>
|
||||
<div class="job-detail__company-info-right">
|
||||
<div class="job-detail__company-meta-item">
|
||||
<span class="job-detail__meta-label">成立时间:</span>
|
||||
<span>{{ job.companyInfo.founded }}</span>
|
||||
</div>
|
||||
<div class="job-detail__company-meta-item">
|
||||
<span class="job-detail__meta-label">公司地址:</span>
|
||||
<span>{{ job.companyInfo.address }}</span>
|
||||
</div>
|
||||
<div class="job-detail__company-meta-item">
|
||||
<span class="job-detail__meta-label">企业规模:</span>
|
||||
<span>{{ job.companyInfo.size }}</span>
|
||||
</div>
|
||||
<div class="job-detail__company-meta-item">
|
||||
<span class="job-detail__meta-label">官网:</span>
|
||||
<a :href="job.companyInfo.website" target="_blank" class="job-detail__company-link">{{ job.companyInfo.website }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 融资 -->
|
||||
<div class="job-detail__card">
|
||||
<h3 class="job-detail__section-title">融资</h3>
|
||||
<p class="job-detail__desc-text">
|
||||
<span class="job-detail__funding-label">当前融资阶段:</span>{{ job.companyInfo.fundingStage }}
|
||||
<span class="job-detail__funding-label" style="margin-left: 0.3rem;">最新估值:</span>{{ job.companyInfo.valuation }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 最新动态 -->
|
||||
<div class="job-detail__card" v-if="job.companyInfo.news.length">
|
||||
<h3 class="job-detail__section-title">最新动态</h3>
|
||||
<div class="job-detail__news-list">
|
||||
<div v-for="(news, i) in job.companyInfo.news" :key="i" class="job-detail__news-item">
|
||||
<p class="job-detail__news-desc">{{ news }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 公司概况 Tab 已移除,点击直接滚动到岗位详情中的公司概况部分 -->
|
||||
</div>
|
||||
</div>
|
||||
<AiChat />
|
||||
|
||||
<!-- 职位不感兴趣反馈弹窗 -->
|
||||
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="jobId" />
|
||||
|
||||
<!-- 职位问题反馈弹窗 -->
|
||||
<JobFeedbackDialog ref="feedbackDialogRef" v-model="showFeedbackDialog" :job-id="jobId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, nextTick, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
import AiChat from '@/components/AiChat.vue'
|
||||
import JobPageHeader from '@/components/JobPageHeader.vue'
|
||||
import JobDislikeDialog from '@/components/JobDislikeDialog.vue'
|
||||
import JobFeedbackDialog from '@/components/JobFeedbackDialog.vue'
|
||||
import { fetchJobDetail } from '@/api/jobs'
|
||||
import type { JobDetailData } from '@/api/jobs'
|
||||
|
||||
// ==================== 路由相关 ====================
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
/** 当前岗位 ID,从路由参数中获取 */
|
||||
const jobId = route.params.id as string
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
/** 工作类型映射:数字 → 中文 */
|
||||
function formatEmploymentType(type: number | undefined): string {
|
||||
const map: Record<number, string> = { 0: '全职', 1: '兼职' }
|
||||
return map[type ?? -1] ?? '未知'
|
||||
}
|
||||
|
||||
/** 学历要求映射:数字 → 中文 */
|
||||
function formatEducation(edu: number | undefined): string {
|
||||
const map: Record<number, string> = { 0: '不限', 1: '大专', 2: '本科', 3: '硕士', 4: '博士' }
|
||||
return map[edu ?? -1] ?? '未知'
|
||||
}
|
||||
|
||||
/** 最低工作年限格式化 */
|
||||
function formatExperience(min: number | undefined): string {
|
||||
if (min === undefined || min === null) return '经验不限'
|
||||
if (min === 0) return '经验不限'
|
||||
return `${min}年以上`
|
||||
}
|
||||
|
||||
/**
|
||||
* 将带序号的文本拆分为列表项
|
||||
* 例如 "1.xxx;2.yyy;3.zzz" → ['xxx', 'yyy', 'zzz']
|
||||
*/
|
||||
function splitNumberedText(text: string | undefined): string[] {
|
||||
if (!text) return []
|
||||
// 按 "数字." 或 "数字、" 分割,过滤空项
|
||||
const items = text.split(/\d+[.、]/).filter((s) => s.trim())
|
||||
return items.map((s) => s.trim().replace(/[;;]$/, ''))
|
||||
}
|
||||
|
||||
// ==================== 页面状态 ====================
|
||||
|
||||
/** 当前激活的内容区 Tab:job-岗位详情 / company-公司概况 */
|
||||
const activeSection = ref<'job' | 'company'>('job')
|
||||
|
||||
/** 公司概况卡片 ref,用于滚动定位 */
|
||||
const companySectionRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/** 是否正在加载 */
|
||||
const loading = ref(false)
|
||||
|
||||
/** 点击公司概况 tab 时,确保显示岗位详情内容并滚动到公司概况卡片 */
|
||||
function scrollToCompany() {
|
||||
activeSection.value = 'job'
|
||||
nextTick(() => {
|
||||
companySectionRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 岗位详情数据 ====================
|
||||
|
||||
/** 匹配度维度列表(岗位匹配环下方的子维度) */
|
||||
const matchItems = ref([
|
||||
{ label: '行业经验', score: 0 },
|
||||
{ label: '核心技能', score: 0 },
|
||||
{ label: '工作经历', score: 0 },
|
||||
])
|
||||
|
||||
/** 技能标签项(matched 表示用户是否具备该技能,可点击切换) */
|
||||
interface SkillTag {
|
||||
/** 技能名称 */
|
||||
name: string
|
||||
/** 是否匹配 */
|
||||
matched: boolean
|
||||
}
|
||||
|
||||
/** 岗位详情响应式数据 */
|
||||
const job = reactive({
|
||||
/** 岗位 ID */
|
||||
id: jobId,
|
||||
/** 公司名称(优先简称) */
|
||||
company: '',
|
||||
/** 公司 Logo URL */
|
||||
companyLogoUrl: '',
|
||||
/** 岗位标题 */
|
||||
title: '',
|
||||
/** 地区名称 */
|
||||
location: '',
|
||||
/** 工作经验要求(格式化后的文案) */
|
||||
experience: '',
|
||||
/** 工作类型(格式化后的文案) */
|
||||
type: '',
|
||||
/** 学历要求(格式化后的文案) */
|
||||
education: '',
|
||||
/** 薪资描述 */
|
||||
salary: '',
|
||||
/** 匹配总分 */
|
||||
matchScore: 0,
|
||||
/** 岗位描述(原始文本) */
|
||||
description: '',
|
||||
/** 岗位标签 */
|
||||
tags: [] as string[],
|
||||
/** 岗位职责列表(从 description 拆分) */
|
||||
responsibilities: [] as string[],
|
||||
/** 技能标签 */
|
||||
requiredSkills: [] as SkillTag[],
|
||||
/** 任职要求列表(从 requirement 拆分) */
|
||||
requirements: [] as string[],
|
||||
/** 加分项 */
|
||||
bonus: '',
|
||||
/** 来源链接 */
|
||||
sourceUrl: '',
|
||||
/** 是否已收藏 */
|
||||
isFavorite: false,
|
||||
/** 公司信息 */
|
||||
companyInfo: {
|
||||
/** 公司类型 */
|
||||
type: '',
|
||||
/** 公司所属行业 */
|
||||
industryName: '',
|
||||
/** 公司简介 */
|
||||
summary: '',
|
||||
/** 公司描述 */
|
||||
description: '',
|
||||
/** 成立时间 */
|
||||
founded: '',
|
||||
/** 公司地址 */
|
||||
address: '',
|
||||
/** 公司规模 */
|
||||
size: '',
|
||||
/** 公司官网 */
|
||||
website: '',
|
||||
/** 融资状态 */
|
||||
fundingStage: '',
|
||||
/** 最新估值 */
|
||||
valuation: '',
|
||||
/** 公司标签 */
|
||||
tags: [] as string[],
|
||||
/** 公司新闻列表 */
|
||||
news: [] as string[],
|
||||
},
|
||||
})
|
||||
|
||||
/** 将接口返回数据填充到页面响应式对象 */
|
||||
function fillJobData(data: JobDetailData) {
|
||||
job.id = data.jobId
|
||||
job.company = data.companyShortName || data.companyName || ''
|
||||
job.companyLogoUrl = data.companyLogoUrl || ''
|
||||
job.title = data.jobTitle || ''
|
||||
job.location = data.regionName || ''
|
||||
job.experience = formatExperience(data.minExperience)
|
||||
job.type = formatEmploymentType(data.employmentType)
|
||||
job.education = formatEducation(data.education)
|
||||
job.salary = data.salary || ''
|
||||
job.matchScore = data.matchScore ?? 0
|
||||
job.description = data.description || ''
|
||||
job.tags = data.tags || []
|
||||
job.sourceUrl = data.sourceUrl || ''
|
||||
job.isFavorite = data.isFavorite ?? false
|
||||
job.bonus = data.bonus || ''
|
||||
|
||||
// 岗位职责:从 description 中按序号拆分
|
||||
job.responsibilities = splitNumberedText(data.description)
|
||||
|
||||
// 任职要求:从 requirement 中按序号拆分
|
||||
job.requirements = splitNumberedText(data.requirement)
|
||||
|
||||
// 技能标签:默认 matched 为 false
|
||||
job.requiredSkills = (data.skillTags || []).map((name) => ({ name, matched: false }))
|
||||
|
||||
// 匹配度详情
|
||||
if (data.matchDetail) {
|
||||
matchItems.value = [
|
||||
{ label: '行业经验', score: data.matchDetail.industryScore ?? 0 },
|
||||
{ label: '核心技能', score: data.matchDetail.skillScore ?? 0 },
|
||||
{ label: '工作经历', score: data.matchDetail.experienceScore ?? 0 },
|
||||
]
|
||||
}
|
||||
|
||||
// 公司信息
|
||||
job.companyInfo.type = data.companyType || ''
|
||||
job.companyInfo.industryName = data.companyIndustryName || ''
|
||||
job.companyInfo.summary = data.companySummary || ''
|
||||
job.companyInfo.description = data.companyDescription || ''
|
||||
job.companyInfo.founded = data.companyFoundedYear ? `${data.companyFoundedYear}年` : ''
|
||||
job.companyInfo.address = data.companyAddress || ''
|
||||
job.companyInfo.size = data.companyScale || ''
|
||||
job.companyInfo.website = data.companyWebsite || ''
|
||||
job.companyInfo.fundingStage = data.companyFinancingStage || ''
|
||||
job.companyInfo.valuation = data.companyLatestValuation || ''
|
||||
job.companyInfo.tags = data.companyTags || []
|
||||
job.companyInfo.news = data.companyNews || []
|
||||
}
|
||||
|
||||
// ==================== 加载岗位详情 ====================
|
||||
|
||||
/** 调用接口获取岗位详情 */
|
||||
async function loadJobDetail() {
|
||||
if (!jobId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchJobDetail(jobId)
|
||||
if (res.code === '0' && res.data) {
|
||||
fillJobData(res.data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载岗位详情失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadJobDetail()
|
||||
})
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
/** 不感兴趣弹窗状态 */
|
||||
const showDislikeDialog = ref(false)
|
||||
const dislikeDialogRef = ref<InstanceType<typeof JobDislikeDialog> | null>(null)
|
||||
|
||||
/** 问题反馈弹窗状态 */
|
||||
const showFeedbackDialog = ref(false)
|
||||
const feedbackDialogRef = ref<InstanceType<typeof JobFeedbackDialog> | null>(null)
|
||||
|
||||
/** 打开不感兴趣弹窗 */
|
||||
function openDislikeDialog() {
|
||||
dislikeDialogRef.value?.resetForm()
|
||||
showDislikeDialog.value = true
|
||||
}
|
||||
|
||||
/** 返回职位列表页 */
|
||||
function goBack() {
|
||||
router.push('/jobs')
|
||||
}
|
||||
|
||||
/** 问题反馈 */
|
||||
function handleFeedback() {
|
||||
feedbackDialogRef.value?.resetForm()
|
||||
showFeedbackDialog.value = true
|
||||
}
|
||||
|
||||
/** 跳转到原链接 */
|
||||
function handleReport() {
|
||||
if (job.sourceUrl) {
|
||||
window.open(job.sourceUrl, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成岗位专属简历 */
|
||||
function handleGenerateResume() {
|
||||
console.log('生成岗位专属简历')
|
||||
}
|
||||
|
||||
/** 去投递 — 跳转到来源链接 */
|
||||
function handleApply() {
|
||||
if (job.sourceUrl) {
|
||||
window.open(job.sourceUrl, '_blank')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,553 @@
|
||||
<template>
|
||||
<div class="jobs-page dflex">
|
||||
<SideNav />
|
||||
<div class="jobs-page__content">
|
||||
<!-- 页面标题 + Tab 切换 -->
|
||||
<JobPageHeader v-model:activeTab="activeTab" />
|
||||
<!-- 筛选条件 -->
|
||||
<div class="jobs-page__filters-bar">
|
||||
<div class="jobs-page__filters">
|
||||
<div class="jobs-page__filter-group">
|
||||
<!-- 筛选条件按钮列表(行业单独用组件) -->
|
||||
<template v-for="filter in filters" :key="filter.label">
|
||||
<!-- 城市筛选:使用地区选择组件(选到市级) -->
|
||||
<RegionSelector
|
||||
v-if="filter.key === 'city'"
|
||||
:regionCodes="selectedRegionCodes"
|
||||
:level="2"
|
||||
:maxSelect="3"
|
||||
@update:regionCodes="onRegionChange"
|
||||
/>
|
||||
<!-- 行业筛选:使用行业选择组件 -->
|
||||
<IndustrySelector
|
||||
v-else-if="filter.key === 'industry'"
|
||||
:industryIds="selectedIndustryIds"
|
||||
:maxSelect="3"
|
||||
@update:industryIds="onIndustryChange"
|
||||
/>
|
||||
<!-- 岗位筛选:使用岗位选择组件 -->
|
||||
<JobCategorySelector
|
||||
v-else-if="filter.key === 'position'"
|
||||
:categoryIds="selectedCategoryIds"
|
||||
:maxSelect="3"
|
||||
@update:categoryIds="onCategoryChange"
|
||||
/>
|
||||
<!-- 其他筛选条件 -->
|
||||
<div
|
||||
v-else
|
||||
class="jobs-page__filter-item"
|
||||
@click="handleFilterClick(filter)"
|
||||
>
|
||||
<span>{{ filter.selected || filter.label }}</span>
|
||||
<svg class="jobs-page__filter-arrow-icon" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<!-- 工作类型下拉菜单 -->
|
||||
<div
|
||||
v-if="filter.key === 'jobType' && showJobTypeDropdown"
|
||||
class="jobs-page__filter-dropdown"
|
||||
@click.stop
|
||||
>
|
||||
<div
|
||||
class="jobs-page__filter-dropdown-item"
|
||||
v-for="option in jobTypeOptions"
|
||||
:key="option.value"
|
||||
:class="{ 'jobs-page__filter-dropdown-item--active': filter.selected === option.label }"
|
||||
@click.stop="selectJobType(filter, option)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="jobs-page__search-box">
|
||||
<svg class="jobs-page__search-svg" viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/>
|
||||
<path d="M11 11L14 14" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<input
|
||||
v-model="searchText"
|
||||
class="jobs-page__search-input"
|
||||
placeholder="搜索职位、公司或关键词"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 职位列表 -->
|
||||
<div ref="jobListRef" class="jobs-page__list pr5" @scroll="onListScroll">
|
||||
<div
|
||||
v-for="(job, index) in jobList"
|
||||
:key="index"
|
||||
class="jobs-page__job-card"
|
||||
:class="{ 'jobs-page__job-card--selected--none': selectedIndex === index }"
|
||||
@click="goToDetail(job)"
|
||||
>
|
||||
<div class="jobs-page__job-main">
|
||||
<!-- 左侧:公司图标 + 职位信息 -->
|
||||
<div class="jobs-page__job-left">
|
||||
<div class="jobs-page__job-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="jobs-page__company-svg">
|
||||
<rect x="3" y="7" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M7 7V5a2 2 0 012-2h6a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M3 13h18" stroke="currentColor" stroke-width="1.5"/>
|
||||
<rect x="10" y="11" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="jobs-page__job-info">
|
||||
<div class="jobs-page__job-title-row">
|
||||
<span class="jobs-page__job-name">{{ job.title }}</span>
|
||||
<button class="jobs-page__job-more" aria-label="更多操作" @click.stop="toggleMenu(index)">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" class="jobs-page__more-svg">
|
||||
<circle cx="3" cy="8" r="1.5"/><circle cx="8" cy="8" r="1.5"/><circle cx="13" cy="8" r="1.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="jobs-page__job-meta pt5">
|
||||
<span>{{ job.regionName }}</span>
|
||||
<span class="jobs-page__job-dot">·</span>
|
||||
<span>{{ job.companyShortName || job.companyName }}</span>
|
||||
<span class="jobs-page__job-dot">·</span>
|
||||
<span>{{ job.categoryName }}</span>
|
||||
</div>
|
||||
<!-- 提示信息 -->
|
||||
<div v-if="(job as any).tip" class="jobs-page__job-tip">
|
||||
<svg viewBox="0 0 14 14" fill="none" class="jobs-page__tip-svg">
|
||||
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1"/>
|
||||
<path d="M7 6.5V10" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="4.5" r="0.5" fill="currentColor"/>
|
||||
</svg>
|
||||
{{ (job as any).tip }}
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="jobs-page__job-actions pt20">
|
||||
<div class="dflex fgrow2 aliite-c flex-warp" >
|
||||
<!-- 标签 -->
|
||||
<div class="jobs-page__job-tags mt10">
|
||||
<span v-for="(tag, ti) in job.tags" :key="ti" class="jobs-page__job-tag">{{ tag }}</span>
|
||||
</div>
|
||||
<div class="dflex-end mt10">
|
||||
<div class="jobs-page__job-action-left">
|
||||
<button class="jobs-page__action-icon-btn" aria-label="不感兴趣" @click.stop="openDislikeDialog(job)">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="jobs-page__action-svg">
|
||||
<path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="jobs-page__action-icon-btn" :class="{ 'jobs-page__action-icon-btn--liked': job.isFavorite }" aria-label="收藏">
|
||||
<svg viewBox="0 0 16 16" :fill="job.isFavorite ? 'currentColor' : 'none'" class="jobs-page__action-svg">
|
||||
<path d="M8 13.7l-1.1-1C3.6 9.8 1.5 7.9 1.5 5.7 1.5 3.9 2.9 2.5 4.7 2.5c1 0 2 .5 2.6 1.2h1.4c.6-.7 1.6-1.2 2.6-1.2 1.8 0 3.2 1.4 3.2 3.2 0 2.2-2.1 4.1-5.4 6.9L8 13.7z" stroke="currentColor" stroke-width="1"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="jobs-page__job-action-right ">
|
||||
<button class="jobs-page__job-helper">
|
||||
<svg viewBox="0 0 14 14" fill="none" class="jobs-page__helper-svg">
|
||||
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1"/>
|
||||
<path d="M5.5 5.5a1.5 1.5 0 113 0c0 .8-.7 1-1.5 1.5V9" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="11" r="0.5" fill="currentColor"/>
|
||||
</svg>
|
||||
问助手
|
||||
</button>
|
||||
<button class="jobs-page__job-apply-btn" :class="{ 'jobs-page__job-apply-btn--active': job.applied }">
|
||||
自动投递
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 弹出菜单 -->
|
||||
<div v-if="job.showMenu" class="jobs-page__job-popup" @click.stop>
|
||||
<div
|
||||
class="jobs-page__job-popup-item"
|
||||
v-for="action in popupActions"
|
||||
:key="action"
|
||||
@click="handlePopupAction(action, job)"
|
||||
>{{ action }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧:匹配度 -->
|
||||
<div class="jobs-page__job-match" :class="matchClass(job.matchScore)">
|
||||
<div class="jobs-page__match-ring">
|
||||
<svg viewBox="0 0 80 80" class="jobs-page__ring-svg">
|
||||
<circle cx="40" cy="40" r="34" stroke-width="5" stroke="#E8E8E8" fill="none" opacity="0.3"/>
|
||||
<circle cx="40" cy="40" r="34" stroke-width="5" fill="none"
|
||||
:stroke="job.matchScore >= 80 ? '#4FC2C9' : '#BFBFBF'"
|
||||
stroke-linecap="round"
|
||||
:stroke-dasharray="2 * Math.PI * 34"
|
||||
:stroke-dashoffset="2 * Math.PI * 34 * (1 - job.matchScore / 100)"
|
||||
transform="rotate(-90 40 40)"
|
||||
/>
|
||||
</svg>
|
||||
<div class="jobs-page__match-score">{{ job.matchScore }}%</div>
|
||||
</div>
|
||||
<div class="jobs-page__match-label">匹配值</div>
|
||||
<div class="jobs-page__match-level">{{ matchLevelText(job.matchScore) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<AiChat />
|
||||
|
||||
<!-- 职位不感兴趣反馈弹窗 -->
|
||||
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="dislikeJobId" />
|
||||
|
||||
<!-- 职位问题反馈弹窗 -->
|
||||
<JobFeedbackDialog ref="feedbackDialogRef" v-model="showFeedbackDialog" :job-id="feedbackJobId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onBeforeUnmount, nextTick, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
import AiChat from '@/components/AiChat.vue'
|
||||
import JobPageHeader from '@/components/JobPageHeader.vue'
|
||||
import JobDislikeDialog from '@/components/JobDislikeDialog.vue'
|
||||
import JobFeedbackDialog from '@/components/JobFeedbackDialog.vue'
|
||||
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
||||
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
||||
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||||
import { fetchJobList } from '@/api/jobs'
|
||||
import type { JobListItem, JobListParams } from '@/api/jobs'
|
||||
|
||||
// ==================== 路由相关 ====================
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
|
||||
// ==================== 页面状态 ====================
|
||||
|
||||
/** 当前激活的 Tab,从 URL query 参数读取,默认"推荐" */
|
||||
const activeTab = ref((route.query.tab as string) || 'recommend')
|
||||
|
||||
/** 搜索框输入内容 */
|
||||
const searchText = ref('')
|
||||
|
||||
/** 当前选中的职位卡片索引 */
|
||||
const selectedIndex = ref(0)
|
||||
|
||||
/** 不感兴趣弹窗的显示状态 */
|
||||
const showDislikeDialog = ref(false)
|
||||
|
||||
/** 当前操作的职位 ID(用于提交不感兴趣反馈) */
|
||||
const dislikeJobId = ref<string | null>(null)
|
||||
|
||||
/** 问题反馈弹窗的显示状态 */
|
||||
const showFeedbackDialog = ref(false)
|
||||
|
||||
/** 当前操作的职位 ID(用于提交问题反馈) */
|
||||
const feedbackJobId = ref<string | null>(null)
|
||||
|
||||
// ==================== 常量数据 ====================
|
||||
|
||||
/** 筛选条件项类型 */
|
||||
interface FilterItem {
|
||||
label: string
|
||||
key: string
|
||||
selected: string
|
||||
}
|
||||
|
||||
/** 筛选条件列表 */
|
||||
const filters = ref<FilterItem[]>([
|
||||
{ label: '城市', key: 'city', selected: '' },
|
||||
{ label: '岗位', key: 'position', selected: '' },
|
||||
{ label: '行业', key: 'industry', selected: '' },
|
||||
{ label: '工作类型', key: 'jobType', selected: '' },
|
||||
])
|
||||
|
||||
/** 工作类型选项映射:label → 接口参数 employmentType(0=全职 1=实习) */
|
||||
const jobTypeOptions: { label: string; value: number }[] = [
|
||||
{ label: '全职', value: 0 },
|
||||
{ label: '实习', value: 1 },
|
||||
]
|
||||
|
||||
/** 工作类型下拉菜单是否显示 */
|
||||
const showJobTypeDropdown = ref(false)
|
||||
|
||||
/** 当前选中的工作类型(integer,对应接口参数 employmentType,null 表示未选) */
|
||||
const selectedEmploymentType = ref<number | null>(null)
|
||||
|
||||
/** 选中的行业 id 数组(integer,对应接口参数 industryIds) */
|
||||
const selectedIndustryIds = ref<number[]>([])
|
||||
|
||||
/** 选中的岗位 id 数组(integer,对应接口参数 categoryIds) */
|
||||
const selectedCategoryIds = ref<number[]>([])
|
||||
|
||||
/** 选中的地区编码数组(string,对应接口参数 regionCodes) */
|
||||
const selectedRegionCodes = ref<string[]>([])
|
||||
|
||||
/** 行业选择变更回调 */
|
||||
function onIndustryChange(ids: number[]) {
|
||||
selectedIndustryIds.value = ids
|
||||
}
|
||||
|
||||
/** 岗位选择变更回调 */
|
||||
function onCategoryChange(ids: number[]) {
|
||||
selectedCategoryIds.value = ids
|
||||
}
|
||||
|
||||
/** 地区选择变更回调 */
|
||||
function onRegionChange(codes: string[]) {
|
||||
selectedRegionCodes.value = codes
|
||||
}
|
||||
|
||||
/** 点击筛选条件按钮 — 仅工作类型展开下拉 */
|
||||
function handleFilterClick(filter: FilterItem) {
|
||||
if (filter.key === 'jobType') {
|
||||
showJobTypeDropdown.value = !showJobTypeDropdown.value
|
||||
}
|
||||
}
|
||||
|
||||
/** 选中工作类型选项 */
|
||||
function selectJobType(filter: FilterItem, option: { label: string; value: number }) {
|
||||
filter.selected = option.label
|
||||
selectedEmploymentType.value = option.value
|
||||
showJobTypeDropdown.value = false
|
||||
}
|
||||
|
||||
/** 点击页面其他区域时关闭下拉菜单 */
|
||||
function closeDropdownOnClickOutside(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.closest('.jobs-page__filter-item')) {
|
||||
showJobTypeDropdown.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeDropdownOnClickOutside)
|
||||
// 进入 Jobs 页时加载公共工具数据(行业分类等)
|
||||
store.dispatch('loadCommonData')
|
||||
|
||||
// 尝试从缓存恢复列表数据(从详情页返回时不重新请求)
|
||||
const cache = store.state.jobListCache
|
||||
if (cache && cache.list.length > 0) {
|
||||
jobList.value = cache.list
|
||||
pageNum.value = cache.pageNum
|
||||
total.value = cache.total
|
||||
savedScrollTop = cache.scrollTop
|
||||
// 清除缓存,避免下次非详情页返回时误用
|
||||
store.commit('SET_JOB_LIST_CACHE', null)
|
||||
// 等 DOM 渲染完成后恢复滚动位置
|
||||
nextTick(() => {
|
||||
if (jobListRef.value) {
|
||||
jobListRef.value.scrollTop = savedScrollTop
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 没有缓存,正常加载
|
||||
loadJobList()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', closeDropdownOnClickOutside)
|
||||
})
|
||||
|
||||
/** 弹出菜单操作项 */
|
||||
const popupActions = ['从列表中移除', '已投递', '复制链接', '问题反馈']
|
||||
|
||||
// ==================== 职位列表项(扩展接口字段,增加前端交互状态) ====================
|
||||
|
||||
/** 职位列表项类型(在接口返回基础上扩展前端交互字段) */
|
||||
interface JobItem extends JobListItem {
|
||||
/** 是否已投递 */
|
||||
applied: boolean
|
||||
/** 是否显示弹出菜单 */
|
||||
showMenu: boolean
|
||||
}
|
||||
|
||||
// ==================== 分页与加载状态 ====================
|
||||
|
||||
/** 当前页码 */
|
||||
const pageNum = ref(1)
|
||||
|
||||
/** 每页条数 */
|
||||
const pageSize = ref(15)
|
||||
|
||||
/** 总记录数 */
|
||||
const total = ref(0)
|
||||
|
||||
/** 是否正在加载(首次加载) */
|
||||
const loading = ref(false)
|
||||
|
||||
/** 是否正在加载下一页 */
|
||||
const loadingMore = ref(false)
|
||||
|
||||
/** 是否已加载全部数据 */
|
||||
const noMore = computed(() => jobList.value.length >= total.value && total.value > 0)
|
||||
|
||||
/** 职位列表数据 */
|
||||
const jobList = ref<JobItem[]>([])
|
||||
|
||||
/** 列表容器 ref,用于监听滚动和恢复滚动位置 */
|
||||
const jobListRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/** 记录离开页面前的滚动位置 */
|
||||
let savedScrollTop = 0
|
||||
// ==================== 加载岗位列表 ====================
|
||||
|
||||
/** 组装请求参数 */
|
||||
function buildParams(): JobListParams {
|
||||
const params: JobListParams = {
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize.value,
|
||||
}
|
||||
// 地区筛选
|
||||
if (selectedRegionCodes.value.length) {
|
||||
params.regionCodes = selectedRegionCodes.value
|
||||
}
|
||||
// 岗位类型筛选
|
||||
if (selectedCategoryIds.value.length) {
|
||||
params.categoryIds = selectedCategoryIds.value
|
||||
}
|
||||
// 行业筛选
|
||||
if (selectedIndustryIds.value.length) {
|
||||
params.industryIds = selectedIndustryIds.value
|
||||
}
|
||||
// 工作类型筛选
|
||||
if (selectedEmploymentType.value !== null) {
|
||||
params.employmentType = selectedEmploymentType.value
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
/** 加载岗位列表数据(首次加载,替换列表) */
|
||||
async function loadJobList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchJobList(buildParams())
|
||||
if (res.code === '0' && res.data) {
|
||||
// 将接口数据映射为前端 JobItem,补充交互状态字段
|
||||
jobList.value = res.data.list.map((item) => ({
|
||||
...item,
|
||||
applied: false,
|
||||
showMenu: false,
|
||||
}))
|
||||
total.value = Number(res.data.total)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载岗位列表失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载下一页数据(追加到列表末尾) */
|
||||
async function loadNextPage() {
|
||||
if (loadingMore.value || noMore.value) return
|
||||
loadingMore.value = true
|
||||
pageNum.value++
|
||||
try {
|
||||
const res = await fetchJobList(buildParams())
|
||||
if (res.code === '0' && res.data) {
|
||||
const newItems = res.data.list.map((item) => ({
|
||||
...item,
|
||||
applied: false,
|
||||
showMenu: false,
|
||||
}))
|
||||
jobList.value.push(...newItems)
|
||||
total.value = Number(res.data.total)
|
||||
}
|
||||
} catch (e) {
|
||||
// 加载失败时回退页码
|
||||
pageNum.value--
|
||||
console.error('加载下一页失败', e)
|
||||
} finally {
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 列表滚动事件 — 滚动到底部时自动加载下一页 */
|
||||
function onListScroll() {
|
||||
const el = jobListRef.value
|
||||
if (!el) return
|
||||
// 距离底部小于 100px 时触发加载
|
||||
const threshold = 100
|
||||
if (el.scrollHeight - el.scrollTop - el.clientHeight < threshold) {
|
||||
loadNextPage()
|
||||
}
|
||||
}
|
||||
|
||||
/** 筛选条件变化时重置到第一页并重新加载 */
|
||||
function reloadFirstPage() {
|
||||
pageNum.value = 1
|
||||
// 筛选条件变化时清除缓存
|
||||
store.commit('SET_JOB_LIST_CACHE', null)
|
||||
loadJobList()
|
||||
}
|
||||
|
||||
// 监听筛选条件变化,自动重新加载
|
||||
watch(
|
||||
[selectedRegionCodes, selectedCategoryIds, selectedIndustryIds, selectedEmploymentType],
|
||||
() => reloadFirstPage(),
|
||||
)
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
/** 切换职位卡片的弹出菜单(同时关闭其他已打开的菜单) */
|
||||
function toggleMenu(index: number) {
|
||||
jobList.value.forEach((j, i) => {
|
||||
j.showMenu = i === index ? !j.showMenu : false
|
||||
})
|
||||
}
|
||||
|
||||
/** 处理弹出菜单操作项点击 — 问题反馈打开反馈弹窗,其他直接关闭菜单 */
|
||||
function handlePopupAction(action: string, job: JobItem) {
|
||||
job.showMenu = false
|
||||
if (action === '问题反馈') {
|
||||
openFeedbackDialog(job)
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据匹配分数返回对应的 CSS 类名 */
|
||||
function matchClass(score: number) {
|
||||
return score >= 80 ? 'jobs-page__job-match--high' : 'jobs-page__job-match--low'
|
||||
}
|
||||
|
||||
/** 根据匹配分数返回匹配等级文案 */
|
||||
function matchLevelText(score: number) {
|
||||
if (score >= 80) return '匹配'
|
||||
if (score >= 60) return '一般匹配'
|
||||
return '匹配度偏低'
|
||||
}
|
||||
|
||||
/** 跳转到岗位详情页 — 先缓存当前列表和滚动位置 */
|
||||
function goToDetail(job: JobItem) {
|
||||
// 保存当前列表状态到 store,返回时恢复
|
||||
store.commit('SET_JOB_LIST_CACHE', {
|
||||
list: jobList.value,
|
||||
pageNum: pageNum.value,
|
||||
total: total.value,
|
||||
scrollTop: jobListRef.value?.scrollTop ?? 0,
|
||||
})
|
||||
router.push(`/jobs/${job.id}`)
|
||||
}
|
||||
|
||||
/** 不感兴趣弹窗和问题反馈弹窗的组件引用 */
|
||||
const dislikeDialogRef = ref<InstanceType<typeof JobDislikeDialog> | null>(null)
|
||||
const feedbackDialogRef = ref<InstanceType<typeof JobFeedbackDialog> | null>(null)
|
||||
|
||||
/** 打开不感兴趣弹窗 — 记录当前职位 ID 并重置表单 */
|
||||
function openDislikeDialog(job: JobItem) {
|
||||
dislikeJobId.value = job.id
|
||||
dislikeDialogRef.value?.resetForm()
|
||||
showDislikeDialog.value = true
|
||||
}
|
||||
|
||||
/** 打开问题反馈弹窗 — 记录当前职位 ID 并重置表单 */
|
||||
function openFeedbackDialog(job: JobItem) {
|
||||
feedbackJobId.value = job.id
|
||||
feedbackDialogRef.value?.resetForm()
|
||||
showFeedbackDialog.value = true
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,579 @@
|
||||
<template>
|
||||
<div class="profile-page dflex">
|
||||
<SideNav />
|
||||
<div class="profile-page__content">
|
||||
<!-- 个人资料编辑抽屉 -->
|
||||
<ProfileEditDrawer
|
||||
v-model="showEditDrawer"
|
||||
:module="editModule"
|
||||
:initial-data="editInitialData"
|
||||
@save="handleSaveEdit"
|
||||
/>
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<div class="profile-page__header">
|
||||
<h2 class="profile-page__title">个人资料 <span class="profile-page__title-tip">ⓘ</span></h2>
|
||||
<p class="profile-page__subtitle">
|
||||
设定个人资料以匹配岗位需求,包括教育背景、实习经历等,大部分信息可以从简历中自动获取,也可以手动修改。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 主体区域:左侧详情 + 右侧入口 -->
|
||||
<div class="profile-page__body">
|
||||
<!-- 个人资料内容组件 -->
|
||||
<ProfilePageContent
|
||||
:profile="profile"
|
||||
@edit="handleEdit"
|
||||
/>
|
||||
|
||||
<!-- 右侧入口 -->
|
||||
<div class="profile-page__sidebar">
|
||||
<!-- 提示卡片 -->
|
||||
<div class="profile-page__tip-card">
|
||||
<p class="profile-page__tip-text">
|
||||
非常棒!你的个人资料已经被完善了,自动填写信息也已变换,现在可以轻松开始投递简历啦!
|
||||
</p>
|
||||
<button class="profile-page__tip-btn" @click="goToJobs">去投递</button>
|
||||
</div>
|
||||
|
||||
<!-- 管理简历入口 -->
|
||||
<div class="profile-page__nav-entry" @click="goToResume">
|
||||
<span class="profile-page__nav-entry-icon">📄</span>
|
||||
<span class="profile-page__nav-entry-text">管理我的简历</span>
|
||||
<svg viewBox="0 0 16 16" fill="none" class="profile-page__nav-arrow">
|
||||
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
|
||||
import ProfilePageContent from '@/components/ProfilePageContent.vue'
|
||||
import { saveProfile, fetchProfile, fetchEducation, saveEducation, fetchWork, saveWork, fetchInternship, saveInternship, fetchProject, saveProject, fetchCompetition, saveCompetition } from '@/api/profile'
|
||||
import type { SaveEducationItem, SaveWorkItem, SaveProjectItem, SaveCompetitionItem } from '@/api/profile'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
/** 页面挂载时加载公共分类数据和个人资料 */
|
||||
onMounted(async () => {
|
||||
if (!store.state.regions.length) {
|
||||
store.dispatch('loadCommonData')
|
||||
}
|
||||
// 请求个人资料主表数据,填充非数组字段
|
||||
await loadProfile()
|
||||
// 请求教育经历数据
|
||||
await loadEducation()
|
||||
// 请求工作经历数据
|
||||
await loadWork()
|
||||
// 请求实习经历数据
|
||||
await loadInternship()
|
||||
// 请求项目经历数据
|
||||
await loadProject()
|
||||
// 请求竞赛经历数据
|
||||
await loadCompetition()
|
||||
})
|
||||
|
||||
/** 加载个人资料主表数据 */
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const res = await fetchProfile()
|
||||
if (res.code === '0' && res.data) {
|
||||
const d = res.data
|
||||
profile.value.name = d.name || ''
|
||||
profile.value.phone = d.mobileNumber || ''
|
||||
profile.value.email = d.email || ''
|
||||
profile.value.idNumber = d.idCard || ''
|
||||
profile.value.regionCode = d.regionCode || ''
|
||||
profile.value.wechat = d.wechatNumber || ''
|
||||
profile.value.skills = d.skills || []
|
||||
profile.value.certificates = d.certificates || []
|
||||
}
|
||||
} catch {
|
||||
console.error('[Profile] 加载个人资料失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载教育经历数据 */
|
||||
async function loadEducation() {
|
||||
try {
|
||||
const res = await fetchEducation()
|
||||
if (res.code === '0' && res.data) {
|
||||
profile.value.education = res.data.map(item => ({
|
||||
school: item.school || '',
|
||||
major: item.major || '',
|
||||
studyType: item.studyType ?? 0,
|
||||
degree: item.degree ?? 2,
|
||||
startDate: item.startDate || '',
|
||||
endDate: item.endDate || '',
|
||||
description: (item.description || []).map(d => ({
|
||||
id: d.id || '',
|
||||
text: d.text || '',
|
||||
})),
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
console.error('[Profile] 加载教育经历失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载工作经历数据 */
|
||||
async function loadWork() {
|
||||
try {
|
||||
const res = await fetchWork()
|
||||
if (res.code === '0' && res.data) {
|
||||
profile.value.works = res.data.map(item => ({
|
||||
companyName: item.companyName || '',
|
||||
position: item.position || '',
|
||||
startDate: item.startDate || '',
|
||||
endDate: item.endDate || '',
|
||||
description: (item.description || []).map(d => ({
|
||||
id: d.id || '',
|
||||
text: d.text || '',
|
||||
})),
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
console.error('[Profile] 加载工作经历失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载实习经历数据 */
|
||||
async function loadInternship() {
|
||||
try {
|
||||
const res = await fetchInternship()
|
||||
if (res.code === '0' && res.data) {
|
||||
profile.value.internships = res.data.map(item => ({
|
||||
companyName: item.companyName || '',
|
||||
position: item.position || '',
|
||||
startDate: item.startDate || '',
|
||||
endDate: item.endDate || '',
|
||||
description: (item.description || []).map(d => ({
|
||||
id: d.id || '',
|
||||
text: d.text || '',
|
||||
})),
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
console.error('[Profile] 加载实习经历失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载项目经历数据 */
|
||||
async function loadProject() {
|
||||
try {
|
||||
const res = await fetchProject()
|
||||
if (res.code === '0' && res.data) {
|
||||
profile.value.projects = res.data.map(item => ({
|
||||
projectName: item.projectName || '',
|
||||
companyName: item.companyName || '',
|
||||
role: item.role || '',
|
||||
startDate: item.startDate || '',
|
||||
endDate: item.endDate || '',
|
||||
description: (item.description || []).map(d => ({
|
||||
id: d.id || '',
|
||||
text: d.text || '',
|
||||
})),
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
console.error('[Profile] 加载项目经历失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载竞赛经历数据 */
|
||||
async function loadCompetition() {
|
||||
try {
|
||||
const res = await fetchCompetition()
|
||||
if (res.code === '0' && res.data) {
|
||||
profile.value.competitions = res.data.map(item => ({
|
||||
competitionName: item.competitionName || '',
|
||||
award: item.award || '',
|
||||
awardDate: item.awardDate || '',
|
||||
description: (item.description || []).map(d => ({
|
||||
id: d.id || '',
|
||||
text: d.text || '',
|
||||
})),
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
console.error('[Profile] 加载竞赛经历失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑抽屉的显示状态 */
|
||||
const showEditDrawer = ref(false)
|
||||
|
||||
/** 当前编辑的模块名称 */
|
||||
const editModule = ref('info')
|
||||
|
||||
/** 当前编辑模块的初始数据 */
|
||||
const editInitialData = ref<Record<string, any>>({})
|
||||
|
||||
/** 保存中的加载状态 */
|
||||
const saving = ref(false)
|
||||
|
||||
// ==================== 个人资料数据(非数组字段由接口填充,数组字段暂用模拟数据) ====================
|
||||
const profile = ref({
|
||||
/** 真实姓名 — 接口字段 name */
|
||||
name: '',
|
||||
/** 手机号码 — 接口字段 mobileNumber */
|
||||
phone: '',
|
||||
/** 邮箱 — 接口字段 email */
|
||||
email: '',
|
||||
/** 身份证号 — 接口字段 idCard */
|
||||
idNumber: '',
|
||||
/** 所在城市编码 — 接口字段 regionCode */
|
||||
regionCode: '',
|
||||
/** 微信号 — 接口字段 wechatNumber */
|
||||
wechat: '',
|
||||
/** 技能标签列表 — 接口字段 skills */
|
||||
skills: [] as string[],
|
||||
/** 证书标签列表 — 接口字段 certificates */
|
||||
certificates: [] as string[],
|
||||
/** 教育经历 — 对应数据库 bg_user_profile_education */
|
||||
education: [] as Array<{
|
||||
school: string
|
||||
major: string
|
||||
studyType: number
|
||||
degree: number
|
||||
startDate: string
|
||||
endDate: string
|
||||
description: Array<{ id: string; text: string }>
|
||||
}>,
|
||||
/** 工作经历 — 对应数据库 bg_user_profile_work */
|
||||
works: [] as Array<{
|
||||
companyName: string
|
||||
position: string
|
||||
startDate: string
|
||||
endDate: string
|
||||
description: Array<{ id: string; text: string }>
|
||||
}>,
|
||||
internships: [] as Array<{
|
||||
companyName: string
|
||||
position: string
|
||||
startDate: string
|
||||
endDate: string
|
||||
description: Array<{ id: string; text: string }>
|
||||
}>,
|
||||
projects: [] as Array<{
|
||||
projectName: string
|
||||
companyName: string
|
||||
role: string
|
||||
startDate: string
|
||||
endDate: string
|
||||
description: Array<{ id: string; text: string }>
|
||||
}>,
|
||||
/** 竞赛经历 — 对应数据库 bg_user_profile_competition */
|
||||
competitions: [] as Array<{
|
||||
competitionName: string
|
||||
award: string
|
||||
awardDate: string
|
||||
description: Array<{ id: string; text: string }>
|
||||
}>,
|
||||
})
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
/** 打开编辑抽屉 — 根据模块名设置初始数据 */
|
||||
function handleEdit(section: string) {
|
||||
editModule.value = section
|
||||
|
||||
// 根据模块名提取对应的初始数据
|
||||
if (section === 'info') {
|
||||
editInitialData.value = {
|
||||
name: profile.value.name,
|
||||
email: profile.value.email,
|
||||
phone: profile.value.phone,
|
||||
/** 城市编码 — 传给编辑抽屉的 RegionSelector */
|
||||
location: profile.value.regionCode,
|
||||
wechat: profile.value.wechat,
|
||||
}
|
||||
} else if (section === 'education') {
|
||||
editInitialData.value = {
|
||||
education: profile.value.education.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: profile.value.works.map(exp => ({
|
||||
companyName: exp.companyName,
|
||||
position: exp.position,
|
||||
startDate: exp.startDate,
|
||||
endDate: exp.endDate,
|
||||
description: exp.description.map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'internship') {
|
||||
editInitialData.value = {
|
||||
internships: profile.value.internships.map(exp => ({
|
||||
companyName: exp.companyName,
|
||||
position: exp.position,
|
||||
startDate: exp.startDate,
|
||||
endDate: exp.endDate,
|
||||
description: exp.description.map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'project') {
|
||||
editInitialData.value = {
|
||||
projects: profile.value.projects.map(proj => ({
|
||||
projectName: proj.projectName,
|
||||
companyName: proj.companyName,
|
||||
role: proj.role,
|
||||
startDate: proj.startDate,
|
||||
endDate: proj.endDate,
|
||||
description: proj.description.map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'competition') {
|
||||
editInitialData.value = {
|
||||
competitions: profile.value.competitions.map(comp => ({
|
||||
competitionName: comp.competitionName,
|
||||
award: comp.award,
|
||||
awardDate: comp.awardDate,
|
||||
description: comp.description.map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'skills') {
|
||||
editInitialData.value = {
|
||||
skills: [...profile.value.skills],
|
||||
}
|
||||
} else if (section === 'certificate') {
|
||||
editInitialData.value = {
|
||||
certificates: [...profile.value.certificates],
|
||||
}
|
||||
} else {
|
||||
editInitialData.value = {}
|
||||
}
|
||||
|
||||
showEditDrawer.value = true
|
||||
}
|
||||
|
||||
/** 保存编辑数据 — 将抽屉返回的数据合并到 profile,并调用对应接口持久化 */
|
||||
async function handleSaveEdit(data: Record<string, any>) {
|
||||
if (editModule.value === 'info') {
|
||||
// ---- 基本信息:调用主表接口保存 ----
|
||||
try {
|
||||
saving.value = true
|
||||
await saveProfile({
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
mobileNumber: data.phone,
|
||||
regionCode: data.location,
|
||||
wechatNumber: data.wechat,
|
||||
})
|
||||
// 接口成功后更新本地数据
|
||||
profile.value.name = data.name
|
||||
profile.value.email = data.email
|
||||
profile.value.phone = data.phone
|
||||
profile.value.wechat = data.wechat
|
||||
/** 将城市编码存入 regionCode,先直接显示编码,后续联调获取接口再做名称转换 */
|
||||
profile.value.regionCode = data.location || ''
|
||||
ElMessage.success('个人信息保存成功')
|
||||
} catch {
|
||||
ElMessage.error('个人信息保存失败,请重试')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
} else if (editModule.value === 'education') {
|
||||
// ---- 教育经历:调用教育经历接口保存 ----
|
||||
try {
|
||||
saving.value = true
|
||||
const payload: SaveEducationItem[] = 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.map((d: any) => ({ id: d.id, text: d.text })),
|
||||
}))
|
||||
await saveEducation(payload)
|
||||
// 接口成功后更新本地数据
|
||||
profile.value.education = data.education.map((edu: any) => ({
|
||||
school: edu.school,
|
||||
major: edu.major,
|
||||
studyType: edu.studyType,
|
||||
degree: edu.degree,
|
||||
startDate: edu.startDate,
|
||||
endDate: edu.endDate,
|
||||
description: edu.description.map((d: any) => ({ ...d })),
|
||||
}))
|
||||
ElMessage.success('教育经历保存成功')
|
||||
} catch {
|
||||
ElMessage.error('教育经历保存失败,请重试')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
} else if (editModule.value === 'work') {
|
||||
// ---- 工作经历:调用工作经历接口保存 ----
|
||||
try {
|
||||
saving.value = true
|
||||
const payload: SaveWorkItem[] = data.works.map((work: any) => ({
|
||||
companyName: work.companyName,
|
||||
position: work.position,
|
||||
startDate: work.startDate,
|
||||
endDate: work.endDate || '',
|
||||
description: work.description.map((d: any) => ({ id: d.id, text: d.text })),
|
||||
}))
|
||||
await saveWork(payload)
|
||||
// 接口成功后更新本地数据
|
||||
profile.value.works = data.works.map((work: any) => ({
|
||||
companyName: work.companyName,
|
||||
position: work.position,
|
||||
startDate: work.startDate,
|
||||
endDate: work.endDate,
|
||||
description: work.description.map((d: any) => ({ ...d })),
|
||||
}))
|
||||
ElMessage.success('工作经历保存成功')
|
||||
} catch {
|
||||
ElMessage.error('工作经历保存失败,请重试')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
} else if (editModule.value === 'internship') {
|
||||
// ---- 实习经历:调用实习经历接口保存 ----
|
||||
try {
|
||||
saving.value = true
|
||||
const payload: SaveWorkItem[] = data.internships.map((intern: any) => ({
|
||||
companyName: intern.companyName,
|
||||
position: intern.position,
|
||||
startDate: intern.startDate,
|
||||
endDate: intern.endDate || '',
|
||||
description: intern.description.map((d: any) => ({ id: d.id, text: d.text })),
|
||||
}))
|
||||
await saveInternship(payload)
|
||||
// 接口成功后更新本地数据
|
||||
profile.value.internships = data.internships.map((intern: any) => ({
|
||||
companyName: intern.companyName,
|
||||
position: intern.position,
|
||||
startDate: intern.startDate,
|
||||
endDate: intern.endDate,
|
||||
description: intern.description.map((d: any) => ({ ...d })),
|
||||
}))
|
||||
ElMessage.success('实习经历保存成功')
|
||||
} catch {
|
||||
ElMessage.error('实习经历保存失败,请重试')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
} else if (editModule.value === 'project') {
|
||||
// ---- 项目经历:调用项目经历接口保存 ----
|
||||
try {
|
||||
saving.value = true
|
||||
const payload: SaveProjectItem[] = data.projects.map((proj: any) => ({
|
||||
projectName: proj.projectName,
|
||||
companyName: proj.companyName || '',
|
||||
role: proj.role || '',
|
||||
startDate: proj.startDate,
|
||||
endDate: proj.endDate || '',
|
||||
description: proj.description.map((d: any) => ({ id: d.id, text: d.text })),
|
||||
}))
|
||||
await saveProject(payload)
|
||||
// 接口成功后更新本地数据
|
||||
profile.value.projects = data.projects.map((proj: any) => ({
|
||||
projectName: proj.projectName,
|
||||
companyName: proj.companyName,
|
||||
role: proj.role,
|
||||
startDate: proj.startDate,
|
||||
endDate: proj.endDate,
|
||||
description: proj.description.map((d: any) => ({ ...d })),
|
||||
}))
|
||||
ElMessage.success('项目经历保存成功')
|
||||
} catch {
|
||||
ElMessage.error('项目经历保存失败,请重试')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
} else if (editModule.value === 'competition') {
|
||||
// ---- 竞赛经历:调用竞赛经历接口保存 ----
|
||||
try {
|
||||
saving.value = true
|
||||
const payload: SaveCompetitionItem[] = data.competitions.map((comp: any) => ({
|
||||
competitionName: comp.competitionName,
|
||||
award: comp.award || '',
|
||||
awardDate: comp.awardDate || '',
|
||||
description: comp.description.map((d: any) => ({ id: d.id, text: d.text })),
|
||||
}))
|
||||
await saveCompetition(payload)
|
||||
// 接口成功后更新本地数据
|
||||
profile.value.competitions = data.competitions.map((comp: any) => ({
|
||||
competitionName: comp.competitionName,
|
||||
award: comp.award,
|
||||
awardDate: comp.awardDate,
|
||||
description: comp.description.map((d: any) => ({ ...d })),
|
||||
}))
|
||||
ElMessage.success('竞赛经历保存成功')
|
||||
} catch {
|
||||
ElMessage.error('竞赛经历保存失败,请重试')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
} else if (editModule.value === 'skills') {
|
||||
// ---- 技能:调用主表接口保存,需要传递完整的个人信息数据 ----
|
||||
try {
|
||||
saving.value = true
|
||||
await saveProfile({
|
||||
name: profile.value.name,
|
||||
email: profile.value.email,
|
||||
mobileNumber: profile.value.phone,
|
||||
idCard: profile.value.idNumber,
|
||||
regionCode: profile.value.regionCode,
|
||||
wechatNumber: profile.value.wechat,
|
||||
skills: [...data.skills],
|
||||
certificates: profile.value.certificates,
|
||||
})
|
||||
profile.value.skills = [...data.skills]
|
||||
ElMessage.success('技能保存成功')
|
||||
} catch {
|
||||
ElMessage.error('技能保存失败,请重试')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
} else if (editModule.value === 'certificate') {
|
||||
// ---- 证书:调用主表接口保存,需要传递完整的个人信息数据 ----
|
||||
try {
|
||||
saving.value = true
|
||||
await saveProfile({
|
||||
name: profile.value.name,
|
||||
email: profile.value.email,
|
||||
mobileNumber: profile.value.phone,
|
||||
idCard: profile.value.idNumber,
|
||||
regionCode: profile.value.regionCode,
|
||||
wechatNumber: profile.value.wechat,
|
||||
skills: profile.value.skills,
|
||||
certificates: [...data.certificates],
|
||||
})
|
||||
profile.value.certificates = [...data.certificates]
|
||||
ElMessage.success('证书保存成功')
|
||||
} catch {
|
||||
ElMessage.error('证书保存失败,请重试')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function goToJobs() {
|
||||
router.push('/jobs')
|
||||
}
|
||||
|
||||
function goToResume() {
|
||||
router.push('/resume')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,166 @@
|
||||
<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">
|
||||
<table 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">创建时间</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>
|
||||
</div>
|
||||
</td>
|
||||
<td class="resume-page__td">{{ item.targetJob }}</td>
|
||||
<td class="resume-page__td">{{ item.updatedAt }}</td>
|
||||
<td class="resume-page__td">{{ item.createdAt }}</td>
|
||||
<td class="resume-page__td resume-page__td--action">
|
||||
<button
|
||||
class="resume-page__more-btn"
|
||||
aria-label="更多操作"
|
||||
@click.stop="toggleMenu(item.id)"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" class="resume-page__more-svg">
|
||||
<circle cx="3" cy="8" r="1.5"/>
|
||||
<circle cx="8" cy="8" r="1.5"/>
|
||||
<circle cx="13" cy="8" r="1.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 弹出菜单 -->
|
||||
<div v-if="activeMenuId === item.id" class="resume-page__popup" @click.stop>
|
||||
<div
|
||||
v-for="action in popupActions"
|
||||
:key="action"
|
||||
class="resume-page__popup-item"
|
||||
@click="handleAction(action, item.id)"
|
||||
>
|
||||
{{ action }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/** 简历列表项类型 */
|
||||
interface ResumeItem {
|
||||
id: string
|
||||
name: string
|
||||
avatarLetter: string
|
||||
avatarColor: string
|
||||
isDefault: boolean
|
||||
targetJob: string
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// ==================== 数据(模拟数据,后续对接接口) ====================
|
||||
|
||||
/** 简历列表 */
|
||||
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个月前',
|
||||
},
|
||||
])
|
||||
|
||||
// ==================== 页面状态 ====================
|
||||
|
||||
/** 当前打开弹出菜单的简历 ID */
|
||||
const activeMenuId = ref<string | null>(null)
|
||||
|
||||
/** 弹出菜单操作项 */
|
||||
const popupActions = ['设为默认简历', '编辑', '导出简历', '删除']
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
/** 切换弹出菜单的显示/隐藏 */
|
||||
function toggleMenu(id: string) {
|
||||
activeMenuId.value = activeMenuId.value === id ? null : id
|
||||
}
|
||||
|
||||
/** 弹出菜单操作点击 */
|
||||
function handleAction(action: string, id: string) {
|
||||
activeMenuId.value = null
|
||||
console.log(action, id)
|
||||
}
|
||||
|
||||
/** 上传简历 */
|
||||
function handleUpload() {
|
||||
console.log('上传简历')
|
||||
}
|
||||
|
||||
/** 跳转到简历详情页 */
|
||||
function goDetail(id: string) {
|
||||
router.push(`/resume/${id}`)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div class="resume-detail dflex">
|
||||
<SideNav />
|
||||
<div class="resume-detail__content">
|
||||
<!-- 顶部标题 -->
|
||||
<h2 class="resume-detail__page-title">我的简历</h2>
|
||||
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="resume-detail__toolbar">
|
||||
<div class="resume-detail__toolbar-left">
|
||||
<button class="resume-detail__back-btn" @click="goBack" aria-label="返回">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__back-icon">
|
||||
<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>
|
||||
</div>
|
||||
<div class="resume-detail__toolbar-right">
|
||||
<button class="resume-detail__tool-btn" @click="handleFeedback">问题反馈</button>
|
||||
<button class="resume-detail__tool-btn" @click="handleEdit">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__tool-icon">
|
||||
<path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
编辑简历信息
|
||||
</button>
|
||||
<button class="resume-detail__tool-btn" @click="handleExport">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__tool-icon">
|
||||
<path d="M3 10v3h10v-3M8 2v8M5 5l3-3 3 3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
导出
|
||||
</button>
|
||||
<button class="resume-detail__tool-btn resume-detail__tool-btn--danger" @click="handleDelete">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__tool-icon">
|
||||
<path d="M4 4h8l-.5 9H4.5L4 4zM6 4V2.5h4V4M2.5 4h11" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 简历评分区域 -->
|
||||
<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>
|
||||
<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-label">紧急修复项</span>
|
||||
</div>
|
||||
<div class="resume-detail__score-item">
|
||||
<span class="resume-detail__score-num">{{ resume.severeCount }}</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-label">可选修复项</span>
|
||||
</div>
|
||||
<button class="resume-detail__diagnose-btn" @click="handleDiagnose">重新诊断</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 简历主体内容 -->
|
||||
<div class="resume-detail__body">
|
||||
<!-- 个人信息卡片 -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resume-detail__contact">
|
||||
<span 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 }}
|
||||
</span>
|
||||
<span 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 }}
|
||||
</span>
|
||||
<span 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 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 教育背景 -->
|
||||
<div 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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- 技能 -->
|
||||
<div class="resume-detail__card">
|
||||
<h3 class="resume-detail__section-title">技能</h3>
|
||||
<div class="resume-detail__skills">
|
||||
<span v-for="(skill, i) in resume.skills" :key="i" class="resume-detail__skill-tag">
|
||||
{{ skill }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
|
||||
// ==================== 路由相关 ====================
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
/** 当前简历 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'],
|
||||
})
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
/** 返回简历列表页 */
|
||||
function goBack() {
|
||||
router.push('/resume')
|
||||
}
|
||||
|
||||
/** 问题反馈 */
|
||||
function handleFeedback() { console.log('问题反馈') }
|
||||
|
||||
/** 编辑简历信息 */
|
||||
function handleEdit() { console.log('编辑简历信息') }
|
||||
|
||||
/** 导出简历 */
|
||||
function handleExport() { console.log('导出') }
|
||||
|
||||
/** 删除简历 */
|
||||
function handleDelete() { console.log('删除') }
|
||||
|
||||
/** 查看评估报告 */
|
||||
function handleViewReport() { console.log('查看评估报告') }
|
||||
|
||||
/** 重新诊断简历 */
|
||||
function handleDiagnose() { console.log('重新诊断') }
|
||||
|
||||
/** 紧急修复项 */
|
||||
function handleUrgentFix() { console.log('紧急修复项') }
|
||||
|
||||
/** 修复简历问题 */
|
||||
function handlePolish() { console.log('修复') }
|
||||
</script>
|
||||
Reference in New Issue
Block a user