1426 lines
47 KiB
Vue
1426 lines
47 KiB
Vue
<template>
|
||
<!-- 求职助手页面 -->
|
||
<div class="agent-page dflex">
|
||
<!-- 左侧导航栏 -->
|
||
<SideNav />
|
||
|
||
<!-- 主内容区域 -->
|
||
<div class="agent-page__content" v-loading="pageLoading" element-loading-text="加载中...">
|
||
<!-- 准备向导 — 第1步到第4步 -->
|
||
<AgentSetupWizard
|
||
v-if="!pageLoading && showSetupWizard"
|
||
:initial-config="agentConfig"
|
||
@complete="handleSetupComplete"
|
||
@launch="handleLaunch"
|
||
/>
|
||
|
||
<!-- 求职助手正式内容区域 -->
|
||
<div v-else-if="!pageLoading" class="agent-main" :class="{ 'agent-main--with-panel': showRightPanel }">
|
||
<!-- 左侧主区域 -->
|
||
<div class="agent-main__left">
|
||
<!-- 顶部固定设置栏 -->
|
||
<div class="agent-main__top-bar">
|
||
<!-- 用户头像 -->
|
||
<div class="agent-main__avatar">
|
||
<svg viewBox="0 0 24 24" fill="none">
|
||
<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.5" />
|
||
<path d="M4 20c0-3.3 3.6-6 8-6s8 2.7 8 6" stroke="currentColor" stroke-width="1.5" />
|
||
</svg>
|
||
</div>
|
||
<!-- 状态标签 -->
|
||
<span v-if="applyJobList.length > 0" class="agent-main__status-tag">准备就绪</span>
|
||
<!-- 提示文字 -->
|
||
<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="待投递列表" @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="handleOpenPendingListPanel"
|
||
@view-all-completed="handleOpenApplyProgressPanel"
|
||
/>
|
||
<!-- 设置按钮 -->
|
||
<button class="agent-main__tool-btn" title="配置" @click="handleOpenSettingsPanel">
|
||
设置
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 中间聊天记录区域(可滚动) -->
|
||
<div ref="chatAreaRef" class="agent-main__chat-area">
|
||
<!-- 遍历对话消息列表 -->
|
||
<template v-for="msg in chatMessages" :key="msg.id">
|
||
<!-- 用户消息 — 右对齐 -->
|
||
<div v-if="msg.type === 'user'" class="agent-main__chat-row agent-main__chat-row--user">
|
||
<div class="agent-main__chat-bubble agent-main__chat-bubble--user">{{ msg.content }}</div>
|
||
</div>
|
||
|
||
<!-- AI 助手消息 — 左对齐 -->
|
||
<div v-else-if="msg.type === 'assistant'" class="agent-main__chat-row">
|
||
<div class="agent-main__chat-bubble">{{ msg.content }}</div>
|
||
</div>
|
||
|
||
<!-- 推荐岗位消息 — 渲染岗位列表组件 -->
|
||
<div v-else-if="msg.type === 'recommend'" class="agent-main__chat-row">
|
||
<!-- 如果 extra 里有岗位数据,用岗位列表组件展示 -->
|
||
<AgentChatJobList
|
||
:summary="getRecommendSummaryFromExtra(msg.extra)"
|
||
:jobs="getRecommendJobsFromExtra(msg.extra)"
|
||
@view-all="handleOpenRecommendPanel(msg.id)"
|
||
@click-job="handleChatJobClick"
|
||
/>
|
||
</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>
|
||
|
||
<!-- 会话岗位列表组件(来自推荐接口,非历史消息) -->
|
||
|
||
<!-- AI 正在回复的加载指示器 -->
|
||
<AiThinkingIndicator v-if="isSending" text="AI正在思考中" />
|
||
</div>
|
||
|
||
<!-- 底部固定输入框 -->
|
||
<div class="agent-main__input-bar">
|
||
<input
|
||
v-model="chatInput"
|
||
class="agent-main__input"
|
||
placeholder="告诉我你想找什么样的工作"
|
||
:disabled="isSending"
|
||
@keyup.enter="handleSendMessage"
|
||
/>
|
||
<button class="agent-main__send-btn" :disabled="isSending" @click="handleSendMessage">
|
||
<svg viewBox="0 0 24 24" fill="none">
|
||
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||
</svg>
|
||
</button>
|
||
</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"
|
||
@close="handleCloseRecommendPanel"
|
||
@view-more="handleViewMoreJobs"
|
||
@toggle="handleToggleJobApply"
|
||
@add-all="handleAddAllJobs"
|
||
@click-job="handleMatchJobClick"
|
||
/>
|
||
<!-- 模式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>
|
||
<!-- 模式4:岗位预览 -->
|
||
<AgentJobPreviewPanel
|
||
v-else-if="rightPanelMode === 'jobPreview'"
|
||
:job-id="previewJobId"
|
||
:application-status="previewJobApplicationStatus"
|
||
@back="handleJobPreviewBack"
|
||
@add="handleJobPreviewAdd"
|
||
@remove="handleJobPreviewRemove"
|
||
/>
|
||
<!-- 模式5:全部的待投递列表 -->
|
||
<AgentPendingJobListPanel
|
||
v-else-if="rightPanelMode === 'pendingList'"
|
||
@close="handleClosePendingListPanel"
|
||
@removed="handlePendingListRemoved"
|
||
/>
|
||
<!-- 模式6:全部的已投递进度 -->
|
||
<AgentApplyProgressPanel
|
||
v-else-if="rightPanelMode === 'applyProgress'"
|
||
@close="handleCloseApplyProgressPanel"
|
||
/>
|
||
<!-- 模式7:设置面板 -->
|
||
<AgentSettingPanel
|
||
v-else-if="rightPanelMode === 'settings'"
|
||
@close="handleCloseSettingsPanel"
|
||
/>
|
||
<!-- 模式7:AI助手设置 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 求职目标设置弹窗(AI 对话 editPreference 工具触发) -->
|
||
<JobGoalDialog v-model="showJobGoalDialog" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||
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 AgentApplyProgress from '@/components/AgentApplyProgress.vue'
|
||
import AgentTaskListDropdown from '@/components/AgentTaskListDropdown.vue'
|
||
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
||
import AgentJobPreviewPanel from '@/components/AgentJobPreviewPanel.vue'
|
||
import AgentPendingJobListPanel from '@/components/AgentPendingJobListPanel.vue'
|
||
import AgentApplyProgressPanel from '@/components/AgentApplyProgressPanel.vue'
|
||
import AgentSettingPanel from '@/components/AgentSettingPanel.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 { fetchAgentTaskList } from '@/api/jobs'
|
||
import type { JobListItem } from '@/api/jobs'
|
||
import { fetchResumeList } from '@/api/resume'
|
||
import { getIntentionCategoryNames, getIntentionRegionNames, getIntentionIndustryNames } from '@/utils/intention'
|
||
import AiThinkingIndicator from '@/components/tools/AiThinkingIndicator.vue'
|
||
import JobGoalDialog from '@/components/JobGoalDialog.vue'
|
||
|
||
/** 页面初始加载状态 */
|
||
const pageLoading = ref(true)
|
||
|
||
/** 是否显示准备向导 */
|
||
const showSetupWizard = ref(true)
|
||
|
||
/** 从接口查询到的配置数据 — 传给向导组件做初始值 */
|
||
const agentConfig = ref<AgentConfig | null>(null)
|
||
|
||
/** 是否显示右侧匹配岗位面板 */
|
||
const showRightPanel = ref(false)
|
||
|
||
/** 当前右侧面板展示的 recommend 消息 ID */
|
||
const activeRecommendMsgId = ref<number | null>(null)
|
||
|
||
/** 对话消息列表 */
|
||
const chatMessages = ref<AgentChatMessage[]>([])
|
||
|
||
/** 根据当前选中的 recommend 消息 ID,解析其 extra 中的岗位列表 */
|
||
const activeRecommendJobs = computed(() => {
|
||
if (activeRecommendMsgId.value === null) return [] as AgentRecommendJob[]
|
||
const msg = chatMessages.value.find(m => m.id === activeRecommendMsgId.value)
|
||
if (!msg || !msg.extra) return [] as AgentRecommendJob[]
|
||
return getRecommendJobsFromExtra(msg.extra)
|
||
})
|
||
|
||
/** 聊天输入框内容 */
|
||
const chatInput = ref('')
|
||
|
||
/** 待投递列表数据 */
|
||
const applyJobList = ref<JobListItem[]>([])
|
||
|
||
/** 待投递列表下拉弹窗是否显示 */
|
||
const showTaskListDropdown = ref(false)
|
||
|
||
/** 正在请求中的岗位 ID 列表(传给子组件控制按钮 loading) */
|
||
const loadingJobIds = ref<number[]>([])
|
||
|
||
/** 右侧面板加载状态(查询投递记录期间显示 loading) */
|
||
const panelLoading = ref(false)
|
||
|
||
/** 默认简历 ID — 发送 AI 对话时需要(字符串避免大整数精度丢失) */
|
||
const defaultResumeId = ref('')
|
||
|
||
/** 是否正在发送消息(防止重复发送 + 显示等待状态) */
|
||
const isSending = ref(false)
|
||
|
||
/** 是否正在投递流程中(防止重复点击开始按钮) */
|
||
const isApplying = ref(false)
|
||
|
||
/** 是否显示求职目标设置弹窗(由 AI 对话 editPreference 工具触发) */
|
||
const showJobGoalDialog = ref(false)
|
||
|
||
/** 是否暂停投递 */
|
||
const isPaused = ref(false)
|
||
|
||
/** 聊天区域 DOM 引用 */
|
||
const chatAreaRef = ref<HTMLElement | null>(null)
|
||
|
||
/** 滚动聊天区域到底部(平滑滚动) */
|
||
function scrollChatToBottom() {
|
||
if (chatAreaRef.value) {
|
||
chatAreaRef.value.scrollTo({ top: chatAreaRef.value.scrollHeight, behavior: 'smooth' })
|
||
}
|
||
}
|
||
|
||
/** 页面挂载时查询配置 */
|
||
onMounted(async () => {
|
||
await loadAgentConfig()
|
||
// 点击页面其他区域关闭待投递列表下拉
|
||
document.addEventListener('click', closeTaskDropdownOnClickOutside)
|
||
})
|
||
|
||
/** 点击外部关闭待投递列表下拉 */
|
||
function closeTaskDropdownOnClickOutside() {
|
||
showTaskListDropdown.value = false
|
||
}
|
||
|
||
/** 查询求职助手配置 */
|
||
async function loadAgentConfig() {
|
||
pageLoading.value = true
|
||
try {
|
||
const res = await fetchAgentConfig()
|
||
pageLoading.value = false
|
||
if (res.code === '0' && res.data) {
|
||
agentConfig.value = res.data
|
||
/* 如果已启用(status=1),直接进入正式内容 */
|
||
if (res.data.status === 1) {
|
||
showSetupWizard.value = false
|
||
await loadChatMessages()
|
||
await loadApplyList()
|
||
await loadDefaultResumeId()
|
||
/* 没有历史消息时才自动请求推荐岗位 */
|
||
if (chatMessages.value.length === 0) {
|
||
await loadRecommendJobs()
|
||
}
|
||
}
|
||
}
|
||
} catch {
|
||
console.error('[Agent] 查询求职助手配置失败')
|
||
} finally {
|
||
|
||
}
|
||
}
|
||
|
||
/** 加载推荐岗位列表 — 请求后将结果作为 recommend 消息存入对话记录 */
|
||
async function loadRecommendJobs() {
|
||
try {
|
||
const res = await fetchAgentRecommend()
|
||
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 })
|
||
|
||
/* 调接口持久化这条 recommend 消息 */
|
||
try {
|
||
await addAgentChatMessage({
|
||
type: 'recommend',
|
||
content: '',
|
||
extra: extraJson,
|
||
})
|
||
} catch {
|
||
console.error('[Agent] 保存推荐消息失败')
|
||
}
|
||
|
||
/* 追加到本地对话消息列表 */
|
||
const now = Math.floor(Date.now() / 1000)
|
||
chatMessages.value.push({
|
||
id: Date.now(),
|
||
type: 'recommend',
|
||
content: '',
|
||
extra: extraJson,
|
||
createTime: { seconds: now, nanos: 0 },
|
||
})
|
||
|
||
/* 滚动到底部 */
|
||
await nextTick()
|
||
scrollChatToBottom()
|
||
}
|
||
} catch {
|
||
console.error('[Agent] 加载推荐岗位失败')
|
||
}
|
||
}
|
||
|
||
/** 加载 Agent 对话消息列表 */
|
||
async function loadChatMessages() {
|
||
try {
|
||
const res = await fetchAgentChatMessages(50)
|
||
if (res.code === '0' && res.data) {
|
||
chatMessages.value = res.data
|
||
/* 加载完成后滚动到底部 */
|
||
await nextTick()
|
||
scrollChatToBottom()
|
||
}
|
||
} catch {
|
||
console.error('[Agent] 加载对话消息失败')
|
||
}
|
||
}
|
||
|
||
/** 加载求职助手任务列表(tab=1 进行中/待投递) */
|
||
async function loadApplyList() {
|
||
try {
|
||
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] 加载求职助手任务列表失败')
|
||
}
|
||
}
|
||
|
||
/** 切换待投递列表下拉弹窗显隐 */
|
||
function toggleTaskListDropdown() {
|
||
/* 投递流程进行中时禁止打开待投递列表 */
|
||
if (isApplying.value) {
|
||
ElMessage.warning('请完成或取消投递流程后再打开编辑待投递列表')
|
||
return
|
||
}
|
||
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)
|
||
}
|
||
}
|
||
|
||
/** 加载默认简历 ID */
|
||
async function loadDefaultResumeId() {
|
||
try {
|
||
const res = await fetchResumeList()
|
||
if (res.code === '0' && res.data && res.data.length > 0) {
|
||
const defaultResume = res.data.find(r => r.isDefault === 1)
|
||
const id = defaultResume?.id || res.data[0]?.id || ''
|
||
defaultResumeId.value = id
|
||
}
|
||
} catch {
|
||
console.error('[Agent] 加载默认简历失败')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将 chatMessages 组装为 AI 对话接口需要的 history 格式
|
||
* user / assistant → content 取 chatMessages.content
|
||
* recommend / apply_progress → content 取 chatMessages.extra
|
||
*/
|
||
function buildChatHistory(): AgentChatHistoryItem[] {
|
||
return chatMessages.value.map(msg => {
|
||
const role = (msg.type === 'user') ? 'user' : 'assistant'
|
||
let content = ''
|
||
if (msg.type === 'user' || msg.type === 'assistant') {
|
||
content = msg.content || ''
|
||
} else {
|
||
/* recommend / apply_progress — 用 extra 作为内容 */
|
||
content = msg.extra || msg.content || ''
|
||
}
|
||
return { role, content }
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 从消息的 extra JSON 中解析推荐岗位列表
|
||
* extra 格式预期为 JSON 字符串,包含 list 字段
|
||
*/
|
||
function getRecommendJobsFromExtra(extra: string): AgentRecommendJob[] {
|
||
if (!extra) return []
|
||
try {
|
||
const parsed = JSON.parse(extra)
|
||
return Array.isArray(parsed.list) ? parsed.list : (Array.isArray(parsed) ? parsed : [])
|
||
} catch {
|
||
return []
|
||
}
|
||
}
|
||
|
||
/** 从消息的 extra JSON 中解析推荐说明文字 */
|
||
function getRecommendSummaryFromExtra(extra: string): string {
|
||
if (!extra) return ''
|
||
try {
|
||
const parsed = JSON.parse(extra)
|
||
return parsed.summary || ''
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 收集 chatMessages 中所有 recommend 消息里的岗位 ID
|
||
* 返回字符串数组,避免大整数精度丢失
|
||
*/
|
||
function collectExcludeJobIds(): string[] {
|
||
const ids: string[] = []
|
||
for (const msg of chatMessages.value) {
|
||
if (msg.type !== 'recommend' || !msg.extra) continue
|
||
const jobs = getRecommendJobsFromExtra(msg.extra)
|
||
for (const job of jobs) {
|
||
ids.push(String(job.id))
|
||
}
|
||
}
|
||
return ids
|
||
}
|
||
|
||
/** 准备向导设置完成 — 调用接口保存配置 */
|
||
async function handleSetupComplete(data: Record<string, any>) {
|
||
try {
|
||
await saveAgentConfig(data as AgentConfig)
|
||
ElMessage.success('配置保存成功')
|
||
} catch {
|
||
ElMessage.error('配置保存失败,请重试')
|
||
}
|
||
}
|
||
|
||
/** 启用求职助手 */
|
||
async function handleLaunch() {
|
||
showSetupWizard.value = false
|
||
ElMessage.success('求职助手已启用')
|
||
/* 进入正式内容后加载对话消息和推荐岗位 */
|
||
await loadChatMessages()
|
||
await loadApplyList()
|
||
await loadDefaultResumeId()
|
||
/* 没有历史消息时才自动请求推荐岗位 */
|
||
if (chatMessages.value.length === 0) {
|
||
await loadRecommendJobs()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新对话消息中 recommend 消息里对应岗位的 applicationStatus
|
||
*/
|
||
function updateJobStatusInMessages(jobId: number, newStatus: number | null) {
|
||
for (const msg of chatMessages.value) {
|
||
if (msg.type !== 'recommend' || !msg.extra) continue
|
||
try {
|
||
const parsed = JSON.parse(msg.extra)
|
||
const list: AgentRecommendJob[] = Array.isArray(parsed.list) ? parsed.list : (Array.isArray(parsed) ? parsed : [])
|
||
const job = list.find(j => j.id === jobId)
|
||
if (job) {
|
||
job.applicationStatus = newStatus
|
||
if (parsed.list) {
|
||
parsed.list = list
|
||
msg.extra = JSON.stringify(parsed)
|
||
} else {
|
||
msg.extra = JSON.stringify(list)
|
||
}
|
||
}
|
||
} catch { /* 解析失败跳过 */ }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 单个岗位添加/移除 — 由子组件 toggle 事件触发
|
||
* 未投递(null) → POST 添加到待投递(-1)
|
||
* 待投递(-1) → DELETE 移除
|
||
*/
|
||
async function handleToggleJobApply(job: AgentRecommendJob) {
|
||
if (loadingJobIds.value.includes(job.id)) return
|
||
|
||
const isRemoving = job.applicationStatus === -1
|
||
loadingJobIds.value.push(job.id)
|
||
try {
|
||
if (isRemoving) {
|
||
await cancelApplyJob(job.id)
|
||
} else {
|
||
await applyJob({ jobId: job.id, status: -1 })
|
||
}
|
||
/* 更新对话消息中的岗位状态 */
|
||
updateJobStatusInMessages(job.id, isRemoving ? null : -1)
|
||
await loadApplyList()
|
||
ElMessage.success(isRemoving ? '岗位已从待投递移除' : '岗位已添加到待投递')
|
||
} catch {
|
||
ElMessage.error('操作失败,请重试')
|
||
} finally {
|
||
loadingJobIds.value = loadingJobIds.value.filter(id => id !== job.id)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 全部添加 — 由子组件 addAll 事件触发
|
||
* 将当前面板中所有未添加的岗位批量设为待投递
|
||
*/
|
||
async function handleAddAllJobs() {
|
||
const pendingJobs = activeRecommendJobs.value.filter(j => j.applicationStatus === null || j.applicationStatus === undefined)
|
||
if (pendingJobs.length === 0) {
|
||
ElMessage.info('所有岗位已添加到待投递')
|
||
return
|
||
}
|
||
for (const job of pendingJobs) {
|
||
if (loadingJobIds.value.includes(job.id)) continue
|
||
loadingJobIds.value.push(job.id)
|
||
try {
|
||
await applyJob({ jobId: job.id, status: -1 })
|
||
updateJobStatusInMessages(job.id, -1)
|
||
} catch { /* 单个失败不阻断后续 */ }
|
||
finally {
|
||
loadingJobIds.value = loadingJobIds.value.filter(id => id !== job.id)
|
||
}
|
||
}
|
||
await loadApplyList()
|
||
ElMessage.success('已全部添加到待投递')
|
||
}
|
||
|
||
/** 查看更多岗位 — 可扩展为加载下一批 */
|
||
function handleViewMoreJobs() {
|
||
// TODO: 后续可对接分页加载更多岗位
|
||
}
|
||
|
||
/** 打开右侧匹配岗位面板 — 先查询投递记录更新岗位状态再显示 */
|
||
async function handleOpenRecommendPanel(msgId: number) {
|
||
/* 投递流程进行中(模式2简历生成 / 模式3简历预览)时禁止打开推荐面板 */
|
||
if (rightPanelMode.value === 'generating' || rightPanelMode.value === 'resume') {
|
||
ElMessage.warning('请完成或退出投递流程后再添加岗位')
|
||
return
|
||
}
|
||
activeRecommendMsgId.value = msgId
|
||
showRightPanel.value = true
|
||
panelLoading.value = true
|
||
|
||
try {
|
||
/* 取出当前 recommend 消息中的岗位 ID 列表 */
|
||
const msg = chatMessages.value.find(m => m.id === msgId)
|
||
if (!msg || !msg.extra) return
|
||
|
||
const jobs = getRecommendJobsFromExtra(msg.extra)
|
||
const jobIds = jobs.map(j => j.id)
|
||
if (jobIds.length === 0) return
|
||
|
||
/* 调接口批量查询投递记录 */
|
||
const res = await fetchApplyByJobIds(jobIds)
|
||
if (res.code === '0' && res.data) {
|
||
/* 构建 jobId → status 映射 */
|
||
const statusMap = new Map<number, number>()
|
||
for (const record of res.data) {
|
||
statusMap.set(record.jobId, record.status)
|
||
}
|
||
/* 更新 extra 中每个岗位的 applicationStatus */
|
||
const parsed = JSON.parse(msg.extra)
|
||
const list: AgentRecommendJob[] = Array.isArray(parsed.list) ? parsed.list : (Array.isArray(parsed) ? parsed : [])
|
||
for (const job of list) {
|
||
if (statusMap.has(job.id)) {
|
||
job.applicationStatus = statusMap.get(job.id)!
|
||
} else {
|
||
/* 接口没有返回该岗位记录 — 说明没有投递记录,设为 null */
|
||
job.applicationStatus = null
|
||
}
|
||
}
|
||
/* 回写 extra */
|
||
if (parsed.list) {
|
||
parsed.list = list
|
||
msg.extra = JSON.stringify(parsed)
|
||
} else {
|
||
msg.extra = JSON.stringify(list)
|
||
}
|
||
}
|
||
} catch {
|
||
console.error('[Agent] 查询岗位投递记录失败')
|
||
} finally {
|
||
panelLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 关闭右侧匹配岗位面板 */
|
||
function handleCloseRecommendPanel() {
|
||
showRightPanel.value = false
|
||
activeRecommendMsgId.value = null
|
||
rightPanelMode.value = 'recommend'
|
||
}
|
||
|
||
// ==================== 岗位预览(模式4) ====================
|
||
|
||
/** 当前预览的岗位 ID */
|
||
const previewJobId = ref('')
|
||
|
||
/** 当前预览岗位的投递状态 */
|
||
const previewJobApplicationStatus = ref<number | null>(null)
|
||
|
||
/** 进入岗位预览前记录的面板模式(用于返回) */
|
||
const prevPanelMode = ref<'recommend' | 'generating' | 'resume' | 'jobPreview' | 'pendingList' | 'applyProgress' | 'settings'>('recommend')
|
||
|
||
/** 打开岗位预览面板 */
|
||
function handleOpenJobPreview(jobId: string | number, applicationStatus?: number | null) {
|
||
previewJobId.value = String(jobId)
|
||
previewJobApplicationStatus.value = applicationStatus ?? null
|
||
prevPanelMode.value = rightPanelMode.value
|
||
rightPanelMode.value = 'jobPreview'
|
||
showRightPanel.value = true
|
||
}
|
||
|
||
/** 岗位预览返回 — 回到之前的面板模式 */
|
||
function handleJobPreviewBack() {
|
||
/* 如果之前是 recommend 模式且有选中的消息,回到推荐面板 */
|
||
if (prevPanelMode.value === 'recommend' && activeRecommendMsgId.value !== null) {
|
||
rightPanelMode.value = 'recommend'
|
||
} else {
|
||
/* 否则关闭面板 */
|
||
showRightPanel.value = false
|
||
rightPanelMode.value = 'recommend'
|
||
}
|
||
}
|
||
|
||
/** 岗位预览中点击添加 — 添加到待投递 */
|
||
async function handleJobPreviewAdd(jobId: string) {
|
||
try {
|
||
await applyJob({ jobId, status: -1 })
|
||
/* 更新对话消息中的岗位状态 */
|
||
updateJobStatusInMessages(Number(jobId), -1)
|
||
previewJobApplicationStatus.value = -1
|
||
await loadApplyList()
|
||
ElMessage.success('岗位已添加到待投递')
|
||
} catch {
|
||
ElMessage.error('操作失败,请重试')
|
||
}
|
||
}
|
||
|
||
/** 岗位预览中点击移出 — 从待投递移除 */
|
||
async function handleJobPreviewRemove(jobId: string) {
|
||
try {
|
||
await cancelApplyJob(jobId)
|
||
/* 更新对话消息中的岗位状态 */
|
||
updateJobStatusInMessages(Number(jobId), null)
|
||
previewJobApplicationStatus.value = null
|
||
await loadApplyList()
|
||
ElMessage.success('岗位已从待投递移除')
|
||
} catch {
|
||
ElMessage.error('操作失败,请重试')
|
||
}
|
||
}
|
||
|
||
/** AgentMatchJobAdd 中点击岗位 — 打开岗位预览 */
|
||
function handleMatchJobClick(job: AgentRecommendJob) {
|
||
handleOpenJobPreview(job.id, job.applicationStatus)
|
||
}
|
||
|
||
/** AgentChatJobList 中点击岗位 — 打开岗位预览 */
|
||
function handleChatJobClick(job: AgentRecommendJob) {
|
||
/* 投递流程进行中时禁止打开岗位详情 */
|
||
if (isApplying.value) {
|
||
ElMessage.warning('请完成或取消投递流程后再查看岗位详情')
|
||
return
|
||
}
|
||
handleOpenJobPreview(job.id, job.applicationStatus)
|
||
}
|
||
|
||
// ==================== 待投递列表面板(模式5) ====================
|
||
|
||
/** 打开待投递列表面板 */
|
||
function handleOpenPendingListPanel() {
|
||
showTaskListDropdown.value = false
|
||
rightPanelMode.value = 'pendingList'
|
||
showRightPanel.value = true
|
||
}
|
||
|
||
/** 关闭待投递列表面板 */
|
||
function handleClosePendingListPanel() {
|
||
showRightPanel.value = false
|
||
rightPanelMode.value = 'recommend'
|
||
}
|
||
|
||
/** 待投递列表面板中移除岗位后同步父组件数据 */
|
||
function handlePendingListRemoved(jobId: string) {
|
||
const idx = applyJobList.value.findIndex(j => j.id === jobId)
|
||
if (idx !== -1) {
|
||
applyJobList.value.splice(idx, 1)
|
||
}
|
||
}
|
||
|
||
// ==================== 申请进度面板(模式6) ====================
|
||
|
||
/** 打开申请进度面板 */
|
||
function handleOpenApplyProgressPanel() {
|
||
showTaskListDropdown.value = false
|
||
rightPanelMode.value = 'applyProgress'
|
||
showRightPanel.value = true
|
||
}
|
||
|
||
/** 关闭申请进度面板 */
|
||
function handleCloseApplyProgressPanel() {
|
||
showRightPanel.value = false
|
||
rightPanelMode.value = 'recommend'
|
||
}
|
||
|
||
// ==================== 设置面板(模式7) ====================
|
||
|
||
/** 打开设置面板 */
|
||
function handleOpenSettingsPanel() {
|
||
rightPanelMode.value = 'settings'
|
||
showRightPanel.value = true
|
||
}
|
||
|
||
/** 关闭设置面板 */
|
||
function handleCloseSettingsPanel() {
|
||
showRightPanel.value = false
|
||
rightPanelMode.value = 'recommend'
|
||
}
|
||
|
||
// ==================== 投递进度流程 ====================
|
||
|
||
/** 右侧面板显示模式 */
|
||
const rightPanelMode = ref<'recommend' | 'generating' | 'resume' | 'jobPreview' | 'pendingList' | 'applyProgress' | 'settings'>('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
|
||
applyCancelled.value = false
|
||
startProgressSimulation()
|
||
|
||
try {
|
||
const res = await optimizeAgentResume({
|
||
resumeId: defaultResumeId.value,
|
||
jobId: targetJob.id,
|
||
})
|
||
|
||
// 如果在等待期间已取消,直接退出
|
||
if (applyCancelled.value) return
|
||
|
||
// 接口返回后涨满进度条
|
||
finishProgress()
|
||
|
||
// 等待 1 秒让用户看到 100%
|
||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||
|
||
// 再次检查取消状态
|
||
if (applyCancelled.value) return
|
||
|
||
// 解析简历数据 — 映射接口返回格式到 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) {
|
||
// 取消导致的异常不提示
|
||
if (applyCancelled.value) return
|
||
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
|
||
}
|
||
|
||
/** 标记当前投递流程是否已被取消(防止异步回调覆盖状态) */
|
||
const applyCancelled = ref(false)
|
||
|
||
/** 取消投递流程 */
|
||
function handleCancelApply(msg: AgentChatMessage) {
|
||
// 标记已取消,阻止进行中的异步回调继续操作
|
||
applyCancelled.value = true
|
||
// 清除进度条定时器
|
||
if (progressTimer) {
|
||
clearInterval(progressTimer)
|
||
progressTimer = null
|
||
}
|
||
// 重置暂停状态
|
||
isPaused.value = false
|
||
// 从消息列表中移除该条 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
|
||
applyCancelled.value = false
|
||
startProgressSimulation()
|
||
|
||
try {
|
||
const res = await optimizeAgentResume({
|
||
resumeId: defaultResumeId.value,
|
||
jobId: targetJob.id,
|
||
})
|
||
|
||
if (applyCancelled.value) return
|
||
|
||
finishProgress()
|
||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||
|
||
if (applyCancelled.value) return
|
||
|
||
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) {
|
||
if (applyCancelled.value) return
|
||
console.error('[Agent] 优化简历失败', e)
|
||
ElMessage.error('简历优化失败,请重试')
|
||
finishProgress()
|
||
showRightPanel.value = false
|
||
rightPanelMode.value = 'recommend'
|
||
isApplying.value = false
|
||
}
|
||
}
|
||
|
||
/** 发送聊天消息 */
|
||
async function handleSendMessage() {
|
||
const text = chatInput.value.trim()
|
||
if (!text || isSending.value) return
|
||
|
||
/* 1. 先把用户消息追加到 chatMessages */
|
||
const now = Math.floor(Date.now() / 1000)
|
||
const userMsg: AgentChatMessage = {
|
||
id: Date.now(),
|
||
type: 'user',
|
||
content: text,
|
||
extra: '',
|
||
createTime: { seconds: now, nanos: 0 },
|
||
}
|
||
chatMessages.value.push(userMsg)
|
||
chatInput.value = ''
|
||
|
||
/* 滚动到底部 */
|
||
await nextTick()
|
||
scrollChatToBottom()
|
||
|
||
/* 2. 同步持久化用户消息到后端 */
|
||
try {
|
||
await addAgentChatMessage({ type: 'user', content: text })
|
||
} catch {
|
||
console.error('[Agent] 保存用户消息失败')
|
||
}
|
||
|
||
/* 3. 组装参数调 AI 对话接口 */
|
||
isSending.value = true
|
||
|
||
/* 等待 loading 指示器渲染后滚动到底部 */
|
||
await nextTick()
|
||
scrollChatToBottom()
|
||
|
||
try {
|
||
const history = buildChatHistory()
|
||
const res = await sendAgentChat({
|
||
message: text,
|
||
resumeId: defaultResumeId.value,
|
||
history,
|
||
jobCategories: getIntentionCategoryNames(),
|
||
regions: getIntentionRegionNames(),
|
||
industries: getIntentionIndustryNames(),
|
||
})
|
||
|
||
/* 处理 AI 回复 */
|
||
if (res.code === 0 && res.data?.message) {
|
||
const aiContent = res.data.message
|
||
const tool = res.data.tool
|
||
const toolParams = res.data.toolParams
|
||
|
||
if (tool === 'recommend') {
|
||
/* AI 要求推荐岗位 — 先插入 AI 的过渡消息 */
|
||
const aiNow = Math.floor(Date.now() / 1000)
|
||
chatMessages.value.push({
|
||
id: Date.now() + 1,
|
||
type: 'assistant',
|
||
content: aiContent,
|
||
extra: '',
|
||
createTime: { seconds: aiNow, nanos: 0 },
|
||
})
|
||
try {
|
||
await addAgentChatMessage({ type: 'assistant', content: aiContent })
|
||
} catch { /* 静默 */ }
|
||
|
||
await nextTick()
|
||
scrollChatToBottom()
|
||
|
||
/* 调用岗位推荐接口 */
|
||
const preference = toolParams?.preference || ''
|
||
const excludeJobIds = collectExcludeJobIds()
|
||
try {
|
||
const recRes = await fetchAgentRecommend({ preference, excludeJobIds })
|
||
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 })
|
||
} catch { /* 静默 */ }
|
||
|
||
/* 追加到本地对话消息列表 */
|
||
const recNow = Math.floor(Date.now() / 1000)
|
||
chatMessages.value.push({
|
||
id: Date.now() + 2,
|
||
type: 'recommend',
|
||
content: '',
|
||
extra: extraJson,
|
||
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()
|
||
}
|
||
} catch {
|
||
console.error('[Agent] 推荐岗位请求失败')
|
||
}
|
||
} else if (tool === 'editPreference') {
|
||
/* AI 要求打开偏好设置弹窗 — 先插入 AI 回复消息,再弹出求职目标设置弹窗 */
|
||
const aiNow = Math.floor(Date.now() / 1000)
|
||
chatMessages.value.push({
|
||
id: Date.now() + 1,
|
||
type: 'assistant',
|
||
content: aiContent,
|
||
extra: '',
|
||
createTime: { seconds: aiNow, nanos: 0 },
|
||
})
|
||
try {
|
||
await addAgentChatMessage({ type: 'assistant', content: aiContent })
|
||
} catch { /* 静默 */ }
|
||
|
||
await nextTick()
|
||
scrollChatToBottom()
|
||
|
||
/* 打开求职目标设置弹窗 */
|
||
showJobGoalDialog.value = true
|
||
} else {
|
||
/* 普通 AI 回复 — 直接插入 assistant 消息 */
|
||
const aiNow = Math.floor(Date.now() / 1000)
|
||
chatMessages.value.push({
|
||
id: Date.now() + 1,
|
||
type: 'assistant',
|
||
content: aiContent,
|
||
extra: '',
|
||
createTime: { seconds: aiNow, nanos: 0 },
|
||
})
|
||
|
||
try {
|
||
await addAgentChatMessage({ type: 'assistant', content: aiContent })
|
||
} catch {
|
||
console.error('[Agent] 保存 AI 回复消息失败')
|
||
}
|
||
|
||
await nextTick()
|
||
scrollChatToBottom()
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('[Agent] AI 对话请求失败', err)
|
||
ElMessage.error('消息发送失败,请重试')
|
||
} finally {
|
||
isSending.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@use '../assets/styles/pages/agent';
|
||
</style>
|