Files
offerpai_web/src/components/ResumeExpInlineEdit.vue
T
2026-07-09 16:26:29 +08:00

297 lines
16 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-edit-inline">
<div class="profile-edit-inline__body pb0">
<!-- 教育经历 -->
<template v-if="moduleType === 'education'">
<div class="profile-edit-inline__fields-row">
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">学校</label><input class="profile-edit-inline__input" placeholder="请输入学校名称" v-model="form.school" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">专业</label><input class="profile-edit-inline__input" placeholder="请输入专业名称" v-model="form.major" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">学历</label><select class="profile-edit-inline__input" v-model="form.degree"><option value="大专">大专</option><option value="本科">本科</option><option value="硕士">硕士</option><option value="博士">博士</option></select></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">学历类型</label><select class="profile-edit-inline__input" v-model="form.studyType"><option value="全日制">全日制</option><option value="非全日制">非全日制</option></select></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">就读时间</label>
<div class="profile-edit-inline__date-range">
<el-date-picker v-model="form.startDate" type="month" placeholder="开始" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
<span class="profile-edit-inline__date-sep">-</span>
<el-date-picker v-model="form.endDate" type="month" placeholder="结束" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
</div>
</div>
</div>
</template>
<!-- 工作经历 -->
<template v-else-if="moduleType === 'work'">
<div class="profile-edit-inline__fields-row">
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">公司名称</label><input class="profile-edit-inline__input" placeholder="请输入公司名称" v-model="form.companyName" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">职位</label><input class="profile-edit-inline__input" placeholder="请输入职位" v-model="form.position" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">时间</label>
<div class="profile-edit-inline__date-range">
<el-date-picker v-model="form.startDate" type="month" placeholder="开始" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
<span class="profile-edit-inline__date-sep">-</span>
<el-date-picker v-model="form.endDate" type="month" placeholder="结束" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
</div>
</div>
</div>
</template>
<!-- 实习经历 -->
<template v-else-if="moduleType === 'internship'">
<div class="profile-edit-inline__fields-row">
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">公司名称</label><input class="profile-edit-inline__input" placeholder="请输入公司名称" v-model="form.companyName" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">职位</label><input class="profile-edit-inline__input" placeholder="请输入职位" v-model="form.position" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">时间</label>
<div class="profile-edit-inline__date-range">
<el-date-picker v-model="form.startDate" type="month" placeholder="开始" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
<span class="profile-edit-inline__date-sep">-</span>
<el-date-picker v-model="form.endDate" type="month" placeholder="结束" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
</div>
</div>
</div>
</template>
<!-- 项目经历 -->
<template v-else-if="moduleType === 'project'">
<div class="profile-edit-inline__fields-row">
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">项目名称</label><input class="profile-edit-inline__input" placeholder="请输入项目名称" v-model="form.projectName" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">项目角色</label><input class="profile-edit-inline__input" placeholder="请输入项目角色" v-model="form.role" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">时间</label>
<div class="profile-edit-inline__date-range">
<el-date-picker v-model="form.startDate" type="month" placeholder="开始" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
<span class="profile-edit-inline__date-sep">-</span>
<el-date-picker v-model="form.endDate" type="month" placeholder="结束" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
</div>
</div>
</div>
</template>
<!-- 竞赛经历 -->
<template v-else-if="moduleType === 'competition'">
<div class="profile-edit-inline__fields-row">
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">竞赛名称</label><input class="profile-edit-inline__input" placeholder="请输入竞赛名称" v-model="form.competitionName" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">获奖名次</label><input class="profile-edit-inline__input" placeholder="请输入获奖名次" v-model="form.award" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">获奖时间</label><el-date-picker v-model="form.awardDate" type="month" placeholder="请选择" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" /></div>
</div>
</template>
<!-- 经历描述 + AI润色 -->
<div class="profile-edit-inline__summary-header">
<label class="profile-edit-inline__label">经历描述</label>
<div class="dib dflex-center w66 h24 lh24 border-ra4 color-f aliite-c bg-linear-1 cursor-po mb4" @click="handleAiPolish" :disabled="aiPolishing">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_589_1500)">
<path d="M4.55306 2.724C4.85206 1.849 6.06106 1.8225 6.41556 2.6445L6.44556 2.7245L6.84906 3.9045C6.94154 4.17512 7.09097 4.42276 7.28728 4.63071C7.48359 4.83867 7.72222 5.00211 7.98706 5.11L8.09556 5.1505L9.27556 5.5535C10.1506 5.8525 10.1771 7.0615 9.35556 7.416L9.27556 7.446L8.09556 7.8495C7.82485 7.94191 7.57712 8.09131 7.36907 8.28763C7.16103 8.48394 6.99751 8.7226 6.88956 8.9875L6.84906 9.0955L6.44606 10.276C6.14706 11.151 4.93806 11.1775 4.58406 10.356L4.55306 10.276L4.15006 9.096C4.05765 8.82529 3.90825 8.57755 3.71193 8.36951C3.51562 8.16146 3.27696 7.99795 3.01206 7.89L2.90406 7.8495L1.72406 7.4465C0.848564 7.1475 0.822064 5.9385 1.64406 5.5845L1.72406 5.5535L2.90406 5.1505C3.17468 5.05803 3.42232 4.9086 3.63028 4.71229C3.83823 4.51597 4.00167 4.27735 4.10956 4.0125L4.15006 3.9045L4.55306 2.724ZM5.49956 3.047L5.09656 4.227C4.95576 4.63965 4.7267 5.01665 4.42533 5.33175C4.12397 5.64684 3.75754 5.89246 3.35156 6.0515L3.22656 6.097L2.04656 6.5L3.22656 6.903C3.63922 7.0438 4.01622 7.27287 4.33131 7.57423C4.64641 7.8756 4.89203 8.24203 5.05106 8.648L5.09656 8.773L5.49956 9.953L5.90256 8.773C6.04337 8.36035 6.27243 7.98335 6.5738 7.66825C6.87516 7.35316 7.24159 7.10754 7.64756 6.9485L7.77256 6.9035L8.95256 6.5L7.77256 6.097C7.35991 5.9562 6.98291 5.72713 6.66782 5.42577C6.35272 5.1244 6.1071 4.75797 5.94806 4.352L5.90306 4.227L5.49956 3.047ZM9.49956 1C9.5931 1 9.68477 1.02624 9.76414 1.07574C9.84351 1.12523 9.90741 1.196 9.94856 1.28L9.97256 1.3385L10.1476 1.8515L10.6611 2.0265C10.7548 2.05835 10.837 2.11731 10.8972 2.19591C10.9574 2.27451 10.9929 2.36921 10.9992 2.46801C11.0055 2.56681 10.9824 2.66527 10.9327 2.75089C10.883 2.83652 10.809 2.90547 10.7201 2.949L10.6611 2.973L10.1481 3.148L9.97306 3.6615C9.94116 3.75521 9.88216 3.83735 9.80353 3.8975C9.7249 3.95764 9.63018 3.9931 9.53138 3.99936C9.43259 4.00563 9.33415 3.98242 9.24856 3.93269C9.16296 3.88296 9.09405 3.80893 9.05056 3.72L9.02656 3.6615L8.85156 3.1485L8.33806 2.9735C8.24432 2.94165 8.16215 2.88269 8.10195 2.80409C8.04175 2.72549 8.00624 2.63079 7.99993 2.53199C7.99361 2.43319 8.01676 2.33473 8.06645 2.24911C8.11615 2.16348 8.19014 2.09453 8.27906 2.051L8.33806 2.027L8.85106 1.852L9.02606 1.3385C9.05978 1.23971 9.12357 1.15395 9.20848 1.09325C9.2934 1.03254 9.39518 0.999937 9.49956 1Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_589_1500">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>
<span class="ml2 fs12 fw600">AI润色</span>
</div>
</div>
<div class="profile-edit-inline__field">
<textarea class="profile-edit-inline__textarea" placeholder="请输入经历描述" v-model="descriptionText" rows="4"></textarea>
</div>
</div>
<!-- AI润色结果保存取消下方 -->
<div v-if="aiPolishResult" class="profile-edit-inline__ai-result">
<div class="profile-edit-inline__ai-result-card">
<p class="profile-edit-inline__ai-result-label"> AI润色结果</p>
<p class="profile-edit-inline__ai-result-text">{{ aiPolishResult }}</p>
<div class="profile-edit-inline__ai-result-actions">
<button class="profile-edit-inline__ai-replace-btn" @click="handleReplaceWithAi">+ 替换为此内容</button>
<button class="profile-edit-inline__ai-regenerate-btn" @click="handleAiPolish" :disabled="aiPolishing"> 重新生成</button>
</div>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="profile-edit-inline__footer mt16">
<button class="profile-edit-inline__save-btn" @click.stop="handleSave">保存</button>
<button class="profile-edit-inline__cancel-btn" @click.stop="handleCancel">取消</button>
</div>
</div>
<!-- 全屏加载遮罩 -->
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
<!-- AI润色步骤进度遮罩 -->
<StepProgressOverlay
v-if="showPolishProgress"
:title="polishProgressTitle"
:subtitle="polishProgressSubtitle"
:steps="polishProgressSteps"
:durations="polishProgressDurations"
:done="polishProgressDone"
@finished="handlePolishProgressFinished"
@close="handlePolishProgressClose"
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { polishDiagnosisIssue } from '@/api/resume'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
import StepProgressOverlay from '@/components/StepProgressOverlay.vue'
/** 描述段落结构 */
interface DescriptionParagraph {
id: string
text: string
}
/** Props */
const props = defineProps<{
/** 经历模块类型 */
moduleType: string
/** 初始数据(编辑已有经历时传入,新增时为空) */
initialData?: Record<string, any>
}>()
/** Emits */
const emit = defineEmits<{
(e: 'save', data: Record<string, any>): void
(e: 'cancel'): void
}>()
/** 表单数据 */
const form = ref<Record<string, any>>({})
/** 经历描述文本(多段用换行拼接) */
const descriptionText = ref('')
/** AI润色结果 */
const aiPolishResult = ref('')
/** AI润色中 */
const aiPolishing = ref(false)
/** 全屏加载是否显示 */
const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
/** 生成短ID */
const generateId = (): string => Math.random().toString(36).substring(2, 8)
/** 初始化表单 */
function initForm() {
aiPolishResult.value = ''
if (props.initialData) {
form.value = { ...props.initialData }
// 将 description 数组拼接为文本
if (props.initialData.description?.length) {
descriptionText.value = props.initialData.description.map((d: DescriptionParagraph) => d.text).join('\n')
} else {
descriptionText.value = ''
}
} else {
// 新增模式 — 空表单
form.value = {}
descriptionText.value = ''
}
}
// 立即初始化
initForm()
// 监听 initialData 变化
watch(() => props.initialData, () => { initForm() }, { deep: true })
/** AI润色 */
async function handleAiPolish() {
const text = descriptionText.value.trim()
if (!text) {
ElMessage.warning('请先输入经历描述')
return
}
aiPolishing.value = true
// 显示步骤进度遮罩
showPolishProgress.value = true
polishProgressDone.value = false
try {
const content = text.split('\n').filter(s => s.trim())
const res = await polishDiagnosisIssue(content)
if (res.code === 0 && res.data?.content) {
pendingPolishResult.value = res.data.content.join('\n')
polishProgressDone.value = true
} else {
showPolishProgress.value = false
aiPolishing.value = false
ElMessage.error(res.msg || '润色失败')
}
} catch {
showPolishProgress.value = false
aiPolishing.value = false
ElMessage.error('请求失败')
}
}
// ==================== AI润色步骤进度遮罩 ====================
/** 润色步骤进度遮罩是否显示 */
const showPolishProgress = ref(false)
/** 润色接口是否已完成 */
const polishProgressDone = ref(false)
/** 大标题 */
const polishProgressTitle = 'Nova正在润色中...'
/** 副标题 */
const polishProgressSubtitle = '正在根据岗位要求润色简历,突出你的核心优势'
/** 4个步骤名称 */
const polishProgressSteps = ['提取核心关键词...', '寻找符合的语义...', '排版文字格式...', '最终生成中...']
/** 每步时长1400毫秒 */
const polishProgressDurations = [1400, 1400, 1400, 1400]
/** 缓存润色结果 */
const pendingPolishResult = ref('')
/** 润色进度走完后填充结果 */
function handlePolishProgressFinished() {
showPolishProgress.value = false
aiPolishing.value = false
if (pendingPolishResult.value) {
aiPolishResult.value = pendingPolishResult.value
ElMessage.success('AI润色完成')
}
pendingPolishResult.value = ''
}
/** 关闭润色进度遮罩 */
function handlePolishProgressClose() {
showPolishProgress.value = false
aiPolishing.value = false
pendingPolishResult.value = ''
}
/** 替换为AI润色内容 */
function handleReplaceWithAi() {
if (aiPolishResult.value) {
descriptionText.value = aiPolishResult.value
}
}
/** 保存 */
function handleSave() {
// 将描述文本按换行拆分为 description 数组
const lines = descriptionText.value.split('\n').filter(s => s.trim())
const description: DescriptionParagraph[] = lines.map(text => ({
id: generateId(),
text,
}))
// 如果有原始 description,保留原有的 id
if (props.initialData?.description?.length) {
props.initialData.description.forEach((d: DescriptionParagraph, i: number) => {
if (i < description.length) {
description[i].id = d.id
}
})
}
emit('save', { ...form.value, description })
}
/** 取消 */
function handleCancel() {
emit('cancel')
}
</script>