AI助手和Nova助手
This commit is contained in:
Vendored
+9
@@ -11,8 +11,15 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
AgentChatJobList: typeof import('./src/components/AgentChatJobList.vue')['default']
|
||||||
|
AgentMatchJobAdd: typeof import('./src/components/AgentMatchJobAdd.vue')['default']
|
||||||
|
AgentSettingsPanel: typeof import('./src/components/AgentSettingsPanel.vue')['default']
|
||||||
|
AgentSetupWizard: typeof import('./src/components/AgentSetupWizard.vue')['default']
|
||||||
AiChat: typeof import('./src/components/AiChat.vue')['default']
|
AiChat: typeof import('./src/components/AiChat.vue')['default']
|
||||||
|
AiThinkingIndicator: typeof import('./src/components/tools/AiThinkingIndicator.vue')['default']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElCarousel: typeof import('element-plus/es')['ElCarousel']
|
||||||
|
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
@@ -20,6 +27,7 @@ declare module 'vue' {
|
|||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
@@ -39,6 +47,7 @@ declare module 'vue' {
|
|||||||
ProfilePageContent: typeof import('./src/components/ProfilePageContent.vue')['default']
|
ProfilePageContent: typeof import('./src/components/ProfilePageContent.vue')['default']
|
||||||
RegionSelector: typeof import('./src/components/tools/RegionSelector.vue')['default']
|
RegionSelector: typeof import('./src/components/tools/RegionSelector.vue')['default']
|
||||||
ResumeAnalysisReportDrawer: typeof import('./src/components/ResumeAnalysisReportDrawer.vue')['default']
|
ResumeAnalysisReportDrawer: typeof import('./src/components/ResumeAnalysisReportDrawer.vue')['default']
|
||||||
|
ResumeEditNameDialog: typeof import('./src/components/ResumeEditNameDialog.vue')['default']
|
||||||
ResumeIssueFixDrawer: typeof import('./src/components/ResumeIssueFixDrawer.vue')['default']
|
ResumeIssueFixDrawer: typeof import('./src/components/ResumeIssueFixDrawer.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
import type { ApiResult } from '@/api/auth'
|
||||||
|
|
||||||
|
// ==================== 求职助手配置相关 ====================
|
||||||
|
|
||||||
|
/** 语言能力项 */
|
||||||
|
export interface LanguageAbility {
|
||||||
|
/** 语种,如英语、日语、法语等 */
|
||||||
|
language?: string
|
||||||
|
/** 掌握程度,可选值:入门/日常会话/商务会话/无障碍沟通/母语 */
|
||||||
|
proficiency?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 求职助手配置数据(查询返回 + 保存请求共用) */
|
||||||
|
export interface AgentConfig {
|
||||||
|
/** 工作类型 1=实习 2=全职 */
|
||||||
|
jobType?: number
|
||||||
|
/** Agent模式 1=协作模式 2=托管模式 */
|
||||||
|
agentMode?: number
|
||||||
|
/** 每周投递目标数量 1=少于20个 2=20到50个 3=多于50个 */
|
||||||
|
weeklyTarget?: number
|
||||||
|
/** 投递时自动针对岗位优化简历 0=关闭 1=开启 */
|
||||||
|
autoOptimizeResume?: number
|
||||||
|
/** 是否愿意接受部门调剂 */
|
||||||
|
acceptDeptTransfer?: string
|
||||||
|
/** 是否接受地点调剂 */
|
||||||
|
acceptLocationTransfer?: string
|
||||||
|
/** 可参加的面试方式 */
|
||||||
|
interviewType?: string[]
|
||||||
|
/** 语言能力 */
|
||||||
|
languages?: LanguageAbility[]
|
||||||
|
/** 预计到岗时间 */
|
||||||
|
availableDate?: string
|
||||||
|
/** 每周可实习天数 */
|
||||||
|
internDaysPerWeek?: string
|
||||||
|
/** 预计实习时长 */
|
||||||
|
internDuration?: string
|
||||||
|
/** 状态 0=未启用 1=已启用(仅查询返回) */
|
||||||
|
status?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询求职助手配置
|
||||||
|
* GET /job-agent/config
|
||||||
|
*/
|
||||||
|
export function fetchAgentConfig() {
|
||||||
|
return request.get<any, ApiResult<AgentConfig>>('/job-agent/config')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存求职助手配置
|
||||||
|
* POST /job-agent/config/save
|
||||||
|
*/
|
||||||
|
export function saveAgentConfig(data: AgentConfig) {
|
||||||
|
return request.post<any, ApiResult>('/job-agent/config/save', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 求职助手岗位推荐 ====================
|
||||||
|
|
||||||
|
/** 匹配度详情 */
|
||||||
|
export interface AgentJobMatchDetail {
|
||||||
|
/** 学历匹配分 */
|
||||||
|
educationScore: number
|
||||||
|
/** 技能匹配分 */
|
||||||
|
skillScore: number
|
||||||
|
/** 经验匹配分 */
|
||||||
|
experienceScore: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 推荐岗位项 */
|
||||||
|
export interface AgentRecommendJob {
|
||||||
|
/** 岗位 ID */
|
||||||
|
id: number
|
||||||
|
/** 岗位名称 */
|
||||||
|
title: string
|
||||||
|
/** 薪资描述 */
|
||||||
|
salary: string
|
||||||
|
/** 公司名称 */
|
||||||
|
companyName: string
|
||||||
|
/** 公司简称 */
|
||||||
|
companyShortName: string
|
||||||
|
/** 公司类型 */
|
||||||
|
companyType: string
|
||||||
|
/** 公司 Logo */
|
||||||
|
companyLogoUrl: string
|
||||||
|
/** 地区名称 */
|
||||||
|
regionName: string
|
||||||
|
/** 岗位类型名称 */
|
||||||
|
categoryName: string
|
||||||
|
/** 岗位标签 */
|
||||||
|
tags: string[]
|
||||||
|
/** 来源链接 */
|
||||||
|
sourceUrl: string
|
||||||
|
/** 是否收藏 */
|
||||||
|
isFavorite: boolean
|
||||||
|
/** 投递状态(null=未投递,-1=待投递,0=已投递 1=面试中 2=有Offer 3=未通过 4=已结束) */
|
||||||
|
applicationStatus: number | null
|
||||||
|
/** 岗位状态(0=有效 1=已下架 2=已过期) */
|
||||||
|
status: number
|
||||||
|
/** 匹配总分(0-90) */
|
||||||
|
matchScore: number
|
||||||
|
/** 匹配度详情 */
|
||||||
|
matchDetail: AgentJobMatchDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 求职助手岗位推荐返回数据 */
|
||||||
|
export interface AgentRecommendResult {
|
||||||
|
/** 推荐说明(20字以内) */
|
||||||
|
summary: string
|
||||||
|
/** 推荐的岗位列表(8-10个) */
|
||||||
|
list: AgentRecommendJob[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 求职助手岗位推荐请求参数 */
|
||||||
|
export interface AgentRecommendParams {
|
||||||
|
/** 用户偏好描述 */
|
||||||
|
preference?: string
|
||||||
|
/** 排除已推荐过的岗位 ID 列表(字符串避免大整数精度丢失) */
|
||||||
|
excludeJobIds?: (number | string)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 求职助手岗位推荐
|
||||||
|
* POST /job/agent/recommend
|
||||||
|
*/
|
||||||
|
export function fetchAgentRecommend(params: AgentRecommendParams = {}) {
|
||||||
|
return request.post<any, ApiResult<AgentRecommendResult>>('/job/agent/recommend', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 岗位投递状态变更 ====================
|
||||||
|
|
||||||
|
/** 岗位投递请求参数 */
|
||||||
|
export interface JobApplyParams {
|
||||||
|
/** 岗位 ID */
|
||||||
|
jobId: number
|
||||||
|
/** 投递状态:-1=待投递 0=已投递 1=面试中 2=有Offer 3=未通过 4=已结束,null=取消 */
|
||||||
|
status: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变更岗位投递状态(添加到待投递等)
|
||||||
|
* POST /job/apply
|
||||||
|
*/
|
||||||
|
export function applyJob(params: JobApplyParams) {
|
||||||
|
return request.post<any, ApiResult>('/job/apply', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消投递 / 从待投递移除
|
||||||
|
* DELETE /job/apply?jobId=xxx
|
||||||
|
* @param jobId 岗位 ID
|
||||||
|
*/
|
||||||
|
export function cancelApplyJob(jobId: number) {
|
||||||
|
return request.delete<any, ApiResult>('/job/apply', {
|
||||||
|
params: { jobId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Agent 对话消息 ====================
|
||||||
|
|
||||||
|
/** 消息创建时间 */
|
||||||
|
export interface InstantTime {
|
||||||
|
/** 距 1970-01-01T00:00:00Z 的秒数 */
|
||||||
|
seconds: number
|
||||||
|
/** 纳秒偏移(0 ~ 999,999,999) */
|
||||||
|
nanos: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Agent 对话消息项 */
|
||||||
|
export interface AgentChatMessage {
|
||||||
|
/** 消息 ID */
|
||||||
|
id: number
|
||||||
|
/** 消息类型:user / assistant / recommend / apply_progress */
|
||||||
|
type: string
|
||||||
|
/** 文本内容 */
|
||||||
|
content: string
|
||||||
|
/** 附加数据 JSON(如推荐岗位列表等) */
|
||||||
|
extra: string
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime: InstantTime
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前 Agent 对话消息列表
|
||||||
|
* GET /job-agent/chat/messages?limit=xxx
|
||||||
|
* @param limit 查询条数
|
||||||
|
*/
|
||||||
|
export function fetchAgentChatMessages(limit: number = 50) {
|
||||||
|
return request.get<any, ApiResult<AgentChatMessage[]>>('/job-agent/chat/messages', {
|
||||||
|
params: { limit },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加对话消息请求参数 */
|
||||||
|
export interface AddChatMessageParams {
|
||||||
|
/** 消息类型:user / assistant / recommend / apply_progress */
|
||||||
|
type: string
|
||||||
|
/** 文本内容 */
|
||||||
|
content?: string
|
||||||
|
/** 附加数据 JSON */
|
||||||
|
extra?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加 Agent 对话消息
|
||||||
|
* POST /job-agent/chat/message
|
||||||
|
*/
|
||||||
|
export function addAgentChatMessage(params: AddChatMessageParams) {
|
||||||
|
return request.post<any, ApiResult>('/job-agent/chat/message', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 批量查询岗位投递记录 ====================
|
||||||
|
|
||||||
|
/** 岗位投递记录项 */
|
||||||
|
export interface JobApplyRecord {
|
||||||
|
/** 岗位 ID */
|
||||||
|
jobId: number
|
||||||
|
/** 投递状态:-1=待投递 0=已投递 1=面试中 2=有Offer 3=未通过 4=已结束 */
|
||||||
|
status: number
|
||||||
|
/** 投递时间 */
|
||||||
|
applyTime: InstantTime
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime: InstantTime
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据岗位 ID 批量查询投递记录
|
||||||
|
* POST /job/apply/listByJobIds
|
||||||
|
* @param jobIds 岗位 ID 列表
|
||||||
|
*/
|
||||||
|
export function fetchApplyByJobIds(jobIds: number[]) {
|
||||||
|
return request.post<any, ApiResult<JobApplyRecord[]>>('/job/apply/listByJobIds', jobIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Agent AI 对话(Python 后端) ====================
|
||||||
|
|
||||||
|
import aiService from '@/utils/aiRequest'
|
||||||
|
import type { AiResult } from '@/utils/aiRequest'
|
||||||
|
|
||||||
|
/** AI 对话历史消息项 */
|
||||||
|
export interface AgentChatHistoryItem {
|
||||||
|
/** 角色:user / assistant */
|
||||||
|
role: string
|
||||||
|
/** 消息内容 */
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Agent AI 对话请求参数 */
|
||||||
|
export interface AgentChatParams {
|
||||||
|
/** 用户输入的消息 */
|
||||||
|
message: string
|
||||||
|
/** 简历 ID(字符串,避免大整数精度丢失) */
|
||||||
|
resumeId: string
|
||||||
|
/** 对话历史 */
|
||||||
|
history?: AgentChatHistoryItem[]
|
||||||
|
/** 意向岗位类型名称 */
|
||||||
|
jobCategories?: string[]
|
||||||
|
/** 意向城市名称 */
|
||||||
|
regions?: string[]
|
||||||
|
/** 意向行业名称 */
|
||||||
|
industries?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Agent AI 对话返回数据 */
|
||||||
|
export interface AgentChatResponse {
|
||||||
|
/** AI 回复的消息内容 */
|
||||||
|
message: string
|
||||||
|
/** 工具调用名称(null=普通回复,recommend=推荐岗位) */
|
||||||
|
tool: string | null
|
||||||
|
/** 工具调用参数 */
|
||||||
|
toolParams: Record<string, any> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent AI 对话(Python 后端)
|
||||||
|
* POST /job-agent/chat
|
||||||
|
*/
|
||||||
|
export function sendAgentChat(params: AgentChatParams) {
|
||||||
|
return aiService.post<any, { data: AiResult<AgentChatResponse> }>('/job-agent/chat', params).then(res => res.data)
|
||||||
|
}
|
||||||
+7
-3
@@ -488,9 +488,11 @@ export interface CustomizeResumeData {
|
|||||||
/**
|
/**
|
||||||
* 查询定制简历结果(AI 接口)
|
* 查询定制简历结果(AI 接口)
|
||||||
* GET /job/customize-resume
|
* GET /job/customize-resume
|
||||||
|
* @param jobId 岗位ID(必需)
|
||||||
*/
|
*/
|
||||||
export function fetchCustomizeResume() {
|
export function fetchCustomizeResume(jobId: string) {
|
||||||
return aiService.get<any, { data: AiResult<CustomizeResumeData | null> }>('/job/customize-resume', {
|
return aiService.get<any, { data: AiResult<CustomizeResumeData | null> }>('/job/customize-resume', {
|
||||||
|
params: { job_id: jobId },
|
||||||
transformResponse: [(data: string) => {
|
transformResponse: [(data: string) => {
|
||||||
try {
|
try {
|
||||||
const processed = data.replace(/:\s*(\d{16,})/g, ':"$1"')
|
const processed = data.replace(/:\s*(\d{16,})/g, ':"$1"')
|
||||||
@@ -585,8 +587,9 @@ export function aiEditResume(params: AiEditResumeParams) {
|
|||||||
* 撤销AI对话编辑简历的修改(AI 接口)
|
* 撤销AI对话编辑简历的修改(AI 接口)
|
||||||
* POST /job/customize-resume/rollback
|
* POST /job/customize-resume/rollback
|
||||||
*/
|
*/
|
||||||
export function rollbackCustomizeResume() {
|
export function rollbackCustomizeResume(jobId: string) {
|
||||||
return aiService.post<any, { data: AiResult<null> }>('/job/customize-resume/rollback', null, {
|
return aiService.post<any, { data: AiResult<null> }>('/job/customize-resume/rollback', null, {
|
||||||
|
params: { job_id: jobId },
|
||||||
transformResponse: [(data: string) => {
|
transformResponse: [(data: string) => {
|
||||||
try {
|
try {
|
||||||
const processed = data.replace(/:\s*(\d{16,})/g, ':"$1"')
|
const processed = data.replace(/:\s*(\d{16,})/g, ':"$1"')
|
||||||
@@ -604,8 +607,9 @@ export function rollbackCustomizeResume() {
|
|||||||
* 输入框失焦或选择器选中后自动调用
|
* 输入框失焦或选择器选中后自动调用
|
||||||
* @param data 定制简历完整数据
|
* @param data 定制简历完整数据
|
||||||
*/
|
*/
|
||||||
export function updateCustomizeResume(data: CustomizeResumeData) {
|
export function updateCustomizeResume(data: CustomizeResumeData,jobId: string) {
|
||||||
return aiService.put<any, { data: AiResult<null> }>('/job/customize-resume', data, {
|
return aiService.put<any, { data: AiResult<null> }>('/job/customize-resume', data, {
|
||||||
|
params: { job_id: jobId },
|
||||||
transformResponse: [(raw: string) => {
|
transformResponse: [(raw: string) => {
|
||||||
try {
|
try {
|
||||||
const processed = raw.replace(/:\s*(\d{16,})/g, ':"$1"')
|
const processed = raw.replace(/:\s*(\d{16,})/g, ':"$1"')
|
||||||
|
|||||||
+157
-9
@@ -23,10 +23,10 @@ export interface ResumeListItem {
|
|||||||
targetPosition?: string
|
targetPosition?: string
|
||||||
/** 是否默认简历 0=否 1=是 */
|
/** 是否默认简历 0=否 1=是 */
|
||||||
isDefault?: number
|
isDefault?: number
|
||||||
/** 简历修改时间 */
|
/** 简历修改时间(毫秒时间戳) */
|
||||||
updateTime?: InstantTime
|
updateTime?: number
|
||||||
/** 简历创建时间 */
|
/** 简历创建时间(毫秒时间戳) */
|
||||||
createTime?: InstantTime
|
createTime?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -275,6 +275,8 @@ export function saveResumeMain(data: SaveResumeMainParams) {
|
|||||||
|
|
||||||
/** 保存简历教育经历参数(单条) */
|
/** 保存简历教育经历参数(单条) */
|
||||||
export interface SaveResumeEducationItem {
|
export interface SaveResumeEducationItem {
|
||||||
|
/** 记录ID(编辑时必传) */
|
||||||
|
id?: string | number
|
||||||
/** 学校名称 */
|
/** 学校名称 */
|
||||||
school?: string
|
school?: string
|
||||||
/** 专业 */
|
/** 专业 */
|
||||||
@@ -292,15 +294,45 @@ export interface SaveResumeEducationItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存简历教育经历(全量覆盖)
|
* 保存简历教育经历(全量覆盖)— 旧接口,保留兼容
|
||||||
* POST /resume/education
|
* POST /resume/education
|
||||||
*/
|
*/
|
||||||
export function saveResumeEducation(resumeId: string, data: SaveResumeEducationItem[]) {
|
export function saveResumeEducation(resumeId: string, data: SaveResumeEducationItem[]) {
|
||||||
return request.post<any, ApiResult>('/resume/education', { resumeId, items: data })
|
return request.post<any, ApiResult>('/resume/education', { resumeId, items: data })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加单条教育经历,返回新记录ID
|
||||||
|
* POST /resume/education/add
|
||||||
|
*/
|
||||||
|
export function addResumeEducation(resumeId: string, data: SaveResumeEducationItem) {
|
||||||
|
return request.post<any, ApiResult<string>>('/resume/education/add', data, {
|
||||||
|
params: { resumeId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID编辑单条教育经历
|
||||||
|
* POST /resume/education/update
|
||||||
|
*/
|
||||||
|
export function updateResumeEducation(data: SaveResumeEducationItem) {
|
||||||
|
return request.post<any, ApiResult>('/resume/education/update', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID删除单条教育经历
|
||||||
|
* POST /resume/education/delete
|
||||||
|
*/
|
||||||
|
export function deleteResumeEducation(id: string) {
|
||||||
|
return request.post<any, ApiResult>('/resume/education/delete', null, {
|
||||||
|
params: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** 保存简历工作经历参数(单条) */
|
/** 保存简历工作经历参数(单条) */
|
||||||
export interface SaveResumeWorkItem {
|
export interface SaveResumeWorkItem {
|
||||||
|
/** 记录ID(编辑时必传) */
|
||||||
|
id?: string | number
|
||||||
/** 公司名称 */
|
/** 公司名称 */
|
||||||
companyName?: string
|
companyName?: string
|
||||||
/** 职位 */
|
/** 职位 */
|
||||||
@@ -314,7 +346,7 @@ export interface SaveResumeWorkItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存简历工作经历(全量覆盖)
|
* 保存简历工作经历(全量覆盖)— 旧接口,保留兼容
|
||||||
* POST /resume/work
|
* POST /resume/work
|
||||||
*/
|
*/
|
||||||
export function saveResumeWork(resumeId: string, data: SaveResumeWorkItem[]) {
|
export function saveResumeWork(resumeId: string, data: SaveResumeWorkItem[]) {
|
||||||
@@ -322,15 +354,73 @@ export function saveResumeWork(resumeId: string, data: SaveResumeWorkItem[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存简历实习经历(全量覆盖)
|
* 添加单条工作经历,返回新记录ID
|
||||||
|
* POST /resume/work/add
|
||||||
|
*/
|
||||||
|
export function addResumeWork(resumeId: string, data: SaveResumeWorkItem) {
|
||||||
|
return request.post<any, ApiResult<string>>('/resume/work/add', data, {
|
||||||
|
params: { resumeId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID编辑单条工作经历
|
||||||
|
* POST /resume/work/update
|
||||||
|
*/
|
||||||
|
export function updateResumeWork(data: SaveResumeWorkItem) {
|
||||||
|
return request.post<any, ApiResult>('/resume/work/update', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID删除单条工作经历
|
||||||
|
* POST /resume/work/delete
|
||||||
|
*/
|
||||||
|
export function deleteResumeWork(id: string) {
|
||||||
|
return request.post<any, ApiResult>('/resume/work/delete', null, {
|
||||||
|
params: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存简历实习经历(全量覆盖)— 旧接口,保留兼容
|
||||||
* POST /resume/internship
|
* POST /resume/internship
|
||||||
*/
|
*/
|
||||||
export function saveResumeInternship(resumeId: string, data: SaveResumeWorkItem[]) {
|
export function saveResumeInternship(resumeId: string, data: SaveResumeWorkItem[]) {
|
||||||
return request.post<any, ApiResult>('/resume/internship', { resumeId, items: data })
|
return request.post<any, ApiResult>('/resume/internship', { resumeId, items: data })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加单条实习经历,返回新记录ID
|
||||||
|
* POST /resume/internship/add
|
||||||
|
*/
|
||||||
|
export function addResumeInternship(resumeId: string, data: SaveResumeWorkItem) {
|
||||||
|
return request.post<any, ApiResult<string>>('/resume/internship/add', data, {
|
||||||
|
params: { resumeId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID编辑单条实习经历
|
||||||
|
* POST /resume/internship/update
|
||||||
|
*/
|
||||||
|
export function updateResumeInternship(data: SaveResumeWorkItem) {
|
||||||
|
return request.post<any, ApiResult>('/resume/internship/update', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID删除单条实习经历
|
||||||
|
* POST /resume/internship/delete
|
||||||
|
*/
|
||||||
|
export function deleteResumeInternship(id: string) {
|
||||||
|
return request.post<any, ApiResult>('/resume/internship/delete', null, {
|
||||||
|
params: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** 保存简历项目经历参数(单条) */
|
/** 保存简历项目经历参数(单条) */
|
||||||
export interface SaveResumeProjectItem {
|
export interface SaveResumeProjectItem {
|
||||||
|
/** 记录ID(编辑时必传) */
|
||||||
|
id?: string | number
|
||||||
/** 项目名称 */
|
/** 项目名称 */
|
||||||
projectName?: string
|
projectName?: string
|
||||||
/** 所属公司 */
|
/** 所属公司 */
|
||||||
@@ -346,15 +436,45 @@ export interface SaveResumeProjectItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存简历项目经历(全量覆盖)
|
* 保存简历项目经历(全量覆盖)— 旧接口,保留兼容
|
||||||
* POST /resume/project
|
* POST /resume/project
|
||||||
*/
|
*/
|
||||||
export function saveResumeProject(resumeId: string, data: SaveResumeProjectItem[]) {
|
export function saveResumeProject(resumeId: string, data: SaveResumeProjectItem[]) {
|
||||||
return request.post<any, ApiResult>('/resume/project', { resumeId, items: data })
|
return request.post<any, ApiResult>('/resume/project', { resumeId, items: data })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加单条项目经历,返回新记录ID
|
||||||
|
* POST /resume/project/add
|
||||||
|
*/
|
||||||
|
export function addResumeProject(resumeId: string, data: SaveResumeProjectItem) {
|
||||||
|
return request.post<any, ApiResult<string>>('/resume/project/add', data, {
|
||||||
|
params: { resumeId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID编辑单条项目经历
|
||||||
|
* POST /resume/project/update
|
||||||
|
*/
|
||||||
|
export function updateResumeProject(data: SaveResumeProjectItem) {
|
||||||
|
return request.post<any, ApiResult>('/resume/project/update', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID删除单条项目经历
|
||||||
|
* POST /resume/project/delete
|
||||||
|
*/
|
||||||
|
export function deleteResumeProject(id: string) {
|
||||||
|
return request.post<any, ApiResult>('/resume/project/delete', null, {
|
||||||
|
params: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** 保存简历竞赛经历参数(单条) */
|
/** 保存简历竞赛经历参数(单条) */
|
||||||
export interface SaveResumeCompetitionItem {
|
export interface SaveResumeCompetitionItem {
|
||||||
|
/** 记录ID(编辑时必传) */
|
||||||
|
id?: string | number
|
||||||
/** 竞赛名称 */
|
/** 竞赛名称 */
|
||||||
competitionName?: string
|
competitionName?: string
|
||||||
/** 获奖情况 */
|
/** 获奖情况 */
|
||||||
@@ -366,13 +486,41 @@ export interface SaveResumeCompetitionItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存简历竞赛经历(全量覆盖)
|
* 保存简历竞赛经历(全量覆盖)— 旧接口,保留兼容
|
||||||
* POST /resume/competition
|
* POST /resume/competition
|
||||||
*/
|
*/
|
||||||
export function saveResumeCompetition(resumeId: string, data: SaveResumeCompetitionItem[]) {
|
export function saveResumeCompetition(resumeId: string, data: SaveResumeCompetitionItem[]) {
|
||||||
return request.post<any, ApiResult>('/resume/competition', { resumeId, items: data })
|
return request.post<any, ApiResult>('/resume/competition', { resumeId, items: data })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加单条竞赛经历,返回新记录ID
|
||||||
|
* POST /resume/competition/add
|
||||||
|
*/
|
||||||
|
export function addResumeCompetition(resumeId: string, data: SaveResumeCompetitionItem) {
|
||||||
|
return request.post<any, ApiResult<string>>('/resume/competition/add', data, {
|
||||||
|
params: { resumeId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID编辑单条竞赛经历
|
||||||
|
* POST /resume/competition/update
|
||||||
|
*/
|
||||||
|
export function updateResumeCompetition(data: SaveResumeCompetitionItem) {
|
||||||
|
return request.post<any, ApiResult>('/resume/competition/update', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID删除单条竞赛经历
|
||||||
|
* POST /resume/competition/delete
|
||||||
|
*/
|
||||||
|
export function deleteResumeCompetition(id: string) {
|
||||||
|
return request.post<any, ApiResult>('/resume/competition/delete', null, {
|
||||||
|
params: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除简历
|
* 删除简历
|
||||||
* POST /resume/delete
|
* POST /resume/delete
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,160 @@
|
|||||||
|
@use '../variables' as *;
|
||||||
|
|
||||||
|
// ==================== Agent会话岗位列表组件样式 ====================
|
||||||
|
.agent-chat-job-list {
|
||||||
|
background: $bg-middle;
|
||||||
|
border-radius: 0.12rem;
|
||||||
|
padding: 0.2rem;
|
||||||
|
margin-top: 0.16rem;
|
||||||
|
|
||||||
|
// 推荐说明文字
|
||||||
|
&__summary {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-dark;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.14rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单个岗位项
|
||||||
|
&__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: $bg-white;
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
padding: 0.14rem 0.16rem;
|
||||||
|
margin-bottom: 0.08rem;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 左侧信息区域
|
||||||
|
&__info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.12rem;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司 Logo
|
||||||
|
&__logo {
|
||||||
|
width: 0.36rem;
|
||||||
|
height: 0.36rem;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
background: $bg-main;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: $text-middle;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0.2rem;
|
||||||
|
height: 0.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 岗位详情
|
||||||
|
&__detail {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司名称
|
||||||
|
&__company {
|
||||||
|
font-size: 0.14rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 岗位名称
|
||||||
|
&__title {
|
||||||
|
font-size: 0.12rem;
|
||||||
|
color: $text-middle;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 0.04rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签行
|
||||||
|
&__tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.06rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单个标签
|
||||||
|
&__tag {
|
||||||
|
font-size: 0.11rem;
|
||||||
|
color: $text-middle;
|
||||||
|
background: $bg-main;
|
||||||
|
padding: 0.02rem 0.08rem;
|
||||||
|
border-radius: 0.03rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配度环形容器
|
||||||
|
&__score {
|
||||||
|
position: relative;
|
||||||
|
width: 0.44rem;
|
||||||
|
height: 0.44rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 环形 SVG
|
||||||
|
&__ring {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配度数字
|
||||||
|
&__score-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 0.11rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部按钮区域
|
||||||
|
&__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 0.14rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看全部岗位按钮
|
||||||
|
&__view-all-btn {
|
||||||
|
height: 0.38rem;
|
||||||
|
padding: 0 0.32rem;
|
||||||
|
background: $btn-dark;
|
||||||
|
color: $bg-white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.19rem;
|
||||||
|
font-size: 0.13rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
line-height: 0.38rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $btn-dark-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
@use '../variables' as *;
|
||||||
|
|
||||||
|
// ==================== Agent匹配岗位添加组件样式 ====================
|
||||||
|
.agent-match-job-add {
|
||||||
|
background: $bg-white;
|
||||||
|
border-radius: 0.12rem;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
// 顶部标题栏
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.18rem 0.2rem;
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题文字
|
||||||
|
&__title {
|
||||||
|
font-size: 0.16rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧操作区
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全部添加按钮
|
||||||
|
&__add-all-btn {
|
||||||
|
height: 0.32rem;
|
||||||
|
padding: 0 0.16rem;
|
||||||
|
background: $bg-white;
|
||||||
|
color: $text-dark;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 0.16rem;
|
||||||
|
font-size: 0.12rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
line-height: 0.3rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $text-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭按钮
|
||||||
|
&__close-btn {
|
||||||
|
width: 0.28rem;
|
||||||
|
height: 0.28rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0.24rem;
|
||||||
|
height: 0.24rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 岗位列表容器(可滚动)
|
||||||
|
&__list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.12rem 0.2rem;
|
||||||
|
|
||||||
|
// 自定义滚动条
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: $border-color;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单个岗位项
|
||||||
|
&__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.14rem 0;
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 左侧信息区域
|
||||||
|
&__info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.12rem;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司 Logo
|
||||||
|
&__logo {
|
||||||
|
width: 0.36rem;
|
||||||
|
height: 0.36rem;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
background: $bg-main;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: $text-middle;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0.2rem;
|
||||||
|
height: 0.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 岗位详情
|
||||||
|
&__detail {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司名称
|
||||||
|
&__company {
|
||||||
|
font-size: 0.14rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 岗位名称
|
||||||
|
&__position {
|
||||||
|
font-size: 0.12rem;
|
||||||
|
color: $text-middle;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 0.04rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签行
|
||||||
|
&__tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.06rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单个标签
|
||||||
|
&__tag {
|
||||||
|
font-size: 0.11rem;
|
||||||
|
color: $text-middle;
|
||||||
|
background: $bg-main;
|
||||||
|
padding: 0.02rem 0.08rem;
|
||||||
|
border-radius: 0.03rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧:匹配度 + 按钮
|
||||||
|
&__right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.12rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配度环形容器
|
||||||
|
&__score {
|
||||||
|
position: relative;
|
||||||
|
width: 0.44rem;
|
||||||
|
height: 0.44rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 环形 SVG
|
||||||
|
&__ring {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配度数字
|
||||||
|
&__score-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 0.11rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加/移除按钮
|
||||||
|
&__action-btn {
|
||||||
|
height: 0.32rem;
|
||||||
|
padding: 0 0.16rem;
|
||||||
|
border-radius: 0.16rem;
|
||||||
|
font-size: 0.12rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
line-height: 0.3rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid $text-dark;
|
||||||
|
background: $bg-white;
|
||||||
|
color: $text-dark;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $text-dark;
|
||||||
|
color: $bg-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除状态 — 深色背景
|
||||||
|
&--remove {
|
||||||
|
background: $text-dark;
|
||||||
|
color: $bg-white;
|
||||||
|
border-color: $text-dark;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部查看更多
|
||||||
|
&__footer {
|
||||||
|
padding: 0.14rem 0.2rem;
|
||||||
|
border-top: 1px solid $border-color;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看更多按钮
|
||||||
|
&__more-btn {
|
||||||
|
font-size: 0.12rem;
|
||||||
|
color: $text-middle;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
@use '../variables' as *;
|
||||||
|
|
||||||
|
// ==================== Agent设置面板样式 ====================
|
||||||
|
.agent-settings-panel {
|
||||||
|
// 表单分组
|
||||||
|
&__group {
|
||||||
|
margin-bottom: 0.28rem;
|
||||||
|
|
||||||
|
&:last-child { margin-bottom: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分组标签
|
||||||
|
&__label {
|
||||||
|
font-size: 0.14rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
margin-bottom: 0.14rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提示图标
|
||||||
|
&__tip {
|
||||||
|
color: $text-light;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 0.04rem;
|
||||||
|
font-size: 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选项按钮行
|
||||||
|
&__options {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单个选项按钮
|
||||||
|
&__option {
|
||||||
|
padding: 0.08rem 0.2rem;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
background: $bg-white;
|
||||||
|
color: $text-dark;
|
||||||
|
font-size: 0.13rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 大号选项(Agent模式、投递目标)
|
||||||
|
&--lg {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.14rem 0.16rem;
|
||||||
|
font-size: 0.14rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 0.08rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中态
|
||||||
|
&--active {
|
||||||
|
background: $bg-main;
|
||||||
|
border-color: $text-dark;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子标签
|
||||||
|
&__sub-label {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-middle;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简历选择行
|
||||||
|
&__resume-select {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.08rem;
|
||||||
|
background: $bg-main;
|
||||||
|
border-radius: 0.08rem;
|
||||||
|
padding: 0.06rem 0.12rem;
|
||||||
|
margin-bottom: 0.16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简历图标
|
||||||
|
&__resume-icon {
|
||||||
|
width: 0.18rem;
|
||||||
|
height: 0.18rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: $text-middle;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉选择器样式覆盖
|
||||||
|
&__select {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.is-focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix) {
|
||||||
|
color: $text-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开关行
|
||||||
|
&__switch-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开关文字
|
||||||
|
&__switch-text {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-dark;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开关副文字
|
||||||
|
&__switch-sub {
|
||||||
|
font-size: 0.12rem;
|
||||||
|
color: $text-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__msg--user &__msg-bubble {
|
&__msg--user &__msg-bubble {
|
||||||
background: #f0f3f6;
|
background: #fff;
|
||||||
color: #1a1a2e;
|
color: #1a1a2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.08rem;
|
gap: 0.08rem;
|
||||||
margin-top: 0.12rem;
|
margin-top: 0.12rem;
|
||||||
padding-right: 0.5rem;
|
margin-bottom: 0.12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__quick-item {
|
&__quick-item {
|
||||||
@@ -135,11 +135,24 @@
|
|||||||
font-size: 0.12rem;
|
font-size: 0.12rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
|
max-width: 80%;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AI 提问靠左 */
|
||||||
|
&--ai {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户提问靠右 */
|
||||||
|
&--user {
|
||||||
|
align-self: flex-end;
|
||||||
|
background: #f0f3f6;
|
||||||
|
border-color: #e0e3e6;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input-area {
|
&__input-area {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@use '../variables' as *;
|
||||||
|
|
||||||
|
.ai-thinking-indicator {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 0.12rem;
|
||||||
|
|
||||||
|
&__bubble {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
padding: 0.12rem 0.14rem;
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-middle;
|
||||||
|
font-style: italic;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.06rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
color: $accent;
|
||||||
|
font-size: 0.16rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -690,7 +690,7 @@
|
|||||||
&__drawer {
|
&__drawer {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: $bg-white;
|
background: $bg-white;
|
||||||
width: 9.4rem;
|
width: 10.4rem;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -753,7 +753,6 @@
|
|||||||
// ==================== 步骤四:预览布局 ====================
|
// ==================== 步骤四:预览布局 ====================
|
||||||
&__preview {
|
&__preview {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.2rem;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
@@ -763,8 +762,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: $bg-white;
|
background: $bg-white;
|
||||||
border: 1px solid $border-color;
|
border-bottom: 1px solid $border-color;
|
||||||
border-radius: 0.08rem;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -814,6 +812,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-bottom: 0.01rem solid $border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI优化结果卡片
|
// AI优化结果卡片
|
||||||
@@ -953,6 +952,15 @@
|
|||||||
&__ai-msg-bubble--loading {
|
&__ai-msg-bubble--loading {
|
||||||
color: $text-middle;
|
color: $text-middle;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.06rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI思考中的转圈加载图标
|
||||||
|
&__ai-loading-icon {
|
||||||
|
color: $accent;
|
||||||
|
font-size: 0.16rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息包裹容器(含撤销气泡)
|
// 消息包裹容器(含撤销气泡)
|
||||||
|
|||||||
@@ -54,6 +54,8 @@
|
|||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
|
break-after: avoid;
|
||||||
|
page-break-after: avoid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分割线
|
// 分割线
|
||||||
@@ -80,6 +82,8 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item-left {
|
&__item-left {
|
||||||
@@ -130,6 +134,8 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: $text-dark;
|
color: $text-dark;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
@use '../variables' as *;
|
||||||
|
|
||||||
|
/* 编辑简历名称弹窗样式 */
|
||||||
|
.resume-edit-name-dialog {
|
||||||
|
.el-dialog__header {
|
||||||
|
padding: 0.2rem 0.2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 0 0.3rem 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.24rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单字段 */
|
||||||
|
&__field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签 */
|
||||||
|
&__label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.16rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
margin-bottom: 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 必填星号 */
|
||||||
|
&__required {
|
||||||
|
color: $danger;
|
||||||
|
margin-right: 0.04rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框 */
|
||||||
|
&__input {
|
||||||
|
.el-input__wrapper {
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
padding: 0.04rem 0.14rem;
|
||||||
|
box-shadow: 0 0 0 1px $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
font-size: 0.14rem;
|
||||||
|
color: $text-dark;
|
||||||
|
height: 0.36rem;
|
||||||
|
line-height: normal;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $text-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部按钮区域 */
|
||||||
|
&__footer {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.16rem;
|
||||||
|
margin-top: 0.08rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮通用 */
|
||||||
|
&__btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 0.46rem;
|
||||||
|
border-radius: 0.23rem;
|
||||||
|
font-size: 0.15rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 取消按钮 */
|
||||||
|
&--cancel {
|
||||||
|
background: $bg-main;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保存按钮 */
|
||||||
|
&--save {
|
||||||
|
background: $accent;
|
||||||
|
color: $bg-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,10 @@
|
|||||||
@use './components/job-resume-custom-edit-panel.scss';
|
@use './components/job-resume-custom-edit-panel.scss';
|
||||||
@use './components/resume-analysis-report-drawer.scss';
|
@use './components/resume-analysis-report-drawer.scss';
|
||||||
@use './components/resume-issue-fix-drawer.scss';
|
@use './components/resume-issue-fix-drawer.scss';
|
||||||
|
@use './components/resume-edit-name-dialog.scss';
|
||||||
|
@use './components/agent-chat-job-list.scss';
|
||||||
|
@use './components/agent-match-job-add.scss';
|
||||||
|
@use './components/ai-thinking-indicator.scss';
|
||||||
|
|
||||||
// 全局样式(优先级最高)
|
// 全局样式(优先级最高)
|
||||||
@use './auto.scss';
|
@use './auto.scss';
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
@use 'sass:color';
|
||||||
@use '../variables' as *;
|
@use '../variables' as *;
|
||||||
|
|
||||||
// ==================== 求职助手页面样式 ====================
|
// ==================== 求职助手页面样式 ====================
|
||||||
.agent-page {
|
.agent-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: $bg-main;
|
background: $bg-main;
|
||||||
|
font-size: 0.14rem;
|
||||||
// 主内容区域(左侧导航栏右边的部分)
|
// 主内容区域(左侧导航栏右边的部分)
|
||||||
&__content {
|
&__content {
|
||||||
margin-left: 2rem;
|
margin-left: 2rem;
|
||||||
@@ -35,13 +36,13 @@
|
|||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
// 激活态 — 深色背景白色文字
|
// 激活态 — 品牌色背景白色文字
|
||||||
&--active {
|
&--active {
|
||||||
background: $text-dark;
|
background: $btn-dark;
|
||||||
|
|
||||||
.agent-page__step-number {
|
.agent-page__step-number {
|
||||||
background: $bg-white;
|
background: $bg-white;
|
||||||
color: $text-dark;
|
color: $btn-dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-page__step-label {
|
.agent-page__step-label {
|
||||||
@@ -117,14 +118,14 @@
|
|||||||
&__intro-header {
|
&__intro-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.12rem;
|
gap: 0.02rem;
|
||||||
margin-bottom: 0.2rem;
|
margin-bottom: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户图标
|
// 用户图标
|
||||||
&__intro-icon {
|
&__intro-icon {
|
||||||
width: 0.28rem;
|
width: 0.20rem;
|
||||||
height: 0.28rem;
|
height: 0.20rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: $text-dark;
|
color: $text-dark;
|
||||||
margin-top: 0.02rem;
|
margin-top: 0.02rem;
|
||||||
@@ -150,7 +151,6 @@
|
|||||||
color: $text-light;
|
color: $text-light;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
margin-bottom: 0.28rem;
|
margin-bottom: 0.28rem;
|
||||||
padding-left: 0.4rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导入个人资料行
|
// 导入个人资料行
|
||||||
@@ -162,7 +162,6 @@
|
|||||||
background: $bg-main;
|
background: $bg-main;
|
||||||
border-radius: 0.08rem;
|
border-radius: 0.08rem;
|
||||||
margin-bottom: 0.28rem;
|
margin-bottom: 0.28rem;
|
||||||
margin-left: 0.4rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导入图标
|
// 导入图标
|
||||||
@@ -190,8 +189,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 0.42rem;
|
height: 0.42rem;
|
||||||
margin-left: 0.4rem;
|
background: $btn-dark;
|
||||||
background: $text-dark;
|
|
||||||
color: $bg-white;
|
color: $bg-white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.21rem;
|
border-radius: 0.21rem;
|
||||||
@@ -245,4 +243,827 @@
|
|||||||
padding-bottom: 0.12rem;
|
padding-bottom: 0.12rem;
|
||||||
border-bottom: 1px solid $border-color;
|
border-bottom: 1px solid $border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 第2步:确认目标 ====================
|
||||||
|
|
||||||
|
// 第2步整体容器
|
||||||
|
&__step2 {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 7rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话区域
|
||||||
|
&__chat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话行:头像 + 文字
|
||||||
|
&__chat-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.04rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话头像
|
||||||
|
&__chat-avatar {
|
||||||
|
width: 0.32rem;
|
||||||
|
height: 0.32rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $bg-main;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: $text-middle;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0.18rem;
|
||||||
|
height: 0.18rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话文字
|
||||||
|
&__chat-text {
|
||||||
|
font-size: 0.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
line-height: 0.32rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正在输入指示器
|
||||||
|
&__chat-typing {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.05rem;
|
||||||
|
background: $bg-main;
|
||||||
|
border-radius: 0.16rem;
|
||||||
|
padding: 0.1rem 0.06rem;
|
||||||
|
|
||||||
|
span {
|
||||||
|
width: 0.06rem;
|
||||||
|
height: 0.06rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $text-light;
|
||||||
|
animation: typingDot 1.4s infinite;
|
||||||
|
|
||||||
|
&:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
&:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typingDot {
|
||||||
|
0%, 60%, 100% { opacity: 0.3; transform: scale(1); }
|
||||||
|
30% { opacity: 1; transform: scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 求职偏好卡片 ====================
|
||||||
|
&__pref-card {
|
||||||
|
background: $bg-main;
|
||||||
|
border-radius: 0.12rem;
|
||||||
|
padding: 0.24rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pref-desc {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-middle;
|
||||||
|
margin-bottom: 0.16rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pref-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.08rem;
|
||||||
|
margin-bottom: 0.24rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pref-tag {
|
||||||
|
padding: 0.06rem 0.16rem;
|
||||||
|
background: $bg-white;
|
||||||
|
border-radius: 0.04rem;
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pref-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pref-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 0.42rem;
|
||||||
|
border-radius: 0.21rem;
|
||||||
|
font-size: 0.14rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
border: none;
|
||||||
|
line-height: 0.42rem;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&--edit {
|
||||||
|
background: $border-color;
|
||||||
|
color: $text-dark;
|
||||||
|
|
||||||
|
&:hover { background: color.adjust($border-color, $lightness: -5%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
&--confirm {
|
||||||
|
background: $btn-dark;
|
||||||
|
color: $bg-white;
|
||||||
|
|
||||||
|
&:hover { background: $btn-dark-hover; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 匹配岗位卡片 ====================
|
||||||
|
&__match-card {
|
||||||
|
background: $bg-main;
|
||||||
|
border-radius: 0.12rem;
|
||||||
|
padding: 0.24rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&__match-desc {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-dark;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.16rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单个岗位项
|
||||||
|
&__match-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: $bg-white;
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
padding: 0.16rem;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
|
||||||
|
&:last-of-type { margin-bottom: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&__match-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.12rem;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司图标
|
||||||
|
&__match-icon {
|
||||||
|
width: 0.36rem;
|
||||||
|
height: 0.36rem;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
background: $bg-main;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: $text-middle;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0.2rem;
|
||||||
|
height: 0.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__match-detail {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__match-company {
|
||||||
|
font-size: 0.14rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__match-position {
|
||||||
|
font-size: 0.12rem;
|
||||||
|
color: $text-middle;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 0.06rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__match-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.06rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__match-tag {
|
||||||
|
font-size: 0.11rem;
|
||||||
|
color: $text-middle;
|
||||||
|
background: $bg-main;
|
||||||
|
padding: 0.02rem 0.08rem;
|
||||||
|
border-radius: 0.03rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配度环形
|
||||||
|
&__match-score {
|
||||||
|
position: relative;
|
||||||
|
width: 0.44rem;
|
||||||
|
height: 0.44rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0 0.16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__match-ring {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__match-score-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 0.11rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点赞/踩按钮
|
||||||
|
&__match-feedback {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.06rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__match-fb-btn {
|
||||||
|
width: 0.3rem;
|
||||||
|
height: 0.3rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: $text-light;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0.18rem;
|
||||||
|
height: 0.18rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover { color: $text-middle; }
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不满意反馈输入框
|
||||||
|
&__dislike-input {
|
||||||
|
margin-top: 0.12rem;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
height: 0.4rem;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 0.08rem;
|
||||||
|
padding: 0 0.14rem;
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-dark;
|
||||||
|
background: $bg-white;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&::placeholder { color: $text-light; }
|
||||||
|
&:focus { border-color: $text-middle; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部优化按钮
|
||||||
|
&__step2-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 0.08rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__optimize-btn {
|
||||||
|
height: 0.44rem;
|
||||||
|
padding: 0 0.4rem;
|
||||||
|
background: $btn-dark;
|
||||||
|
color: $bg-white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.22rem;
|
||||||
|
font-size: 0.14rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
line-height: 0.44rem;
|
||||||
|
|
||||||
|
&:hover { background: $btn-dark-hover; }
|
||||||
|
&:active { transform: scale(0.98); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 第3步:网申常见问题 ====================
|
||||||
|
|
||||||
|
// 第3步整体容器 — 左右两栏布局
|
||||||
|
&__step3 {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.24rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧表单包裹
|
||||||
|
&__form-wrapper {
|
||||||
|
background: $bg-white;
|
||||||
|
border-radius: 0.12rem;
|
||||||
|
padding: 0.28rem;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单标题
|
||||||
|
&__form-title {
|
||||||
|
font-size: 0.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-dark;
|
||||||
|
margin-bottom: 0.24rem;
|
||||||
|
padding-bottom: 0.12rem;
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单分组
|
||||||
|
&__form-group {
|
||||||
|
margin-bottom: 0.28rem;
|
||||||
|
|
||||||
|
&:last-child { margin-bottom: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单标签
|
||||||
|
&__form-label {
|
||||||
|
font-size: 0.14rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
margin-bottom: 0.12rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选项按钮行
|
||||||
|
&__form-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单个选项按钮
|
||||||
|
&__form-option {
|
||||||
|
padding: 0.08rem 0.24rem;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
background: $bg-white;
|
||||||
|
color: $text-dark;
|
||||||
|
font-size: 0.13rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $text-middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中态
|
||||||
|
&--active {
|
||||||
|
background: $btn-dark;
|
||||||
|
color: $bg-white;
|
||||||
|
border-color: $btn-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉选择器行
|
||||||
|
&__form-selects {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一下拉选择器样式覆盖
|
||||||
|
&__form-select {
|
||||||
|
width: 1.4rem;
|
||||||
|
|
||||||
|
// 覆盖 Element Plus 选择器样式以匹配页面风格
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
padding: 0.02rem 0.12rem;
|
||||||
|
background: $bg-white;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.is-focus {
|
||||||
|
border-color: $accent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-dark;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $text-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix) {
|
||||||
|
color: $text-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 第3步下半部分:插件安装 ====================
|
||||||
|
|
||||||
|
// 安装步骤说明文字
|
||||||
|
&__install-steps {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-middle;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-bottom: 0.28rem;
|
||||||
|
|
||||||
|
p { margin: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件区域
|
||||||
|
&__plugin-section {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件标题
|
||||||
|
&__plugin-title {
|
||||||
|
font-size: 0.18rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-dark;
|
||||||
|
margin-bottom: 0.24rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 浏览器按钮行
|
||||||
|
&__browser-btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单个浏览器按钮
|
||||||
|
&__browser-btn {
|
||||||
|
padding: 0.08rem 0.2rem;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
background: $bg-white;
|
||||||
|
color: $text-dark;
|
||||||
|
font-size: 0.13rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $accent;
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 指引图片幻灯片
|
||||||
|
&__guide-slide {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 0.08rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 设置完成页 ====================
|
||||||
|
|
||||||
|
// 完成页容器 — 居中布局
|
||||||
|
&__complete {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成页占位图标
|
||||||
|
&__complete-icon {
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
margin-bottom: 0.32rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成页标题
|
||||||
|
&__complete-title {
|
||||||
|
font-size: 0.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-dark;
|
||||||
|
margin: 0 0 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成页描述
|
||||||
|
&__complete-desc {
|
||||||
|
font-size: 0.14rem;
|
||||||
|
color: $text-middle;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用按钮
|
||||||
|
&__complete-btn {
|
||||||
|
padding: 0.12rem 0.36rem;
|
||||||
|
background: $btn-dark;
|
||||||
|
color: $bg-white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.24rem;
|
||||||
|
font-size: 0.15rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
|
||||||
|
&:hover { background: $btn-dark-hover; }
|
||||||
|
&:active { transform: scale(0.98); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== 求职助手正式内容区域 ====================
|
||||||
|
.agent-main {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
// 有右侧面板时取消居中
|
||||||
|
&--with-panel {
|
||||||
|
//justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 左侧主区域
|
||||||
|
&__left {
|
||||||
|
flex: 1;
|
||||||
|
height: calc(100vh - 0.6rem);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧匹配岗位面板
|
||||||
|
&__right {
|
||||||
|
flex: 1;
|
||||||
|
height: calc(100vh - 0.6rem);
|
||||||
|
margin-left: 0.2rem;
|
||||||
|
max-width: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 顶部固定设置栏 ====================
|
||||||
|
&__top-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.1rem;
|
||||||
|
padding: 0.12rem 0.16rem;
|
||||||
|
background: $bg-white;
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: 0.16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户头像
|
||||||
|
&__avatar {
|
||||||
|
width: 0.3rem;
|
||||||
|
height: 0.3rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $bg-main;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: $text-middle;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0.18rem;
|
||||||
|
height: 0.18rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态标签
|
||||||
|
&__status-tag {
|
||||||
|
font-size: 0.12rem;
|
||||||
|
color: $accent;
|
||||||
|
background: rgba(79, 194, 201, 0.1);
|
||||||
|
padding: 0.03rem 0.1rem;
|
||||||
|
border-radius: 0.04rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态提示文字
|
||||||
|
&__status-text {
|
||||||
|
font-size: 0.12rem;
|
||||||
|
color: $text-middle;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始按钮
|
||||||
|
&__start-btn {
|
||||||
|
height: 0.3rem;
|
||||||
|
padding: 0 0.14rem;
|
||||||
|
background: $text-dark;
|
||||||
|
color: $bg-white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.15rem;
|
||||||
|
font-size: 0.12rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
line-height: 0.3rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧工具按钮区
|
||||||
|
&__tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.06rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具按钮
|
||||||
|
&__tool-btn {
|
||||||
|
width: 0.28rem;
|
||||||
|
height: 0.28rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: $text-middle;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0.18rem;
|
||||||
|
height: 0.18rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $bg-main;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 中间聊天记录区域 ====================
|
||||||
|
&__chat-area {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.16rem 0.1rem;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
|
||||||
|
// 自定义滚动条
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: $border-color;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 对话消息样式 ====================
|
||||||
|
|
||||||
|
// 对话行
|
||||||
|
&__chat-row {
|
||||||
|
margin-bottom: 0.14rem;
|
||||||
|
|
||||||
|
// 用户消息 — 右对齐
|
||||||
|
&--user {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话气泡
|
||||||
|
&__chat-bubble {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 85%;
|
||||||
|
font-size: 0.14rem;
|
||||||
|
color: $text-dark;
|
||||||
|
line-height: 1.7;
|
||||||
|
padding: 0.1rem 0.16rem;
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
background: $bg-white;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
// 用户消息气泡 — 深色背景
|
||||||
|
&--user {
|
||||||
|
background: $bg-main;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 思考中的加载气泡
|
||||||
|
&--loading {
|
||||||
|
color: $text-middle;
|
||||||
|
font-style: italic;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.06rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 思考中的转圈加载图标
|
||||||
|
&__loading-icon {
|
||||||
|
color: $accent;
|
||||||
|
font-size: 0.16rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 底部固定输入框 ====================
|
||||||
|
&__input-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.1rem;
|
||||||
|
padding: 0.12rem 0.16rem;
|
||||||
|
background: $bg-white;
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输入框
|
||||||
|
&__input {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-dark;
|
||||||
|
background: transparent;
|
||||||
|
line-height: 0.3rem;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $text-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送按钮
|
||||||
|
&__send-btn {
|
||||||
|
width: 0.32rem;
|
||||||
|
height: 0.32rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: $text-dark;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0.2rem;
|
||||||
|
height: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -439,8 +439,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 0.1rem;
|
gap: 0.1rem;
|
||||||
margin-top: 0.18rem;
|
margin-top: 0.10rem;
|
||||||
padding-top: 0.14rem;
|
padding-bottom: 0.14rem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 问题类型按钮组(三选一)
|
// 问题类型按钮组(三选一)
|
||||||
|
|||||||
@@ -163,8 +163,8 @@
|
|||||||
// 弹出菜单
|
// 弹出菜单
|
||||||
&__popup {
|
&__popup {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0.3rem;
|
right: 0.46rem;
|
||||||
top: 0.1rem;
|
top: 0.18rem;
|
||||||
background: $bg-white;
|
background: $bg-white;
|
||||||
border: 1px solid $border-color;
|
border: 1px solid $border-color;
|
||||||
border-radius: 0.1rem;
|
border-radius: 0.1rem;
|
||||||
@@ -201,3 +201,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 导出简历弹窗样式修正 ====================
|
||||||
|
.resume-export-dialog {
|
||||||
|
font-size: 0.14rem;
|
||||||
|
.el-dialog__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 0.2rem 0.3rem 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修正 radio 组件在 100px 基准下的尺寸 */
|
||||||
|
&__radio-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.16rem;
|
||||||
|
|
||||||
|
.el-radio {
|
||||||
|
font-size: 14px;
|
||||||
|
height: auto;
|
||||||
|
line-height: normal;
|
||||||
|
|
||||||
|
.el-radio__inner {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-radio__label {
|
||||||
|
font-size: 14px;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__footer {
|
||||||
|
padding: 0.12rem 0.3rem 0.24rem;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary {
|
||||||
|
background: $btn-dark;
|
||||||
|
border-color: $btn-dark;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $btn-dark-hover;
|
||||||
|
border-color: $btn-dark-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ $selected-color: #F6FCFC;
|
|||||||
// 页面主背景色
|
// 页面主背景色
|
||||||
$bg-main: #F3F4F5;
|
$bg-main: #F3F4F5;
|
||||||
|
|
||||||
|
// 页面中间色景色(用于页面主题色背景和白色背景分隔,用于某些模块组件的组件外层背景等)
|
||||||
|
$bg-middle: #E6E6E6;
|
||||||
|
|
||||||
// 白色背景(卡片、输入框等)
|
// 白色背景(卡片、输入框等)
|
||||||
$bg-white: #FFFFFF;
|
$bg-white: #FFFFFF;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Agent会话岗位列表组件 — 聊天区域中显示推荐岗位卡片 -->
|
||||||
|
<div class="agent-chat-job-list">
|
||||||
|
<!-- 推荐说明文字 -->
|
||||||
|
<div class="agent-chat-job-list__summary">{{ summary }}</div>
|
||||||
|
|
||||||
|
<!-- 岗位列表(只显示前3个) -->
|
||||||
|
<div
|
||||||
|
v-for="job in displayJobs"
|
||||||
|
:key="job.id"
|
||||||
|
class="agent-chat-job-list__item"
|
||||||
|
>
|
||||||
|
<!-- 左侧:公司图标 + 岗位信息 -->
|
||||||
|
<div class="agent-chat-job-list__info">
|
||||||
|
<!-- 公司 Logo -->
|
||||||
|
<div class="agent-chat-job-list__logo">
|
||||||
|
<img v-if="job.companyLogoUrl" :src="job.companyLogoUrl" :alt="job.companyShortName" />
|
||||||
|
<svg v-else viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M3 21V7l9-4 9 4v14H3z" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M9 21v-6h6v6" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- 岗位详情 -->
|
||||||
|
<div class="agent-chat-job-list__detail">
|
||||||
|
<div class="agent-chat-job-list__company">{{ job.companyShortName || job.companyName }}</div>
|
||||||
|
<div class="agent-chat-job-list__title">{{ job.title }}</div>
|
||||||
|
<!-- 标签 -->
|
||||||
|
<div class="agent-chat-job-list__tags">
|
||||||
|
<span v-if="job.regionName" class="agent-chat-job-list__tag">{{ job.regionName }}</span>
|
||||||
|
<span v-if="job.categoryName" class="agent-chat-job-list__tag">{{ job.categoryName }}</span>
|
||||||
|
<span v-for="tag in job.tags?.slice(0, 2)" :key="tag" class="agent-chat-job-list__tag">{{ tag }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:匹配度环形 -->
|
||||||
|
<div class="agent-chat-job-list__score">
|
||||||
|
<svg class="agent-chat-job-list__ring" viewBox="0 0 44 44">
|
||||||
|
<!-- 背景圆环 -->
|
||||||
|
<circle cx="22" cy="22" r="18" fill="none" stroke="#E8E8E8" stroke-width="3" />
|
||||||
|
<!-- 进度圆环 -->
|
||||||
|
<circle
|
||||||
|
cx="22" cy="22" r="18" fill="none"
|
||||||
|
stroke="#4FC2C9" stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
:stroke-dasharray="2 * Math.PI * 18"
|
||||||
|
:stroke-dashoffset="2 * Math.PI * 18 * (1 - (job.matchScore || 0) / 100)"
|
||||||
|
transform="rotate(-90 22 22)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="agent-chat-job-list__score-text">{{ job.matchScore || 0 }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 查看全部岗位按钮 -->
|
||||||
|
<div class="agent-chat-job-list__footer">
|
||||||
|
<button class="agent-chat-job-list__view-all-btn" @click="handleViewAll">查看全部岗位</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { AgentRecommendJob } from '@/api/agent'
|
||||||
|
|
||||||
|
/** 组件 Props */
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 推荐说明文字 */
|
||||||
|
summary: string
|
||||||
|
/** 完整岗位列表数据 */
|
||||||
|
jobs: AgentRecommendJob[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** 事件 */
|
||||||
|
const emit = defineEmits<{
|
||||||
|
/** 点击查看全部岗位 */
|
||||||
|
(e: 'viewAll'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** 只显示前3个岗位 */
|
||||||
|
const displayJobs = computed(() => props.jobs.slice(0, 3))
|
||||||
|
|
||||||
|
/** 点击查看全部岗位 */
|
||||||
|
function handleViewAll() {
|
||||||
|
emit('viewAll')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use '../assets/styles/components/agent-chat-job-list';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Agent匹配岗位添加组件 — 右侧面板显示全部推荐岗位 -->
|
||||||
|
<div class="agent-match-job-add" v-loading="panelLoading" element-loading-text="加载中...">
|
||||||
|
<!-- 顶部标题栏 -->
|
||||||
|
<div class="agent-match-job-add__header">
|
||||||
|
<span class="agent-match-job-add__title">匹配岗位</span>
|
||||||
|
<div class="agent-match-job-add__actions">
|
||||||
|
<!-- 全部添加按钮 -->
|
||||||
|
<button class="agent-match-job-add__add-all-btn" @click="handleAddAll">
|
||||||
|
<span>+ 全部添加</span>
|
||||||
|
</button>
|
||||||
|
<!-- 关闭按钮 -->
|
||||||
|
<button class="agent-match-job-add__close-btn" @click="emit('close')">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="#1A1A2E" />
|
||||||
|
<path d="M15 9l-6 6M9 9l6 6" stroke="#fff" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 岗位列表 -->
|
||||||
|
<div class="agent-match-job-add__list">
|
||||||
|
<div
|
||||||
|
v-for="job in jobs"
|
||||||
|
:key="job.id"
|
||||||
|
class="agent-match-job-add__item"
|
||||||
|
>
|
||||||
|
<!-- 左侧:公司图标 + 岗位信息 -->
|
||||||
|
<div class="agent-match-job-add__info">
|
||||||
|
<!-- 公司 Logo -->
|
||||||
|
<div class="agent-match-job-add__logo">
|
||||||
|
<img v-if="job.companyLogoUrl" :src="job.companyLogoUrl" :alt="job.companyShortName" />
|
||||||
|
<svg v-else viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M3 21V7l9-4 9 4v14H3z" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M9 21v-6h6v6" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- 岗位详情 -->
|
||||||
|
<div class="agent-match-job-add__detail">
|
||||||
|
<div class="agent-match-job-add__company">{{ job.companyShortName || job.companyName }}</div>
|
||||||
|
<div class="agent-match-job-add__position">{{ job.title }}</div>
|
||||||
|
<!-- 标签 -->
|
||||||
|
<div class="agent-match-job-add__tags">
|
||||||
|
<span v-if="job.regionName" class="agent-match-job-add__tag">{{ job.regionName }}</span>
|
||||||
|
<span v-if="job.categoryName" class="agent-match-job-add__tag">{{ job.categoryName }}</span>
|
||||||
|
<span v-for="tag in job.tags?.slice(0, 2)" :key="tag" class="agent-match-job-add__tag">{{ tag }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:匹配度 + 操作按钮 -->
|
||||||
|
<div class="agent-match-job-add__right">
|
||||||
|
<!-- 匹配度环形 -->
|
||||||
|
<div class="agent-match-job-add__score">
|
||||||
|
<svg class="agent-match-job-add__ring" viewBox="0 0 44 44">
|
||||||
|
<circle cx="22" cy="22" r="18" fill="none" stroke="#E8E8E8" stroke-width="3" />
|
||||||
|
<circle
|
||||||
|
cx="22" cy="22" r="18" fill="none"
|
||||||
|
stroke="#4FC2C9" stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
:stroke-dasharray="2 * Math.PI * 18"
|
||||||
|
:stroke-dashoffset="2 * Math.PI * 18 * (1 - (job.matchScore || 0) / 100)"
|
||||||
|
transform="rotate(-90 22 22)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="agent-match-job-add__score-text">{{ job.matchScore || 0 }}%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加按钮 — 仅 applicationStatus === null 时显示 -->
|
||||||
|
<button
|
||||||
|
v-if="job.applicationStatus === null || job.applicationStatus === undefined"
|
||||||
|
class="agent-match-job-add__action-btn"
|
||||||
|
:disabled="loadingJobIds.includes(job.id)"
|
||||||
|
@click="handleToggle(job)"
|
||||||
|
>+ 添加</button>
|
||||||
|
|
||||||
|
<!-- 移除按钮 — 仅 applicationStatus === -1 时显示 -->
|
||||||
|
<button
|
||||||
|
v-else-if="job.applicationStatus === -1"
|
||||||
|
class="agent-match-job-add__action-btn agent-match-job-add__action-btn--remove"
|
||||||
|
:disabled="loadingJobIds.includes(job.id)"
|
||||||
|
@click="handleToggle(job)"
|
||||||
|
>移出</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部查看更多 -->
|
||||||
|
<div class="agent-match-job-add__footer">
|
||||||
|
<button class="agent-match-job-add__more-btn" @click="emit('viewMore')">查看更多岗位</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AgentRecommendJob } from '@/api/agent'
|
||||||
|
|
||||||
|
/** 组件 Props */
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 完整岗位列表数据 */
|
||||||
|
jobs: AgentRecommendJob[]
|
||||||
|
/** 正在请求中的岗位 ID 列表(父组件传入,控制按钮 loading) */
|
||||||
|
loadingJobIds: number[]
|
||||||
|
/** 面板整体加载状态 */
|
||||||
|
panelLoading: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** 事件 — 组件只负责通知父组件,不直接调接口 */
|
||||||
|
const emit = defineEmits<{
|
||||||
|
/** 关闭面板 */
|
||||||
|
(e: 'close'): void
|
||||||
|
/** 查看更多岗位 */
|
||||||
|
(e: 'viewMore'): void
|
||||||
|
/** 单个岗位添加/移除操作 */
|
||||||
|
(e: 'toggle', job: AgentRecommendJob): void
|
||||||
|
/** 全部添加操作 */
|
||||||
|
(e: 'addAll'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** 点击添加/移除 — 通知父组件处理 */
|
||||||
|
function handleToggle(job: AgentRecommendJob) {
|
||||||
|
emit('toggle', job)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全部添加 — 通知父组件处理(只传 applicationStatus 为 null 的岗位) */
|
||||||
|
function handleAddAll() {
|
||||||
|
emit('addAll')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use '../assets/styles/components/agent-match-job-add';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Agent设置面板 — 第4步配置求职助手 -->
|
||||||
|
<div class="agent-settings-panel">
|
||||||
|
<!-- Agent模式 -->
|
||||||
|
<div class="agent-settings-panel__group">
|
||||||
|
<div class="agent-settings-panel__label">
|
||||||
|
Agent模式
|
||||||
|
<el-tooltip content="协作模式:每次投递前需要你确认;托管模式:全自动投递无需确认" placement="top">
|
||||||
|
<span class="agent-settings-panel__tip">ⓘ</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="agent-settings-panel__options">
|
||||||
|
<button
|
||||||
|
class="agent-settings-panel__option agent-settings-panel__option--lg"
|
||||||
|
:class="{ 'agent-settings-panel__option--active': agentMode === 1 }"
|
||||||
|
@click="agentMode = 1"
|
||||||
|
>协作模式</button>
|
||||||
|
<button
|
||||||
|
class="agent-settings-panel__option agent-settings-panel__option--lg"
|
||||||
|
:class="{ 'agent-settings-panel__option--active': agentMode === 2 }"
|
||||||
|
@click="agentMode = 2"
|
||||||
|
>托管模式</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 投递目标 -->
|
||||||
|
<div class="agent-settings-panel__group">
|
||||||
|
<div class="agent-settings-panel__label">
|
||||||
|
投递目标
|
||||||
|
<el-tooltip content="每周自动投递的岗位数量目标" placement="top">
|
||||||
|
<span class="agent-settings-panel__tip">ⓘ</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="agent-settings-panel__options">
|
||||||
|
<button
|
||||||
|
v-for="goal in weeklyTargetOptions"
|
||||||
|
:key="goal.value"
|
||||||
|
class="agent-settings-panel__option agent-settings-panel__option--lg"
|
||||||
|
:class="{ 'agent-settings-panel__option--active': weeklyTarget === goal.value }"
|
||||||
|
@click="weeklyTarget = goal.value"
|
||||||
|
>{{ goal.label }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 简历设置 -->
|
||||||
|
<div class="agent-settings-panel__group">
|
||||||
|
<div class="agent-settings-panel__label">
|
||||||
|
简历设置
|
||||||
|
<el-tooltip content="选择投递时使用的默认简历" placement="top">
|
||||||
|
<span class="agent-settings-panel__tip">ⓘ</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 设置默认简历 -->
|
||||||
|
<div class="agent-settings-panel__sub-label">设置默认简历</div>
|
||||||
|
<div class="agent-settings-panel__resume-select">
|
||||||
|
<span class="agent-settings-panel__resume-icon">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none"><path d="M10 1H4a1.5 1.5 0 00-1.5 1.5v11A1.5 1.5 0 004 15h8a1.5 1.5 0 001.5-1.5V4.5L10 1z" stroke="currentColor" stroke-width="1"/><path d="M10 1v3.5h3.5" stroke="currentColor" stroke-width="1"/></svg>
|
||||||
|
</span>
|
||||||
|
<el-select
|
||||||
|
v-model="selectedResumeId"
|
||||||
|
placeholder="请选择简历"
|
||||||
|
class="agent-settings-panel__select"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="r in resumeList"
|
||||||
|
:key="r.id"
|
||||||
|
:label="r.resumeName"
|
||||||
|
:value="r.id || ''"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自动优化简历开关 -->
|
||||||
|
<div class="agent-settings-panel__switch-row">
|
||||||
|
<div class="agent-settings-panel__switch-text">
|
||||||
|
<span>在投递时帮我针对岗位自动优化简历</span>
|
||||||
|
<el-tooltip content="MVP只补充缺少技能" placement="top">
|
||||||
|
<span class="agent-settings-panel__tip">ⓘ</span>
|
||||||
|
</el-tooltip>
|
||||||
|
<br/>
|
||||||
|
<span class="agent-settings-panel__switch-sub">(MVP只补充缺少技能)</span>
|
||||||
|
</div>
|
||||||
|
<el-switch v-model="autoOptimizeSwitch" :active-color="accentColor" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
|
import { fetchResumeList } from '@/api/resume'
|
||||||
|
import type { ResumeListItem } from '@/api/resume'
|
||||||
|
import type { AgentConfig } from '@/api/agent'
|
||||||
|
|
||||||
|
/** 组件 Props — 接收初始配置 */
|
||||||
|
const props = defineProps<{
|
||||||
|
initialConfig?: AgentConfig | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** 强调色 — 传给 el-switch 的 active-color */
|
||||||
|
const accentColor = '#4FC2C9'
|
||||||
|
|
||||||
|
/** Agent模式:1=协作模式 2=托管模式 */
|
||||||
|
const agentMode = ref(1)
|
||||||
|
|
||||||
|
/** 投递目标选项 — value 对应接口 weeklyTarget 字段 */
|
||||||
|
const weeklyTargetOptions = [
|
||||||
|
{ label: '< 20 个/周', value: 1 },
|
||||||
|
{ label: '20-50 个/周', value: 2 },
|
||||||
|
{ label: '> 50 个/周', value: 3 },
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 当前选中的投递目标 */
|
||||||
|
const weeklyTarget = ref(2)
|
||||||
|
|
||||||
|
/** 简历列表 */
|
||||||
|
const resumeList = ref<ResumeListItem[]>([])
|
||||||
|
|
||||||
|
/** 当前选中的简历 ID */
|
||||||
|
const selectedResumeId = ref('')
|
||||||
|
|
||||||
|
/** 自动优化简历开关(布尔值,用于 el-switch) */
|
||||||
|
const autoOptimizeSwitch = ref(true)
|
||||||
|
|
||||||
|
/** 自动优化简历 — 转为接口需要的 integer(0=关闭 1=开启) */
|
||||||
|
const autoOptimizeResume = computed(() => autoOptimizeSwitch.value ? 1 : 0)
|
||||||
|
|
||||||
|
/** 监听初始配置 — 填充第4步表单 */
|
||||||
|
watch(() => props.initialConfig, (cfg) => {
|
||||||
|
if (!cfg) return
|
||||||
|
if (cfg.agentMode) agentMode.value = cfg.agentMode
|
||||||
|
if (cfg.weeklyTarget) weeklyTarget.value = cfg.weeklyTarget
|
||||||
|
if (cfg.autoOptimizeResume !== undefined) autoOptimizeSwitch.value = cfg.autoOptimizeResume === 1
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
/** 获取当前设置数据 — 供父组件调用 */
|
||||||
|
function getData() {
|
||||||
|
return {
|
||||||
|
agentMode: agentMode.value,
|
||||||
|
weeklyTarget: weeklyTarget.value,
|
||||||
|
autoOptimizeResume: autoOptimizeResume.value,
|
||||||
|
defaultResumeId: selectedResumeId.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 暴露给父组件 */
|
||||||
|
defineExpose({ getData })
|
||||||
|
|
||||||
|
/** 页面挂载时加载简历列表 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadResumeList()
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 加载简历列表 */
|
||||||
|
async function loadResumeList() {
|
||||||
|
try {
|
||||||
|
const res = await fetchResumeList()
|
||||||
|
if (res.code === '0' && res.data) {
|
||||||
|
resumeList.value = res.data
|
||||||
|
// 默认选中 isDefault=1 的简历,否则选第一个
|
||||||
|
const defaultResume = res.data.find(r => r.isDefault === 1)
|
||||||
|
if (defaultResume && defaultResume.id) {
|
||||||
|
selectedResumeId.value = defaultResume.id
|
||||||
|
} else if (res.data.length > 0 && res.data[0].id) {
|
||||||
|
selectedResumeId.value = res.data[0].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.error('[AgentSettingsPanel] 加载简历列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use '../assets/styles/components/agent-settings-panel';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 求职助手准备向导 — 第1步到第4步 -->
|
||||||
|
<div class="agent-page__wizard">
|
||||||
|
<!-- 个人资料编辑抽屉 -->
|
||||||
|
<ProfileEditDrawer
|
||||||
|
v-model="showEditDrawer"
|
||||||
|
:module="editModule"
|
||||||
|
:initial-data="editInitialData"
|
||||||
|
@save="handleSaveEdit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 顶部步骤导航条 -->
|
||||||
|
<div class="agent-page__steps">
|
||||||
|
<template v-for="(step, index) in steps" :key="index">
|
||||||
|
<div
|
||||||
|
class="agent-page__step"
|
||||||
|
:class="{ 'agent-page__step--active': currentStep === index + 1 }"
|
||||||
|
>
|
||||||
|
<span class="agent-page__step-number">{{ index + 1 }}</span>
|
||||||
|
<span class="agent-page__step-label">{{ step }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="index < steps.length - 1" class="agent-page__step-arrow">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主体内容区域 -->
|
||||||
|
<div class="agent-page__main">
|
||||||
|
<!-- ========== 第1步:确认个人资料 ========== -->
|
||||||
|
<template v-if="currentStep === 1">
|
||||||
|
<div class="agent-page__left">
|
||||||
|
<div class="agent-page__intro-card">
|
||||||
|
<div class="agent-page__intro-header">
|
||||||
|
<div class="agent-page__intro-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none"><path d="M12 12c2.7 0 5-2.3 5-5s-2.3-5-5-5-5 2.3-5 5 2.3 5 5 5zm0 2c-3.3 0-10 1.7-10 5v2h20v-2c0-3.3-6.7-5-10-5z" fill="currentColor"/></svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="agent-page__intro-title">在开始之前请先确认你的个人资料是否无误</h2>
|
||||||
|
</div>
|
||||||
|
<p class="agent-page__intro-desc">请仔细对你的资料评估(包括个人信息、教育背景、工作履历及技能),确保资料齐全,让我能顺利为你开启求职之旅。</p>
|
||||||
|
<div class="agent-page__import-row">
|
||||||
|
<div class="agent-page__import-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="agent-page__import-text">{{ profile.name }}的个人资料</span>
|
||||||
|
</div>
|
||||||
|
<div class="dflex-center aliite-c">
|
||||||
|
<button class="agent-page__confirm-btn" @click="handleNext">确认并进入</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="agent-page__right">
|
||||||
|
<div class="agent-page__profile-wrapper">
|
||||||
|
<div class="agent-page__profile-title">个人档案</div>
|
||||||
|
<ProfilePageContent :profile="profile" @edit="handleEdit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ========== 第2步:确认目标 ========== -->
|
||||||
|
<template v-if="currentStep === 2">
|
||||||
|
<div class="agent-page__step2">
|
||||||
|
<div class="agent-page__chat">
|
||||||
|
<div class="agent-page__chat-row">
|
||||||
|
<div class="agent-page__chat-avatar"><svg viewBox="0 0 24 24" fill="none"><path d="M12 12c2.7 0 5-2.3 5-5s-2.3-5-5-5-5 2.3-5 5 2.3 5 5 5zm0 2c-3.3 0-10 1.7-10 5v2h20v-2c0-3.3-6.7-5-10-5z" fill="currentColor"/></svg></div>
|
||||||
|
<span class="agent-page__chat-text">接下来,我想确认一下我为你搜寻的职位方向是否准确。</span>
|
||||||
|
</div>
|
||||||
|
<div class="p20 bg-f border-ra10">
|
||||||
|
<div class="agent-page__pref-card">
|
||||||
|
<p class="agent-page__pref-desc">这些是你之前设定的求职偏好。如有需要,请随时修改。</p>
|
||||||
|
<div class="agent-page__pref-tags">
|
||||||
|
<span v-for="name in intentionCategoryNames" :key="'cat-' + name" class="agent-page__pref-tag">{{ name }}</span>
|
||||||
|
<span v-for="name in intentionIndustryNames" :key="'ind-' + name" class="agent-page__pref-tag">{{ name }}</span>
|
||||||
|
<span v-for="name in intentionRegionNames" :key="'reg-' + name" class="agent-page__pref-tag">{{ name }}</span>
|
||||||
|
<span class="agent-page__pref-tag">{{ intentionEmploymentLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="agent-page__pref-actions">
|
||||||
|
<button class="agent-page__pref-btn agent-page__pref-btn--edit" @click="showJobGoalDialog = true">编辑</button>
|
||||||
|
<button class="agent-page__pref-btn agent-page__pref-btn--confirm" @click="handleNext">确认并继续</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="loadingMatchJobs" class="agent-page__chat-row">
|
||||||
|
<div class="agent-page__chat-avatar"><svg viewBox="0 0 24 24" fill="none"><path d="M12 12c2.7 0 5-2.3 5-5s-2.3-5-5-5-5 2.3-5 5 2.3 5 5 5zm0 2c-3.3 0-10 1.7-10 5v2h20v-2c0-3.3-6.7-5-10-5z" fill="currentColor"/></svg></div>
|
||||||
|
<div class="agent-page__chat-typing"><span></span><span></span><span></span></div>
|
||||||
|
</div>
|
||||||
|
<div v-if="matchedJobs.length > 0" class="p20 bg-f border-ra10">
|
||||||
|
<div class="agent-page__match-card">
|
||||||
|
<p class="agent-page__match-desc">这些是为你匹配的岗位,请选择你想投递的岗位,如对某个岗位不满意请告诉我。</p>
|
||||||
|
<div v-for="(job, i) in matchedJobs" :key="i" class="agent-page__match-item">
|
||||||
|
<div class="agent-page__match-info">
|
||||||
|
<div class="agent-page__match-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M3 21h18M3 7v14M9 3v4M15 3v4M9 3h6M5 7h14a2 2 0 012 2v10H3V9a2 2 0 012-2z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||||
|
<div class="agent-page__match-detail">
|
||||||
|
<div class="agent-page__match-company">{{ job.companyShortName || job.companyName }}</div>
|
||||||
|
<div class="agent-page__match-position">{{ job.title }}</div>
|
||||||
|
<div class="agent-page__match-tags"><span v-for="(t, j) in job.tags" :key="j" class="agent-page__match-tag">{{ t }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="agent-page__match-score">
|
||||||
|
<svg viewBox="0 0 40 40" class="agent-page__match-ring"><circle cx="20" cy="20" r="16" fill="none" stroke="#E8E8E8" stroke-width="3"/><circle cx="20" cy="20" r="16" fill="none" stroke="#000" stroke-width="3" :stroke-dasharray="100.53" :stroke-dashoffset="100.53 * (1 - job.matchScore / 100)" stroke-linecap="round" transform="rotate(-90 20 20)"/></svg>
|
||||||
|
<span class="agent-page__match-score-text">{{ job.matchScore }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="agent-page__match-feedback">
|
||||||
|
<button class="agent-page__match-fb-btn" :class="{ 'agent-page__match-fb-btn--active': job.feedback === 'like' }" @click="job.feedback = job.feedback === 'like' ? '' : 'like'"><svg viewBox="0 0 24 24" fill="none"><path d="M7 22V11l5-9 1.5 1 -1 5h6.5a2 2 0 012 2.2l-1.5 8A2 2 0 0117.6 20H7z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M4 11H2v11h2a1 1 0 001-1V12a1 1 0 00-1-1z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg></button>
|
||||||
|
<button class="agent-page__match-fb-btn" :class="{ 'agent-page__match-fb-btn--active': job.feedback === 'dislike' }" @click="handleDislike(i)"><svg viewBox="0 0 24 24" fill="none"><path d="M17 2v11l-5 9-1.5-1 1-5H5a2 2 0 01-2-2.2l1.5-8A2 2 0 016.4 4H17z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M20 13h2V2h-2a1 1 0 00-1 1v9a1 1 0 001 1z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="showDislikeInput" class="agent-page__dislike-input"><input v-model="dislikeReason" type="text" placeholder="你为什么对这个岗位不满意?" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="matchedJobs.length > 0" class="agent-page__step2-footer"><button class="agent-page__optimize-btn" @click="loadMatchedJobs">优化我的岗位匹配</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ========== 第3步:网申常见问题 + 插件安装 ========== -->
|
||||||
|
<template v-if="currentStep === 3">
|
||||||
|
<!-- 上半部分:网申常见问题 -->
|
||||||
|
<div v-if="step3Sub === 1" class="agent-page__step3">
|
||||||
|
<div class="agent-page__left">
|
||||||
|
<div class="agent-page__intro-card">
|
||||||
|
<div class="agent-page__intro-header"><div class="agent-page__intro-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M12 12c2.7 0 5-2.3 5-5s-2.3-5-5-5-5 2.3-5 5 2.3 5 5 5zm0 2c-3.3 0-10 1.7-10 5v2h20v-2c0-3.3-6.7-5-10-5z" fill="currentColor"/></svg></div><h2 class="agent-page__intro-title">现在我们来开启自动投递吧</h2></div>
|
||||||
|
<p class="agent-page__intro-desc">以下是校招网申常见填写信息。<br/>现在填写一次,我即可在自动申请时帮你填写信息。</p>
|
||||||
|
<div class="dflex-center aliite-c"><button class="agent-page__confirm-btn" @click="step3Sub = 2">确认并继续</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="agent-page__right">
|
||||||
|
<div class="agent-page__form-wrapper">
|
||||||
|
<div class="agent-page__form-title">网申常见问题</div>
|
||||||
|
<div class="agent-page__form-group"><div class="agent-page__form-label">是否愿意接受部门调剂?</div><div class="agent-page__form-options"><button class="agent-page__form-option" :class="{ 'agent-page__form-option--active': step3Form.acceptDeptTransfer === '是,服从调剂' }" @click="step3Form.acceptDeptTransfer = '是,服从调剂'">是,服从调剂</button><button class="agent-page__form-option" :class="{ 'agent-page__form-option--active': step3Form.acceptDeptTransfer === '否,不调剂' }" @click="step3Form.acceptDeptTransfer = '否,不调剂'">否,不调剂</button></div></div>
|
||||||
|
<div class="agent-page__form-group"><div class="agent-page__form-label">是否接受地点调剂?</div><div class="agent-page__form-options"><button class="agent-page__form-option" :class="{ 'agent-page__form-option--active': step3Form.acceptLocationTransfer === '是' }" @click="step3Form.acceptLocationTransfer = '是'">是</button><button class="agent-page__form-option" :class="{ 'agent-page__form-option--active': step3Form.acceptLocationTransfer === '否' }" @click="step3Form.acceptLocationTransfer = '否'">否</button></div></div>
|
||||||
|
<div class="agent-page__form-group"><div class="agent-page__form-label">可以参加面试的方式?</div><div class="agent-page__form-options"><button class="agent-page__form-option" :class="{ 'agent-page__form-option--active': step3Form.interviewType.includes('线下面试') }" @click="toggleInterviewType('线下面试')">线下面试</button><button class="agent-page__form-option" :class="{ 'agent-page__form-option--active': step3Form.interviewType.includes('线上远程') }" @click="toggleInterviewType('线上远程')">线上远程</button></div></div>
|
||||||
|
<div class="agent-page__form-group"><div class="agent-page__form-label">你的语言能力?</div><div class="agent-page__form-selects"><el-select v-model="step3Form.languages[0].language" placeholder="语种" class="agent-page__form-select"><el-option v-for="lang in languageOptions" :key="lang" :label="lang" :value="lang" /></el-select><el-select v-model="step3Form.languages[0].proficiency" placeholder="掌握程度" class="agent-page__form-select"><el-option v-for="p in proficiencyOptions" :key="p" :label="p" :value="p" /></el-select></div></div>
|
||||||
|
<div class="agent-page__form-group"><div class="agent-page__form-label">预计到岗时间?</div><div class="agent-page__form-selects"><el-select v-model="step3Form.availableDate" placeholder="请选择" class="agent-page__form-select"><el-option v-for="d in availableDateOptions" :key="d" :label="d" :value="d" /></el-select></div></div>
|
||||||
|
<template v-if="isInternship">
|
||||||
|
<div class="agent-page__form-group"><div class="agent-page__form-label">每周可实习天数?</div><div class="agent-page__form-selects"><el-select v-model="step3Form.internDaysPerWeek" placeholder="请选择" class="agent-page__form-select"><el-option v-for="d in internDaysOptions" :key="d" :label="d" :value="d" /></el-select></div></div>
|
||||||
|
<div class="agent-page__form-group"><div class="agent-page__form-label">预计实习时长?</div><div class="agent-page__form-selects"><el-select v-model="step3Form.internDuration" placeholder="请选择" class="agent-page__form-select"><el-option v-for="d in internDurationOptions" :key="d" :label="d" :value="d" /></el-select></div></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 下半部分:插件安装 -->
|
||||||
|
<div v-if="step3Sub === 2" class="agent-page__step3">
|
||||||
|
<div class="agent-page__left">
|
||||||
|
<div class="agent-page__intro-card">
|
||||||
|
<div class="agent-page__intro-header"><div class="agent-page__intro-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M12 12c2.7 0 5-2.3 5-5s-2.3-5-5-5-5 2.3-5 5 2.3 5 5 5zm0 2c-3.3 0-10 1.7-10 5v2h20v-2c0-3.3-6.7-5-10-5z" fill="currentColor"/></svg></div><h2 class="agent-page__intro-title">现在我们来开启自动投递吧</h2></div>
|
||||||
|
<p class="agent-page__intro-desc">简单三步安装 Chrome 扩展程序<br/>实现自动填表,并一站式追踪所有申请进度。</p>
|
||||||
|
<div class="agent-page__install-steps"><p>1. 点击 [ 安装扩展程序 ],前往 Chrome 应用商店。</p><p>2. 点击 "添加至 Chrome" 并完成安装。</p><p>3. 刷新本页,然后点击下方按钮完成同步。</p></div>
|
||||||
|
<div class="dflex-center aliite-c"><button class="agent-page__confirm-btn" @click="handleNext">我已安装</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="agent-page__right">
|
||||||
|
<div class="agent-page__form-wrapper">
|
||||||
|
<div class="agent-page__form-title">自动填写插件</div>
|
||||||
|
<div class="agent-page__plugin-section">
|
||||||
|
<h3 class="agent-page__plugin-title">自动填写插件</h3>
|
||||||
|
<div class="agent-page__browser-btns"><button v-for="b in browserList" :key="b.key" class="agent-page__browser-btn" @click="openBrowserGuide(b.key)">{{ b.label }}</button></div>
|
||||||
|
<div class="dflex-center aliite-c" style="margin-top: 0.24rem;"><button class="agent-page__confirm-btn" @click="downloadExtension">下载插件</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 浏览器安装指引弹窗 -->
|
||||||
|
<el-dialog v-model="showBrowserGuide" :title="currentBrowserLabel + ' 安装指引'" width="80%" top="5vh" destroy-on-close>
|
||||||
|
<el-carousel :autoplay="false" indicator-position="outside" height="70vh" arrow="always">
|
||||||
|
<el-carousel-item v-for="(img, i) in currentBrowserImages" :key="i"><div class="agent-page__guide-slide"><img :src="img" :alt="currentBrowserLabel + ' 安装步骤 ' + (i + 1)" /></div></el-carousel-item>
|
||||||
|
</el-carousel>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ========== 第4步:配置求职助手 ========== -->
|
||||||
|
<template v-if="currentStep === 4">
|
||||||
|
<div v-if="!setupComplete" class="agent-page__step3">
|
||||||
|
<div class="agent-page__left">
|
||||||
|
<div class="agent-page__intro-card">
|
||||||
|
<div class="agent-page__intro-header"><div class="agent-page__intro-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M12 12c2.7 0 5-2.3 5-5s-2.3-5-5-5-5 2.3-5 5 2.3 5 5 5zm0 2c-3.3 0-10 1.7-10 5v2h20v-2c0-3.3-6.7-5-10-5z" fill="currentColor"/></svg></div><h2 class="agent-page__intro-title">马上就好啦!请选择你想要的投递模式</h2></div>
|
||||||
|
<p class="agent-page__intro-desc">我已为你准备了关于自动化程度、简历生成及求职目标的灵活选项。请完成配置并确认。</p>
|
||||||
|
<div class="dflex-center aliite-c"><button class="agent-page__confirm-btn" @click="handleStep4Complete">设置完成</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="agent-page__right">
|
||||||
|
<div class="agent-page__form-wrapper">
|
||||||
|
<div class="agent-page__form-title">Agent设置</div>
|
||||||
|
<AgentSettingsPanel ref="settingsPanelRef" :initial-config="initialConfig" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="agent-page__complete">
|
||||||
|
<div class="agent-page__complete-icon"><svg viewBox="0 0 80 80" fill="none"><rect x="8" y="8" width="64" height="64" rx="8" fill="#E8E8E8"/><circle cx="40" cy="32" r="8" fill="#BFBFBF"/><path d="M20 60 L32 44 L44 52 L56 36 L64 44 L64 64 L20 64Z" fill="#BFBFBF"/></svg></div>
|
||||||
|
<h2 class="agent-page__complete-title">恭喜你!你的求职助手已经准备好。</h2>
|
||||||
|
<p class="agent-page__complete-desc">启用求职助手后你可以随时调整求职偏好及简历信息。</p>
|
||||||
|
<button class="agent-page__complete-btn" @click="handleLaunchAgent">启用求职助手开始投递</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 求职目标设置弹窗 -->
|
||||||
|
<JobGoalDialog v-model="showJobGoalDialog" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import ProfilePageContent from '@/components/ProfilePageContent.vue'
|
||||||
|
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
|
||||||
|
import JobGoalDialog from '@/components/JobGoalDialog.vue'
|
||||||
|
import AgentSettingsPanel from '@/components/AgentSettingsPanel.vue'
|
||||||
|
import { saveProfile, saveEducation, saveWork, saveInternship, saveProject, saveCompetition, fetchProfile, fetchEducation, fetchWork, fetchInternship, fetchProject, fetchCompetition } from '@/api/profile'
|
||||||
|
import type { SaveEducationItem, SaveWorkItem, SaveProjectItem, SaveCompetitionItem } from '@/api/profile'
|
||||||
|
import { fetchJobList } from '@/api/jobs'
|
||||||
|
import type { JobListItem } from '@/api/jobs'
|
||||||
|
import { resolveRegionName } from '@/utils/region'
|
||||||
|
import { resolveIndustryName } from '@/utils/industry'
|
||||||
|
import { resolveJobCategoryName } from '@/utils/jobCategory'
|
||||||
|
import type { AgentConfig } from '@/api/agent'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
/** 组件 Props — 接收父组件传入的初始配置数据 */
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 从接口查询到的初始配置,可能为空 */
|
||||||
|
initialConfig?: AgentConfig | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** 组件事件 — complete: 设置完成时传回完整配置数据;launch: 点击启用按钮 */
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'complete', data: Record<string, any>): void
|
||||||
|
(e: 'launch'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// ==================== 步骤导航 ====================
|
||||||
|
const steps = ['确认个人资料', '确认目标', '开启自动申请', '配置求职助手']
|
||||||
|
const currentStep = ref(1)
|
||||||
|
|
||||||
|
/** 进入下一步 */
|
||||||
|
function handleNext() {
|
||||||
|
if (currentStep.value < steps.length) currentStep.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 编辑抽屉状态 ====================
|
||||||
|
const showEditDrawer = ref(false)
|
||||||
|
const editModule = ref('info')
|
||||||
|
const editInitialData = ref<Record<string, any>>({})
|
||||||
|
|
||||||
|
// ==================== 个人档案数据 ====================
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!store.state.regions.length) store.dispatch('loadCommonData')
|
||||||
|
store.dispatch('loadJobIntention')
|
||||||
|
await loadProfile()
|
||||||
|
await loadEducation()
|
||||||
|
await loadWork()
|
||||||
|
await loadInternship()
|
||||||
|
await loadProject()
|
||||||
|
await loadCompetition()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadProfile() {
|
||||||
|
try {
|
||||||
|
const res = await fetchProfile()
|
||||||
|
if (res.code === '0' && res.data) {
|
||||||
|
const d = res.data
|
||||||
|
profile.value.name = d.name || ''; profile.value.phone = d.mobileNumber || ''
|
||||||
|
profile.value.email = d.email || ''; profile.value.idNumber = d.idCard || ''
|
||||||
|
profile.value.regionCode = d.regionCode || ''; profile.value.wechat = d.wechatNumber || ''
|
||||||
|
profile.value.skills = d.skills || []; profile.value.certificates = d.certificates || []
|
||||||
|
profile.value.portfolioUrl = d.portfolioUrl || ''
|
||||||
|
}
|
||||||
|
} catch { console.error('[AgentSetupWizard] 加载个人资料失败') }
|
||||||
|
}
|
||||||
|
async function loadEducation() {
|
||||||
|
try {
|
||||||
|
const res = await fetchEducation()
|
||||||
|
if (res.code === '0' && res.data) {
|
||||||
|
profile.value.education = res.data.map(item => ({ school: item.school || '', major: item.major || '', studyType: item.studyType ?? 0, degree: item.degree ?? 2, startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||||
|
}
|
||||||
|
} catch { console.error('[AgentSetupWizard] 加载教育经历失败') }
|
||||||
|
}
|
||||||
|
async function loadWork() {
|
||||||
|
try {
|
||||||
|
const res = await fetchWork()
|
||||||
|
if (res.code === '0' && res.data) {
|
||||||
|
profile.value.works = res.data.map(item => ({ companyName: item.companyName || '', position: item.position || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||||
|
}
|
||||||
|
} catch { console.error('[AgentSetupWizard] 加载工作经历失败') }
|
||||||
|
}
|
||||||
|
async function loadInternship() {
|
||||||
|
try {
|
||||||
|
const res = await fetchInternship()
|
||||||
|
if (res.code === '0' && res.data) {
|
||||||
|
profile.value.internships = res.data.map(item => ({ companyName: item.companyName || '', position: item.position || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||||
|
}
|
||||||
|
} catch { console.error('[AgentSetupWizard] 加载实习经历失败') }
|
||||||
|
}
|
||||||
|
async function loadProject() {
|
||||||
|
try {
|
||||||
|
const res = await fetchProject()
|
||||||
|
if (res.code === '0' && res.data) {
|
||||||
|
profile.value.projects = res.data.map(item => ({ projectName: item.projectName || '', companyName: item.companyName || '', role: item.role || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||||
|
}
|
||||||
|
} catch { console.error('[AgentSetupWizard] 加载项目经历失败') }
|
||||||
|
}
|
||||||
|
async function loadCompetition() {
|
||||||
|
try {
|
||||||
|
const res = await fetchCompetition()
|
||||||
|
if (res.code === '0' && res.data) {
|
||||||
|
profile.value.competitions = res.data.map(item => ({ competitionName: item.competitionName || '', award: item.award || '', awardDate: item.awardDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||||
|
}
|
||||||
|
} catch { console.error('[AgentSetupWizard] 加载竞赛经历失败') }
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = ref({
|
||||||
|
name: '', phone: '', email: '', idNumber: '', regionCode: '', portfolioUrl: '', wechat: '',
|
||||||
|
skills: [] as string[], certificates: [] as string[],
|
||||||
|
education: [] as Array<{ school: string; major: string; studyType: number; degree: number; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||||||
|
works: [] as Array<{ companyName: string; position: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||||||
|
internships: [] as Array<{ companyName: string; position: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||||||
|
projects: [] as Array<{ projectName: string; companyName: string; role: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||||||
|
competitions: [] as Array<{ competitionName: string; award: string; awardDate: string; description: Array<{ id: string; text: string }> }>,
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 编辑抽屉事件处理 ====================
|
||||||
|
function handleEdit(section: string) {
|
||||||
|
editModule.value = section
|
||||||
|
if (section === 'info') { editInitialData.value = { name: profile.value.name, email: profile.value.email, phone: profile.value.phone, location: profile.value.regionCode, wechat: profile.value.wechat } }
|
||||||
|
else if (section === 'education') { editInitialData.value = { education: profile.value.education.map(edu => ({ ...edu, description: edu.description.map(d => ({ ...d })) })) } }
|
||||||
|
else if (section === 'work') { editInitialData.value = { works: profile.value.works.map(exp => ({ ...exp, description: exp.description.map(d => ({ ...d })) })) } }
|
||||||
|
else if (section === 'internship') { editInitialData.value = { internships: (profile.value.internships || []).map(exp => ({ ...exp, description: exp.description.map(d => ({ ...d })) })) } }
|
||||||
|
else if (section === 'project') { editInitialData.value = { projects: (profile.value.projects || []).map(proj => ({ ...proj, description: proj.description.map(d => ({ ...d })) })) } }
|
||||||
|
else if (section === 'competition') { editInitialData.value = { competitions: profile.value.competitions.map(comp => ({ ...comp, description: comp.description.map(d => ({ ...d })) })) } }
|
||||||
|
else if (section === 'portfolio') { editInitialData.value = { portfolioUrl: profile.value.portfolioUrl } }
|
||||||
|
else if (section === 'skills') { editInitialData.value = { skills: [...profile.value.skills] } }
|
||||||
|
else if (section === 'certificate') { editInitialData.value = { certificates: [...(profile.value.certificates || [])] } }
|
||||||
|
else { editInitialData.value = {} }
|
||||||
|
showEditDrawer.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存中的加载状态 */
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
/** 保存编辑数据 — 调用接口持久化 */
|
||||||
|
async function handleSaveEdit(data: Record<string, any>) {
|
||||||
|
if (editModule.value === 'info') {
|
||||||
|
try { saving.value = true; await saveProfile({ name: data.name, email: data.email, mobileNumber: data.phone, regionCode: data.location, wechatNumber: data.wechat }); profile.value.name = data.name; profile.value.email = data.email; profile.value.phone = data.phone; profile.value.wechat = data.wechat; profile.value.regionCode = data.location || ''; ElMessage.success('个人信息保存成功') } catch { ElMessage.error('个人信息保存失败,请重试') } finally { saving.value = false }
|
||||||
|
} else if (editModule.value === 'education') {
|
||||||
|
try { saving.value = true; const payload: SaveEducationItem[] = data.education.map((edu: any) => ({ school: edu.school, major: edu.major, degree: edu.degree, studyType: edu.studyType, startDate: edu.startDate, endDate: edu.endDate, description: edu.description.map((d: any) => ({ id: d.id, text: d.text })) })); await saveEducation(payload); profile.value.education = data.education.map((edu: any) => ({ school: edu.school, major: edu.major, studyType: edu.studyType, degree: edu.degree, startDate: edu.startDate, endDate: edu.endDate, description: edu.description.map((d: any) => ({ ...d })) })); ElMessage.success('教育经历保存成功') } catch { ElMessage.error('教育经历保存失败,请重试') } finally { saving.value = false }
|
||||||
|
} else if (editModule.value === 'work') {
|
||||||
|
try { saving.value = true; const payload: SaveWorkItem[] = data.works.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 })) })); await saveWork(payload); profile.value.works = data.works.map((w: any) => ({ companyName: w.companyName, position: w.position, startDate: w.startDate, endDate: w.endDate, description: w.description.map((d: any) => ({ ...d })) })); ElMessage.success('工作经历保存成功') } catch { ElMessage.error('工作经历保存失败,请重试') } finally { saving.value = false }
|
||||||
|
} else if (editModule.value === 'internship') {
|
||||||
|
try { saving.value = true; const payload: SaveWorkItem[] = data.internships.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 })) })); await saveInternship(payload); profile.value.internships = data.internships.map((i: any) => ({ companyName: i.companyName, position: i.position, startDate: i.startDate, endDate: i.endDate, description: i.description.map((d: any) => ({ ...d })) })); ElMessage.success('实习经历保存成功') } catch { ElMessage.error('实习经历保存失败,请重试') } finally { saving.value = false }
|
||||||
|
} else if (editModule.value === 'project') {
|
||||||
|
try { saving.value = true; const payload: SaveProjectItem[] = data.projects.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 })) })); await saveProject(payload); profile.value.projects = data.projects.map((p: any) => ({ projectName: p.projectName, companyName: p.companyName, role: p.role, startDate: p.startDate, endDate: p.endDate, description: p.description.map((d: any) => ({ ...d })) })); ElMessage.success('项目经历保存成功') } catch { ElMessage.error('项目经历保存失败,请重试') } finally { saving.value = false }
|
||||||
|
} else if (editModule.value === 'competition') {
|
||||||
|
try { saving.value = true; const payload: SaveCompetitionItem[] = data.competitions.map((c: any) => ({ competitionName: c.competitionName, award: c.award || '', awardDate: c.awardDate || '', description: c.description.map((d: any) => ({ id: d.id, text: d.text })) })); await saveCompetition(payload); profile.value.competitions = data.competitions.map((c: any) => ({ competitionName: c.competitionName, award: c.award, awardDate: c.awardDate, description: c.description.map((d: any) => ({ ...d })) })); ElMessage.success('竞赛经历保存成功') } catch { ElMessage.error('竞赛经历保存失败,请重试') } finally { saving.value = false }
|
||||||
|
} else if (editModule.value === 'portfolio') {
|
||||||
|
try { saving.value = true; await saveProfile({ name: profile.value.name, email: profile.value.email, mobileNumber: profile.value.phone, regionCode: profile.value.regionCode, wechatNumber: profile.value.wechat, skills: profile.value.skills, certificates: profile.value.certificates, portfolioUrl: data.portfolioUrl }); profile.value.portfolioUrl = data.portfolioUrl; ElMessage.success('作品集保存成功') } catch { ElMessage.error('作品集保存失败,请重试') } finally { saving.value = false }
|
||||||
|
} else if (editModule.value === 'skills') {
|
||||||
|
try { saving.value = true; await saveProfile({ name: profile.value.name, email: profile.value.email, mobileNumber: profile.value.phone, regionCode: profile.value.regionCode, wechatNumber: profile.value.wechat, skills: [...data.skills], certificates: profile.value.certificates }); profile.value.skills = [...data.skills]; ElMessage.success('技能保存成功') } catch { ElMessage.error('技能保存失败,请重试') } finally { saving.value = false }
|
||||||
|
} else if (editModule.value === 'certificate') {
|
||||||
|
try { saving.value = true; await saveProfile({ name: profile.value.name, email: profile.value.email, mobileNumber: profile.value.phone, regionCode: profile.value.regionCode, wechatNumber: profile.value.wechat, skills: profile.value.skills, certificates: [...data.certificates] }); profile.value.certificates = [...data.certificates]; ElMessage.success('证书保存成功') } catch { ElMessage.error('证书保存失败,请重试') } finally { saving.value = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 第2步:确认目标 ====================
|
||||||
|
const showJobGoalDialog = ref(false)
|
||||||
|
const intentionCategoryNames = computed(() => (store.state.jobIntention.categoryIds || []).map((id: number) => resolveJobCategoryName(id)))
|
||||||
|
const intentionIndustryNames = computed(() => (store.state.jobIntention.industryIds || []).map((id: number) => resolveIndustryName(id)))
|
||||||
|
const intentionRegionNames = computed(() => (store.state.jobIntention.regionCodes || []).map((code: string) => resolveRegionName(code)))
|
||||||
|
const intentionEmploymentLabel = computed(() => store.state.jobIntention.employmentType === 1 ? '实习' : '全职')
|
||||||
|
|
||||||
|
interface MatchedJobItem extends JobListItem { feedback: string }
|
||||||
|
const matchedJobs = ref<MatchedJobItem[]>([])
|
||||||
|
const loadingMatchJobs = ref(false)
|
||||||
|
const showDislikeInput = ref(false)
|
||||||
|
const dislikeReason = ref('')
|
||||||
|
|
||||||
|
watch(showJobGoalDialog, (n, o) => { if (o === true && n === false) loadMatchedJobs() })
|
||||||
|
watch(currentStep, (val) => { if (val === 2) loadMatchedJobs() })
|
||||||
|
|
||||||
|
async function loadMatchedJobs() {
|
||||||
|
loadingMatchJobs.value = true; matchedJobs.value = []
|
||||||
|
try {
|
||||||
|
const intention = store.state.jobIntention
|
||||||
|
const res = await fetchJobList({ pageNum: 1, pageSize: 30, regionCodes: intention.regionCodes?.length ? intention.regionCodes : undefined, categoryIds: intention.categoryIds?.length ? intention.categoryIds : undefined, industryIds: intention.industryIds?.length ? intention.industryIds : undefined, employmentType: intention.employmentType ?? undefined })
|
||||||
|
if (res.code === '0' && res.data && res.data.list.length > 0) {
|
||||||
|
const shuffled = [...res.data.list].sort(() => Math.random() - 0.5)
|
||||||
|
matchedJobs.value = shuffled.slice(0, 3).map(item => ({ ...item, feedback: '' }))
|
||||||
|
}
|
||||||
|
} catch (e) { console.error('[AgentSetupWizard] 加载匹配岗位失败', e) }
|
||||||
|
finally { loadingMatchJobs.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDislike(index: number) {
|
||||||
|
const job = matchedJobs.value[index]
|
||||||
|
if (job.feedback === 'dislike') { job.feedback = ''; showDislikeInput.value = false }
|
||||||
|
else { job.feedback = 'dislike'; showDislikeInput.value = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 第3步:网申常见问题 ====================
|
||||||
|
const isInternship = computed(() => store.state.jobIntention.employmentType === 1)
|
||||||
|
const step3Sub = ref(1)
|
||||||
|
const step3Form = reactive({
|
||||||
|
acceptDeptTransfer: '', acceptLocationTransfer: '',
|
||||||
|
interviewType: [] as string[],
|
||||||
|
languages: [{ language: '', proficiency: '' }] as Array<{ language: string; proficiency: string }>,
|
||||||
|
availableDate: '', internDaysPerWeek: '', internDuration: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 监听初始配置数据 — 填充第3步表单 */
|
||||||
|
watch(() => props.initialConfig, (cfg) => {
|
||||||
|
if (!cfg) return
|
||||||
|
step3Form.acceptDeptTransfer = cfg.acceptDeptTransfer || ''
|
||||||
|
step3Form.acceptLocationTransfer = cfg.acceptLocationTransfer || ''
|
||||||
|
step3Form.interviewType = cfg.interviewType ? [...cfg.interviewType] : []
|
||||||
|
step3Form.languages = cfg.languages?.length ? cfg.languages.map(l => ({ language: l.language || '', proficiency: l.proficiency || '' })) : [{ language: '', proficiency: '' }]
|
||||||
|
step3Form.availableDate = cfg.availableDate || ''
|
||||||
|
step3Form.internDaysPerWeek = cfg.internDaysPerWeek || ''
|
||||||
|
step3Form.internDuration = cfg.internDuration || ''
|
||||||
|
}, { immediate: true })
|
||||||
|
const languageOptions = ['英语', '日语', '法语', '德语', '韩语', '西班牙语', '俄语']
|
||||||
|
const proficiencyOptions = ['入门', '日常会话', '商务会话', '无障碍沟通', '母语']
|
||||||
|
const availableDateOptions = ['一周以内', '两周以内', '一个月以内', '一个月以上']
|
||||||
|
const internDaysOptions = ['3天及以上', '4天及以上', '5天及以上']
|
||||||
|
const internDurationOptions = ['3个月', '4个月', '5个月', '6个月及以上']
|
||||||
|
function toggleInterviewType(type: string) { const idx = step3Form.interviewType.indexOf(type); idx >= 0 ? step3Form.interviewType.splice(idx, 1) : step3Form.interviewType.push(type) }
|
||||||
|
|
||||||
|
// ==================== 第3步下半部分:插件安装 ====================
|
||||||
|
const extensionDownloadUrl = 'https://offerpie.oss-cn-guangzhou.aliyuncs.com/extension/chrome-mv3-prod.rar'
|
||||||
|
const browserList = [{ key: 'chrome', label: 'Chrome浏览器' }, { key: 'edge', label: 'Edge浏览器' }, { key: 'safari', label: 'Safari浏览器' }, { key: '360', label: '360浏览器' }, { key: 'qq', label: 'QQ浏览器' }]
|
||||||
|
const browserImageCount: Record<string, number> = { chrome: 3, edge: 3, safari: 3, '360': 3, qq: 3 }
|
||||||
|
const guidanceImageBase = 'http://offerpie.oss-cn-guangzhou.aliyuncs.com/extension/guidance_image'
|
||||||
|
const showBrowserGuide = ref(false)
|
||||||
|
const currentBrowserKey = ref('')
|
||||||
|
const currentBrowserLabel = computed(() => browserList.find(b => b.key === currentBrowserKey.value)?.label || '')
|
||||||
|
const currentBrowserImages = computed(() => { const key = currentBrowserKey.value; if (!key) return []; const count = browserImageCount[key] || 3; return Array.from({ length: count }, (_, i) => `${guidanceImageBase}/guidance-image-${key}-${String(i + 1).padStart(2, '0')}.png`) })
|
||||||
|
function openBrowserGuide(key: string) { currentBrowserKey.value = key; showBrowserGuide.value = true }
|
||||||
|
function downloadExtension() { window.open(extensionDownloadUrl, '_blank') }
|
||||||
|
|
||||||
|
// ==================== 第4步:配置求职助手 ====================
|
||||||
|
const settingsPanelRef = ref<InstanceType<typeof AgentSettingsPanel> | null>(null)
|
||||||
|
const setupComplete = ref(false)
|
||||||
|
|
||||||
|
/** 设置完成 — 收集第3步+第4步数据,通过 emit 传回父组件 */
|
||||||
|
function handleStep4Complete() {
|
||||||
|
const step4Data = settingsPanelRef.value?.getData()
|
||||||
|
const allSettings = {
|
||||||
|
jobType: store.state.jobIntention.employmentType === 1 ? 1 : 2,
|
||||||
|
agentMode: step4Data?.agentMode ?? 1,
|
||||||
|
weeklyTarget: step4Data?.weeklyTarget ?? 2,
|
||||||
|
autoOptimizeResume: step4Data?.autoOptimizeResume ?? 1,
|
||||||
|
acceptDeptTransfer: step3Form.acceptDeptTransfer,
|
||||||
|
acceptLocationTransfer: step3Form.acceptLocationTransfer,
|
||||||
|
interviewType: [...step3Form.interviewType],
|
||||||
|
languages: step3Form.languages.filter(l => l.language),
|
||||||
|
availableDate: step3Form.availableDate,
|
||||||
|
internDaysPerWeek: step3Form.internDaysPerWeek,
|
||||||
|
internDuration: step3Form.internDuration,
|
||||||
|
status:1,
|
||||||
|
}
|
||||||
|
emit('complete', allSettings)
|
||||||
|
setupComplete.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启用求职助手 */
|
||||||
|
function handleLaunchAgent() { emit('launch') }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '../assets/styles/pages/agent';
|
||||||
|
</style>
|
||||||
+182
-28
@@ -25,27 +25,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 动态消息列表 -->
|
<!-- 快捷问题(仅岗位详情页显示,3个用户提问靠右) -->
|
||||||
<div
|
<div v-if="userQuestions.length > 0" class="ai-chat__quick-questions">
|
||||||
v-for="(msg, i) in messages"
|
|
||||||
:key="i"
|
|
||||||
class="ai-chat__msg"
|
|
||||||
:class="msg.role === 'ai' ? 'ai-chat__msg--ai' : 'ai-chat__msg--user'"
|
|
||||||
>
|
|
||||||
<div class="ai-chat__msg-bubble">{{ msg.content }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 快捷问题 -->
|
|
||||||
<div class="ai-chat__quick-questions">
|
|
||||||
<div
|
<div
|
||||||
v-for="(q, i) in quickQuestions"
|
v-for="(q, i) in userQuestions"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="ai-chat__quick-item"
|
class="ai-chat__quick-item ai-chat__quick-item--user"
|
||||||
@click="sendQuickQuestion(q)"
|
@click="sendQuickQuestion(q)"
|
||||||
>
|
>
|
||||||
{{ q }}
|
{{ q }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 动态消息列表 -->
|
||||||
|
<div
|
||||||
|
v-for="(msg, i) in messages"
|
||||||
|
:key="i"
|
||||||
|
class="ai-chat__msg"
|
||||||
|
:class="msg.role === 'assistant' ? 'ai-chat__msg--ai' : 'ai-chat__msg--user'"
|
||||||
|
>
|
||||||
|
<div class="ai-chat__msg-bubble" v-html="msg.content"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI 正在思考中加载指示器 -->
|
||||||
|
<AiThinkingIndicator v-if="aiLoading" text="AI正在思考中" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部输入框 -->
|
<!-- 底部输入框 -->
|
||||||
@@ -55,8 +58,9 @@
|
|||||||
class="ai-chat__input"
|
class="ai-chat__input"
|
||||||
placeholder="搜索职位、公司或关键词..."
|
placeholder="搜索职位、公司或关键词..."
|
||||||
@keyup.enter="sendMessage"
|
@keyup.enter="sendMessage"
|
||||||
|
:disabled="aiLoading"
|
||||||
/>
|
/>
|
||||||
<button class="ai-chat__send-btn" @click="sendMessage">
|
<button class="ai-chat__send-btn" @click="sendMessage" :disabled="aiLoading">
|
||||||
<span>➤</span>
|
<span>➤</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,8 +71,26 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed, watch, nextTick, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
import MemberDialog from '@/components/MemberDialog.vue'
|
import MemberDialog from '@/components/MemberDialog.vue'
|
||||||
|
import AiThinkingIndicator from '@/components/tools/AiThinkingIndicator.vue'
|
||||||
|
import { sendNovaChat } from '@/utils/aiRequest'
|
||||||
|
import type { NovaChatHistoryItem } from '@/utils/aiRequest'
|
||||||
|
import { fetchResumeList } from '@/api/resume'
|
||||||
|
|
||||||
|
// ==================== Props ====================
|
||||||
|
|
||||||
|
/** 组件属性 */
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 岗位 ID,由父页面传入 */
|
||||||
|
jobId?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** 当前路由(用于判断是否在岗位详情页) */
|
||||||
|
const currentRoute = useRoute()
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
// ==================== 状态 ====================
|
// ==================== 状态 ====================
|
||||||
|
|
||||||
@@ -81,26 +103,102 @@ const inputText = ref('')
|
|||||||
/** 消息列表容器 DOM 引用(用于滚动控制) */
|
/** 消息列表容器 DOM 引用(用于滚动控制) */
|
||||||
const messagesRef = ref<HTMLElement>()
|
const messagesRef = ref<HTMLElement>()
|
||||||
|
|
||||||
|
/** AI 是否正在请求中 */
|
||||||
|
const aiLoading = ref(false)
|
||||||
|
|
||||||
|
/** 默认简历 ID */
|
||||||
|
const defaultResumeId = ref<string>('')
|
||||||
|
|
||||||
// ==================== 类型定义 ====================
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
/** 聊天消息类型 */
|
/** 聊天消息类型 */
|
||||||
interface ChatMsg {
|
interface ChatMsg {
|
||||||
role: 'ai' | 'user'
|
/** 角色:assistant-AI助手 user-用户 */
|
||||||
|
role: 'assistant' | 'user'
|
||||||
|
/** 消息内容 */
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 本地缓存 ====================
|
||||||
|
|
||||||
|
/** 获取当前用户的缓存 key */
|
||||||
|
function getCacheKey(): string {
|
||||||
|
// 用 sessionStorage 中的登录状态 + 一个简单标识区分用户
|
||||||
|
// 如果有更精确的用户 ID 可以替换
|
||||||
|
const userId = sessionStorage.getItem('userId') || 'anonymous'
|
||||||
|
return `nova_chat_${userId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 localStorage 加载聊天记录 */
|
||||||
|
function loadChatFromCache(): ChatMsg[] {
|
||||||
|
try {
|
||||||
|
const key = getCacheKey()
|
||||||
|
const cached = localStorage.getItem(key)
|
||||||
|
if (cached) {
|
||||||
|
return JSON.parse(cached)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 解析失败忽略
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存聊天记录到 localStorage */
|
||||||
|
function saveChatToCache() {
|
||||||
|
try {
|
||||||
|
const key = getCacheKey()
|
||||||
|
localStorage.setItem(key, JSON.stringify(messages.value))
|
||||||
|
} catch {
|
||||||
|
// 存储失败忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 数据 ====================
|
// ==================== 数据 ====================
|
||||||
|
|
||||||
/** 聊天消息列表 */
|
/** 聊天消息列表 */
|
||||||
const messages = ref<ChatMsg[]>([])
|
const messages = ref<ChatMsg[]>([])
|
||||||
|
|
||||||
/** 快捷问题列表(点击后自动发送) */
|
/** 是否在岗位详情页 */
|
||||||
const quickQuestions = [
|
const isJobDetailPage = computed(() => currentRoute.name === 'JobDetail' || currentRoute.path.startsWith('/jobs/'))
|
||||||
'你想知道关于这个岗位的什么信息?',
|
|
||||||
'告诉我这个工作为什么适合我?',
|
/** 快捷问题列表(仅岗位详情页显示的 3 个用户提问) */
|
||||||
'我想修改这个岗位,怎么优化简历?',
|
const quickQuestions = computed(() => {
|
||||||
'帮我针对这个岗位生成一份面试攻略',
|
if (!isJobDetailPage.value) return []
|
||||||
]
|
return [
|
||||||
|
'告诉我这个工作为什么适合我?',
|
||||||
|
'我想修改这个岗位,怎么优化简历?',
|
||||||
|
'帮我针对这个岗位生成一份面试攻略',
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 用户提问列表(等同于 quickQuestions) */
|
||||||
|
const userQuestions = computed(() => quickQuestions.value)
|
||||||
|
|
||||||
|
// ==================== 滚动控制 ====================
|
||||||
|
|
||||||
|
/** 滚动到聊天区域底部 */
|
||||||
|
function scrollToBottom() {
|
||||||
|
nextTick(() => {
|
||||||
|
if (messagesRef.value) {
|
||||||
|
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 加载默认简历 ID ====================
|
||||||
|
|
||||||
|
/** 加载默认简历 ID(用于 Nova 对话接口) */
|
||||||
|
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)
|
||||||
|
defaultResumeId.value = defaultResume?.id || res.data[0]?.id || ''
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 加载失败忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 事件处理 ====================
|
// ==================== 事件处理 ====================
|
||||||
|
|
||||||
@@ -111,10 +209,66 @@ function sendQuickQuestion(question: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 发送消息(回车或点击发送按钮触发) */
|
/** 发送消息(回车或点击发送按钮触发) */
|
||||||
function sendMessage() {
|
async function sendMessage() {
|
||||||
if (!inputText.value.trim()) return
|
if (!inputText.value.trim() || aiLoading.value) return
|
||||||
messages.value.push({ role: 'user', content: inputText.value.trim() })
|
|
||||||
// TODO: 接入AI聊天接口
|
const userText = inputText.value.trim()
|
||||||
inputText.value = ''
|
inputText.value = ''
|
||||||
|
|
||||||
|
// 添加用户消息
|
||||||
|
messages.value.push({ role: 'user', content: userText })
|
||||||
|
saveChatToCache()
|
||||||
|
scrollToBottom()
|
||||||
|
|
||||||
|
// 构建历史对话(最近 20 条)
|
||||||
|
const history: NovaChatHistoryItem[] = messages.value
|
||||||
|
.slice(-20)
|
||||||
|
.map(m => ({ role: m.role, content: m.content }))
|
||||||
|
|
||||||
|
// 调用 Nova 对话接口
|
||||||
|
aiLoading.value = true
|
||||||
|
scrollToBottom()
|
||||||
|
try {
|
||||||
|
const res = await sendNovaChat({
|
||||||
|
message: userText,
|
||||||
|
resumeId: defaultResumeId.value || 0,
|
||||||
|
jobId: props.jobId || undefined,
|
||||||
|
history,
|
||||||
|
})
|
||||||
|
// 添加 AI 回复消息
|
||||||
|
const aiReply = res?.data?.message || res?.message || '抱歉,我暂时无法回答这个问题。'
|
||||||
|
messages.value.push({ role: 'assistant', content: aiReply })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Nova 对话请求失败', e)
|
||||||
|
messages.value.push({ role: 'assistant', content: '网络异常,请稍后重试。' })
|
||||||
|
} finally {
|
||||||
|
aiLoading.value = false
|
||||||
|
saveChatToCache()
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 生命周期 ====================
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 从缓存加载聊天记录
|
||||||
|
messages.value = loadChatFromCache()
|
||||||
|
if (messages.value.length > 0) {
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
// 加载默认简历 ID
|
||||||
|
if (store.state.isAuthenticated) {
|
||||||
|
loadDefaultResumeId()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听 jobId 变化(Jobs 页面点击"问助手"按钮时触发),自动插入 AI 提问消息
|
||||||
|
watch(() => props.jobId, (newId, oldId) => {
|
||||||
|
if (newId && newId !== oldId && !isJobDetailPage.value) {
|
||||||
|
// Jobs 页面:以 AI 身份插入提问到对话记录
|
||||||
|
messages.value.push({ role: 'assistant', content: '你想知道关于这个岗位的什么信息?' })
|
||||||
|
saveChatToCache()
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -286,9 +286,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- AI正在回复的加载指示器 -->
|
<!-- AI正在回复的加载指示器 -->
|
||||||
<div v-if="aiLoading" class="job-resume-custom-dialog__ai-msg job-resume-custom-dialog__ai-msg--ai">
|
<AiThinkingIndicator v-if="aiLoading" text="AI正在思考中" />
|
||||||
<div class="job-resume-custom-dialog__ai-msg-bubble job-resume-custom-dialog__ai-msg-bubble--loading">AI正在思考中...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- AI输入框 -->
|
<!-- AI输入框 -->
|
||||||
<div class="job-resume-custom-dialog__ai-input-area">
|
<div class="job-resume-custom-dialog__ai-input-area">
|
||||||
@@ -307,7 +305,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 编辑内容 — 折叠手风琴式编辑面板 -->
|
<!-- 编辑内容 — 折叠手风琴式编辑面板 -->
|
||||||
<div v-if="previewTab === 'edit'" class="job-resume-custom-dialog__preview-edit">
|
<div v-if="previewTab === 'edit'" class="job-resume-custom-dialog__preview-edit">
|
||||||
<JobResumeCustomEditPanel :resumeData="customResumeRawData" @update="onEditPanelUpdate" />
|
<JobResumeCustomEditPanel :resumeData="customResumeRawData" :jobId="jobId" @update="onEditPanelUpdate" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -318,7 +316,7 @@
|
|||||||
<button class="job-resume-custom-dialog__primary-btn" @click="handleDrawerNext">立即定制简历</button>
|
<button class="job-resume-custom-dialog__primary-btn" @click="handleDrawerNext">立即定制简历</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 步骤四专属底部:下载简历 + 立即去投递 -->
|
<!-- 步骤四专属底部:下载简历 + 立即去投递 -->
|
||||||
<div v-if="currentStep === 4" class="job-resume-custom-dialog__preview-footer">
|
<div v-if="currentStep === 4" class="job-resume-custom-dialog__preview-footer mt10 pt30">
|
||||||
<!-- 左侧:下载简历按钮(带下拉) -->
|
<!-- 左侧:下载简历按钮(带下拉) -->
|
||||||
<div class="job-resume-custom-dialog__download-wrap">
|
<div class="job-resume-custom-dialog__download-wrap">
|
||||||
<button class="job-resume-custom-dialog__download-btn" @click="toggleDownloadMenu">下载简历</button>
|
<button class="job-resume-custom-dialog__download-btn" @click="toggleDownloadMenu">下载简历</button>
|
||||||
@@ -347,14 +345,15 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, nextTick, watch } from 'vue'
|
import { ref, computed, nextTick, watch } from 'vue'
|
||||||
import html2pdf from 'html2pdf.js'
|
|
||||||
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
||||||
import JobResumeCustomEditPanel from '@/components/JobResumeCustomEditPanel.vue'
|
import JobResumeCustomEditPanel from '@/components/JobResumeCustomEditPanel.vue'
|
||||||
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
||||||
|
import { exportResumePdf, exportResumeWord } from '@/utils/resumeExport'
|
||||||
import { fetchResumeList } from '@/api/resume'
|
import { fetchResumeList } from '@/api/resume'
|
||||||
import type { ResumeListItem } from '@/api/resume'
|
import type { ResumeListItem } from '@/api/resume'
|
||||||
import { fetchCustomizeResume, generateCustomizeResume, aiEditResume, rollbackCustomizeResume } from '@/api/jobs'
|
import { fetchCustomizeResume, generateCustomizeResume, aiEditResume, rollbackCustomizeResume } from '@/api/jobs'
|
||||||
import type { CustomizeResumeData, AiEditChatMessage } from '@/api/jobs'
|
import type { CustomizeResumeData, AiEditChatMessage } from '@/api/jobs'
|
||||||
|
import AiThinkingIndicator from '@/components/tools/AiThinkingIndicator.vue'
|
||||||
|
|
||||||
// ==================== 类型定义 ====================
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
@@ -458,7 +457,7 @@ async function fetchAndLoadCustomResume() {
|
|||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
// 第一步:查询是否已有定制简历
|
// 第一步:查询是否已有定制简历
|
||||||
let queryRes = await fetchCustomizeResume()
|
let queryRes = await fetchCustomizeResume(props.jobId)
|
||||||
// if (queryRes.code === 0 && queryRes.data) {
|
// if (queryRes.code === 0 && queryRes.data) {
|
||||||
// // 已有定制简历,直接填充数据并跳转预览
|
// // 已有定制简历,直接填充数据并跳转预览
|
||||||
// fillCustomResumeData(queryRes.data)
|
// fillCustomResumeData(queryRes.data)
|
||||||
@@ -480,7 +479,7 @@ async function fetchAndLoadCustomResume() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 第三步:生成成功后再次查询获取简历数据
|
// 第三步:生成成功后再次查询获取简历数据
|
||||||
queryRes = await fetchCustomizeResume()
|
queryRes = await fetchCustomizeResume(props.jobId)
|
||||||
if (queryRes.code === 0 && queryRes.data) {
|
if (queryRes.code === 0 && queryRes.data) {
|
||||||
fillCustomResumeData(queryRes.data)
|
fillCustomResumeData(queryRes.data)
|
||||||
currentStep.value = 4
|
currentStep.value = 4
|
||||||
@@ -845,7 +844,7 @@ async function sendAiMessage(text: string) {
|
|||||||
oldResumeTemplateData.value = JSON.parse(JSON.stringify(resumeTemplateData.value))
|
oldResumeTemplateData.value = JSON.parse(JSON.stringify(resumeTemplateData.value))
|
||||||
|
|
||||||
// 重新查询定制简历数据来刷新简历预览
|
// 重新查询定制简历数据来刷新简历预览
|
||||||
const queryRes = await fetchCustomizeResume()
|
const queryRes = await fetchCustomizeResume(props.jobId)
|
||||||
if (queryRes.code === 0 && queryRes.data) {
|
if (queryRes.code === 0 && queryRes.data) {
|
||||||
fillCustomResumeData(queryRes.data)
|
fillCustomResumeData(queryRes.data)
|
||||||
// 开启差异对比模式
|
// 开启差异对比模式
|
||||||
@@ -894,14 +893,14 @@ async function confirmRollback() {
|
|||||||
if (idx < 0) return
|
if (idx < 0) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await rollbackCustomizeResume()
|
const res = await rollbackCustomizeResume(props.jobId)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
// 标记该消息为已撤销
|
// 标记该消息为已撤销
|
||||||
aiMessages.value[idx].rollbackStatus = 'done'
|
aiMessages.value[idx].rollbackStatus = 'done'
|
||||||
// 关闭差异对比模式
|
// 关闭差异对比模式
|
||||||
isShowDiff.value = false
|
isShowDiff.value = false
|
||||||
// 重新查询简历数据刷新预览
|
// 重新查询简历数据刷新预览
|
||||||
const queryRes = await fetchCustomizeResume()
|
const queryRes = await fetchCustomizeResume(props.jobId)
|
||||||
if (queryRes.code === 0 && queryRes.data) {
|
if (queryRes.code === 0 && queryRes.data) {
|
||||||
fillCustomResumeData(queryRes.data)
|
fillCustomResumeData(queryRes.data)
|
||||||
}
|
}
|
||||||
@@ -929,78 +928,22 @@ function toggleDownloadMenu() {
|
|||||||
async function handleDownload(type: 'pdf' | 'word') {
|
async function handleDownload(type: 'pdf' | 'word') {
|
||||||
showDownloadMenu.value = false
|
showDownloadMenu.value = false
|
||||||
|
|
||||||
if (type === 'pdf') {
|
const element = resumeTemplateRef.value?.resumeRef
|
||||||
// 通过 JobResumeTemplate 组件暴露的 resumeRef 获取简历DOM
|
if (!element) {
|
||||||
const element = resumeTemplateRef.value?.resumeRef
|
console.error('[下载简历] 无法获取简历模板DOM')
|
||||||
if (!element) {
|
return
|
||||||
console.error('[下载简历] 无法获取简历模板DOM')
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// html2pdf 配置选项
|
const fileName = (resumeTemplateData.value.name || '简历') + '_定制简历'
|
||||||
const options = {
|
|
||||||
margin: [10, 10, 10, 10] as [number, number, number, number],
|
|
||||||
filename: `${resumeTemplateData.value.name || '简历'}_定制简历.pdf`,
|
|
||||||
image: { type: 'jpeg', quality: 0.98 },
|
|
||||||
html2canvas: { scale: 2, useCORS: true, logging: false },
|
|
||||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' as const },
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await html2pdf().set(options).from(element).save()
|
if (type === 'pdf') {
|
||||||
} catch (err) {
|
await exportResumePdf(element, fileName)
|
||||||
console.error('[下载简历] PDF生成失败', err)
|
} else {
|
||||||
}
|
exportResumeWord(element, fileName)
|
||||||
} else {
|
|
||||||
// 将简历HTML转为Word文档并下载(使用HTML格式的.doc文件,Word可正常打开)
|
|
||||||
const element = resumeTemplateRef.value?.resumeRef
|
|
||||||
if (!element) {
|
|
||||||
console.error('[下载简历] 无法获取简历模板DOM')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取页面样式表内容
|
|
||||||
const styleSheets = Array.from(document.styleSheets)
|
|
||||||
let cssText = ''
|
|
||||||
styleSheets.forEach((sheet) => {
|
|
||||||
try {
|
|
||||||
Array.from(sheet.cssRules).forEach((rule) => {
|
|
||||||
cssText += rule.cssText + '\n'
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
// 跨域样式表无法读取,跳过
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组装完整HTML文档(Word可识别的HTML格式)
|
|
||||||
const fullHtml = `
|
|
||||||
<html xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
||||||
xmlns:w="urn:schemas-microsoft-com:office:word"
|
|
||||||
xmlns="http://www.w3.org/TR/REC-html40">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="ProgId" content="Word.Document">
|
|
||||||
<meta name="Generator" content="Microsoft Word 15">
|
|
||||||
<style>${cssText}</style>
|
|
||||||
</head>
|
|
||||||
<body>${element.outerHTML}</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
|
|
||||||
// 生成Blob并触发下载
|
|
||||||
const blob = new Blob([fullHtml], { type: 'application/msword' })
|
|
||||||
const fileName = `${resumeTemplateData.value.name || '简历'}_定制简历.doc`
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = URL.createObjectURL(blob)
|
|
||||||
link.download = fileName
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
URL.revokeObjectURL(link.href)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[下载简历] Word生成失败', err)
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[下载简历] 导出失败', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -247,6 +247,8 @@ interface EditFormData {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** 定制简历数据(从父组件传入) */
|
/** 定制简历数据(从父组件传入) */
|
||||||
resumeData: CustomizeResumeData
|
resumeData: CustomizeResumeData
|
||||||
|
/** 岗位ID(父组件传入,用于自动保存接口调用) */
|
||||||
|
jobId: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
/** 数据变更时通知父组件同步更新简历模板预览 */
|
/** 数据变更时通知父组件同步更新简历模板预览 */
|
||||||
@@ -406,7 +408,7 @@ function autoSave() {
|
|||||||
// 通知父组件同步更新简历模板预览
|
// 通知父组件同步更新简历模板预览
|
||||||
emit('update', payload)
|
emit('update', payload)
|
||||||
try {
|
try {
|
||||||
await updateCustomizeResume(payload)
|
await updateCustomizeResume(payload,props.jobId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[JobResumeCustomEditPanel] 自动保存失败', e)
|
console.error('[JobResumeCustomEditPanel] 自动保存失败', e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,18 @@
|
|||||||
<div v-for="(edu, idx) in resumeData.educations" :key="'edu-' + idx" class="resume-html__item">
|
<div v-for="(edu, idx) in resumeData.educations" :key="'edu-' + idx" class="resume-html__item">
|
||||||
<div class="resume-html__item-header">
|
<div class="resume-html__item-header">
|
||||||
<div class="resume-html__item-left">
|
<div class="resume-html__item-left">
|
||||||
<span class="resume-html__item-main">{{ edu.school }},{{ edu.major }},{{ degreeText(edu.degree) }}</span>
|
<span class="resume-html__item-main">
|
||||||
|
<!-- 教育经历标题差异对比 -->
|
||||||
|
<template v-if="showDiff">
|
||||||
|
<template v-for="(seg, si) in diffText(
|
||||||
|
oldResumeData?.educations?.[idx] ? (oldResumeData.educations[idx].school + ',' + oldResumeData.educations[idx].major + ',' + degreeText(oldResumeData.educations[idx].degree)) : '',
|
||||||
|
edu.school + ',' + edu.major + ',' + degreeText(edu.degree)
|
||||||
|
)" :key="'eduh-' + si">
|
||||||
|
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ edu.school }},{{ edu.major }},{{ degreeText(edu.degree) }}</template>
|
||||||
|
</span>
|
||||||
<span v-if="edu.description && edu.description.length" class="resume-html__item-desc">
|
<span v-if="edu.description && edu.description.length" class="resume-html__item-desc">
|
||||||
<!-- 教育经历描述差异对比 -->
|
<!-- 教育经历描述差异对比 -->
|
||||||
<template v-if="showDiff">
|
<template v-if="showDiff">
|
||||||
@@ -70,7 +81,18 @@
|
|||||||
<div class="resume-html__divider"></div>
|
<div class="resume-html__divider"></div>
|
||||||
<div v-for="(work, idx) in resumeData.workExperiences" :key="'work-' + idx" class="resume-html__item">
|
<div v-for="(work, idx) in resumeData.workExperiences" :key="'work-' + idx" class="resume-html__item">
|
||||||
<div class="resume-html__item-header">
|
<div class="resume-html__item-header">
|
||||||
<span class="resume-html__item-main">{{ work.companyName }},{{ work.position }}</span>
|
<span class="resume-html__item-main">
|
||||||
|
<!-- 工作经历标题差异对比 -->
|
||||||
|
<template v-if="showDiff">
|
||||||
|
<template v-for="(seg, si) in diffText(
|
||||||
|
oldResumeData?.workExperiences?.[idx] ? (oldResumeData.workExperiences[idx].companyName + ',' + oldResumeData.workExperiences[idx].position) : '',
|
||||||
|
work.companyName + ',' + work.position
|
||||||
|
)" :key="'workh-' + si">
|
||||||
|
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ work.companyName }},{{ work.position }}</template>
|
||||||
|
</span>
|
||||||
<div class="resume-html__item-right">
|
<div class="resume-html__item-right">
|
||||||
<span class="resume-html__item-location" v-if="work.location">{{ work.location }}</span>
|
<span class="resume-html__item-location" v-if="work.location">{{ work.location }}</span>
|
||||||
<span class="resume-html__item-date">{{ work.startDate }} — {{ work.endDate || '至今' }}</span>
|
<span class="resume-html__item-date">{{ work.startDate }} — {{ work.endDate || '至今' }}</span>
|
||||||
@@ -96,7 +118,18 @@
|
|||||||
<div class="resume-html__divider"></div>
|
<div class="resume-html__divider"></div>
|
||||||
<div v-for="(intern, idx) in resumeData.internships" :key="'intern-' + idx" class="resume-html__item">
|
<div v-for="(intern, idx) in resumeData.internships" :key="'intern-' + idx" class="resume-html__item">
|
||||||
<div class="resume-html__item-header">
|
<div class="resume-html__item-header">
|
||||||
<span class="resume-html__item-main">{{ intern.companyName }},{{ intern.position }}</span>
|
<span class="resume-html__item-main">
|
||||||
|
<!-- 实习经历标题差异对比 -->
|
||||||
|
<template v-if="showDiff">
|
||||||
|
<template v-for="(seg, si) in diffText(
|
||||||
|
oldResumeData?.internships?.[idx] ? (oldResumeData.internships[idx].companyName + ',' + oldResumeData.internships[idx].position) : '',
|
||||||
|
intern.companyName + ',' + intern.position
|
||||||
|
)" :key="'internh-' + si">
|
||||||
|
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ intern.companyName }},{{ intern.position }}</template>
|
||||||
|
</span>
|
||||||
<div class="resume-html__item-right">
|
<div class="resume-html__item-right">
|
||||||
<span class="resume-html__item-location" v-if="intern.location">{{ intern.location }}</span>
|
<span class="resume-html__item-location" v-if="intern.location">{{ intern.location }}</span>
|
||||||
<span class="resume-html__item-date">{{ intern.startDate }} — {{ intern.endDate || '至今' }}</span>
|
<span class="resume-html__item-date">{{ intern.startDate }} — {{ intern.endDate || '至今' }}</span>
|
||||||
@@ -122,9 +155,28 @@
|
|||||||
<div class="resume-html__divider"></div>
|
<div class="resume-html__divider"></div>
|
||||||
<div v-for="(proj, idx) in resumeData.projects" :key="'proj-' + idx" class="resume-html__item">
|
<div v-for="(proj, idx) in resumeData.projects" :key="'proj-' + idx" class="resume-html__item">
|
||||||
<div class="resume-html__item-header">
|
<div class="resume-html__item-header">
|
||||||
<span class="resume-html__item-main">{{ proj.projectName }}{{ proj.role ? ',' + proj.role : '' }}</span>
|
<span class="resume-html__item-main">
|
||||||
|
<!-- 项目经历标题差异对比 -->
|
||||||
|
<template v-if="showDiff">
|
||||||
|
<template v-for="(seg, si) in diffText(
|
||||||
|
oldResumeData?.projects?.[idx] ? (oldResumeData.projects[idx].projectName + (oldResumeData.projects[idx].role ? ',' + oldResumeData.projects[idx].role : '')) : '',
|
||||||
|
proj.projectName + (proj.role ? ',' + proj.role : '')
|
||||||
|
)" :key="'projh-' + si">
|
||||||
|
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ proj.projectName }}{{ proj.role ? ',' + proj.role : '' }}</template>
|
||||||
|
</span>
|
||||||
<div class="resume-html__item-right">
|
<div class="resume-html__item-right">
|
||||||
<span class="resume-html__item-location" v-if="proj.companyName">{{ proj.companyName }}</span>
|
<span class="resume-html__item-location" v-if="proj.companyName">
|
||||||
|
<!-- 项目所属公司差异对比 -->
|
||||||
|
<template v-if="showDiff">
|
||||||
|
<template v-for="(seg, si) in diffText(getOldFieldText(oldResumeData?.projects, idx, 'companyName'), proj.companyName)" :key="'projc-' + si">
|
||||||
|
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ proj.companyName }}</template>
|
||||||
|
</span>
|
||||||
<span class="resume-html__item-date">{{ proj.startDate }} — {{ proj.endDate || '至今' }}</span>
|
<span class="resume-html__item-date">{{ proj.startDate }} — {{ proj.endDate || '至今' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,7 +200,18 @@
|
|||||||
<div class="resume-html__divider"></div>
|
<div class="resume-html__divider"></div>
|
||||||
<div v-for="(comp, idx) in resumeData.competitions" :key="'comp-' + idx" class="resume-html__item">
|
<div v-for="(comp, idx) in resumeData.competitions" :key="'comp-' + idx" class="resume-html__item">
|
||||||
<div class="resume-html__item-header">
|
<div class="resume-html__item-header">
|
||||||
<span class="resume-html__item-main">{{ comp.competitionName }}{{ comp.award ? ',' + comp.award : '' }}</span>
|
<span class="resume-html__item-main">
|
||||||
|
<!-- 竞赛经历标题差异对比 -->
|
||||||
|
<template v-if="showDiff">
|
||||||
|
<template v-for="(seg, si) in diffText(
|
||||||
|
oldResumeData?.competitions?.[idx] ? (oldResumeData.competitions[idx].competitionName + (oldResumeData.competitions[idx].award ? ',' + oldResumeData.competitions[idx].award : '')) : '',
|
||||||
|
comp.competitionName + (comp.award ? ',' + comp.award : '')
|
||||||
|
)" :key="'comph-' + si">
|
||||||
|
<span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ comp.competitionName }}{{ comp.award ? ',' + comp.award : '' }}</template>
|
||||||
|
</span>
|
||||||
<span class="resume-html__item-date" v-if="comp.awardDate">{{ comp.awardDate }}</span>
|
<span class="resume-html__item-date" v-if="comp.awardDate">{{ comp.awardDate }}</span>
|
||||||
</div>
|
</div>
|
||||||
<ul v-if="comp.description && comp.description.length" class="resume-html__desc-list">
|
<ul v-if="comp.description && comp.description.length" class="resume-html__desc-list">
|
||||||
@@ -325,14 +388,23 @@ function diffText(oldText?: string, newText?: string): DiffSegment[] {
|
|||||||
return computeDiff(oldText, newText)
|
return computeDiff(oldText, newText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从旧数据列表中取出指定索引项的某个字段值
|
||||||
|
* @param oldList 旧数据列表
|
||||||
|
* @param itemIdx 项索引
|
||||||
|
* @param field 字段名
|
||||||
|
*/
|
||||||
|
function getOldFieldText(oldList: any[] | undefined, itemIdx: number, field: string): string {
|
||||||
|
if (!oldList || !oldList[itemIdx]) return ''
|
||||||
|
return oldList[itemIdx][field] || ''
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对比带description数组的经历列表中某一项描述文本的差异
|
* 对比带description数组的经历列表中某一项描述文本的差异
|
||||||
* 适用于工作经历、实习经历等有description数组的结构
|
|
||||||
* @param oldList 旧数据列表
|
* @param oldList 旧数据列表
|
||||||
* @param itemIdx 经历项索引
|
* @param itemIdx 经历项索引
|
||||||
* @param descIdx 描述段落索引
|
* @param descIdx 描述段落索引
|
||||||
* @param newText 新文本
|
* @param newText 新文本
|
||||||
* @param type 类型标识(用于区分projects和competitions等不同结构)
|
|
||||||
*/
|
*/
|
||||||
function diffListText(oldList: any[] | undefined, itemIdx: number, descIdx: number, newText: string, _type?: string): DiffSegment[] {
|
function diffListText(oldList: any[] | undefined, itemIdx: number, descIdx: number, newText: string, _type?: string): DiffSegment[] {
|
||||||
if (!oldList || !oldList[itemIdx] || !oldList[itemIdx].description || !oldList[itemIdx].description[descIdx]) {
|
if (!oldList || !oldList[itemIdx] || !oldList[itemIdx].description || !oldList[itemIdx].description[descIdx]) {
|
||||||
|
|||||||
@@ -773,6 +773,8 @@ import RegionSelector from '@/components/tools/RegionSelector.vue'
|
|||||||
|
|
||||||
/** 教育经历单条数据结构 — 对应数据库 bg_user_profile_education */
|
/** 教育经历单条数据结构 — 对应数据库 bg_user_profile_education */
|
||||||
interface EducationItem {
|
interface EducationItem {
|
||||||
|
/** 记录ID(已有经历从后端获取,新增经历无此字段) */
|
||||||
|
id?: string
|
||||||
/** 学校名称 */
|
/** 学校名称 */
|
||||||
school: string
|
school: string
|
||||||
/** 专业 */
|
/** 专业 */
|
||||||
@@ -791,6 +793,8 @@ interface EducationItem {
|
|||||||
|
|
||||||
/** 实习经历单条数据结构 — 对应数据库 bg_user_profile_internship */
|
/** 实习经历单条数据结构 — 对应数据库 bg_user_profile_internship */
|
||||||
interface InternshipItem {
|
interface InternshipItem {
|
||||||
|
/** 记录ID(已有经历从后端获取,新增经历无此字段) */
|
||||||
|
id?: string
|
||||||
/** 公司名称 */
|
/** 公司名称 */
|
||||||
companyName: string
|
companyName: string
|
||||||
/** 职位 */
|
/** 职位 */
|
||||||
@@ -805,6 +809,8 @@ interface InternshipItem {
|
|||||||
|
|
||||||
/** 工作经历单条数据结构 — 对应数据库 bg_user_profile_work */
|
/** 工作经历单条数据结构 — 对应数据库 bg_user_profile_work */
|
||||||
interface WorkItem {
|
interface WorkItem {
|
||||||
|
/** 记录ID(已有经历从后端获取,新增经历无此字段) */
|
||||||
|
id?: string
|
||||||
/** 公司名称 */
|
/** 公司名称 */
|
||||||
companyName: string
|
companyName: string
|
||||||
/** 职位 */
|
/** 职位 */
|
||||||
@@ -827,6 +833,8 @@ interface DescriptionParagraph {
|
|||||||
|
|
||||||
/** 项目经历单条数据结构 — 对应数据库 bg_user_profile_project */
|
/** 项目经历单条数据结构 — 对应数据库 bg_user_profile_project */
|
||||||
interface ProjectItem {
|
interface ProjectItem {
|
||||||
|
/** 记录ID(已有经历从后端获取,新增经历无此字段) */
|
||||||
|
id?: string
|
||||||
/** 项目名称 */
|
/** 项目名称 */
|
||||||
projectName: string
|
projectName: string
|
||||||
/** 所属公司 */
|
/** 所属公司 */
|
||||||
@@ -843,6 +851,8 @@ interface ProjectItem {
|
|||||||
|
|
||||||
/** 竞赛经历单条数据结构 — 对应数据库 bg_user_profile_competition */
|
/** 竞赛经历单条数据结构 — 对应数据库 bg_user_profile_competition */
|
||||||
interface CompetitionItem {
|
interface CompetitionItem {
|
||||||
|
/** 记录ID(已有经历从后端获取,新增经历无此字段) */
|
||||||
|
id?: string
|
||||||
/** 竞赛名称 */
|
/** 竞赛名称 */
|
||||||
competitionName: string
|
competitionName: string
|
||||||
/** 获奖情况 */
|
/** 获奖情况 */
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 编辑简历名称与目标岗位弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
title=""
|
||||||
|
width="4.6rem"
|
||||||
|
:show-close="true"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
class="resume-edit-name-dialog"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="resume-edit-name-dialog__body">
|
||||||
|
<!-- 简历名称 -->
|
||||||
|
<div class="resume-edit-name-dialog__field">
|
||||||
|
<label class="resume-edit-name-dialog__label">
|
||||||
|
<span class="resume-edit-name-dialog__required">*</span>简历名称
|
||||||
|
</label>
|
||||||
|
<el-input
|
||||||
|
v-model="form.resumeName"
|
||||||
|
placeholder="请输入简历名称"
|
||||||
|
maxlength="50"
|
||||||
|
class="resume-edit-name-dialog__input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 目标岗位 -->
|
||||||
|
<div class="resume-edit-name-dialog__field">
|
||||||
|
<label class="resume-edit-name-dialog__label">目标岗位</label>
|
||||||
|
<el-input
|
||||||
|
v-model="form.targetPosition"
|
||||||
|
placeholder="请输入岗位名称"
|
||||||
|
maxlength="50"
|
||||||
|
class="resume-edit-name-dialog__input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 按钮区域 -->
|
||||||
|
<div class="resume-edit-name-dialog__footer">
|
||||||
|
<button class="resume-edit-name-dialog__btn resume-edit-name-dialog__btn--cancel" @click="handleClose">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="resume-edit-name-dialog__btn resume-edit-name-dialog__btn--save"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="handleSave"
|
||||||
|
>
|
||||||
|
{{ saving ? '保存中...' : '保存' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import { saveResumeMain } from '@/api/resume'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
// ==================== Props & Emits ====================
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 控制弹窗显隐 */
|
||||||
|
modelValue: boolean
|
||||||
|
/** 简历 ID */
|
||||||
|
resumeId: string
|
||||||
|
/** 当前简历名称 */
|
||||||
|
resumeName?: string
|
||||||
|
/** 当前目标岗位 */
|
||||||
|
targetPosition?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', val: boolean): void
|
||||||
|
/** 保存成功后通知父组件刷新 */
|
||||||
|
(e: 'saved'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// ==================== 弹窗显隐 ====================
|
||||||
|
|
||||||
|
/** 弹窗可见状态 */
|
||||||
|
const visible = ref(false)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
visible.value = val
|
||||||
|
if (val) {
|
||||||
|
// 打开时回填当前值
|
||||||
|
form.resumeName = props.resumeName || ''
|
||||||
|
form.targetPosition = props.targetPosition || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(visible, (val) => {
|
||||||
|
if (!val) emit('update:modelValue', false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 表单数据 ====================
|
||||||
|
|
||||||
|
/** 编辑表单 */
|
||||||
|
const form = reactive({
|
||||||
|
resumeName: '',
|
||||||
|
targetPosition: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 保存中状态 */
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
// ==================== 事件处理 ====================
|
||||||
|
|
||||||
|
/** 关闭弹窗 */
|
||||||
|
function handleClose() {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存简历名称和目标岗位 */
|
||||||
|
async function handleSave() {
|
||||||
|
if (!form.resumeName.trim()) {
|
||||||
|
ElMessage.warning('请输入简历名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const res = await saveResumeMain({
|
||||||
|
resumeId: props.resumeId,
|
||||||
|
resumeName: form.resumeName.trim(),
|
||||||
|
targetPosition: form.targetPosition.trim(),
|
||||||
|
})
|
||||||
|
if (res.code === '0') {
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
emit('saved')
|
||||||
|
handleClose()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '保存失败')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('保存失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<!-- AI正在思考中的加载指示器 -->
|
||||||
|
<div class="ai-thinking-indicator">
|
||||||
|
<div class="ai-thinking-indicator__bubble">
|
||||||
|
{{ text }}<el-icon class="ai-thinking-indicator__icon is-loading"><Loading /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
/** 组件属性 */
|
||||||
|
defineProps<{
|
||||||
|
/** 提示文字,默认"AI正在思考中" */
|
||||||
|
text?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
+45
-3
@@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
/** 默认 Token */
|
/** 默认 Token(Nova 对话接口使用) */
|
||||||
const DEFAULT_TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIwMzUyNTM4OTg5MTk4NzA0NjUsInV1SWQiOiI2MmQ5MDE2NTcyNzY0ZmNjODNjZTIyYjRjODA5ZmU5MiJ9.eE-Q5rio5J5kxkS-XPYdmk-1Tgvg6kj6NGoKWMFNU14'
|
export const DEFAULT_TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIwMzUyNTM4OTg5MTk4NzA0NjUsInV1SWQiOiI2MmQ5MDE2NTcyNzY0ZmNjODNjZTIyYjRjODA5ZmU5MiJ9.eE-Q5rio5J5kxkS-XPYdmk-1Tgvg6kj6NGoKWMFNU14'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 端专用 axios 实例
|
* AI 端专用 axios 实例
|
||||||
@@ -17,7 +17,7 @@ const aiService = axios.create({
|
|||||||
* 浏览器安全策略不允许 JS 直接设置 Cookie header,改用 Token header 传递
|
* 浏览器安全策略不允许 JS 直接设置 Cookie header,改用 Token header 传递
|
||||||
*/
|
*/
|
||||||
aiService.interceptors.request.use((config) => {
|
aiService.interceptors.request.use((config) => {
|
||||||
config.headers['Token'] = DEFAULT_TOKEN
|
// config.headers['Token'] = DEFAULT_TOKEN
|
||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -71,3 +71,45 @@ export function uploadResume(file: File) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default aiService
|
export default aiService
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== Nova 对话接口 ====================
|
||||||
|
|
||||||
|
/** Nova 对话历史消息项 */
|
||||||
|
export interface NovaChatHistoryItem {
|
||||||
|
/** 角色:user-用户 assistant-AI助手 */
|
||||||
|
role: 'user' | 'assistant'
|
||||||
|
/** 消息内容 */
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nova 对话请求参数 */
|
||||||
|
export interface NovaChatParams {
|
||||||
|
/** 用户输入的消息 */
|
||||||
|
message: string
|
||||||
|
/** 简历 ID */
|
||||||
|
resumeId: number | string
|
||||||
|
/** 当前浏览岗位 ID,不传则无岗位上下文 */
|
||||||
|
jobId?: number | string
|
||||||
|
/** 历史对话,前端维护 */
|
||||||
|
history?: NovaChatHistoryItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nova 对话返回数据 */
|
||||||
|
export interface NovaChatResponse {
|
||||||
|
/** AI 回复,Markdown 格式 */
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nova AI 对话
|
||||||
|
* POST /nova-chat/chat
|
||||||
|
* @param params 对话参数
|
||||||
|
*/
|
||||||
|
export function sendNovaChat(params: NovaChatParams) {
|
||||||
|
return aiService.post<NovaChatResponse>('/nova-chat/chat', params, {
|
||||||
|
headers: {
|
||||||
|
Token: DEFAULT_TOKEN,
|
||||||
|
},
|
||||||
|
}).then(res => res.data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import html2pdf from 'html2pdf.js'
|
||||||
|
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
||||||
|
import {
|
||||||
|
fetchResumeMain, fetchResumeEducation, fetchResumeWork,
|
||||||
|
fetchResumeInternship, fetchResumeProject, fetchResumeCompetition,
|
||||||
|
} from '@/api/resume'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将简历DOM导出为PDF文件
|
||||||
|
* @param element 简历DOM元素
|
||||||
|
* @param fileName 文件名(不含扩展名)
|
||||||
|
*/
|
||||||
|
export async function exportResumePdf(element: HTMLElement, fileName: string) {
|
||||||
|
const options = {
|
||||||
|
margin: [10, 10, 10, 10] as [number, number, number, number],
|
||||||
|
filename: `${fileName}.pdf`,
|
||||||
|
image: { type: 'jpeg' as const, quality: 0.98 },
|
||||||
|
html2canvas: { scale: 2, useCORS: true, logging: false },
|
||||||
|
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' as const },
|
||||||
|
pagebreak: { mode: ['css', 'legacy'] },
|
||||||
|
}
|
||||||
|
await html2pdf().set(options).from(element).save()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将简历DOM导出为Word文件
|
||||||
|
* @param element 简历DOM元素
|
||||||
|
* @param fileName 文件名(不含扩展名)
|
||||||
|
*/
|
||||||
|
export function exportResumeWord(element: HTMLElement, fileName: string) {
|
||||||
|
// Word专用内联样式(不使用页面全局样式,避免 rem/100px 基准干扰)
|
||||||
|
// 字体在第一版基础上放大50%,边距通过Word XML指令设置
|
||||||
|
const wordCss = `
|
||||||
|
@page WordSection1 {
|
||||||
|
size: 595.3pt 841.9pt;
|
||||||
|
mso-page-orientation: portrait;
|
||||||
|
mso-header-margin: 0pt;
|
||||||
|
mso-footer-margin: 0pt;
|
||||||
|
mso-paper-source: 0;
|
||||||
|
margin-top: 72pt;
|
||||||
|
margin-right: 54pt;
|
||||||
|
margin-bottom: 72pt;
|
||||||
|
margin-left: 54pt;
|
||||||
|
}
|
||||||
|
div.WordSection1 { page: WordSection1; }
|
||||||
|
body { font-family: 'SimSun', 'Songti SC', serif; color: #000; line-height: 1.6; margin: 0; padding: 0; }
|
||||||
|
.job-resume-template { width: 100%; background: #fff; box-sizing: border-box; }
|
||||||
|
.resume-html { padding: 0; font-family: 'SimSun', 'Songti SC', serif; color: #000; line-height: 1.6; }
|
||||||
|
.resume-html__name { font-size: 24pt; font-weight: 700; margin: 0 0 4.8pt 0; line-height: 1.3; }
|
||||||
|
.resume-html__contact { font-size: 12pt; color: #000; margin-bottom: 3.6pt; line-height: 1.5; }
|
||||||
|
.resume-html__contact-row { display: flex; align-items: center; gap: 0; }
|
||||||
|
.resume-html__separator { margin: 0 4.8pt; color: #777; }
|
||||||
|
.resume-html__section-title { font-size: 17.3pt; font-weight: 700; color: #000; margin-top: 20pt; margin-bottom: 10pt; line-height: 1.3; }
|
||||||
|
.resume-html__divider { height: 1.5px; background: #000; margin-bottom: 9.6pt; }
|
||||||
|
.resume-html__summary { font-size: 12pt; line-height: 1.7; margin-bottom: 4.8pt; }
|
||||||
|
.resume-html__item { margin-bottom: 9.6pt; }
|
||||||
|
.resume-html__item-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 3.6pt; }
|
||||||
|
.resume-html__item-left { display: flex; flex-direction: column; gap: 1.2pt; }
|
||||||
|
.resume-html__item-main { font-size: 13.2pt; font-weight: 600; color: #000; line-height: 1.4; }
|
||||||
|
.resume-html__item-right { display: flex; align-items: center; gap: 4.8pt; flex-shrink: 0; text-align: right; }
|
||||||
|
.resume-html__item-location { font-size: 12pt; color: #000; }
|
||||||
|
.resume-html__item-date { font-size: 12pt; color: #000; white-space: nowrap; }
|
||||||
|
.resume-html__item-desc { font-size: 12pt; color: #000; line-height: 1.5; }
|
||||||
|
.resume-html__desc-list { margin: 0; padding-left: 0; list-style: none; mso-list: none; }
|
||||||
|
.resume-html__desc-list li { font-size: 12pt; line-height: 1.7; color: #000; list-style: none; mso-list: none; margin-left: 0; padding-left: 24pt; text-indent: 0; }
|
||||||
|
.resume-html__skills { font-size: 12pt; line-height: 1.7; }
|
||||||
|
.resume-html__skill-row { margin-bottom: 2.4pt; }
|
||||||
|
.resume-html__skill-label { font-weight: 600; }
|
||||||
|
.resume-html__diff-highlight { background-color: #D4EDDA; color: #155724; border-radius: 2px; padding: 0 1px; }
|
||||||
|
`
|
||||||
|
|
||||||
|
// 组装Word可识别的HTML文档
|
||||||
|
// Word页面边距通过XML指令设置(单位twips,1pt=20twips,1英寸=1440twips)
|
||||||
|
// 上下边距 2cm ≈ 1134twips,左右边距 2.5cm ≈ 1418twips
|
||||||
|
const parts: string[] = []
|
||||||
|
parts.push('<html xmlns:o="urn:schemas-microsoft-com:office:office"')
|
||||||
|
parts.push(' xmlns:w="urn:schemas-microsoft-com:office:word"')
|
||||||
|
parts.push(' xmlns="http://www.w3.org/TR/REC-html40">')
|
||||||
|
parts.push('<head>')
|
||||||
|
parts.push('<meta charset="utf-8">')
|
||||||
|
parts.push('<meta name="ProgId" content="Word.Document">')
|
||||||
|
parts.push('<meta name="Generator" content="Microsoft Word 15">')
|
||||||
|
parts.push('<!--[if gte mso 9]><xml>')
|
||||||
|
parts.push('<w:WordDocument>')
|
||||||
|
parts.push('<w:View>Print</w:View>')
|
||||||
|
parts.push('<w:Zoom>100</w:Zoom>')
|
||||||
|
parts.push('</w:WordDocument>')
|
||||||
|
parts.push('</xml><![endif]-->')
|
||||||
|
parts.push('<style>' + wordCss + '</style>')
|
||||||
|
parts.push('</head>')
|
||||||
|
parts.push('<body>')
|
||||||
|
parts.push('<!--[if gte mso 9]><xml>')
|
||||||
|
parts.push('<w:WordDocument>')
|
||||||
|
parts.push('<w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel>')
|
||||||
|
parts.push('</w:WordDocument>')
|
||||||
|
parts.push('</xml><![endif]-->')
|
||||||
|
parts.push('<div class="WordSection1">' + element.outerHTML + '</div>')
|
||||||
|
parts.push('</body>')
|
||||||
|
parts.push('</html>')
|
||||||
|
const fullHtml = parts.join('\n')
|
||||||
|
|
||||||
|
// 生成Blob并触发下载
|
||||||
|
const blob = new Blob([fullHtml], { type: 'application/msword' })
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.download = `${fileName}.doc`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 学历文字转数字 */
|
||||||
|
function degreeToNumber(degree?: string): number {
|
||||||
|
const map: Record<string, number> = { '大专': 1, '本科': 2, '硕士': 3, '博士': 4 }
|
||||||
|
return map[degree || ''] || 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 描述段落映射辅助函数 */
|
||||||
|
function mapDesc(list?: { id?: string; text?: string }[]) {
|
||||||
|
return (list || []).map(d => ({ id: d.id, text: d.text || '' }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载简历完整数据并组装为模板格式
|
||||||
|
* @param resumeId 简历ID
|
||||||
|
*/
|
||||||
|
export async function loadResumeTemplateData(resumeId: string): Promise<ResumeTemplateData | null> {
|
||||||
|
const [mainRes, eduRes, workRes, internRes, projRes, compRes] = await Promise.all([
|
||||||
|
fetchResumeMain(resumeId),
|
||||||
|
fetchResumeEducation(resumeId),
|
||||||
|
fetchResumeWork(resumeId),
|
||||||
|
fetchResumeInternship(resumeId),
|
||||||
|
fetchResumeProject(resumeId),
|
||||||
|
fetchResumeCompetition(resumeId),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (mainRes.code !== '0' || !mainRes.data) return null
|
||||||
|
|
||||||
|
const r = mainRes.data
|
||||||
|
return {
|
||||||
|
name: r.name || '未填写姓名',
|
||||||
|
email: r.email || '',
|
||||||
|
mobileNumber: r.mobileNumber || '',
|
||||||
|
wechatNumber: r.wechatNumber || '',
|
||||||
|
summary: r.summary || '',
|
||||||
|
educations: (eduRes.data || []).map(e => ({
|
||||||
|
school: e.school || '',
|
||||||
|
major: e.major || '',
|
||||||
|
degree: degreeToNumber(e.degree),
|
||||||
|
startDate: e.startDate || '',
|
||||||
|
endDate: e.endDate || '',
|
||||||
|
description: mapDesc(e.description),
|
||||||
|
})),
|
||||||
|
workExperiences: (workRes.data || []).map(w => ({
|
||||||
|
companyName: w.companyName || '',
|
||||||
|
position: w.position || '',
|
||||||
|
startDate: w.startDate || '',
|
||||||
|
endDate: w.endDate || '',
|
||||||
|
description: mapDesc(w.description),
|
||||||
|
})),
|
||||||
|
internships: (internRes.data || []).map(i => ({
|
||||||
|
companyName: i.companyName || '',
|
||||||
|
position: i.position || '',
|
||||||
|
startDate: i.startDate || '',
|
||||||
|
endDate: i.endDate || '',
|
||||||
|
description: mapDesc(i.description),
|
||||||
|
})),
|
||||||
|
projects: (projRes.data || []).map(p => ({
|
||||||
|
projectName: p.projectName || '',
|
||||||
|
companyName: p.companyName || '',
|
||||||
|
role: p.role || '',
|
||||||
|
startDate: p.startDate || '',
|
||||||
|
endDate: p.endDate || '',
|
||||||
|
description: mapDesc(p.description),
|
||||||
|
})),
|
||||||
|
competitions: (compRes.data || []).map(c => ({
|
||||||
|
competitionName: c.competitionName || '',
|
||||||
|
award: c.award || '',
|
||||||
|
awardDate: c.awardDate || '',
|
||||||
|
description: mapDesc(c.description),
|
||||||
|
})),
|
||||||
|
skills: r.skills || [],
|
||||||
|
certificates: r.certificates || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
+662
-391
File diff suppressed because it is too large
Load Diff
@@ -266,7 +266,7 @@
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<AiChat />
|
<AiChat :job-id="jobId" />
|
||||||
|
|
||||||
<!-- 职位不感兴趣反馈弹窗 -->
|
<!-- 职位不感兴趣反馈弹窗 -->
|
||||||
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="jobId" />
|
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="jobId" />
|
||||||
|
|||||||
+10
-2
@@ -176,7 +176,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="jobs-page__job-action-right ">
|
<div class="jobs-page__job-action-right ">
|
||||||
<button class="jobs-page__job-helper ml5">
|
<button class="jobs-page__job-helper ml5" @click.stop="askAssistant(job)">
|
||||||
<svg viewBox="0 0 14 14" fill="none" class="jobs-page__helper-svg">
|
<svg viewBox="0 0 14 14" fill="none" class="jobs-page__helper-svg">
|
||||||
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1"/>
|
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1"/>
|
||||||
<path d="M5.5 5.5a1.5 1.5 0 113 0c0 .8-.7 1-1.5 1.5V9" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
<path d="M5.5 5.5a1.5 1.5 0 113 0c0 .8-.7 1-1.5 1.5V9" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
||||||
@@ -239,7 +239,7 @@
|
|||||||
<div v-else-if="noMore && jobList.length > 0" class="jobs-page__loading-more">没有更多了</div>
|
<div v-else-if="noMore && jobList.length > 0" class="jobs-page__loading-more">没有更多了</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AiChat />
|
<AiChat :job-id="currentAskJobId" />
|
||||||
|
|
||||||
<!-- 职位不感兴趣反馈弹窗 -->
|
<!-- 职位不感兴趣反馈弹窗 -->
|
||||||
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="dislikeJobId" @disliked="removeDislikedJob" />
|
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="dislikeJobId" @disliked="removeDislikedJob" />
|
||||||
@@ -299,6 +299,14 @@ const showFeedbackDialog = ref(false)
|
|||||||
/** 当前操作的职位 ID(用于提交问题反馈) */
|
/** 当前操作的职位 ID(用于提交问题反馈) */
|
||||||
const feedbackJobId = ref<string | null>(null)
|
const feedbackJobId = ref<string | null>(null)
|
||||||
|
|
||||||
|
/** 当前问助手的岗位 ID(传给 AiChat 组件) */
|
||||||
|
const currentAskJobId = ref<string>('')
|
||||||
|
|
||||||
|
/** 点击"问助手"按钮,传入岗位 ID 给 AiChat */
|
||||||
|
function askAssistant(job: JobItem) {
|
||||||
|
currentAskJobId.value = job.id
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 收藏统计 ====================
|
// ==================== 收藏统计 ====================
|
||||||
|
|
||||||
/** 收藏总数(用于 Tab 标签显示) */
|
/** 收藏总数(用于 Tab 标签显示) */
|
||||||
|
|||||||
+131
-8
@@ -73,15 +73,53 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 编辑简历名称弹窗 -->
|
||||||
|
<ResumeEditNameDialog
|
||||||
|
v-model="editNameVisible"
|
||||||
|
:resume-id="editResumeId"
|
||||||
|
:resume-name="editResumeName"
|
||||||
|
:target-position="editTargetPosition"
|
||||||
|
@saved="loadResumeList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 导出简历格式选择弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="exportDialogVisible"
|
||||||
|
title="导出简历"
|
||||||
|
width="3.6rem"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
class="resume-export-dialog"
|
||||||
|
>
|
||||||
|
<!-- 格式选择 -->
|
||||||
|
<el-radio-group v-model="exportFormat" class="resume-export-dialog__radio-group">
|
||||||
|
<el-radio value="pdf">PDF 简历</el-radio>
|
||||||
|
<el-radio value="word">Word 简历</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="exportDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="exporting" @click="doExport">下载</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 隐藏的简历模板,用于导出时渲染DOM -->
|
||||||
|
<div v-if="exportTemplateData" style="position:absolute;left:-9999px;top:0;">
|
||||||
|
<JobResumeTemplate ref="exportTemplateRef" :resume-data="exportTemplateData" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import SideNav from '@/components/SideNav.vue'
|
import SideNav from '@/components/SideNav.vue'
|
||||||
|
import ResumeEditNameDialog from '@/components/ResumeEditNameDialog.vue'
|
||||||
|
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
||||||
|
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
||||||
|
import { exportResumePdf, exportResumeWord, loadResumeTemplateData } from '@/utils/resumeExport'
|
||||||
import { uploadResume } from '@/utils/aiRequest'
|
import { uploadResume } from '@/utils/aiRequest'
|
||||||
import { fetchResumeList, deleteResume, type ResumeListItem } from '@/api/resume'
|
import {
|
||||||
|
fetchResumeList, deleteResume, type ResumeListItem,
|
||||||
|
} from '@/api/resume'
|
||||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
|
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
|
||||||
// ElLoading.service() 是命令式调用,按需引入插件不会自动加载其样式,需手动引入
|
// ElLoading.service() 是命令式调用,按需引入插件不会自动加载其样式,需手动引入
|
||||||
import 'element-plus/es/components/loading/style/css'
|
import 'element-plus/es/components/loading/style/css'
|
||||||
@@ -110,12 +148,12 @@ interface ResumeItem {
|
|||||||
// ==================== 工具方法 ====================
|
// ==================== 工具方法 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 Instant 时间戳转为友好的相对时间文案
|
* 将毫秒时间戳转为友好的相对时间文案
|
||||||
* @param instant 后端返回的 Instant 对象
|
* @param timestamp 后端返回的毫秒级时间戳
|
||||||
*/
|
*/
|
||||||
function formatTime(instant?: { seconds?: number; nanos?: number }): string {
|
function formatTime(timestamp?: number): string {
|
||||||
if (!instant?.seconds) return '-'
|
if (!timestamp) return '-'
|
||||||
const date = new Date(instant.seconds * 1000)
|
const date = new Date(timestamp)
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const diff = now - date.getTime()
|
const diff = now - date.getTime()
|
||||||
const minutes = Math.floor(diff / 60000)
|
const minutes = Math.floor(diff / 60000)
|
||||||
@@ -175,8 +213,78 @@ onMounted(() => {
|
|||||||
/** 当前打开弹出菜单的简历 ID */
|
/** 当前打开弹出菜单的简历 ID */
|
||||||
const activeMenuId = ref<string | null>(null)
|
const activeMenuId = ref<string | null>(null)
|
||||||
|
|
||||||
|
// ==================== 编辑简历名称弹窗状态 ====================
|
||||||
|
|
||||||
|
/** 编辑弹窗是否可见 */
|
||||||
|
const editNameVisible = ref(false)
|
||||||
|
/** 当前编辑的简历 ID */
|
||||||
|
const editResumeId = ref('')
|
||||||
|
/** 当前编辑的简历名称 */
|
||||||
|
const editResumeName = ref('')
|
||||||
|
/** 当前编辑的目标岗位 */
|
||||||
|
const editTargetPosition = ref('')
|
||||||
|
|
||||||
|
// ==================== 导出简历弹窗状态 ====================
|
||||||
|
|
||||||
|
/** 导出弹窗是否可见 */
|
||||||
|
const exportDialogVisible = ref(false)
|
||||||
|
/** 导出格式:pdf 或 word */
|
||||||
|
const exportFormat = ref<'pdf' | 'word'>('pdf')
|
||||||
|
/** 导出中状态 */
|
||||||
|
const exporting = ref(false)
|
||||||
|
/** 当前导出的简历 ID */
|
||||||
|
const exportResumeId = ref('')
|
||||||
|
/** 当前导出的简历名称(用于文件名) */
|
||||||
|
const exportResumeName = ref('')
|
||||||
|
/** 导出用的简历模板数据 */
|
||||||
|
const exportTemplateData = ref<ResumeTemplateData | null>(null)
|
||||||
|
/** 导出用的简历模板组件引用 */
|
||||||
|
const exportTemplateRef = ref<InstanceType<typeof JobResumeTemplate> | null>(null)
|
||||||
|
|
||||||
|
/** 执行导出下载 */
|
||||||
|
async function doExport() {
|
||||||
|
exporting.value = true
|
||||||
|
try {
|
||||||
|
// 1. 加载简历完整数据
|
||||||
|
const data = await loadResumeTemplateData(exportResumeId.value)
|
||||||
|
if (!data) {
|
||||||
|
ElMessage.error('获取简历数据失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 设置模板数据,等待DOM渲染
|
||||||
|
exportTemplateData.value = data
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// 3. 获取渲染后的DOM
|
||||||
|
const element = exportTemplateRef.value?.resumeRef
|
||||||
|
if (!element) {
|
||||||
|
ElMessage.error('简历模板渲染失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = exportResumeName.value || '简历'
|
||||||
|
|
||||||
|
if (exportFormat.value === 'pdf') {
|
||||||
|
await exportResumePdf(element, fileName)
|
||||||
|
} else {
|
||||||
|
exportResumeWord(element, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('导出成功')
|
||||||
|
exportDialogVisible.value = false
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[导出简历] 失败', err)
|
||||||
|
ElMessage.error('导出失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
exporting.value = false
|
||||||
|
// 清理隐藏模板数据
|
||||||
|
exportTemplateData.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 弹出菜单操作项 */
|
/** 弹出菜单操作项 */
|
||||||
const popupActions = ['设为默认简历', '编辑', '导出简历', '删除']
|
const popupActions = ['设为默认简历', '编辑名称岗位', '导出简历', '删除']
|
||||||
|
|
||||||
// ==================== 事件处理 ====================
|
// ==================== 事件处理 ====================
|
||||||
|
|
||||||
@@ -213,6 +321,21 @@ async function handleAction(action: string, id: string) {
|
|||||||
ElMessage.error('删除失败,请稍后重试')
|
ElMessage.error('删除失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (action === '编辑名称岗位') {
|
||||||
|
// 找到当前简历数据,打开编辑弹窗
|
||||||
|
const target = resumeList.value.find(r => r.id === id)
|
||||||
|
if (target) {
|
||||||
|
editResumeId.value = id
|
||||||
|
editResumeName.value = target.name
|
||||||
|
editTargetPosition.value = target.targetJob
|
||||||
|
editNameVisible.value = true
|
||||||
|
}
|
||||||
|
} else if (action === '导出简历') {
|
||||||
|
// 打开导出格式选择弹窗
|
||||||
|
exportResumeId.value = id
|
||||||
|
const target = resumeList.value.find(r => r.id === id)
|
||||||
|
exportResumeName.value = target?.name || '简历'
|
||||||
|
exportDialogVisible.value = true
|
||||||
} else {
|
} else {
|
||||||
console.log(action, id)
|
console.log(action, id)
|
||||||
}
|
}
|
||||||
|
|||||||
+309
-101
@@ -69,7 +69,7 @@
|
|||||||
<div class="resume-detail__score-bar">
|
<div class="resume-detail__score-bar">
|
||||||
<div class="resume-detail__score-left">
|
<div class="resume-detail__score-left">
|
||||||
<!-- 评级 -->
|
<!-- 评级 -->
|
||||||
<span class="resume-detail__score-avatar">
|
<span v-if="hasDiagnosis" class="resume-detail__score-avatar">
|
||||||
{{ diagnosisReport.grade }}
|
{{ diagnosisReport.grade }}
|
||||||
</span>
|
</span>
|
||||||
<!-- 有诊断报告时显示评级和查看链接 -->
|
<!-- 有诊断报告时显示评级和查看链接 -->
|
||||||
@@ -82,15 +82,18 @@
|
|||||||
<!-- 有诊断报告时显示三项计数和重新诊断按钮 -->
|
<!-- 有诊断报告时显示三项计数和重新诊断按钮 -->
|
||||||
<template v-if="hasDiagnosis">
|
<template v-if="hasDiagnosis">
|
||||||
<div class="resume-detail__score-item">
|
<div class="resume-detail__score-item">
|
||||||
<span class="resume-detail__score-num">{{ diagnosisReport.urgentTotal || 0 }}</span>
|
<!-- 从 issues 中 status===0 的子项汇总(report 数据修复后未更新,暂不使用 diagnosisReport.urgentTotal) -->
|
||||||
|
<span class="resume-detail__score-num">{{ issuesUrgentTotal }}</span>
|
||||||
<span class="resume-detail__score-label">紧急修复</span>
|
<span class="resume-detail__score-label">紧急修复</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="resume-detail__score-item">
|
<div class="resume-detail__score-item">
|
||||||
<span class="resume-detail__score-num">{{ diagnosisReport.importantTotal || 0 }}</span>
|
<!-- 从 issues 中 status===0 的子项汇总(report 数据修复后未更新,暂不使用 diagnosisReport.importantTotal) -->
|
||||||
|
<span class="resume-detail__score-num">{{ issuesImportantTotal }}</span>
|
||||||
<span class="resume-detail__score-label">重点优化</span>
|
<span class="resume-detail__score-label">重点优化</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="resume-detail__score-item">
|
<div class="resume-detail__score-item">
|
||||||
<span class="resume-detail__score-num">{{ diagnosisReport.expressionTotal || 0 }}</span>
|
<!-- 从 issues 中 status===0 的子项汇总(report 数据修复后未更新,暂不使用 diagnosisReport.expressionTotal) -->
|
||||||
|
<span class="resume-detail__score-num">{{ issuesExpressionTotal }}</span>
|
||||||
<span class="resume-detail__score-label">表达提升</span>
|
<span class="resume-detail__score-label">表达提升</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="resume-detail__diagnose-btn" @click="handleDiagnose">重新诊断</button>
|
<button class="resume-detail__diagnose-btn" @click="handleDiagnose">重新诊断</button>
|
||||||
@@ -189,7 +192,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 教育背景 -->
|
<!-- 教育背景经历 -->
|
||||||
<div v-if="educationList.length" class="resume-detail__card">
|
<div v-if="educationList.length" class="resume-detail__card">
|
||||||
<div class="resume-detail__section-header">
|
<div class="resume-detail__section-header">
|
||||||
<h3 class="resume-detail__section-title">教育背景</h3>
|
<h3 class="resume-detail__section-title">教育背景</h3>
|
||||||
@@ -414,11 +417,22 @@ import {
|
|||||||
fetchResumeDiagnosis,
|
fetchResumeDiagnosis,
|
||||||
triggerResumeDiagnosis,
|
triggerResumeDiagnosis,
|
||||||
saveResumeMain,
|
saveResumeMain,
|
||||||
saveResumeEducation,
|
addResumeEducation,
|
||||||
saveResumeWork,
|
updateResumeEducation,
|
||||||
saveResumeInternship,
|
deleteResumeEducation,
|
||||||
saveResumeProject,
|
addResumeWork,
|
||||||
saveResumeCompetition,
|
updateResumeWork,
|
||||||
|
deleteResumeWork,
|
||||||
|
addResumeInternship,
|
||||||
|
updateResumeInternship,
|
||||||
|
deleteResumeInternship,
|
||||||
|
addResumeProject,
|
||||||
|
updateResumeProject,
|
||||||
|
deleteResumeProject,
|
||||||
|
addResumeCompetition,
|
||||||
|
updateResumeCompetition,
|
||||||
|
deleteResumeCompetition,
|
||||||
|
deleteResume,
|
||||||
type ResumeMainData,
|
type ResumeMainData,
|
||||||
type ResumeEducation,
|
type ResumeEducation,
|
||||||
type ResumeWork,
|
type ResumeWork,
|
||||||
@@ -450,6 +464,36 @@ const gradeLabel = computed(() => {
|
|||||||
return map[diagnosisReport.value.grade || ''] || '未评级'
|
return map[diagnosisReport.value.grade || ''] || '未评级'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 issues 中 status===0 的子项汇总紧急修复数
|
||||||
|
* (report 数据修复后未实时更新,暂用 issues 汇总代替 diagnosisReport.urgentTotal,等后面接口改了用回来)
|
||||||
|
*/
|
||||||
|
const issuesUrgentTotal = computed(() => {
|
||||||
|
return diagnosisIssues.value
|
||||||
|
.filter(i => i.status === 0)
|
||||||
|
.reduce((sum, i) => sum + sumSubCounts(i.urgentIssues), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 issues 中 status===0 的子项汇总重点优化数
|
||||||
|
* (report 数据修复后未实时更新,暂用 issues 汇总代替 diagnosisReport.importantTotal,等后面接口改了用回来)
|
||||||
|
*/
|
||||||
|
const issuesImportantTotal = computed(() => {
|
||||||
|
return diagnosisIssues.value
|
||||||
|
.filter(i => i.status === 0)
|
||||||
|
.reduce((sum, i) => sum + sumSubCounts(i.importantIssues), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 issues 中 status===0 的子项汇总表达提升数
|
||||||
|
* (report 数据修复后未实时更新,暂用 issues 汇总代替 diagnosisReport.expressionTotal,等后面接口改了用回来)
|
||||||
|
*/
|
||||||
|
const issuesExpressionTotal = computed(() => {
|
||||||
|
return diagnosisIssues.value
|
||||||
|
.filter(i => i.status === 0)
|
||||||
|
.reduce((sum, i) => sum + sumSubCounts(i.expressionIssues), 0)
|
||||||
|
})
|
||||||
|
|
||||||
/** 每个子经历的问题操作区当前选中类型,key 为 "moduleType_recordId" */
|
/** 每个子经历的问题操作区当前选中类型,key 为 "moduleType_recordId" */
|
||||||
const issueActiveTypes = reactive<Record<string, IssueType>>({})
|
const issueActiveTypes = reactive<Record<string, IssueType>>({})
|
||||||
|
|
||||||
@@ -458,11 +502,37 @@ function getIssuesByModule(moduleType: string): DiagnosisIssue[] {
|
|||||||
return diagnosisIssues.value.filter(i => i.moduleType === moduleType)
|
return diagnosisIssues.value.filter(i => i.moduleType === moduleType)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据模块类型和记录 ID 获取对应的 issue */
|
/** 根据模块类型和列表索引获取对应的 issue(用于 ID 不匹配的模块) */
|
||||||
|
function getIssueByIndex(moduleType: string, index: number): DiagnosisIssue | undefined {
|
||||||
|
return getIssuesByModule(moduleType)[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据模块类型和记录 ID 获取对应的 issue(备用,后端 ID 修复后可恢复使用) */
|
||||||
function getIssueByRecord(moduleType: string, recordId: string): DiagnosisIssue | undefined {
|
function getIssueByRecord(moduleType: string, recordId: string): DiagnosisIssue | undefined {
|
||||||
return diagnosisIssues.value.find(i => i.moduleType === moduleType && i.moduleRecordId === recordId)
|
return diagnosisIssues.value.find(i => i.moduleType === moduleType && i.moduleRecordId === recordId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 根据模块类型和记录 ID 获取对应的 issue — 当前因后端 ID 不一致,内部走索引匹配 */
|
||||||
|
// function getIssueByRecord(moduleType: string, recordId: string): DiagnosisIssue | undefined {
|
||||||
|
// // 个人概述用 resumeId 匹配
|
||||||
|
// if (moduleType === 'summary') {
|
||||||
|
// return diagnosisIssues.value.find(i => i.moduleType === 'summary')
|
||||||
|
// }
|
||||||
|
// // 其他经历模块:根据列表索引匹配(后端 moduleRecordId 与经历 id 不一致的临时方案)
|
||||||
|
// const listMap: Record<string, any[]> = {
|
||||||
|
// education: educationList.value,
|
||||||
|
// work: workList.value,
|
||||||
|
// internship: internshipList.value,
|
||||||
|
// project: projectList.value,
|
||||||
|
// competition: competitionList.value,
|
||||||
|
// }
|
||||||
|
// const list = listMap[moduleType]
|
||||||
|
// if (!list) return undefined
|
||||||
|
// const idx = list.findIndex((item: any) => item.id === recordId)
|
||||||
|
// if (idx === -1) return undefined
|
||||||
|
// return getIssueByIndex(moduleType, idx)
|
||||||
|
// }
|
||||||
|
|
||||||
/** 计算单个 issue 的子类型计数,将 urgentIssues/importantIssues/expressionIssues 的 value 求和 */
|
/** 计算单个 issue 的子类型计数,将 urgentIssues/importantIssues/expressionIssues 的 value 求和 */
|
||||||
function sumSubCounts(obj?: Record<string, number>): number {
|
function sumSubCounts(obj?: Record<string, number>): number {
|
||||||
if (!obj) return 0
|
if (!obj) return 0
|
||||||
@@ -614,8 +684,30 @@ function handleEdit() { console.log('编辑简历信息') }
|
|||||||
/** 导出简历 */
|
/** 导出简历 */
|
||||||
function handleExport() { console.log('导出') }
|
function handleExport() { console.log('导出') }
|
||||||
|
|
||||||
/** 删除简历 */
|
/** 删除简历 — 二次确认后调用删除接口,成功后返回简历列表 */
|
||||||
function handleDelete() { console.log('删除') }
|
async function handleDelete() {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除这份简历吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
// 调用删除接口
|
||||||
|
const res = await deleteResume(resumeId)
|
||||||
|
if (res.code === '0') {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
// 返回简历列表页
|
||||||
|
router.push('/resume')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 用户取消删除或接口报错
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('删除失败,请稍后重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 查看评估报告 */
|
/** 查看评估报告 */
|
||||||
function handleViewReport() {
|
function handleViewReport() {
|
||||||
@@ -741,8 +833,8 @@ async function handleFixSubmit(content: string[]) {
|
|||||||
record.description[idx].text = text
|
record.description[idx].text = text
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// 调用对应模块的保存接口(全量覆盖)
|
// 调用对应模块的单条编辑接口(仅更新当前这条经历)
|
||||||
await saveModuleData(moduleType)
|
await updateSingleRecord(moduleType, record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -757,50 +849,55 @@ async function handleFixSubmit(content: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据模块类型调用对应的保存接口(全量覆盖) */
|
/** 根据模块类型调用对应的单条编辑接口(问题修复场景,只更新一条经历的内容) */
|
||||||
async function saveModuleData(moduleType: string) {
|
async function updateSingleRecord(moduleType: string, record: any) {
|
||||||
if (moduleType === 'education') {
|
if (moduleType === 'education') {
|
||||||
await saveResumeEducation(resumeId, educationList.value.map(edu => ({
|
await updateResumeEducation({
|
||||||
school: edu.school,
|
id: record.id,
|
||||||
major: edu.major,
|
school: record.school,
|
||||||
degree: edu.degree,
|
major: record.major,
|
||||||
studyType: edu.studyType,
|
degree: record.degree,
|
||||||
startDate: edu.startDate,
|
studyType: record.studyType,
|
||||||
endDate: edu.endDate,
|
startDate: record.startDate,
|
||||||
description: edu.description,
|
endDate: record.endDate,
|
||||||
})))
|
description: record.description,
|
||||||
|
})
|
||||||
} else if (moduleType === 'work') {
|
} else if (moduleType === 'work') {
|
||||||
await saveResumeWork(resumeId, workList.value.map(w => ({
|
await updateResumeWork({
|
||||||
companyName: w.companyName,
|
id: record.id,
|
||||||
position: w.position,
|
companyName: record.companyName,
|
||||||
startDate: w.startDate,
|
position: record.position,
|
||||||
endDate: w.endDate,
|
startDate: record.startDate,
|
||||||
description: w.description,
|
endDate: record.endDate,
|
||||||
})))
|
description: record.description,
|
||||||
|
})
|
||||||
} else if (moduleType === 'internship') {
|
} else if (moduleType === 'internship') {
|
||||||
await saveResumeInternship(resumeId, internshipList.value.map(i => ({
|
await updateResumeInternship({
|
||||||
companyName: i.companyName,
|
id: record.id,
|
||||||
position: i.position,
|
companyName: record.companyName,
|
||||||
startDate: i.startDate,
|
position: record.position,
|
||||||
endDate: i.endDate,
|
startDate: record.startDate,
|
||||||
description: i.description,
|
endDate: record.endDate,
|
||||||
})))
|
description: record.description,
|
||||||
|
})
|
||||||
} else if (moduleType === 'project') {
|
} else if (moduleType === 'project') {
|
||||||
await saveResumeProject(resumeId, projectList.value.map(p => ({
|
await updateResumeProject({
|
||||||
projectName: p.projectName,
|
id: record.id,
|
||||||
companyName: p.companyName,
|
projectName: record.projectName,
|
||||||
role: p.role,
|
companyName: record.companyName,
|
||||||
startDate: p.startDate,
|
role: record.role,
|
||||||
endDate: p.endDate,
|
startDate: record.startDate,
|
||||||
description: p.description,
|
endDate: record.endDate,
|
||||||
})))
|
description: record.description,
|
||||||
|
})
|
||||||
} else if (moduleType === 'competition') {
|
} else if (moduleType === 'competition') {
|
||||||
await saveResumeCompetition(resumeId, competitionList.value.map(c => ({
|
await updateResumeCompetition({
|
||||||
competitionName: c.competitionName,
|
id: record.id,
|
||||||
award: c.award,
|
competitionName: record.competitionName,
|
||||||
awardDate: c.awardDate,
|
award: record.award,
|
||||||
description: c.description,
|
awardDate: record.awardDate,
|
||||||
})))
|
description: record.description,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -815,6 +912,21 @@ const editModule = ref('info')
|
|||||||
/** 当前编辑模块的初始数据 */
|
/** 当前编辑模块的初始数据 */
|
||||||
const editInitialData = ref<Record<string, any>>({})
|
const editInitialData = ref<Record<string, any>>({})
|
||||||
|
|
||||||
|
/** 打开编辑抽屉时记录的教育经历ID快照,用于保存时做增删改diff */
|
||||||
|
const editSnapshotEducationIds = ref<string[]>([])
|
||||||
|
|
||||||
|
/** 打开编辑抽屉时记录的工作经历ID快照 */
|
||||||
|
const editSnapshotWorkIds = ref<string[]>([])
|
||||||
|
|
||||||
|
/** 打开编辑抽屉时记录的实习经历ID快照 */
|
||||||
|
const editSnapshotInternshipIds = ref<string[]>([])
|
||||||
|
|
||||||
|
/** 打开编辑抽屉时记录的项目经历ID快照 */
|
||||||
|
const editSnapshotProjectIds = ref<string[]>([])
|
||||||
|
|
||||||
|
/** 打开编辑抽屉时记录的竞赛经历ID快照 */
|
||||||
|
const editSnapshotCompetitionIds = ref<string[]>([])
|
||||||
|
|
||||||
/** 打开编辑抽屉 — 根据模块名设置初始数据 */
|
/** 打开编辑抽屉 — 根据模块名设置初始数据 */
|
||||||
function openEditDrawer(section: string) {
|
function openEditDrawer(section: string) {
|
||||||
editModule.value = section
|
editModule.value = section
|
||||||
@@ -832,8 +944,11 @@ function openEditDrawer(section: string) {
|
|||||||
summary: resumeMain.value.summary || '',
|
summary: resumeMain.value.summary || '',
|
||||||
}
|
}
|
||||||
} else if (section === 'education') {
|
} else if (section === 'education') {
|
||||||
|
// 记录当前教育经历的ID快照,用于保存时做增删改diff
|
||||||
|
editSnapshotEducationIds.value = educationList.value.map(edu => edu.id || '').filter(Boolean)
|
||||||
editInitialData.value = {
|
editInitialData.value = {
|
||||||
education: educationList.value.map(edu => ({
|
education: educationList.value.map(edu => ({
|
||||||
|
id: edu.id,
|
||||||
school: edu.school || '',
|
school: edu.school || '',
|
||||||
major: edu.major || '',
|
major: edu.major || '',
|
||||||
studyType: edu.studyType || '全日制',
|
studyType: edu.studyType || '全日制',
|
||||||
@@ -844,8 +959,11 @@ function openEditDrawer(section: string) {
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
} else if (section === 'work') {
|
} else if (section === 'work') {
|
||||||
|
// 记录当前工作经历的ID快照
|
||||||
|
editSnapshotWorkIds.value = workList.value.map(w => w.id || '').filter(Boolean)
|
||||||
editInitialData.value = {
|
editInitialData.value = {
|
||||||
works: workList.value.map(w => ({
|
works: workList.value.map(w => ({
|
||||||
|
id: w.id,
|
||||||
companyName: w.companyName || '',
|
companyName: w.companyName || '',
|
||||||
position: w.position || '',
|
position: w.position || '',
|
||||||
startDate: w.startDate || '',
|
startDate: w.startDate || '',
|
||||||
@@ -854,8 +972,11 @@ function openEditDrawer(section: string) {
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
} else if (section === 'internship') {
|
} else if (section === 'internship') {
|
||||||
|
// 记录当前实习经历的ID快照
|
||||||
|
editSnapshotInternshipIds.value = internshipList.value.map(i => i.id || '').filter(Boolean)
|
||||||
editInitialData.value = {
|
editInitialData.value = {
|
||||||
internships: internshipList.value.map(i => ({
|
internships: internshipList.value.map(i => ({
|
||||||
|
id: i.id,
|
||||||
companyName: i.companyName || '',
|
companyName: i.companyName || '',
|
||||||
position: i.position || '',
|
position: i.position || '',
|
||||||
startDate: i.startDate || '',
|
startDate: i.startDate || '',
|
||||||
@@ -864,8 +985,11 @@ function openEditDrawer(section: string) {
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
} else if (section === 'project') {
|
} else if (section === 'project') {
|
||||||
|
// 记录当前项目经历的ID快照
|
||||||
|
editSnapshotProjectIds.value = projectList.value.map(p => p.id || '').filter(Boolean)
|
||||||
editInitialData.value = {
|
editInitialData.value = {
|
||||||
projects: projectList.value.map(p => ({
|
projects: projectList.value.map(p => ({
|
||||||
|
id: p.id,
|
||||||
projectName: p.projectName || '',
|
projectName: p.projectName || '',
|
||||||
companyName: p.companyName || '',
|
companyName: p.companyName || '',
|
||||||
role: p.role || '',
|
role: p.role || '',
|
||||||
@@ -875,8 +999,11 @@ function openEditDrawer(section: string) {
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
} else if (section === 'competition') {
|
} else if (section === 'competition') {
|
||||||
|
// 记录当前竞赛经历的ID快照
|
||||||
|
editSnapshotCompetitionIds.value = competitionList.value.map(c => c.id || '').filter(Boolean)
|
||||||
editInitialData.value = {
|
editInitialData.value = {
|
||||||
competitions: competitionList.value.map(c => ({
|
competitions: competitionList.value.map(c => ({
|
||||||
|
id: c.id,
|
||||||
competitionName: c.competitionName || '',
|
competitionName: c.competitionName || '',
|
||||||
award: c.award || '',
|
award: c.award || '',
|
||||||
awardDate: c.awardDate || '',
|
awardDate: c.awardDate || '',
|
||||||
@@ -926,57 +1053,135 @@ async function handleSaveEdit(data: Record<string, any>) {
|
|||||||
})
|
})
|
||||||
resumeMain.value.summary = data.summary
|
resumeMain.value.summary = data.summary
|
||||||
} else if (editModule.value === 'education') {
|
} else if (editModule.value === 'education') {
|
||||||
// 保存教育经历
|
// 教育经历 — 通过diff对比打开编辑时的ID快照,分别调用增/改/删接口
|
||||||
await saveResumeEducation(resumeId, data.education.map((edu: any) => ({
|
const oldIds = new Set(editSnapshotEducationIds.value)
|
||||||
school: edu.school,
|
const newItems: any[] = data.education
|
||||||
major: edu.major,
|
const newIds = new Set(newItems.filter((edu: any) => edu.id).map((edu: any) => edu.id))
|
||||||
degree: edu.degree,
|
|
||||||
studyType: edu.studyType,
|
// 1. 找出被删除的经历(旧ID中有,新数据中没有的)
|
||||||
startDate: edu.startDate,
|
const deletedIds = editSnapshotEducationIds.value.filter(id => !newIds.has(id))
|
||||||
endDate: edu.endDate,
|
// 2. 遍历新数据,有id且在旧快照中存在的是编辑,没有id的是新增
|
||||||
description: edu.description,
|
const addPromises: Promise<any>[] = []
|
||||||
})))
|
const updatePromises: Promise<any>[] = []
|
||||||
educationList.value = data.education.map((edu: any) => ({ ...edu, description: edu.description.map((d: any) => ({ ...d })) }))
|
const deletePromises = deletedIds.map(id => deleteResumeEducation(id))
|
||||||
|
|
||||||
|
for (const edu of newItems) {
|
||||||
|
const itemData = {
|
||||||
|
school: edu.school,
|
||||||
|
major: edu.major,
|
||||||
|
degree: edu.degree,
|
||||||
|
studyType: edu.studyType,
|
||||||
|
startDate: edu.startDate,
|
||||||
|
endDate: edu.endDate,
|
||||||
|
description: edu.description,
|
||||||
|
}
|
||||||
|
if (edu.id && oldIds.has(edu.id)) {
|
||||||
|
// 已有经历 — 调用编辑接口
|
||||||
|
updatePromises.push(updateResumeEducation({ ...itemData, id: edu.id }))
|
||||||
|
} else {
|
||||||
|
// 新增经历 — 调用添加接口
|
||||||
|
addPromises.push(addResumeEducation(resumeId, itemData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并发执行所有增删改请求
|
||||||
|
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
|
||||||
} else if (editModule.value === 'work') {
|
} else if (editModule.value === 'work') {
|
||||||
// 保存工作经历
|
// 工作经历 — diff增删改
|
||||||
await saveResumeWork(resumeId, data.works.map((w: any) => ({
|
const oldIds = new Set(editSnapshotWorkIds.value)
|
||||||
companyName: w.companyName,
|
const newItems: any[] = data.works
|
||||||
position: w.position,
|
const newIds = new Set(newItems.filter((w: any) => w.id).map((w: any) => w.id))
|
||||||
startDate: w.startDate,
|
const deletedIds = editSnapshotWorkIds.value.filter(id => !newIds.has(id))
|
||||||
endDate: w.endDate,
|
const addPromises: Promise<any>[] = []
|
||||||
description: w.description,
|
const updatePromises: Promise<any>[] = []
|
||||||
})))
|
const deletePromises = deletedIds.map(id => deleteResumeWork(id))
|
||||||
workList.value = data.works.map((w: any) => ({ ...w, description: w.description.map((d: any) => ({ ...d })) }))
|
for (const w of newItems) {
|
||||||
|
const itemData = {
|
||||||
|
companyName: w.companyName,
|
||||||
|
position: w.position,
|
||||||
|
startDate: w.startDate,
|
||||||
|
endDate: w.endDate,
|
||||||
|
description: w.description,
|
||||||
|
}
|
||||||
|
if (w.id && oldIds.has(w.id)) {
|
||||||
|
updatePromises.push(updateResumeWork({ ...itemData, id: w.id }))
|
||||||
|
} else {
|
||||||
|
addPromises.push(addResumeWork(resumeId, itemData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
|
||||||
} else if (editModule.value === 'internship') {
|
} else if (editModule.value === 'internship') {
|
||||||
// 保存实习经历
|
// 实习经历 — diff增删改
|
||||||
await saveResumeInternship(resumeId, data.internships.map((i: any) => ({
|
const oldIds = new Set(editSnapshotInternshipIds.value)
|
||||||
companyName: i.companyName,
|
const newItems: any[] = data.internships
|
||||||
position: i.position,
|
const newIds = new Set(newItems.filter((i: any) => i.id).map((i: any) => i.id))
|
||||||
startDate: i.startDate,
|
const deletedIds = editSnapshotInternshipIds.value.filter(id => !newIds.has(id))
|
||||||
endDate: i.endDate,
|
const addPromises: Promise<any>[] = []
|
||||||
description: i.description,
|
const updatePromises: Promise<any>[] = []
|
||||||
})))
|
const deletePromises = deletedIds.map(id => deleteResumeInternship(id))
|
||||||
internshipList.value = data.internships.map((i: any) => ({ ...i, description: i.description.map((d: any) => ({ ...d })) }))
|
for (const i of newItems) {
|
||||||
|
const itemData = {
|
||||||
|
companyName: i.companyName,
|
||||||
|
position: i.position,
|
||||||
|
startDate: i.startDate,
|
||||||
|
endDate: i.endDate,
|
||||||
|
description: i.description,
|
||||||
|
}
|
||||||
|
if (i.id && oldIds.has(i.id)) {
|
||||||
|
updatePromises.push(updateResumeInternship({ ...itemData, id: i.id }))
|
||||||
|
} else {
|
||||||
|
addPromises.push(addResumeInternship(resumeId, itemData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
|
||||||
} else if (editModule.value === 'project') {
|
} else if (editModule.value === 'project') {
|
||||||
// 保存项目经历
|
// 项目经历 — diff增删改
|
||||||
await saveResumeProject(resumeId, data.projects.map((p: any) => ({
|
const oldIds = new Set(editSnapshotProjectIds.value)
|
||||||
projectName: p.projectName,
|
const newItems: any[] = data.projects
|
||||||
companyName: p.companyName,
|
const newIds = new Set(newItems.filter((p: any) => p.id).map((p: any) => p.id))
|
||||||
role: p.role,
|
const deletedIds = editSnapshotProjectIds.value.filter(id => !newIds.has(id))
|
||||||
startDate: p.startDate,
|
const addPromises: Promise<any>[] = []
|
||||||
endDate: p.endDate,
|
const updatePromises: Promise<any>[] = []
|
||||||
description: p.description,
|
const deletePromises = deletedIds.map(id => deleteResumeProject(id))
|
||||||
})))
|
for (const p of newItems) {
|
||||||
projectList.value = data.projects.map((p: any) => ({ ...p, description: p.description.map((d: any) => ({ ...d })) }))
|
const itemData = {
|
||||||
|
projectName: p.projectName,
|
||||||
|
companyName: p.companyName,
|
||||||
|
role: p.role,
|
||||||
|
startDate: p.startDate,
|
||||||
|
endDate: p.endDate,
|
||||||
|
description: p.description,
|
||||||
|
}
|
||||||
|
if (p.id && oldIds.has(p.id)) {
|
||||||
|
updatePromises.push(updateResumeProject({ ...itemData, id: p.id }))
|
||||||
|
} else {
|
||||||
|
addPromises.push(addResumeProject(resumeId, itemData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
|
||||||
} else if (editModule.value === 'competition') {
|
} else if (editModule.value === 'competition') {
|
||||||
// 保存竞赛经历
|
// 竞赛经历 — diff增删改
|
||||||
await saveResumeCompetition(resumeId, data.competitions.map((c: any) => ({
|
const oldIds = new Set(editSnapshotCompetitionIds.value)
|
||||||
competitionName: c.competitionName,
|
const newItems: any[] = data.competitions
|
||||||
award: c.award,
|
const newIds = new Set(newItems.filter((c: any) => c.id).map((c: any) => c.id))
|
||||||
awardDate: c.awardDate,
|
const deletedIds = editSnapshotCompetitionIds.value.filter(id => !newIds.has(id))
|
||||||
description: c.description,
|
const addPromises: Promise<any>[] = []
|
||||||
})))
|
const updatePromises: Promise<any>[] = []
|
||||||
competitionList.value = data.competitions.map((c: any) => ({ ...c, description: c.description.map((d: any) => ({ ...d })) }))
|
const deletePromises = deletedIds.map(id => deleteResumeCompetition(id))
|
||||||
|
for (const c of newItems) {
|
||||||
|
const itemData = {
|
||||||
|
competitionName: c.competitionName,
|
||||||
|
award: c.award,
|
||||||
|
awardDate: c.awardDate,
|
||||||
|
description: c.description,
|
||||||
|
}
|
||||||
|
if (c.id && oldIds.has(c.id)) {
|
||||||
|
updatePromises.push(updateResumeCompetition({ ...itemData, id: c.id }))
|
||||||
|
} else {
|
||||||
|
addPromises.push(addResumeCompetition(resumeId, itemData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
|
||||||
} else if (editModule.value === 'portfolio') {
|
} else if (editModule.value === 'portfolio') {
|
||||||
// 保存作品集链接到简历主表
|
// 保存作品集链接到简历主表
|
||||||
await saveResumeMain({
|
await saveResumeMain({
|
||||||
@@ -999,6 +1204,9 @@ async function handleSaveEdit(data: Record<string, any>) {
|
|||||||
})
|
})
|
||||||
resumeMain.value.certificates = [...data.certificates]
|
resumeMain.value.certificates = [...data.certificates]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存成功后重新加载简历数据,确保 ID 等字段与服务端一致
|
||||||
|
await loadResumeDetail()
|
||||||
} catch {
|
} catch {
|
||||||
console.error('[ResumeDetail] 保存失败')
|
console.error('[ResumeDetail] 保存失败')
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ export default defineConfig({
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/ai-api': {
|
'/ai-api': {
|
||||||
target: 'http://192.168.31.135:8000',
|
target: 'http://192.168.31.133:8000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/ai-api/, ''),
|
rewrite: (path) => path.replace(/^\/ai-api/, ''),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user