简历优化和岗位简历

This commit is contained in:
2026-04-13 21:34:40 +08:00
parent a39835be01
commit 1c91818494
32 changed files with 3850 additions and 320 deletions
+405
View File
@@ -0,0 +1,405 @@
<template>
<!-- 简历评估问题修复抽屉 从右侧滑入占满屏幕高度宽度 8rem -->
<Teleport to="body">
<!-- 遮罩层 -->
<Transition name="fix-drawer-overlay">
<div
v-if="modelValue"
class="fix-drawer-overlay"
@click="handleClose"
/>
</Transition>
<!-- 抽屉主体 -->
<Transition name="fix-drawer-slide">
<div v-if="modelValue" class="fix-drawer" @click.stop>
<!-- 顶部栏折叠按钮 + 模块标题 -->
<div class="fix-drawer__header">
<button class="fix-drawer__close-btn" @click="handleClose" aria-label="收起">
<svg viewBox="0 0 16 16" fill="none" class="fix-drawer__close-icon">
<path d="M10 3l-5 5 5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<h3 class="fix-drawer__title">{{ moduleTitle }}</h3>
</div>
<!-- 可滚动内容区 -->
<div class="fix-drawer__body">
<!-- 原文部分 -->
<div class="fix-drawer__section">
<h4 class="fix-drawer__section-title">原文</h4>
<div class="fix-drawer__card">
<p class="fix-drawer__original-text">{{ originalText }}</p>
</div>
</div>
<!-- 问题检查部分 -->
<div class="fix-drawer__section">
<h4 class="fix-drawer__section-title">问题检查</h4>
<div class="fix-drawer__card">
<!-- 发现问题 -->
<p class="fix-drawer__label">发现问题</p>
<p class="fix-drawer__text">{{ matchedIssue?.finding || '暂无' }}</p>
<!-- 重要性说明 -->
<p class="fix-drawer__label">重要性说明</p>
<p class="fix-drawer__text">{{ matchedIssue?.importance || '暂无' }}</p>
<!-- 改进建议 -->
<p class="fix-drawer__label">改进建议</p>
<p class="fix-drawer__text">{{ matchedIssue?.suggestion || '暂无' }}</p>
</div>
</div>
<!-- AI 改进后的版本 -->
<div class="fix-drawer__section">
<h4 class="fix-drawer__section-title">AI改写后的版本</h4>
<div class="fix-drawer__card">
<p class="fix-drawer__diff-text">
<span
v-for="(seg, idx) in diffSegments"
:key="idx"
:class="{ 'fix-drawer__highlight': seg.highlight }"
>{{ seg.text }}</span>
</p>
</div>
</div>
<!-- 自己改写区域 每个 optimizedContent 子元素对应一个输入框 -->
<div class="fix-drawer__section">
<h4 class="fix-drawer__section-title">自己改写</h4>
<div class="fix-drawer__card">
<textarea
v-for="(_, idx) in userRewriteList"
:key="'rewrite-' + idx"
v-model="userRewriteList[idx]"
class="fix-drawer__textarea"
:placeholder="'第 ' + (idx + 1) + ' 段改写内容…'"
:ref="(el: any) => setTextareaRef(el, idx)"
@input="autoResize(idx)"
/>
<!-- AI 优化按钮 -->
<div class="fix-drawer__textarea-footer">
<button class="fix-drawer__ai-btn" @click="handleAiPolish">AI优化</button>
</div>
</div>
</div>
<!-- AI 润色结果区域 调用接口返回后显示 -->
<div v-if="polishResult.length" class="fix-drawer__section">
<h4 class="fix-drawer__section-title">AI润色结果</h4>
<div class="fix-drawer__card">
<p
v-for="(text, idx) in polishResult"
:key="'polish-' + idx"
class="fix-drawer__polish-text"
>{{ text }}</p>
<!-- 替换按钮 -->
<div class="fix-drawer__textarea-footer">
<button class="fix-drawer__replace-btn" @click="handleReplace">替换</button>
</div>
</div>
</div>
<!-- 用户评价区域 始终显示 -->
<div class="fix-drawer__section">
<div class="fix-drawer__card fix-drawer__feedback-card">
<span class="fix-drawer__feedback-label">你认为这个修改建议有用吗</span>
<div class="fix-drawer__feedback-actions">
<!-- 有用按钮 -->
<button
class="fix-drawer__feedback-btn"
:class="{ 'fix-drawer__feedback-btn--active': feedbackValue === 1 }"
@click="handleFeedback(1)"
>
<svg viewBox="0 0 16 16" fill="none" class="fix-drawer__feedback-icon">
<path d="M4.5 7v6.5h-2a1 1 0 01-1-1V8a1 1 0 011-1h2zm1 6.5h5.3a1.5 1.5 0 001.47-1.2l1.08-5.4A1 1 0 0012.37 5.5H9V3a1.5 1.5 0 00-1.5-1.5L5.5 7z" stroke="currentColor" stroke-width="1.1" stroke-linejoin="round"/>
</svg>
有用
</button>
<!-- 无用按钮 -->
<button
class="fix-drawer__feedback-btn"
:class="{ 'fix-drawer__feedback-btn--active': feedbackValue === 2 }"
@click="handleFeedback(2)"
>
<svg viewBox="0 0 16 16" fill="none" class="fix-drawer__feedback-icon fix-drawer__feedback-icon--down">
<path d="M4.5 7v6.5h-2a1 1 0 01-1-1V8a1 1 0 011-1h2zm1 6.5h5.3a1.5 1.5 0 001.47-1.2l1.08-5.4A1 1 0 0012.37 5.5H9V3a1.5 1.5 0 00-1.5-1.5L5.5 7z" stroke="currentColor" stroke-width="1.1" stroke-linejoin="round"/>
</svg>
无用
</button>
</div>
</div>
</div>
</div>
<!-- 底部提交按钮 -->
<div class="fix-drawer__footer">
<button class="fix-drawer__submit-btn" @click="handleSubmit">提交新版本</button>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { computed, ref, watch, nextTick } from 'vue'
import type { DiagnosisIssue, DescriptionParagraph } from '@/api/resume'
import { polishDiagnosisIssue, feedbackDiagnosisIssue, resolveDiagnosisIssue } from '@/api/resume'
import { computeDiff, type DiffSegment } from '@/utils/textDiff'
// ==================== Props ====================
const props = defineProps<{
/** 控制抽屉显示/隐藏 */
modelValue: boolean
/** 模块类型:summary / education / work / internship / project / competition */
moduleType: string
/** 当前记录 ID(个人概述时为 resumeId,经历时为该段经历的 id) */
recordId: string
/** 原文内容 — 个人概述时为 summary 字符串,经历时为 description 数组 */
originalContent: string | DescriptionParagraph[]
/** 该记录对应的诊断问题 */
issue: DiagnosisIssue | undefined
}>()
const emit = defineEmits<{
(e: 'update:modelValue', val: boolean): void
/** 提交新版本:携带自己改写的最终内容数组 */
(e: 'submit', content: string[]): void
}>()
// ==================== 模块标题映射 ====================
/** 模块类型到中文标题的映射 */
const moduleTitleMap: Record<string, string> = {
summary: '个人概述',
education: '教育背景',
work: '工作经历',
internship: '实习经历',
project: '项目经历',
competition: '竞赛经历',
}
/** 抽屉标题 */
const moduleTitle = computed(() => moduleTitleMap[props.moduleType] || '问题修复')
// ==================== 原文处理 ====================
/** 将原文内容统一转为字符串(description 数组的 text 逐行拼接) */
const originalText = computed(() => {
if (typeof props.originalContent === 'string') {
return props.originalContent
}
if (Array.isArray(props.originalContent)) {
return props.originalContent.map(d => d.text || '').join('\n')
}
return ''
})
// ==================== 匹配的诊断问题 ====================
/** 当前记录匹配的 issue */
const matchedIssue = computed(() => props.issue)
// ==================== AI 改进内容(optimizedContent 数组) ====================
/** optimizedContent 子元素数组 — 兼容字符串(个人概述)和数组对象(经历模块) */
const optimizedItems = computed<any[]>(() => {
const oc = matchedIssue.value?.optimizedContent
if (!oc) return []
// 个人概述:optimizedContent 是字符串
if (typeof oc === 'string') return [oc]
if (Array.isArray(oc)) return oc
return []
})
/** AI 改进后的文本(统一拼接为字符串,用于差异对比) */
const optimizedText = computed(() => {
if (!optimizedItems.value.length) return ''
const oc = matchedIssue.value?.optimizedContent
// 个人概述:直接就是字符串
if (typeof oc === 'string') return oc
// 经历模块:数组对象,取每个子元素的 optimizedContent 字段拼接
return optimizedItems.value
.map((item: any) => item.optimizedContent || item.text || '')
.join('\n')
})
/** 差异对比片段 */
const diffSegments = computed<DiffSegment[]>(() => {
if (!optimizedText.value) return []
return computeDiff(originalText.value, optimizedText.value)
})
// ==================== 自己改写(多输入框) ====================
/** 用户自己改写的内容列表 — 每个 optimizedContent 子元素对应一个输入框 */
const userRewriteList = ref<string[]>([])
/** 抽屉打开时用 AI 改写内容初始化每个输入框 */
watch(() => props.modelValue, (val) => {
if (val) {
// 用 optimizedContent 初始化每个输入框(兼容字符串和数组对象)
const oc = matchedIssue.value?.optimizedContent
if (typeof oc === 'string') {
// 个人概述:单个字符串 → 单个输入框
userRewriteList.value = [oc]
} else {
userRewriteList.value = optimizedItems.value.map(
(item: any) => item.optimizedContent || item.text || ''
)
}
// 如果没有 optimizedContent,至少给一个空输入框
if (!userRewriteList.value.length) {
userRewriteList.value = ['']
}
// 清空上次的润色结果
polishResult.value = []
// 重置评价状态
feedbackValue.value = 0
// 初始化 textarea 高度
initAllTextareaHeight()
}
})
// ==================== AI 润色(调用接口) ====================
/** AI 润色返回的结果数组 */
const polishResult = ref<string[]>([])
/** 调用 AI 润色接口 */
async function handleAiPolish() {
const issueId = matchedIssue.value?.id
if (!issueId) {
ElMessage.warning('当前没有可润色的问题')
return
}
// 收集每个输入框的内容
const content = userRewriteList.value.map(s => s.trim()).filter(Boolean)
if (!content.length) {
ElMessage.warning('请先输入改写内容')
return
}
// 全屏加载提示
const loading = ElLoading.service({
lock: true,
text: 'AI 润色中,请耐心等待…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
try {
const res = await polishDiagnosisIssue(issueId, content)
if (res.code === 0 && res.data?.content) {
polishResult.value = res.data.content
ElMessage.success('润色完成')
} else {
ElMessage.error(res.msg || '润色失败,请稍后重试')
}
} catch {
ElMessage.error('润色请求失败,请稍后重试')
} finally {
loading.close()
}
}
/** 点击替换按钮 — 以 AI 润色结果段数为准,替换自己改写的输入框 */
function handleReplace() {
userRewriteList.value = [...polishResult.value]
// 替换完成后清空润色结果区域
polishResult.value = []
ElMessage.success('已替换')
// 重新计算 textarea 高度
initAllTextareaHeight()
}
// ==================== 用户评价 ====================
/** 当前评价状态:0=未评价 1=有用 2=无用 */
const feedbackValue = ref(0)
/** 提交用户评价 */
async function handleFeedback(value: number) {
const issueId = matchedIssue.value?.id
if (!issueId) return
try {
const res = await feedbackDiagnosisIssue(issueId, value)
if (res.code === 0) {
feedbackValue.value = value
ElMessage.success('已收到,感谢您的评价')
} else {
ElMessage.error(res.msg || '评价提交失败')
}
} catch {
ElMessage.error('评价请求失败,请稍后重试')
}
}
// ==================== textarea 自适应高度 ====================
/** textarea DOM 引用数组 */
const textareaRefs: HTMLTextAreaElement[] = []
/** 收集 textarea ref */
function setTextareaRef(el: HTMLTextAreaElement | null, idx: number) {
if (el) textareaRefs[idx] = el
}
/** 根据内容自动调整 textarea 高度 */
function autoResize(idx: number) {
const el = textareaRefs[idx]
if (!el) return
el.style.height = 'auto'
el.style.height = el.scrollHeight + 'px'
}
/** 初始化所有 textarea 高度(抽屉打开后 DOM 渲染完成时调用) */
function initAllTextareaHeight() {
nextTick(() => {
userRewriteList.value.forEach((_, idx) => autoResize(idx))
})
}
// ==================== 事件处理 ====================
/** 提交新版本 — 调用 resolve 接口标记已处理,然后通知父组件更新数据 */
async function handleSubmit() {
const issueId = matchedIssue.value?.id
if (!issueId) {
ElMessage.warning('当前没有可提交的问题')
return
}
// 全屏加载提示
const loading = ElLoading.service({
lock: true,
text: '正在提交新版本…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
try {
// 1. 调用 resolve 接口标记问题已处理
const res = await resolveDiagnosisIssue(issueId)
if (res.code !== 0) {
ElMessage.error(res.msg || '标记已处理失败')
return
}
// 2. 关闭抽屉并通知父组件携带最终改写内容
emit('update:modelValue', false)
emit('submit', [...userRewriteList.value])
} catch {
ElMessage.error('提交失败,请稍后重试')
} finally {
loading.close()
}
}
/** 关闭抽屉 */
function handleClose() {
emit('update:modelValue', false)
}
</script>