Files
autoAiWorkSys/admin/src/views/account/resume_info_detail.vue
张成 6efd77d2b5 1
2025-12-26 13:12:53 +08:00

519 lines
19 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>
<FloatPanel
ref="floatPanel"
title="简历详情"
position="right"
:show-back="true"
back-text="返回"
@back="handleBack"
>
<template #header-right>
<Button type="info" @click="handleSyncOnline" :loading="syncing" style="margin-right: 8px;">同步在线简历</Button>
<Button type="primary" @click="handleAnalyzeAI" :loading="analyzing">AI 分析</Button>
</template>
<div class="resume-detail-content" v-if="resumeData">
<Spin fix v-if="loading">
<Icon type="ios-loading" size="18" class="spin-icon-load"></Icon>
<div>加载中...</div>
</Spin>
<!-- 基本信息 -->
<Card title="基本信息" style="margin-bottom: 16px;">
<Row :gutter="16">
<Col span="12">
<div class="info-item">
<span class="label">姓名</span>
<span class="value">{{ resumeData.fullName || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">性别</span>
<span class="value">{{ resumeData.gender || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">年龄</span>
<span class="value">{{ resumeData.age || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">电话</span>
<span class="value">{{ resumeData.phone || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">邮箱</span>
<span class="value">{{ resumeData.email || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">所在地</span>
<span class="value">{{ resumeData.location || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">工作年限</span>
<span class="value">{{ resumeData.workYears || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">当前职位</span>
<span class="value">{{ resumeData.currentPosition || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">当前公司</span>
<span class="value">{{ resumeData.currentCompany || '-' }}</span>
</div>
</Col>
</Row>
</Card>
<!-- 教育背景 -->
<Card title="教育背景" style="margin-bottom: 16px;">
<Row :gutter="16">
<Col span="12">
<div class="info-item">
<span class="label">学历</span>
<span class="value">{{ resumeData.education || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">专业</span>
<span class="value">{{ resumeData.major || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">毕业院校</span>
<span class="value">{{ resumeData.school || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">毕业年份</span>
<span class="value">{{ resumeData.graduationYear || '-' }}</span>
</div>
</Col>
</Row>
</Card>
<!-- 期望信息 -->
<Card title="期望信息" style="margin-bottom: 16px;">
<Row :gutter="16">
<Col span="12">
<div class="info-item">
<span class="label">期望职位</span>
<span class="value">{{ resumeData.expectedPosition || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">期望薪资</span>
<span class="value">{{ resumeData.expectedSalary || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">期望地点</span>
<span class="value">{{ resumeData.expectedLocation || '-' }}</span>
</div>
</Col>
<Col span="12">
<div class="info-item">
<span class="label">期望行业</span>
<span class="value">{{ resumeData.expectedIndustry || '-' }}</span>
</div>
</Col>
</Row>
</Card>
<!-- 技能标签 -->
<Card title="技能标签" style="margin-bottom: 16px;">
<div v-if="skillTags && skillTags.length > 0">
<Tag v-for="(skill, index) in skillTags" :key="index" color="blue" style="margin: 4px;">{{ skill }}</Tag>
</div>
<div v-else class="no-data">暂无技能标签</div>
</Card>
<!-- 技能描述 -->
<Card title="技能描述" style="margin-bottom: 16px;" v-if="resumeData.skillDescription">
<div class="text-content">{{ resumeData.skillDescription }}</div>
</Card>
<!-- 工作经历 -->
<Card title="工作经历" style="margin-bottom: 16px;" v-if="workExperience && workExperience.length > 0">
<Timeline>
<TimelineItem v-for="(work, index) in workExperience" :key="index">
<p class="work-title">
<strong>{{ work.position }}</strong>
<span class="work-company"> - {{ work.company }}</span>
</p>
<p class="work-time" v-if="work.startDate || work.endDate">
{{ work.startDate }} - {{ work.endDate || '至今' }}
</p>
<p class="work-content" v-if="work.content">{{ work.content }}</p>
</TimelineItem>
</Timeline>
</Card>
<!-- 项目经验 -->
<Card title="项目经验" style="margin-bottom: 16px;" v-if="projectExperience && projectExperience.length > 0">
<div v-for="(project, index) in projectExperience" :key="index" class="project-item" style="margin-bottom: 20px;">
<p class="project-title">
<strong>{{ project.name }}</strong>
<span class="project-role" v-if="project.role"> - {{ project.role }}</span>
</p>
<p class="project-time" v-if="project.startDate || project.endDate">
{{ project.startDate }} - {{ project.endDate || '至今' }}
</p>
<p class="project-desc" v-if="project.description">{{ project.description }}</p>
<p class="project-performance" v-if="project.performance">
<strong>项目成果</strong>{{ project.performance }}
</p>
</div>
</Card>
<!-- 简历内容 -->
<Card title="简历完整内容" style="margin-bottom: 16px;" v-if="resumeData.resumeContent">
<div class="text-content" style="white-space: pre-wrap;">{{ resumeData.resumeContent }}</div>
</Card>
<!-- AI 分析结果 -->
<Card title="AI 分析结果" style="margin-bottom: 16px;">
<Row :gutter="16">
<Col span="24">
<div class="info-item">
<span class="label">竞争力评分</span>
<span class="value" :style="{ color: getCompetitivenessColor(resumeData.aiCompetitiveness), fontSize: '18px', fontWeight: 'bold' }">
{{ resumeData.aiCompetitiveness || 0 }}
</span>
<Progress
:percent="resumeData.aiCompetitiveness || 0"
:stroke-color="getCompetitivenessColor(resumeData.aiCompetitiveness)"
style="margin-top: 8px; width: 300px;"
/>
</div>
</Col>
<Col span="24" v-if="resumeData.aiStrengths">
<div class="info-item ai-section">
<div class="ai-section-header">
<Icon type="ios-thumbs-up" color="#19be6b" size="18" />
<span class="label">优势分析</span>
</div>
<div class="text-content ai-content">{{ resumeData.aiStrengths }}</div>
</div>
</Col>
<Col span="24" v-if="resumeData.aiWeaknesses">
<div class="info-item ai-section">
<div class="ai-section-header">
<Icon type="ios-warning" color="#ff9900" size="18" />
<span class="label">劣势分析</span>
</div>
<div class="text-content ai-content">{{ resumeData.aiWeaknesses }}</div>
</div>
</Col>
<Col span="24" v-if="resumeData.aiCareerSuggestion">
<div class="info-item ai-section">
<div class="ai-section-header">
<Icon type="ios-bulb" color="#2d8cf0" size="18" />
<span class="label">职业建议</span>
</div>
<div class="text-content ai-content">{{ resumeData.aiCareerSuggestion }}</div>
</div>
</Col>
<Col span="24" v-if="aiSkillTags && aiSkillTags.length > 0">
<div class="info-item">
<span class="label">AI 提取的技能标签</span>
<div style="margin-top: 8px;">
<Tag v-for="(skill, index) in aiSkillTags" :key="index" color="green" style="margin: 4px;">{{ skill }}</Tag>
</div>
</div>
</Col>
<Col span="24" v-if="!resumeData.aiCompetitiveness && !resumeData.aiStrengths">
<div class="no-data">
<Icon type="ios-information-circle" size="24" color="#c5c8ce" />
<p>暂无 AI 分析结果请点击上方"AI 分析"按钮进行分析</p>
</div>
</Col>
</Row>
</Card>
<!-- 原始数据可展开 -->
<Card title="原始数据" style="margin-bottom: 16px;">
<Collapse>
<Panel name="originalData">
查看原始 JSON 数据
<template slot="content">
<pre class="json-preview">{{ JSON.stringify(originalDataObj, null, 2) }}</pre>
</template>
</Panel>
</Collapse>
</Card>
</div>
</FloatPanel>
</template>
<script>
import resumeInfoServer from '@/api/profile/resume_info_server.js'
export default {
name: 'ResumeInfoDetail',
data() {
return {
loading: false,
analyzing: false,
syncing: false,
resumeData: null,
skillTags: [],
workExperience: [],
projectExperience: [],
aiSkillTags: [],
originalDataObj: {}
}
},
methods: {
async show(resumeId) {
this.$refs.floatPanel.show()
await this.loadResumeData(resumeId)
},
async loadResumeData(resumeId) {
this.loading = true
try {
const res = await resumeInfoServer.getById(resumeId)
this.resumeData = res.data || {}
// 解析 JSON 字段
this.skillTags = this.parseJsonField(this.resumeData.skills) || []
this.workExperience = this.parseJsonField(this.resumeData.workExperience) || []
this.projectExperience = this.parseJsonField(this.resumeData.projectExperience) || []
this.aiSkillTags = this.parseJsonField(this.resumeData.aiSkillTags) || []
// 解析原始数据
this.originalDataObj = this.parseJsonField(this.resumeData.originalData) || {}
} catch (error) {
console.error('加载简历详情失败:', error)
this.$Message.error('加载简历详情失败')
} finally {
this.loading = false
}
},
parseJsonField(field) {
if (!field) return null
if (typeof field === 'string') {
try {
return JSON.parse(field)
} catch (e) {
return field
}
}
return field
},
async handleSyncOnline() {
if (!this.resumeData || !this.resumeData.resumeId) {
this.$Message.warning('简历ID不存在')
return
}
if (!this.resumeData.sn_code) {
this.$Message.warning('该简历未绑定设备,无法同步在线简历')
return
}
this.syncing = true
try {
const res = await resumeInfoServer.syncOnline(this.resumeData.resumeId)
this.$Message.success(res.message || '同步在线简历成功')
// 重新加载数据
await this.loadResumeData(this.resumeData.resumeId)
} catch (error) {
console.error('同步在线简历失败:', error)
// 优先从 error.response.data.message 获取,然后是 error.message
const errorMsg = error.response?.data?.message || error.message || '请稍后重试'
this.$Message.error(errorMsg)
} finally {
this.syncing = false
}
},
async handleAnalyzeAI() {
if (!this.resumeData || !this.resumeData.resumeId) {
this.$Message.warning('简历ID不存在')
return
}
this.analyzing = true
try {
await resumeInfoServer.analyzeWithAI(this.resumeData.resumeId)
this.$Message.success('AI 分析完成')
// 重新加载数据
await this.loadResumeData(this.resumeData.resumeId)
} catch (error) {
console.error('AI 分析失败:', error)
// 优先从 error.response.data.message 获取,然后是 error.message
const errorMsg = error.response?.data?.message || error.message || '请稍后重试'
this.$Message.error(errorMsg)
} finally {
this.analyzing = false
}
},
handleBack() {
this.$refs.floatPanel.hide()
this.$emit('on-close')
},
getCompetitivenessColor(score) {
if (!score) return '#666'
if (score >= 80) return '#19be6b'
if (score >= 60) return '#2d8cf0'
return '#ed4014'
}
}
}
</script>
<style scoped>
.resume-detail-content {
padding: 0;
position: relative;
min-height: 400px;
}
.info-item {
margin-bottom: 12px;
}
.info-item .label {
font-weight: 600;
color: #515a6e;
margin-right: 8px;
}
.info-item .value {
color: #2d8cf0;
}
.text-content {
color: #515a6e;
line-height: 1.6;
}
.work-title {
font-size: 14px;
margin-bottom: 4px;
}
.work-company {
color: #808695;
font-weight: normal;
}
.work-time {
color: #808695;
font-size: 12px;
margin-bottom: 8px;
}
.work-content {
color: #515a6e;
line-height: 1.6;
margin-top: 8px;
}
.project-item {
padding-bottom: 16px;
border-bottom: 1px solid #e8eaec;
}
.project-item:last-child {
border-bottom: none;
}
.project-title {
font-size: 14px;
margin-bottom: 4px;
}
.project-role {
color: #808695;
font-weight: normal;
}
.project-time {
color: #808695;
font-size: 12px;
margin-bottom: 8px;
}
.project-desc {
color: #515a6e;
line-height: 1.6;
margin-top: 8px;
}
.project-performance {
color: #19be6b;
line-height: 1.6;
margin-top: 8px;
}
.no-data {
color: #c5c8ce;
text-align: center;
padding: 40px 20px;
}
.no-data p {
margin-top: 12px;
font-size: 14px;
}
/* AI 分析区域样式 */
.ai-section {
margin-bottom: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 6px;
border-left: 3px solid #2d8cf0;
}
.ai-section-header {
display: flex;
align-items: center;
margin-bottom: 12px;
gap: 8px;
}
.ai-section-header .label {
font-size: 15px;
font-weight: 600;
color: #17233d;
margin-right: 0;
}
.ai-content {
padding-left: 26px;
line-height: 1.8;
color: #515a6e;
}
.json-preview {
background: #f8f8f9;
padding: 16px;
border-radius: 4px;
max-height: 500px;
overflow-y: auto;
font-size: 12px;
line-height: 1.5;
}
</style>