Files
offerpai_web/src/views/Profile.vue
T
2026-07-09 11:45:36 +08:00

757 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="profile-page dflex">
<SideNav />
<div class="profile-page__content">
<!-- 个人资料编辑抽屉 -->
<ProfileEditDrawer
v-model="showEditDrawer"
:module="editModule"
:initial-data="editInitialData"
@save="handleSaveEdit"
/>
<!-- 欢迎上传简历弹窗无个人资料主表记录时自动弹出 -->
<ResumeUploadDialog v-model="showWelcomeDialog" @confirm="handleWelcomeUpload" />
<!-- 页面标题 -->
<div class="profile-page__header">
<h2 class="profile-page__title">个人资料</h2>
</div>
<!-- 主体区域左侧详情 + 右侧入口 -->
<div class="profile-page__body">
<!-- 个人资料内容组件 -->
<ProfilePageContent
:profile="profile"
@edit="handleEdit"
@save="handleInlineSave"
@exp-update="handleExpUpdate"
@exp-add="handleExpAdd"
@exp-delete="handleExpDelete"
/>
<!-- 右侧入口 -->
</div>
</div>
</div>
<!-- 全屏加载遮罩 -->
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
</template>
<script setup lang="ts">
import { ref, onMounted, watch, computed } 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 ResumeUploadDialog from '@/components/ResumeUploadDialog.vue'
import { saveProfile, fetchProfile, fetchEducation, saveEducation, fetchWork, saveWork, fetchInternship, saveInternship, fetchProject, saveProject, fetchCompetition, saveCompetition, syncProfileFromResume } from '@/api/profile'
import { uploadResume } from '@/utils/aiRequest'
import { ElMessage, ElMessageBox } from 'element-plus'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
import type { SaveEducationItem, SaveWorkItem, SaveProjectItem, SaveCompetitionItem } from '@/api/profile'
const router = useRouter()
const store = useStore()
// ==================== 全屏加载状态 ====================
/** 全屏加载是否显示 */
const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
/** 页面挂载时加载公共分类数据和个人资料 */
onMounted(async () => {
if (!store.state.regions.length) {
store.dispatch('loadCommonData')
}
// 请求个人资料主表数据,判断是否需要弹出欢迎弹窗
const profileRes = await fetchProfile()
if (profileRes.code === '0' && (!profileRes.data || profileRes.data === null)) {
// 无个人资料数据,弹出欢迎上传弹窗
showWelcomeDialog.value = true
return
}
// 有数据,正常填充
if (profileRes.code === '0' && profileRes.data) {
const d = profileRes.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 || []
profile.value.portfolioUrl = d.portfolioUrl || ''
}
// 请求教育经历数据
await loadEducation()
// 请求工作经历数据
await loadWork()
// 请求实习经历数据
await loadInternship()
// 请求项目经历数据
await loadProject()
// 请求竞赛经历数据
await loadCompetition()
})
/** 加载教育经历数据 */
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 showWelcomeDialog = ref(false)
/** 欢迎弹窗确认上传回调 — 上传简历并同步个人资料,完成后刷新页面 */
async function handleWelcomeUpload(file: File) {
fullLoadingText.value = '简历解析中,请耐心等待…'
fullLoading.value = true
try {
const res = await uploadResume(file)
if (res.code === 0 && res.data?.resumeId) {
// 继续同步个人资料
fullLoadingText.value = '正在同步个人资料…'
await syncProfileFromResume(String(res.data.resumeId))
fullLoading.value = false
ElMessage.success('简历上传并同步成功')
// 刷新页面数据
router.go(0)
} else {
fullLoading.value = false
ElMessage.error(res.msg || '上传失败')
}
} catch {
fullLoading.value = false
ElMessage.error('上传失败,请稍后重试')
}
}
/** 登录状态 */
const isAuthenticated = computed(() => store.state.isAuthenticated)
/** 监听登录状态变化 — 登录成功后检查个人资料是否存在 */
watch(isAuthenticated, async (newVal, oldVal) => {
if (newVal && !oldVal) {
try {
const profileRes = await fetchProfile()
if (profileRes.code === '0' && (!profileRes.data || profileRes.data === null)) {
showWelcomeDialog.value = true
}
} catch {
// 接口异常不阻塞
}
}
})
/** 当前编辑的模块名称 */
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[],
/** 作品集链接 — 接口字段 portfolioUrl */
portfolioUrl: '',
/** 教育经历 — 对应数据库 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 === 'portfolio') {
editInitialData.value = {
portfolioUrl: profile.value.portfolioUrl,
}
} 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,
portfolioUrl: data.portfolioUrl,
})
// 接口成功后更新本地数据
profile.value.name = data.name
profile.value.email = data.email
profile.value.phone = data.phone
profile.value.wechat = data.wechat
profile.value.portfolioUrl = data.portfolioUrl || ''
/** 将城市编码存入 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 === 'portfolio') {
// ---- 作品集:调用主表接口保存,需要传递完整的个人信息数据 ----
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: profile.value.certificates,
portfolioUrl: data.portfolioUrl,
})
profile.value.portfolioUrl = data.portfolioUrl
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')
}
/** 内联保存处理(个人信息/技能/证书模块从组件内直接保存) */
async function handleInlineSave(section: string, data: Record<string, any>) {
// 复用已有的 handleSaveEdit 逻辑
editModule.value = section
await handleSaveEdit(data)
}
/** 单条经历更新 — 将编辑后的数据替换到完整列表中再整体保存 */
async function handleExpUpdate(moduleType: string, index: number, data: Record<string, any>) {
try {
if (moduleType === 'education') {
const list = profile.value.education.map((item, i) => i === index ? data : item) as any[]
await saveEducation(list)
await loadEducation()
} else if (moduleType === 'work') {
const list = profile.value.works.map((item, i) => i === index ? data : item) as any[]
await saveWork(list)
await loadWork()
} else if (moduleType === 'internship') {
const list = profile.value.internships.map((item, i) => i === index ? data : item) as any[]
await saveInternship(list)
await loadInternship()
} else if (moduleType === 'project') {
const list = profile.value.projects.map((item, i) => i === index ? data : item) as any[]
await saveProject(list)
await loadProject()
} else if (moduleType === 'competition') {
const list = profile.value.competitions.map((item, i) => i === index ? data : item) as any[]
await saveCompetition(list)
await loadCompetition()
}
ElMessage.success('保存成功')
} catch {
ElMessage.error('保存失败')
}
}
/** 新增经历 */
async function handleExpAdd(moduleType: string, data: Record<string, any>) {
try {
if (moduleType === 'education') {
const list = [...profile.value.education, data] as any[]
await saveEducation(list)
await loadEducation()
} else if (moduleType === 'work') {
const list = [...profile.value.works, data] as any[]
await saveWork(list)
await loadWork()
} else if (moduleType === 'internship') {
const list = [...profile.value.internships, data] as any[]
await saveInternship(list)
await loadInternship()
} else if (moduleType === 'project') {
const list = [...profile.value.projects, data] as any[]
await saveProject(list)
await loadProject()
} else if (moduleType === 'competition') {
const list = [...profile.value.competitions, data] as any[]
await saveCompetition(list)
await loadCompetition()
}
ElMessage.success('添加成功')
} catch {
ElMessage.error('添加失败')
}
}
/** 删除单条经历 — 从列表中移除对应索引后全量保存 */
async function handleExpDelete(moduleType: string, index: number) {
try {
await ElMessageBox.confirm('确定要删除这条经历吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return // 用户取消
}
try {
if (moduleType === 'education') {
const list = profile.value.education.filter((_, i) => i !== index) as any[]
await saveEducation(list)
await loadEducation()
} else if (moduleType === 'work') {
const list = profile.value.works.filter((_, i) => i !== index) as any[]
await saveWork(list)
await loadWork()
} else if (moduleType === 'internship') {
const list = profile.value.internships.filter((_, i) => i !== index) as any[]
await saveInternship(list)
await loadInternship()
} else if (moduleType === 'project') {
const list = profile.value.projects.filter((_, i) => i !== index) as any[]
await saveProject(list)
await loadProject()
} else if (moduleType === 'competition') {
const list = profile.value.competitions.filter((_, i) => i !== index) as any[]
await saveCompetition(list)
await loadCompetition()
}
ElMessage.success('删除成功')
} catch {
ElMessage.error('删除失败')
}
}
</script>