新会员页面
This commit is contained in:
+579
-79
@@ -28,26 +28,37 @@
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 状态标签 -->
|
||||
<span class="agent-main__status-tag">准备就绪</span>
|
||||
<span v-if="applyJobList.length > 0" class="agent-main__status-tag">准备就绪</span>
|
||||
<!-- 提示文字 -->
|
||||
<span class="agent-main__status-text">已添加一个岗位,随时可以开始投递</span>
|
||||
<!-- 开始按钮 -->
|
||||
<button class="agent-main__start-btn">开始 ▸</button>
|
||||
<span class="agent-main__status-text">{{ applyJobList.length > 0 ? `已添加${applyJobList.length}个岗位,随时可以开始投递` : '请添加岗位' }}</span>
|
||||
<!-- 开始按钮(投递中时置灰不可点击,暂停时显示继续可点击) -->
|
||||
<button
|
||||
class="agent-main__start-btn"
|
||||
:disabled="isApplying && !isPaused"
|
||||
@click="handleTopBarStartClick"
|
||||
>{{ isApplying ? (isPaused ? '继续' : '投递中...') : '开始 ▸' }}</button>
|
||||
<!-- 右侧工具按钮 -->
|
||||
<div class="agent-main__tools">
|
||||
<!-- 设置按钮 -->
|
||||
<button class="agent-main__tool-btn" title="设置">
|
||||
<!-- 待投递列表按钮 -->
|
||||
<button class="agent-main__tool-btn" title="待投递列表" @click.stop="toggleTaskListDropdown">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 待投递列表下拉弹窗 -->
|
||||
<AgentTaskListDropdown
|
||||
v-if="showTaskListDropdown"
|
||||
:pending-list="applyJobList"
|
||||
@removed="handleTaskRemoved"
|
||||
@view-all="showTaskListDropdown = false"
|
||||
/>
|
||||
<!-- 齿轮按钮 -->
|
||||
<button class="agent-main__tool-btn" title="配置">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M12 1v2m0 18v2m-9-11h2m18 0h2m-3.3-7.7-1.4 1.4M4.7 19.3l1.4-1.4m0-11.8L4.7 4.7m14.6 14.6-1.4-1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- <button class="agent-main__tool-btn" title="配置">-->
|
||||
<!-- <svg viewBox="0 0 24 24" fill="none">-->
|
||||
<!-- <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5" />-->
|
||||
<!-- <path d="M12 1v2m0 18v2m-9-11h2m18 0h2m-3.3-7.7-1.4 1.4M4.7 19.3l1.4-1.4m0-11.8L4.7 4.7m14.6 14.6-1.4-1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />-->
|
||||
<!-- </svg>-->
|
||||
<!-- </button>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,16 +80,26 @@
|
||||
<div v-else-if="msg.type === 'recommend'" class="agent-main__chat-row">
|
||||
<!-- 如果 extra 里有岗位数据,用岗位列表组件展示 -->
|
||||
<AgentChatJobList
|
||||
v-if="getRecommendJobsFromExtra(msg.extra).length > 0"
|
||||
:summary="getRecommendSummaryFromExtra(msg.extra)"
|
||||
:jobs="getRecommendJobsFromExtra(msg.extra)"
|
||||
@view-all="handleOpenRecommendPanel(msg.id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 投递进度消息 -->
|
||||
<div v-else-if="msg.type === 'apply_progress'" class="agent-main__chat-row">
|
||||
<div class="agent-main__chat-bubble">{{ msg.content }}</div>
|
||||
<!-- 投递进度消息 — 仅显示最新 recommend 之后的 apply_progress -->
|
||||
<div v-else-if="msg.type === 'apply_progress' && isVisibleApplyProgress(msg.id)" class="agent-main__chat-row">
|
||||
<AgentApplyProgress
|
||||
:job-info="getApplyProgressJobInfo(msg.extra)"
|
||||
:current-step="getApplyProgressStep(msg.extra)"
|
||||
:resume-name="getApplyProgressResumeName(msg.extra)"
|
||||
:paused="isPaused"
|
||||
@cancel="handleCancelApply(msg)"
|
||||
@confirm-resume="handleConfirmResumeStep(msg)"
|
||||
@skip="handleSkipApply(msg)"
|
||||
@applied="handleAppliedStep(msg)"
|
||||
@go-apply="handleGoApplyStep(msg)"
|
||||
@toggle-pause="handleTogglePause"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -105,9 +126,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧匹配岗位添加面板 -->
|
||||
<!-- 右侧匹配岗位添加面板 / 简历生成进度 / 简历预览 -->
|
||||
<div v-if="showRightPanel" class="agent-main__right">
|
||||
<!-- 模式1:匹配岗位添加面板 -->
|
||||
<AgentMatchJobAdd
|
||||
v-if="rightPanelMode === 'recommend'"
|
||||
:jobs="activeRecommendJobs"
|
||||
:loading-job-ids="loadingJobIds"
|
||||
:panel-loading="panelLoading"
|
||||
@@ -116,6 +139,21 @@
|
||||
@toggle="handleToggleJobApply"
|
||||
@add-all="handleAddAllJobs"
|
||||
/>
|
||||
<!-- 模式2:简历生成进度 -->
|
||||
<div v-else-if="rightPanelMode === 'generating'" class="agent-resume-generating">
|
||||
<div class="agent-resume-generating__progress-bar">
|
||||
<div class="agent-resume-generating__progress-fill" :style="{ width: generateProgress + '%' }"></div>
|
||||
</div>
|
||||
<div class="agent-resume-generating__title">正在生成岗位专属简历</div>
|
||||
<div class="agent-resume-generating__hint">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="agent-resume-generating__hint-icon"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/><path d="M8 5v3M8 10.5v.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||
<span>生成简历大约需要10-20秒</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模式3:简历预览 -->
|
||||
<div v-else-if="rightPanelMode === 'resume'" class="agent-main__right-resume">
|
||||
<JobResumeTemplate :resume-data="applyResumeData" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,12 +166,16 @@ import SideNav from '@/components/SideNav.vue'
|
||||
import AgentSetupWizard from '@/components/AgentSetupWizard.vue'
|
||||
import AgentChatJobList from '@/components/AgentChatJobList.vue'
|
||||
import AgentMatchJobAdd from '@/components/AgentMatchJobAdd.vue'
|
||||
import { fetchAgentConfig, saveAgentConfig, fetchAgentRecommend, fetchAgentChatMessages, addAgentChatMessage, applyJob, cancelApplyJob, fetchApplyByJobIds, sendAgentChat } from '@/api/agent'
|
||||
import AgentApplyProgress from '@/components/AgentApplyProgress.vue'
|
||||
import AgentTaskListDropdown from '@/components/AgentTaskListDropdown.vue'
|
||||
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
||||
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
||||
import { fetchAgentConfig, saveAgentConfig, fetchAgentRecommend, fetchAgentChatMessages, addAgentChatMessage, applyJob, cancelApplyJob, fetchApplyByJobIds, sendAgentChat, optimizeAgentResume } from '@/api/agent'
|
||||
import type { AgentConfig, AgentRecommendJob, AgentChatMessage, AgentChatHistoryItem } from '@/api/agent'
|
||||
import { fetchApplyList } from '@/api/jobs'
|
||||
import { fetchAgentTaskList } from '@/api/jobs'
|
||||
import type { JobListItem } from '@/api/jobs'
|
||||
import { fetchResumeList } from '@/api/resume'
|
||||
import store from '@/stores'
|
||||
import { getIntentionCategoryNames, getIntentionRegionNames, getIntentionIndustryNames } from '@/utils/intention'
|
||||
import AiThinkingIndicator from '@/components/tools/AiThinkingIndicator.vue'
|
||||
|
||||
/** 页面初始加载状态 */
|
||||
@@ -165,9 +207,12 @@ const activeRecommendJobs = computed(() => {
|
||||
/** 聊天输入框内容 */
|
||||
const chatInput = ref('')
|
||||
|
||||
/** 投递列表数据 */
|
||||
/** 待投递列表数据 */
|
||||
const applyJobList = ref<JobListItem[]>([])
|
||||
|
||||
/** 待投递列表下拉弹窗是否显示 */
|
||||
const showTaskListDropdown = ref(false)
|
||||
|
||||
/** 正在请求中的岗位 ID 列表(传给子组件控制按钮 loading) */
|
||||
const loadingJobIds = ref<number[]>([])
|
||||
|
||||
@@ -178,7 +223,15 @@ const panelLoading = ref(false)
|
||||
const defaultResumeId = ref('')
|
||||
|
||||
/** 是否正在发送消息(防止重复发送 + 显示等待状态) */
|
||||
const isSending = ref(false)/** 聊天区域 DOM 引用 */
|
||||
const isSending = ref(false)
|
||||
|
||||
/** 是否正在投递流程中(防止重复点击开始按钮) */
|
||||
const isApplying = ref(false)
|
||||
|
||||
/** 是否暂停投递 */
|
||||
const isPaused = ref(false)
|
||||
|
||||
/** 聊天区域 DOM 引用 */
|
||||
const chatAreaRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/** 滚动聊天区域到底部(平滑滚动) */
|
||||
@@ -191,8 +244,15 @@ function scrollChatToBottom() {
|
||||
/** 页面挂载时查询配置 */
|
||||
onMounted(async () => {
|
||||
await loadAgentConfig()
|
||||
// 点击页面其他区域关闭待投递列表下拉
|
||||
document.addEventListener('click', closeTaskDropdownOnClickOutside)
|
||||
})
|
||||
|
||||
/** 点击外部关闭待投递列表下拉 */
|
||||
function closeTaskDropdownOnClickOutside() {
|
||||
showTaskListDropdown.value = false
|
||||
}
|
||||
|
||||
/** 查询求职助手配置 */
|
||||
async function loadAgentConfig() {
|
||||
pageLoading.value = true
|
||||
@@ -227,6 +287,20 @@ async function loadRecommendJobs() {
|
||||
if (res.code === '0' && res.data && res.data.list?.length > 0) {
|
||||
const summary = res.data.summary || ''
|
||||
const jobList = res.data.list
|
||||
|
||||
// if(jobList.length==0){
|
||||
// const aiNow = Math.floor(Date.now() / 1000)
|
||||
// chatMessages.value.push({
|
||||
// id: Date.now() + 1,
|
||||
// type: 'assistant',
|
||||
// content: '暂无相关职位推荐',
|
||||
// extra: '',
|
||||
// createTime: { seconds: aiNow, nanos: 0 },
|
||||
// })
|
||||
// try {
|
||||
// await addAgentChatMessage({ type: 'assistant', content: '暂无相关职位推荐' })
|
||||
// } catch { /* 静默 */ }
|
||||
// }
|
||||
/* 将完整岗位数据序列化为 extra JSON */
|
||||
const extraJson = JSON.stringify({ summary, list: jobList })
|
||||
|
||||
@@ -275,15 +349,31 @@ async function loadChatMessages() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载岗位投递列表 */
|
||||
/** 加载求职助手任务列表(tab=1 进行中/待投递) */
|
||||
async function loadApplyList() {
|
||||
try {
|
||||
const res = await fetchApplyList({ pageNum: 1, pageSize: 100,status:0 })
|
||||
const res = await fetchAgentTaskList({ pageNum: 1, pageSize: 100, tab: 1 })
|
||||
if (res.code === '0' && res.data) {
|
||||
applyJobList.value = res.data.list || []
|
||||
}
|
||||
} catch {
|
||||
console.error('[Agent] 加载投递列表失败')
|
||||
console.error('[Agent] 加载求职助手任务列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换待投递列表下拉弹窗显隐 */
|
||||
function toggleTaskListDropdown() {
|
||||
showTaskListDropdown.value = !showTaskListDropdown.value
|
||||
if (showTaskListDropdown.value) {
|
||||
loadApplyList()
|
||||
}
|
||||
}
|
||||
|
||||
/** 岗位从待投递列表中移除后的回调 */
|
||||
function handleTaskRemoved(jobId: string) {
|
||||
const idx = applyJobList.value.findIndex(j => j.id === jobId)
|
||||
if (idx !== -1) {
|
||||
applyJobList.value.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,58 +410,6 @@ function buildChatHistory(): AgentChatHistoryItem[] {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 store 中获取意向岗位类型名称列表
|
||||
* 根据 jobIntention.categoryIds 在 jobCategories 树中查找名称
|
||||
*/
|
||||
function getIntentionCategoryNames(): string[] {
|
||||
const ids = store.state.jobIntention.categoryIds || []
|
||||
if (ids.length === 0) return []
|
||||
const names: string[] = []
|
||||
for (const group of store.state.jobCategories) {
|
||||
for (const child of group.children || []) {
|
||||
for (const leaf of child.children || []) {
|
||||
if (ids.includes(Number(leaf.id))) names.push(leaf.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 store 中获取意向城市名称列表
|
||||
* 根据 jobIntention.regionCodes 在 regions 树中查找名称
|
||||
*/
|
||||
function getIntentionRegionNames(): string[] {
|
||||
const codes = store.state.jobIntention.regionCodes || []
|
||||
if (codes.length === 0) return []
|
||||
const names: string[] = []
|
||||
for (const province of store.state.regions) {
|
||||
if (codes.includes(province.code)) { names.push(province.name); continue }
|
||||
for (const city of province.children || []) {
|
||||
if (codes.includes(city.code)) names.push(city.name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 store 中获取意向行业名称列表
|
||||
* 根据 jobIntention.industryIds 在 industries 树中查找名称
|
||||
*/
|
||||
function getIntentionIndustryNames(): string[] {
|
||||
const ids = store.state.jobIntention.industryIds || []
|
||||
if (ids.length === 0) return []
|
||||
const names: string[] = []
|
||||
for (const group of store.state.industries) {
|
||||
if (ids.includes(Number(group.id))) { names.push(group.name); continue }
|
||||
for (const child of group.children || []) {
|
||||
if (ids.includes(Number(child.id))) names.push(child.name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/**
|
||||
* 从消息的 extra JSON 中解析推荐岗位列表
|
||||
* extra 格式预期为 JSON 字符串,包含 list 字段
|
||||
@@ -478,6 +516,7 @@ async function handleToggleJobApply(job: AgentRecommendJob) {
|
||||
}
|
||||
/* 更新对话消息中的岗位状态 */
|
||||
updateJobStatusInMessages(job.id, isRemoving ? null : -1)
|
||||
await loadApplyList()
|
||||
ElMessage.success(isRemoving ? '岗位已从待投递移除' : '岗位已添加到待投递')
|
||||
} catch {
|
||||
ElMessage.error('操作失败,请重试')
|
||||
@@ -507,6 +546,7 @@ async function handleAddAllJobs() {
|
||||
loadingJobIds.value = loadingJobIds.value.filter(id => id !== job.id)
|
||||
}
|
||||
}
|
||||
await loadApplyList()
|
||||
ElMessage.success('已全部添加到待投递')
|
||||
}
|
||||
|
||||
@@ -568,6 +608,451 @@ async function handleOpenRecommendPanel(msgId: number) {
|
||||
function handleCloseRecommendPanel() {
|
||||
showRightPanel.value = false
|
||||
activeRecommendMsgId.value = null
|
||||
rightPanelMode.value = 'recommend'
|
||||
}
|
||||
|
||||
// ==================== 投递进度流程 ====================
|
||||
|
||||
/** 右侧面板显示模式:recommend-岗位推荐 / generating-简历生成中 / resume-简历预览 */
|
||||
const rightPanelMode = ref<'recommend' | 'generating' | 'resume'>('recommend')
|
||||
|
||||
/** 简历生成进度百分比(0-100) */
|
||||
const generateProgress = ref(0)
|
||||
|
||||
/** 当前投递流程的简历数据 */
|
||||
const applyResumeData = ref<ResumeTemplateData>({} as ResumeTemplateData)
|
||||
|
||||
/** 当前投递流程的简历名称 */
|
||||
const applyResumeName = ref('')
|
||||
|
||||
/** 进度条定时器 */
|
||||
let progressTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
/** 启动模拟进度条(慢慢增长到 90%,接口返回后快速涨满) */
|
||||
function startProgressSimulation() {
|
||||
generateProgress.value = 0
|
||||
let elapsed = 0
|
||||
progressTimer = setInterval(() => {
|
||||
elapsed += 300
|
||||
// 前 5 秒涨到 30%,5-15 秒涨到 70%,15-25 秒涨到 90%
|
||||
if (generateProgress.value < 30) {
|
||||
generateProgress.value += 2
|
||||
} else if (generateProgress.value < 70) {
|
||||
generateProgress.value += 1
|
||||
} else if (generateProgress.value < 90) {
|
||||
generateProgress.value += 0.5
|
||||
}
|
||||
// 最多到 90%
|
||||
if (generateProgress.value >= 90) {
|
||||
generateProgress.value = 90
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
/** 停止进度条并涨满 */
|
||||
function finishProgress() {
|
||||
if (progressTimer) {
|
||||
clearInterval(progressTimer)
|
||||
progressTimer = null
|
||||
}
|
||||
generateProgress.value = 100
|
||||
}
|
||||
|
||||
/** 学历文字转数字(接口返回中文,模板需要数字) */
|
||||
function degreeToNumber(degree?: string): number {
|
||||
const map: Record<string, number> = { '大专': 1, '本科': 2, '硕士': 3, '博士': 4 }
|
||||
return map[degree || ''] || 2
|
||||
}
|
||||
|
||||
/** 将优化简历接口返回数据映射为 ResumeTemplateData 格式 */
|
||||
function mapOptimizeResumeToTemplate(data: any): ResumeTemplateData {
|
||||
const r = data.resume || {}
|
||||
return {
|
||||
name: r.name || '未填写姓名',
|
||||
email: r.email || '',
|
||||
mobileNumber: r.mobileNumber || '',
|
||||
wechatNumber: r.wechatNumber || '',
|
||||
summary: r.summary || '',
|
||||
educations: (data.education || []).map((e: any) => ({
|
||||
school: e.school || '',
|
||||
major: e.major || '',
|
||||
degree: degreeToNumber(e.degree),
|
||||
startDate: e.startDate || '',
|
||||
endDate: e.endDate || '',
|
||||
description: (e.description || []).map((d: any) => ({ id: d.id, text: d.text || '' })),
|
||||
})),
|
||||
workExperiences: (data.work || []).map((w: any) => ({
|
||||
companyName: w.companyName || '',
|
||||
position: w.position || '',
|
||||
startDate: w.startDate || '',
|
||||
endDate: w.endDate || '',
|
||||
description: (w.description || []).map((d: any) => ({ id: d.id, text: d.text || '' })),
|
||||
})),
|
||||
internships: (data.internship || []).map((i: any) => ({
|
||||
companyName: i.companyName || '',
|
||||
position: i.position || '',
|
||||
startDate: i.startDate || '',
|
||||
endDate: i.endDate || '',
|
||||
description: (i.description || []).map((d: any) => ({ id: d.id, text: d.text || '' })),
|
||||
})),
|
||||
projects: (data.project || []).map((p: any) => ({
|
||||
projectName: p.projectName || '',
|
||||
companyName: p.companyName || '',
|
||||
role: p.role || '',
|
||||
startDate: p.startDate || '',
|
||||
endDate: p.endDate || '',
|
||||
description: (p.description || []).map((d: any) => ({ id: d.id, text: d.text || '' })),
|
||||
})),
|
||||
competitions: (data.competition || []).map((c: any) => ({
|
||||
competitionName: c.competitionName || '',
|
||||
award: c.award || '',
|
||||
awardDate: c.awardDate || '',
|
||||
description: (c.description || []).map((d: any) => ({ id: d.id, text: d.text || '' })),
|
||||
})),
|
||||
skills: r.skills || [],
|
||||
certificates: r.certificates || [],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击"开始"按钮 — 启动投递流程
|
||||
* 1. 刷新待投递列表
|
||||
* 2. 取第一个岗位
|
||||
* 3. 创建 apply_progress 消息
|
||||
* 4. 开始第1步:优化简历
|
||||
*/
|
||||
async function handleStartApply() {
|
||||
// 防抖:投递中不可重复点击
|
||||
if (isApplying.value) return
|
||||
isApplying.value = true
|
||||
|
||||
// 刷新待投递列表
|
||||
await loadApplyList()
|
||||
|
||||
if (applyJobList.value.length === 0) {
|
||||
ElMessage.warning('暂无待投递岗位,请先添加岗位')
|
||||
isApplying.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 取第一个岗位
|
||||
const targetJob = applyJobList.value[0]
|
||||
|
||||
// 构建 extra 数据
|
||||
const extraData = {
|
||||
jobInfo: targetJob,
|
||||
resumeInfo: null as any,
|
||||
step: 1,
|
||||
}
|
||||
|
||||
// 创建 apply_progress 消息并追加到对话列表
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const applyMsg: AgentChatMessage = {
|
||||
id: Date.now(),
|
||||
type: 'apply_progress',
|
||||
content: '',
|
||||
extra: JSON.stringify(extraData),
|
||||
createTime: { seconds: now, nanos: 0 },
|
||||
}
|
||||
chatMessages.value.push(applyMsg)
|
||||
|
||||
await nextTick()
|
||||
scrollChatToBottom()
|
||||
|
||||
// ========== 第1步:优化简历 ==========
|
||||
// 打开右侧面板,显示生成进度
|
||||
rightPanelMode.value = 'generating'
|
||||
showRightPanel.value = true
|
||||
startProgressSimulation()
|
||||
|
||||
try {
|
||||
const res = await optimizeAgentResume({
|
||||
resumeId: defaultResumeId.value,
|
||||
jobId: targetJob.id,
|
||||
})
|
||||
|
||||
// 接口返回后涨满进度条
|
||||
finishProgress()
|
||||
|
||||
// 等待 1 秒让用户看到 100%
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 解析简历数据 — 映射接口返回格式到 ResumeTemplateData
|
||||
const apiData = res?.data || res
|
||||
if (apiData) {
|
||||
const resumeResult = mapOptimizeResumeToTemplate(apiData)
|
||||
// 保存简历数据
|
||||
applyResumeData.value = resumeResult
|
||||
applyResumeName.value = resumeResult.name || '岗位专属简历'
|
||||
|
||||
// 更新 extra:存入 resumeInfo,step 进到 2
|
||||
extraData.resumeInfo = apiData
|
||||
extraData.step = 2
|
||||
applyMsg.extra = JSON.stringify(extraData)
|
||||
|
||||
// 切换右侧面板为简历预览
|
||||
rightPanelMode.value = 'resume'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Agent] 优化简历失败', e)
|
||||
ElMessage.error('简历优化失败,请重试')
|
||||
finishProgress()
|
||||
showRightPanel.value = false
|
||||
rightPanelMode.value = 'recommend'
|
||||
isApplying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== apply_progress 消息辅助方法 ====================
|
||||
|
||||
/** 从 extra 中解析岗位信息 */
|
||||
function getApplyProgressJobInfo(extra: string): JobListItem | null {
|
||||
try {
|
||||
const parsed = JSON.parse(extra)
|
||||
return parsed.jobInfo || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 extra 中解析当前步骤 */
|
||||
function getApplyProgressStep(extra: string): number {
|
||||
try {
|
||||
const parsed = JSON.parse(extra)
|
||||
return parsed.step || 1
|
||||
} catch {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 extra 中解析简历名称 */
|
||||
function getApplyProgressResumeName(extra: string): string {
|
||||
try {
|
||||
const parsed = JSON.parse(extra)
|
||||
return parsed.resumeInfo?.name || '岗位专属简历'
|
||||
} catch {
|
||||
return '岗位专属简历'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* apply_progress 显示模式
|
||||
* 'afterRecommend' — 显示最新 recommend 之后的所有 apply_progress
|
||||
* 'latestOnly' — 只显示最新一条 apply_progress
|
||||
* 切换此变量即可手动切换显示效果
|
||||
*/
|
||||
const applyProgressDisplayMode = ref<'afterRecommend' | 'latestOnly'>('latestOnly')
|
||||
|
||||
/**
|
||||
* 判断 apply_progress 消息是否应该显示
|
||||
*/
|
||||
function isVisibleApplyProgress(msgId: number): boolean {
|
||||
if (applyProgressDisplayMode.value === 'latestOnly') {
|
||||
// 只显示最新一条 apply_progress
|
||||
for (let i = chatMessages.value.length - 1; i >= 0; i--) {
|
||||
if (chatMessages.value[i].type === 'apply_progress') {
|
||||
return chatMessages.value[i].id === msgId
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// afterRecommend 模式:显示最新 recommend 之后的所有 apply_progress
|
||||
let lastRecommendIdx = -1
|
||||
for (let i = chatMessages.value.length - 1; i >= 0; i--) {
|
||||
if (chatMessages.value[i].type === 'recommend') {
|
||||
lastRecommendIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
const msgIdx = chatMessages.value.findIndex(m => m.id === msgId)
|
||||
if (lastRecommendIdx === -1) return true
|
||||
return msgIdx > lastRecommendIdx
|
||||
}
|
||||
|
||||
// ==================== apply_progress 步骤事件处理 ====================
|
||||
|
||||
/** 顶部栏按钮点击 — 未投递时开始,暂停时恢复 */
|
||||
function handleTopBarStartClick() {
|
||||
if (isApplying.value && isPaused.value) {
|
||||
// 恢复投递
|
||||
isPaused.value = false
|
||||
} else if (!isApplying.value) {
|
||||
// 开始新的投递
|
||||
handleStartApply()
|
||||
}
|
||||
}
|
||||
|
||||
/** 暂停/恢复投递(由 AgentApplyProgress 组件触发) */
|
||||
function handleTogglePause() {
|
||||
isPaused.value = !isPaused.value
|
||||
}
|
||||
|
||||
/** 取消投递流程 */
|
||||
function handleCancelApply(msg: AgentChatMessage) {
|
||||
// 从消息列表中移除该条 apply_progress
|
||||
const idx = chatMessages.value.findIndex(m => m.id === msg.id)
|
||||
if (idx !== -1) {
|
||||
chatMessages.value.splice(idx, 1)
|
||||
}
|
||||
// 关闭右侧面板
|
||||
showRightPanel.value = false
|
||||
rightPanelMode.value = 'recommend'
|
||||
// 投递流程结束
|
||||
isApplying.value = false
|
||||
}
|
||||
|
||||
/** 第2步:确认简历 */
|
||||
function handleConfirmResumeStep(msg: AgentChatMessage) {
|
||||
try {
|
||||
const extraData = JSON.parse(msg.extra)
|
||||
// 勾选第2步,进入第3步
|
||||
extraData.step = 3
|
||||
msg.extra = JSON.stringify(extraData)
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
|
||||
/** 第3步:跳过投递 */
|
||||
function handleSkipApply(msg: AgentChatMessage) {
|
||||
try {
|
||||
const extraData = JSON.parse(msg.extra)
|
||||
// 勾选第3步,进入第4步(自动完成)
|
||||
extraData.step = 4
|
||||
msg.extra = JSON.stringify(extraData)
|
||||
// 持久化保存
|
||||
persistApplyProgressMessage(msg)
|
||||
// 自动开始下一个岗位投递(不关闭右侧面板,避免闪烁)
|
||||
autoStartNextApply()
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
|
||||
/** 第3步:我已投递 — 调用 /job/apply 接口 */
|
||||
async function handleAppliedStep(msg: AgentChatMessage) {
|
||||
try {
|
||||
const extraData = JSON.parse(msg.extra)
|
||||
const jobInfo = extraData.jobInfo
|
||||
if (!jobInfo) return
|
||||
|
||||
// 调用投递接口,status=0 表示已投递,jobId 传字符串避免大整数精度丢失
|
||||
const res = await applyJob({ jobId: String(jobInfo.id), status: 0 })
|
||||
if (res.code === '0') {
|
||||
// 勾选第3步,进入第4步
|
||||
extraData.step = 4
|
||||
msg.extra = JSON.stringify(extraData)
|
||||
ElMessage.success('投递成功')
|
||||
// 持久化保存
|
||||
persistApplyProgressMessage(msg)
|
||||
// 自动开始下一个岗位投递(不关闭右侧面板,避免闪烁)
|
||||
autoStartNextApply()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Agent] 投递失败', e)
|
||||
ElMessage.error('投递失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
/** 第3步:去投递 — 打开原链接 */
|
||||
function handleGoApplyStep(msg: AgentChatMessage) {
|
||||
try {
|
||||
const extraData = JSON.parse(msg.extra)
|
||||
const sourceUrl = extraData.jobInfo?.sourceUrl
|
||||
if (sourceUrl) {
|
||||
window.open(sourceUrl, '_blank')
|
||||
}
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
|
||||
/** 持久化保存 apply_progress 消息到后端 */
|
||||
async function persistApplyProgressMessage(msg: AgentChatMessage) {
|
||||
try {
|
||||
await addAgentChatMessage({
|
||||
type: 'apply_progress',
|
||||
content: msg.content,
|
||||
extra: msg.extra,
|
||||
})
|
||||
} catch {
|
||||
console.error('[Agent] 保存投递进度消息失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动开始下一个岗位投递
|
||||
* 当前岗位完成(step=4)后,如果未暂停,自动刷新列表并开始下一个
|
||||
*/
|
||||
async function autoStartNextApply() {
|
||||
// 如果已暂停,不自动继续
|
||||
if (isPaused.value) {
|
||||
isApplying.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 刷新待投递列表
|
||||
await loadApplyList()
|
||||
|
||||
if (applyJobList.value.length === 0) {
|
||||
// 没有更多待投递岗位,结束流程
|
||||
isApplying.value = false
|
||||
showRightPanel.value = false
|
||||
rightPanelMode.value = 'recommend'
|
||||
ElMessage.success('所有待投递岗位已处理完毕')
|
||||
return
|
||||
}
|
||||
|
||||
// 取下一个岗位,开始新一轮投递
|
||||
const targetJob = applyJobList.value[0]
|
||||
const extraData = {
|
||||
jobInfo: targetJob,
|
||||
resumeInfo: null as any,
|
||||
step: 1,
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const applyMsg: AgentChatMessage = {
|
||||
id: Date.now(),
|
||||
type: 'apply_progress',
|
||||
content: '',
|
||||
extra: JSON.stringify(extraData),
|
||||
createTime: { seconds: now, nanos: 0 },
|
||||
}
|
||||
chatMessages.value.push(applyMsg)
|
||||
|
||||
await nextTick()
|
||||
scrollChatToBottom()
|
||||
|
||||
// 第1步:优化简历
|
||||
rightPanelMode.value = 'generating'
|
||||
showRightPanel.value = true
|
||||
startProgressSimulation()
|
||||
|
||||
try {
|
||||
const res = await optimizeAgentResume({
|
||||
resumeId: defaultResumeId.value,
|
||||
jobId: targetJob.id,
|
||||
})
|
||||
|
||||
finishProgress()
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
const apiData = res?.data || res
|
||||
if (apiData) {
|
||||
const resumeResult = mapOptimizeResumeToTemplate(apiData)
|
||||
applyResumeData.value = resumeResult
|
||||
applyResumeName.value = resumeResult.name || '岗位专属简历'
|
||||
|
||||
extraData.resumeInfo = apiData
|
||||
extraData.step = 2
|
||||
applyMsg.extra = JSON.stringify(extraData)
|
||||
|
||||
rightPanelMode.value = 'resume'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Agent] 优化简历失败', e)
|
||||
ElMessage.error('简历优化失败,请重试')
|
||||
finishProgress()
|
||||
showRightPanel.value = false
|
||||
rightPanelMode.value = 'recommend'
|
||||
isApplying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 发送聊天消息 */
|
||||
@@ -644,11 +1129,11 @@ async function handleSendMessage() {
|
||||
const excludeJobIds = collectExcludeJobIds()
|
||||
try {
|
||||
const recRes = await fetchAgentRecommend({ preference, excludeJobIds })
|
||||
if (recRes.code === '0' && recRes.data && recRes.data.list?.length > 0) {
|
||||
if (recRes.code === '0' && recRes.data ) {
|
||||
const summary = recRes.data.summary || ''
|
||||
const jobList = recRes.data.list
|
||||
const extraJson = JSON.stringify({ summary, list: jobList })
|
||||
|
||||
// const contentV = recRes.data.list?.length==0?'':''
|
||||
/* 持久化 recommend 消息 */
|
||||
try {
|
||||
await addAgentChatMessage({ type: 'recommend', content: '', extra: extraJson })
|
||||
@@ -664,6 +1149,21 @@ async function handleSendMessage() {
|
||||
createTime: { seconds: recNow, nanos: 0 },
|
||||
})
|
||||
|
||||
// await nextTick()
|
||||
// scrollChatToBottom()
|
||||
// } else {
|
||||
// /* 推荐列表为空 — 插入提示消息 */
|
||||
// const emptyNow = Math.floor(Date.now() / 1000)
|
||||
// chatMessages.value.push({
|
||||
// id: Date.now() + 2,
|
||||
// type: 'assistant',
|
||||
// content: '暂无相关职位推荐',
|
||||
// extra: '',
|
||||
// createTime: { seconds: emptyNow, nanos: 0 },
|
||||
// })
|
||||
// try {
|
||||
// await addAgentChatMessage({ type: 'assistant', content: '暂无相关职位推荐' })
|
||||
// } catch { /* 静默 */ }
|
||||
await nextTick()
|
||||
scrollChatToBottom()
|
||||
}
|
||||
|
||||
+9
-25
@@ -28,32 +28,16 @@
|
||||
<p class="home-hero__desc">智能匹配职位、自动填写申请、量身定制简历、推荐内部人脉——不到1分钟,统统搞定!</p>
|
||||
<button class="home-hero__cta" @click="router.push('/jobs')">免费体验</button>
|
||||
</div>
|
||||
<!-- 右侧视频展示区 -->
|
||||
<div class="home-hero__right">
|
||||
<div class="home-hero__card">
|
||||
<div class="home-hero__card-dots">
|
||||
<span class="dot dot--red"></span>
|
||||
<span class="dot dot--yellow"></span>
|
||||
<span class="dot dot--green"></span>
|
||||
</div>
|
||||
<div class="home-hero__card-body">
|
||||
<div class="home-hero__card-user">
|
||||
<div class="home-hero__card-avatar"></div>
|
||||
<div class="home-hero__card-info">
|
||||
<h4>求职者 Alex</h4>
|
||||
<div class="home-hero__card-tags">
|
||||
<span class="tag">React</span>
|
||||
<span class="tag">Tailwind</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-hero__card-match">98%</div>
|
||||
</div>
|
||||
<div class="home-hero__card-lines">
|
||||
<div class="line-full"></div>
|
||||
<div class="line-3q"></div>
|
||||
</div>
|
||||
<div class="home-hero__card-result">为您匹配了 5 个高星职位</div>
|
||||
</div>
|
||||
</div>
|
||||
<video
|
||||
class="home-hero__video"
|
||||
src="https://jsxq-image-static.oss-cn-shenzhen.aliyuncs.com/aiJob/find/open-intention-video.mp4"
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+1
-1
@@ -550,7 +550,7 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
/** 弹出菜单操作项 */
|
||||
const popupActions = ['从列表中移除', '已投递', '复制链接', '问题反馈']
|
||||
const popupActions = ['从列表中移除', '已投递', '复制链接']
|
||||
|
||||
// ==================== 职位列表项(扩展接口字段,增加前端交互状态) ====================
|
||||
|
||||
|
||||
Reference in New Issue
Block a user