新版交互调整

This commit is contained in:
2026-06-24 19:03:41 +08:00
parent 065553f53e
commit 8143a89dab
37 changed files with 4929 additions and 4409 deletions
+2
View File
@@ -57,7 +57,9 @@ declare module 'vue' {
RegionSelector: typeof import('./src/components/tools/RegionSelector.vue')['default']
ResumeAnalysisReportDrawer: typeof import('./src/components/ResumeAnalysisReportDrawer.vue')['default']
ResumeEditNameDialog: typeof import('./src/components/ResumeEditNameDialog.vue')['default']
ResumeExpInlineEdit: typeof import('./src/components/ResumeExpInlineEdit.vue')['default']
ResumeExportDialog: typeof import('./src/components/ResumeExportDialog.vue')['default']
ResumeIssueFixAllDrawer: typeof import('./src/components/ResumeIssueFixAllDrawer.vue')['default']
ResumeIssueFixDrawer: typeof import('./src/components/ResumeIssueFixDrawer.vue')['default']
ResumeUploadDialog: typeof import('./src/components/ResumeUploadDialog.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
+2
View File
@@ -21,6 +21,8 @@ export interface AgentConfig {
weeklyTarget?: number
/** 投递时自动针对岗位优化简历 0=关闭 1=开启 */
autoOptimizeResume?: number
/** 自动匹配分析 0=关闭 1=开启 */
autoMatchAnalysis?: number
/** 是否愿意接受部门调剂 */
acceptDeptTransfer?: string
/** 是否接受地点调剂 */
+2
View File
@@ -303,6 +303,8 @@ export interface JobDetailData {
salary: string
/** 工作类型 0=全职 1=兼职 */
employmentType: number
/** 招聘分类 0=社招 1=校招 2=实习 3=其他 */
recruitCategory: number
/** 学历要求 0=不限 1=大专 2=本科 3=硕士 4=博士 */
education: number
/** 最低工作年限 */
+15 -6
View File
@@ -531,6 +531,16 @@ export function deleteResume(resumeId: string) {
})
}
/**
* 设为默认简历
* POST /resume/setDefault
*/
export function setDefaultResume(resumeId: string) {
return request.post<any, ApiResult>('/resume/setDefault', null, {
params: { resumeId },
})
}
// ==================== 简历诊断相关 ====================
/** 诊断报告数据 */
@@ -644,13 +654,12 @@ export interface PolishIssueData {
}
/**
* 对诊断问题进行 AI 润色(AI 接口)
* POST /resume/diagnose/issue/{issue_id}/polish
* @param issueId 问题 ID
* @param content 用户编辑后的文本段落数组
* AI 润色简历段落(Python 后端接口)
* POST /resume/polish
* @param content 待润色的简历段落文本数组
*/
export function polishDiagnosisIssue(issueId: string, content: string[]) {
return aiService.post<AiResult<PolishIssueData>>(`/resume/diagnose/issue/${issueId}/polish`, { content }, {
export function polishDiagnosisIssue(content: string[]) {
return aiService.post<AiResult<PolishIssueData>>('/resume/polish', { content }, {
transformResponse: [(data: string) => {
try {
const processed = data.replace(/:\s*(\d{16,})/g, ':"$1"')
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

@@ -1,19 +1,26 @@
@use '../variables' as *;
// ==================== Agent会话岗位列表组件样式 ====================
// ==================== Agent推荐岗位列表组件样式 ====================
.agent-chat-job-list {
background: $bg-middle;
border-radius: 0.12rem;
padding: 0.2rem;
margin-top: 0.16rem;
padding: 0.24rem;
font-size: 0.14rem;
// 标题
&__header {
font-size: 0.18rem;
font-weight: 700;
color: $text-dark;
margin-bottom: 0.06rem;
}
// 推荐说明文字
&__summary {
font-size: 0.13rem;
color: $text-dark;
font-weight: 500;
margin-bottom: 0.14rem;
line-height: 1.6;
color: $text-middle;
margin-bottom: 0.18rem;
line-height: 1.5;
}
// 单个岗位项
@@ -23,68 +30,82 @@
justify-content: space-between;
background: $bg-white;
border-radius: 0.1rem;
padding: 0.14rem 0.16rem;
margin-bottom: 0.08rem;
cursor: pointer;
padding: 0.16rem 0.2rem;
margin-bottom: 0.1rem;
&:last-of-type {
margin-bottom: 0;
}
}
// 左侧信息区域
// 左侧信息区域(可点击)
&__info {
display: flex;
align-items: center;
gap: 0.12rem;
gap: 0.14rem;
flex: 1;
min-width: 0;
cursor: pointer;
}
// 公司 Logo
&__logo {
width: 0.36rem;
height: 0.36rem;
border-radius: 0.06rem;
background: $bg-main;
display: flex;
align-items: center;
justify-content: center;
// 匹配度环形容器
&__score {
position: relative;
width: 0.48rem;
height: 0.48rem;
flex-shrink: 0;
color: $text-middle;
overflow: hidden;
}
img {
// 环形 SVG
&__ring {
width: 100%;
height: 100%;
object-fit: cover;
}
svg {
width: 0.2rem;
height: 0.2rem;
}
// 匹配度数字
&__score-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.11rem;
font-weight: 700;
color: $text-dark;
}
// 岗位详情
&__detail {
min-width: 0;
flex: 1;
}
// 公司名
&__company {
font-size: 0.14rem;
// 岗位标题行(岗位名 + 公司名
&__title-row {
display: flex;
align-items: center;
gap: 0.08rem;
margin-bottom: 0.04rem;
}
// 岗位名称
&__title {
font-size: 0.15rem;
font-weight: 600;
color: $text-dark;
line-height: 1.4;
}
// 岗位名称
&__title {
font-size: 0.12rem;
// 公司名前的圆点
&__company-dot {
font-size: 0.08rem;
color: $text-light;
}
// 公司名称
&__company {
font-size: 0.13rem;
color: $text-middle;
line-height: 1.4;
margin-bottom: 0.04rem;
white-space: nowrap;
}
// 标签行
@@ -103,55 +124,109 @@
border-radius: 0.03rem;
}
// 匹配度环形容器
&__score {
position: relative;
width: 0.44rem;
height: 0.44rem;
// 右侧操作按钮区域
&__actions {
display: flex;
align-items: center;
gap: 0.14rem;
flex-shrink: 0;
margin-left: 0.12rem;
margin-left: 0.16rem;
}
// 环形 SVG
&__ring {
width: 100%;
height: 100%;
// 不再推荐按钮
&__dismiss-btn {
font-size: 0.12rem;
color: $text-light;
cursor: pointer;
white-space: nowrap;
transition: color 0.2s ease;
&:hover {
color: $text-middle;
}
}
// 匹配度数字
&__score-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.11rem;
font-weight: 600;
color: $text-dark;
// 加入投递按钮
&__add-btn {
height: 0.32rem;
padding: 0 0.16rem;
background: $accent;
color: $bg-white;
border: none;
border-radius: 0.16rem;
font-size: 0.12rem;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s ease;
line-height: 0.32rem;
&:hover {
opacity: 0.85;
}
&:disabled {
cursor: not-allowed;
opacity: 0.7;
}
// 已加入状态
&--added {
background: $bg-main;
color: $text-middle;
border: 1px solid $border-color;
}
}
// 底部按钮区域
&__footer {
display: flex;
justify-content: center;
margin-top: 0.14rem;
gap: 0.16rem;
margin-top: 0.2rem;
}
// 查看全部岗位按钮
&__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;
// 换一批按钮
&__refresh-btn {
height: 0.4rem;
padding: 0 0.4rem;
background: $bg-white;
color: $text-dark;
border: 1px solid $border-color;
border-radius: 0.2rem;
font-size: 0.14rem;
font-weight: 500;
cursor: pointer;
transition: all 0.25s ease;
line-height: 0.38rem;
transition: all 0.2s ease;
line-height: 0.4rem;
&:hover {
background: $btn-dark-hover;
border-color: $accent;
color: $accent;
}
&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
// 全部添加按钮
&__add-all-btn {
height: 0.4rem;
padding: 0 0.4rem;
background: $accent;
color: $bg-white;
border: none;
border-radius: 0.2rem;
font-size: 0.14rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
line-height: 0.4rem;
&:hover {
opacity: 0.85;
}
&:active {
@@ -1,326 +1,185 @@
@use '../variables' as *;
/* Agent设置面板样式 */
// ==================== Agent设置面板样式(精简版) ====================
.agent-setting-panel {
display: flex;
flex-direction: column;
height: 100%;
background: $bg-white;
border-radius: 0.2rem;
overflow: hidden;
background: $bg-middle;
border-radius: 0.12rem;
font-size: 0.14rem;
overflow-y: auto;
/* 顶部标题栏 */
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.16rem 0.2rem;
border-bottom: 1px solid $border-color;
// 隐藏滚动条
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar { display: none; }
// 内容区域
&__body {
padding: 0.28rem 0.24rem;
}
&__title {
// 每个设置区块
&__section {
margin-bottom: 0.28rem;
&:last-child {
margin-bottom: 0;
}
}
// 区块头部(标题 + 编辑按钮)
&__section-header {
display: flex;
align-items: center;
gap: 0.1rem;
margin-bottom: 0.12rem;
}
// 区块标题
&__section-title {
font-size: 0.16rem;
font-weight: 600;
font-weight: 700;
color: $text-dark;
}
/* 关闭按钮 */
&__close-btn {
width: 0.28rem;
height: 0.28rem;
border: none;
background: none;
// 编辑按钮
&__edit-btn {
font-size: 0.13rem;
color: $accent;
cursor: pointer;
padding: 0;
white-space: nowrap;
&:hover {
opacity: 0.7;
}
}
// 个人资料信息
&__profile-info {
padding-left: 0.02rem;
}
&__profile-name {
font-size: 0.18rem;
font-weight: 600;
color: $text-dark;
margin-bottom: 0.04rem;
}
&__profile-sub {
font-size: 0.13rem;
color: $text-middle;
}
// 求职目标标签
&__goal-tags {
display: flex;
flex-wrap: wrap;
gap: 0.08rem;
}
&__goal-tag {
padding: 0.06rem 0.14rem;
border: 1px solid $accent;
border-radius: 0.16rem;
font-size: 0.13rem;
color: $accent;
background: transparent;
}
// 空状态文字
&__empty-text {
font-size: 0.13rem;
color: $text-light;
}
// 简历选择下拉
&__resume-select {
width: 100%;
margin-top: 0.06rem;
.el-input__wrapper {
border-radius: 0.08rem;
}
}
// 投递偏好项
&__pref-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 0.12rem 0;
& + & {
border-top: none;
}
}
&__pref-left {
display: flex;
align-items: flex-start;
gap: 0.1rem;
flex: 1;
min-width: 0;
}
// 勾选图标
&__pref-check {
width: 0.2rem;
height: 0.2rem;
flex-shrink: 0;
color: $text-light;
margin-top: 0.02rem;
svg {
width: 100%;
height: 100%;
}
}
/* 内容区域(可滚动) */
&__body {
flex: 1;
overflow-y: auto;
padding: 0.2rem;
}
/* ========== 设置项区块 ========== */
&__section {
margin-bottom: 0.2rem;
border-bottom: 1px solid $border-color;
padding-bottom: 0.16rem;
&:last-child {
border-bottom: none;
margin-bottom: 0;
&--active {
color: $text-dark;
}
}
/* 区块标题行 */
&__section-header {
// 偏好文字
&__pref-text {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding: 0.04rem 0;
flex-direction: column;
gap: 0.03rem;
}
/* 区块标题 */
&__section-title {
font-size: 0.18rem;
font-weight: 600;
&__pref-label {
font-size: 0.14rem;
font-weight: 500;
color: $text-dark;
}
/* 编辑按钮区域(文字 + 箭头) */
&__section-action {
display: flex;
align-items: center;
gap: 0.04rem;
}
&__section-action-text {
&__pref-desc {
font-size: 0.12rem;
color: $accent;
}
/* 方向箭头 */
&__arrow {
width: 0.14rem;
height: 0.14rem;
color: $accent;
transition: transform 0.25s ease;
}
/* 展开时箭头朝上 */
&__arrow--up {
transform: rotate(180deg);
}
/* 收起时的简要信息预览 */
&__section-summary {
margin-top: 0.1rem;
font-size: 0.12rem;
color: $text-middle;
line-height: 1.6;
}
&__section-empty {
color: $text-light;
font-style: italic;
line-height: 1.4;
}
/* 展开时的内容区域 */
&__section-content {
margin-top: 0.14rem;
}
/* ========== 求职目标标签 ========== */
&__goal-tags {
display: flex;
flex-wrap: wrap;
gap: 0.08rem;
margin-top: 0.1rem;
}
&__goal-tag {
display: inline-block;
padding: 0.04rem 0.12rem;
background: $theme-color;
border: 1px solid $accent;
border-radius: 0.14rem;
font-size: 0.12rem;
color: $accent;
}
/* 编辑图标 */
&__edit-icon {
width: 0.13rem;
height: 0.13rem;
color: $accent;
}
/* ========== 浏览器插件 ========== */
// 浏览器按钮
&__browser-btns {
display: flex;
flex-wrap: wrap;
gap: 0.1rem;
margin-top: 0.12rem;
margin-top: 0.08rem;
}
&__browser-btn {
padding: 0.08rem 0.18rem;
background: $bg-white;
border: 1px solid $border-color;
border-radius: 0.08rem;
font-size: 0.13rem;
color: $text-dark;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
&:hover {
border-color: $accent;
background: $theme-color;
}
}
&__download-wrap {
margin-top: 0.16rem;
text-align: center;
}
&__download-btn {
padding: 0.1rem 0.4rem;
background: $gradient-bg;
color: $bg-white;
border: none;
border-radius: 0.24rem;
font-size: 0.14rem;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.9;
}
}
/* 浏览器安装指引弹窗图片 */
&__guide-slide {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
/* ========== 求职助手配置表单 ========== */
&__config-group {
margin-top: 0.16rem;
}
&__config-label {
font-size: 0.13rem;
font-weight: 600;
color: $text-dark;
margin-bottom: 0.08rem;
}
&__config-sub {
font-size: 0.12rem;
color: $text-middle;
margin-bottom: 0.06rem;
}
&__config-options {
display: flex;
flex-wrap: wrap;
gap: 0.08rem;
}
&__config-btn {
padding: 0.07rem 0.16rem;
padding: 0.08rem 0.2rem;
background: $bg-white;
border: 1px solid $border-color;
border-radius: 0.2rem;
font-size: 0.12rem;
font-size: 0.13rem;
color: $text-dark;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
transition: all 0.2s ease;
&:hover {
border-color: $accent;
}
&--active {
border-color: $accent;
background: $theme-color;
color: $accent;
}
}
&__config-selects {
display: flex;
gap: 0.1rem;
}
&__config-select {
min-width: 1.2rem;
&--full {
flex: 1;
}
}
/* 简历选择行 */
&__resume-row {
display: flex;
align-items: center;
gap: 0.08rem;
}
&__resume-icon {
width: 0.18rem;
height: 0.18rem;
color: $text-middle;
flex-shrink: 0;
}
/* 开关行 */
&__switch-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.1rem;
}
&__switch-text {
font-size: 0.12rem;
color: $text-dark;
line-height: 1.6;
}
&__switch-sub {
font-size: 0.11rem;
color: $text-light;
}
&__tip {
font-size: 0.12rem;
color: $text-light;
cursor: help;
margin-left: 0.04rem;
}
/* 提交按钮 */
&__submit-wrap {
margin-top: 0.24rem;
text-align: center;
}
&__submit-btn {
padding: 0.1rem 0.5rem;
background: $gradient-bg;
color: $bg-white;
border: none;
border-radius: 0.24rem;
font-size: 0.14rem;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.9;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
}
@@ -1180,6 +1180,14 @@
border-radius: 0.08rem;
}
/* 微信二维码 iframe */
&__wechat-qr-iframe {
width: 2.2rem;
height: 2.2rem;
border: none;
border-radius: 0.08rem;
}
&__qrcode-placeholder {
width: 100%;
height: 100%;
@@ -1,95 +1,50 @@
@use '../variables' as *;
@use 'sass:color';
// ==================== 个人资料编辑抽屉 ====================
// ==================== 个人资料内联编辑组件 ====================
// 遮罩层
.profile-drawer-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: $overlay-bg;
z-index: 2000;
}
// 抽屉主体 — 固定在右侧,占满屏幕高度
// 重置 font-size + line-height 避免 html font-size:100px 导致行高撑大间距
.profile-drawer {
position: fixed;
top: 0;
right: 0;
width: 6.2rem;
height: var(--app-height, 100vh);
background: $bg-white;
z-index: 2001;
box-shadow: -0.04rem 0 0.2rem rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
// 内联编辑容器 — 直接嵌入页面卡片中
.profile-edit-inline {
font-size: 14px;
line-height: 1.5;
background: #F7F7F7;
border-radius: 0.12rem;
padding: 0.1rem;
box-sizing: border-box;
// 顶部栏
&__header {
display: flex;
align-items: center;
gap: 0.1rem;
padding: 0.16rem 0.2rem;
border-bottom: 1px solid $border-color;
flex-shrink: 0;
}
&__close-btn {
background: none;
border: none;
padding: 0.04rem;
cursor: pointer;
color: $text-dark;
display: flex;
align-items: center;
border-radius: 0.04rem;
transition: all 0.2s;
&:hover {
color: $accent;
background: $theme-color;
}
}
&__close-icon {
width: 0.16rem;
height: 0.16rem;
}
&__title {
font-size: 0.16rem;
font-weight: 700;
color: $text-dark;
margin: 0;
}
// 表单内容区 — 中间可滚动
// 表单内容区
&__body {
flex: 1;
overflow-y: auto;
padding: 0.2rem 0.2rem 3rem;
// 地区选择器下拉面板提升层级,避免被其他元素遮挡
.region-selector__panel {
z-index: 2100;
padding: 0.1rem 0;
}
// 基本信息横排字段容器(姓名、电话、微信、邮箱、城市一行排列,允许换行)
&__fields-row {
display: flex;
flex-wrap: wrap;
gap: 0.16rem;
}
// 横排中的单个字段项
&__field-item {
flex: 1;
min-width: 1.6rem;
}
// 作品集链接整行字段
&__field-full {
margin-top: 0.16rem;
}
// 单个字段
&__field {
margin-bottom: 0.18rem;
margin-bottom: 0.02rem;
}
&__label {
font-size: 0.13rem;
display: block;
font-size: 0.12rem;
font-weight: 600;
color: $text-dark;
color: $text-middle;
margin-bottom: 0.06rem;
}
@@ -101,12 +56,12 @@
&__input {
width: 100%;
box-sizing: border-box;
padding: 0.1rem 0.14rem;
padding: 0.08rem 0.12rem;
font-size: 0.13rem;
color: $text-dark;
background: $bg-main;
border: 1px solid transparent;
border-radius: 0.06rem;
background: $bg-white;
border: 1px solid $border-color;
border-radius: 0.04rem;
outline: none;
transition: all 0.2s;
@@ -120,21 +75,22 @@
}
}
// 多行文本输入框(经历描述等)
// 多行文本输入框
&__textarea {
width: 100%;
box-sizing: border-box;
padding: 0.1rem 0.14rem;
padding: 0.08rem 0.12rem;
font-size: 0.13rem;
color: $text-dark;
background: $bg-main;
border: 1px solid transparent;
border-radius: 0.06rem;
background: $bg-white;
border: 1px solid $border-color;
border-radius: 0.04rem;
outline: none;
transition: all 0.2s;
resize: vertical;
font-family: inherit;
line-height: 1.6;
padding-right: 0.3rem;
&::placeholder {
color: $text-light;
@@ -144,33 +100,26 @@
border-color: $accent;
background: $bg-white;
}
// 矮版 textarea — 用于分段描述,右侧留出删除按钮空间
&--short {
padding-right: 0.36rem;
}
}
// ==================== 描述段落编辑 ====================
// 单条描述段落容器 — textarea + 右侧删除按钮
// 描述段落容器
&__desc-item {
position: relative;
margin-bottom: 0.08rem;
}
// 描述段落内的删除按钮(叉号)
// 描述删除按钮
&__desc-remove {
position: absolute;
right: 0.18rem;
top: 0.08rem;
right: 0.08rem;
top: 0.06rem;
background: none;
border: none;
padding: 0.02rem;
padding: 0.02rem 0.04rem;
cursor: pointer;
color: $text-light;
display: flex;
align-items: center;
font-size: 0.14rem;
line-height: 1;
border-radius: 0.03rem;
transition: all 0.2s;
@@ -179,91 +128,7 @@
}
}
&__desc-remove-icon {
width: 0.12rem;
height: 0.12rem;
}
// 小号新增按钮 — 用于"新增一段描述"
&__add-btn--small {
font-size: 0.11rem;
padding: 0.04rem 0.12rem;
margin-top: 0.04rem;
margin-bottom: 0;
}
// 空状态占位
&__empty {
text-align: center;
padding: 0.6rem 0;
}
&__empty-text {
font-size: 0.14rem;
color: $text-light;
}
// ==================== 教育经历模块 ====================
// 单条教育经历卡片
&__edu-card {
padding-bottom: 0.2rem;
margin-bottom: 0.2rem;
border-bottom: 1px solid $border-color;
&:last-of-type {
border-bottom: none;
}
}
// 教育经历标题栏
&__edu-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.14rem;
}
// 序号标识
&__edu-index {
display: flex;
align-items: center;
gap: 0.06rem;
font-size: 0.14rem;
font-weight: 600;
color: $text-dark;
}
&__edu-index-icon {
width: 0.1rem;
height: 0.1rem;
color: $accent;
}
// 删除按钮
&__edu-delete {
background: none;
border: none;
padding: 0.04rem;
cursor: pointer;
color: $text-light;
display: flex;
align-items: center;
border-radius: 0.04rem;
transition: all 0.2s;
&:hover {
color: $danger;
background: rgba($danger, 0.06);
}
}
&__edu-delete-icon {
width: 0.15rem;
height: 0.15rem;
}
// 横排字段容器(学院+专业、学历+GPA、入学+毕业时间)
// 横排字段容器
&__row {
display: flex;
gap: 0.12rem;
@@ -275,60 +140,81 @@
min-width: 0;
}
// 下拉选择器包裹(带箭头图标)
&__select-wrap {
position: relative;
// ==================== 经历卡片模块 ====================
&__edu-card {
padding-bottom: 0.16rem;
margin-bottom: 0.16rem;
border-bottom: 1px solid $border-color;
&:last-of-type {
border-bottom: none;
}
}
&__select-wrap &__input {
padding-right: 0.3rem;
&__edu-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.12rem;
}
&__select-arrow {
position: absolute;
right: 0.1rem;
top: 50%;
transform: translateY(-50%);
width: 0.12rem;
height: 0.12rem;
&__edu-index {
font-size: 0.13rem;
font-weight: 600;
color: $text-dark;
}
&__edu-delete {
background: none;
border: none;
padding: 0.04rem 0.08rem;
cursor: pointer;
color: $text-light;
pointer-events: none;
font-size: 0.12rem;
border-radius: 0.04rem;
transition: all 0.2s;
&:hover {
color: $danger;
background: rgba($danger, 0.06);
}
}
// 新增教育经历按钮
// 新增按钮
&__add-btn {
text-align: center;
background: none;
border: 1px solid $border-color;
border-radius: 0.2rem;
padding: 0.06rem 0.16rem;
border: 1px dashed $border-color;
border-radius: 0.04rem;
padding: 0.04rem 0.12rem;
font-size: 0.12rem;
color: $text-dark;
color: $text-middle;
cursor: pointer;
transition: all 0.2s;
margin-top: 0.04rem;
margin-top: 0.06rem;
&:hover {
border-color: $accent;
color: $accent;
}
&--full {
width: 100%;
margin-top: 0.12rem;
padding: 0.08rem;
}
}
&__add-icon {
font-size: 0.14rem;
font-weight: 600;
}
// ==================== 日期选择器样式覆盖 ====================
// Element Plus 的 el-date-picker 在 html font-size:100px 下需要用 px 还原
// ==================== 日期选择器 ====================
&__date-picker {
width: 100% !important;
.el-input__wrapper {
background: $bg-main !important;
border: 1px solid transparent !important;
border-radius: 6px !important;
border: 1px solid $border-color !important;
border-radius: 4px !important;
box-shadow: none !important;
padding: 4px 12px !important;
font-size: 13px !important;
@@ -336,7 +222,7 @@
height: auto !important;
&:hover {
border-color: $border-color !important;
border-color: $accent !important;
}
&.is-focus {
@@ -368,26 +254,147 @@
}
}
// ==================== 技能模块 ====================
// 日期范围容器(开始-结束横排)
&__date-range {
display: flex;
align-items: center;
gap: 0.04rem;
}
&__date-sep {
color: $text-light;
font-size: 0.12rem;
}
// ==================== AI润色功能 ====================
// 个人概述标题栏(含AI润色按钮)
&__summary-header {
display: flex;
align-items: center;
gap: 0.12rem;
margin-bottom: 0.08rem;
margin-top: 0.1rem;
}
// AI润色按钮
&__ai-polish-btn {
padding: 0.01rem 0.06rem;
font-size: 0.11rem;
background: $accent;
color: $bg-white;
border: 1px solid $accent;
border-radius: 0.04rem;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
>span>span{
color: #FCD34D;
}
&:hover {
background: $accent;
color: $bg-white;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// AI润色结果区域
&__ai-result {
margin-top: 0.16rem;
}
&__ai-result-label {
font-size: 0.12rem;
font-weight: 600;
color: $accent;
margin-bottom: 0.08rem;
}
&__ai-result-card {
padding: 0.1rem 0.12rem;
background: $theme-color;
border: 1px solid rgba($accent, 0.2);
border-radius: 0.06rem;
}
&__ai-result-text {
font-size: 0.13rem;
color: $text-dark;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-word;
}
// AI结果操作按钮组
&__ai-result-actions {
display: flex;
align-items: center;
gap: 0.12rem;
margin-top: 0.1rem;
}
// 替换为此内容按钮
&__ai-replace-btn {
padding: 0.04rem 0.12rem;
font-size: 0.12rem;
color: #fff;
background: $accent;
border: 1px solid $accent;
border-radius: 0.04rem;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: $accent;
color: $bg-white;
}
}
// 重新生成按钮
&__ai-regenerate-btn {
padding: 0.04rem 0.12rem;
font-size: 0.12rem;
color: $accent;
background: none;
border: 1px solid $accent;
border-radius: 0.04rem;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: $accent;
color: $accent;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// ==================== 技能/证书标签 ====================
// 技能标签列表容器
&__skills-list {
display: flex;
flex-wrap: wrap;
gap: 0.08rem;
margin-bottom: 0.2rem;
margin-bottom: 0.16rem;
min-height: 0.4rem;
}
// 单个技能标签
&__skill-tag {
display: inline-flex;
align-items: center;
gap: 0.06rem;
padding: 0.06rem 0.1rem 0.06rem 0.14rem;
padding: 0.04rem 0.1rem 0.04rem 0.12rem;
background: $bg-main;
border-radius: 0.16rem;
font-size: 0.13rem;
border-radius: 0.14rem;
font-size: 0.12rem;
color: $text-dark;
transition: all 0.2s;
@@ -396,12 +403,10 @@
}
}
// 技能标签文本
&__skill-text {
line-height: 1.4;
}
// 技能标签删除按钮
&__skill-remove {
background: none;
border: none;
@@ -424,23 +429,23 @@
height: 0.12rem;
}
// 底部保存按钮
// ==================== 底部操作按钮 ====================
&__footer {
padding: 0.16rem 0.2rem;
border-top: 1px solid $border-color;
text-align: center;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0.12rem;
padding-top: 0.04rem;
}
&__save-btn {
width: 2.0rem;
padding: 0.1rem 0;
font-size: 0.14rem;
padding: 0.06rem 0.2rem;
font-size: 0.13rem;
font-weight: 600;
color: $bg-white;
background: $btn-dark;
border: none;
border-radius: 0.24rem;
border-radius: 0.2rem;
cursor: pointer;
transition: all 0.2s;
@@ -448,11 +453,26 @@
background: $btn-dark-hover;
}
}
&__cancel-btn {
padding: 0.06rem 0.2rem;
font-size: 0.13rem;
font-weight: 600;
color: $accent;
background: transparent;
border: 1px solid $accent;
border-radius: 0.2rem;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: $theme-color;
}
}
}
// ==================== 过渡动画 ====================
// ==================== 日期选择器弹出面板样式覆盖 ====================
// 日期选择器弹出面板样式覆盖(teleport 到 body,需在全局作用域)
.profile-drawer-date-popper {
font-size: 14px !important;
line-height: 1.5 !important;
@@ -474,25 +494,3 @@
padding: 8px 0 !important;
}
}
// 遮罩层淡入淡出
.profile-drawer-overlay-enter-active,
.profile-drawer-overlay-leave-active {
transition: opacity 0.3s ease;
}
.profile-drawer-overlay-enter-from,
.profile-drawer-overlay-leave-to {
opacity: 0;
}
// 抽屉滑入滑出
.profile-drawer-slide-enter-active,
.profile-drawer-slide-leave-active {
transition: transform 0.3s ease;
}
.profile-drawer-slide-enter-from,
.profile-drawer-slide-leave-to {
transform: translateX(100%);
}
@@ -5,16 +5,66 @@
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.16rem;
padding-bottom: 0.4rem;
// 卡片通用
&__card {
gap: 0;
padding: 0.2rem 0.24rem 0;
background: $bg-white;
border-radius: 0.12rem;
padding: 0.2rem 0.24rem;
font-size: 14px;
font-size: 0.14rem;
overflow: hidden;
// Tab 导航栏(固定不动)
&__tabs {
display: flex;
align-items: center;
gap: 0.24rem;
padding-bottom: 0.14rem;
border-bottom: 1px solid $border-color;
flex-shrink: 0;
}
&__tab-item {
font-size: 0.14rem;
color: $text-middle;
cursor: pointer;
padding-bottom: 0.06rem;
border-bottom: 2px solid transparent;
transition: all 0.2s;
white-space: nowrap;
&:hover {
color: $text-dark;
}
&--active {
color: $text-dark;
font-weight: 600;
border-bottom-color: $text-dark;
}
}
// 内容滚动区域
&__body {
flex: 1;
overflow-y: auto;
padding-bottom: 0.2rem;
// 隐藏滚动条
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none;
}
// 卡片通用(不再有独立背景,只做分隔)
&__card {
padding: 0.16rem 0;
line-height: 1.5;
border-radius: 0.08rem;
&:not(:last-child) {
//border-bottom: 1px solid $border-color;
}
}
&__card-header {
@@ -22,6 +72,8 @@
align-items: center;
justify-content: space-between;
margin-bottom: 0.14rem;
padding: 0.06rem 0.14rem;
border-radius: 0.06rem;
}
&__card-title {
@@ -29,6 +81,21 @@
font-weight: 700;
color: $text-dark;
margin: 0;
position: relative;
padding-bottom: 0.02rem;
// 底部装饰线
&::after {
content: '';
position: absolute;
left: 0;
bottom: 0.02rem;
width: 100%;
height: 0.06rem;
background: $accent;
border-radius: 0.02rem;
z-index: -1;
}
}
&__edit-btn {
@@ -81,7 +148,6 @@
padding: 0.12rem 0;
&:not(:last-child) {
border-bottom: 1px solid $border-color;
}
}
@@ -170,6 +236,7 @@
display: flex;
flex-wrap: wrap;
gap: 0.08rem;
padding: 0 0.14rem;
}
&__tag {
@@ -187,4 +254,77 @@
background: $theme-color;
}
}
// ---- 新版内联编辑按钮(品牌色图标+文字,hover显示) ----
&__edit-btn-inline {
background: none;
border: none;
color: $accent;
cursor: pointer;
padding: 0.04rem 0.08rem;
border-radius: 0.04rem;
display: flex;
align-items: center;
gap: 0.04rem;
font-size: 0.13rem;
font-weight: 500;
opacity: 0;
pointer-events: none;
transition: all 0.2s;
flex-shrink: 0;
margin-left: auto;
.el-icon {
font-size: 0.15rem;
}
&:hover {
background: $theme-color;
}
}
// 卡片hover时显示编辑按钮(个人信息/技能/证书)
&__card-hover:hover &__edit-btn-inline {
opacity: 1;
pointer-events: auto;
}
&__card-hover:hover {
background: #F7F7F7;
}
// 标题行hover显示添加按钮(5大经历)
&__header-hover:hover {
background: #F7F7F7;
border-radius: 0.06rem;
}
&__header-hover:hover &__edit-btn-inline {
opacity: 1;
pointer-events: auto;
}
// 经历条目hover显示编辑按钮
&__section-item {
padding: 0.12rem 0.14rem;
border-radius: 0.08rem;
&:hover {
background: #F7F7F7;
}
&:not(:last-child) {
}
}
// 条目hover时显示编辑按钮
&__section-item:hover &__item-edit-btn {
opacity: 1;
pointer-events: auto;
}
// 条目内编辑按钮
&__item-edit-btn {
margin-left: 0.08rem;
}
}
@@ -1,24 +1,51 @@
@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 {
/* 编辑简历名称弹窗 — 遮罩层 */
.resume-edit-name-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: $overlay-bg;
z-index: 2200;
display: flex;
flex-wrap: wrap;
gap: 0.24rem;
align-items: center;
justify-content: center;
}
/* 弹窗主体 */
.resume-edit-name-dialog {
width: 4.6rem;
background: $bg-white;
border-radius: 0.12rem;
padding: 0.32rem 0.3rem;
position: relative;
font-size: 0.14rem;
box-shadow: 0 0.04rem 0.2rem rgba(0, 0, 0, 0.15);
/* 关闭按钮 */
&__close {
position: absolute;
top: 0.14rem;
right: 0.16rem;
border: none;
background: none;
font-size: 0.16rem;
color: $text-light;
cursor: pointer;
padding: 0.04rem;
line-height: 1;
transition: color 0.2s;
&:hover {
color: $text-dark;
}
}
/* 表单字段 */
&__field {
width: 100%;
margin-bottom: 0.24rem;
}
/* 标签 */
@@ -38,27 +65,28 @@
/* 输入框 */
&__input {
.el-input__wrapper {
width: 100%;
height: 0.44rem;
border: 1px solid $border-color;
border-radius: 0.1rem;
padding: 0.04rem 0.14rem;
box-shadow: 0 0 0 1px $border-color;
}
.el-input__inner {
padding: 0 0.14rem;
font-size: 0.14rem;
color: $text-dark;
height: 0.36rem;
line-height: normal;
outline: none;
box-sizing: border-box;
transition: border-color 0.2s;
&::placeholder {
color: $text-light;
}
&:focus {
border-color: $accent;
}
}
/* 底部按钮区域 */
&__footer {
width: 100%;
display: flex;
gap: 0.16rem;
margin-top: 0.08rem;
@@ -0,0 +1,252 @@
@use '../variables' as *;
// ==================== 一键修复抽屉(全模块) ====================
// 遮罩层
.fix-all-drawer-overlay {
position: fixed;
inset: 0;
background: $overlay-bg;
z-index: 2000;
}
// 遮罩层过渡动画
.fix-all-drawer-overlay-enter-active,
.fix-all-drawer-overlay-leave-active {
transition: opacity 0.3s ease;
}
.fix-all-drawer-overlay-enter-from,
.fix-all-drawer-overlay-leave-to {
opacity: 0;
}
// 抽屉主体
.fix-all-drawer {
position: fixed;
top: 0;
right: 0;
width: 8rem;
height: var(--app-height, 100vh);
background: $bg-main;
z-index: 2001;
display: flex;
flex-direction: column;
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.08);
font-size: 0.14rem;
// ---- 顶部栏 ----
&__header {
display: flex;
align-items: center;
gap: 0.1rem;
padding: 0.16rem 0.24rem;
border-bottom: 1px solid $border-color;
background: $bg-white;
flex-shrink: 0;
}
&__close-btn {
width: 0.28rem;
height: 0.28rem;
border-radius: 50%;
border: none;
background: none;
color: $text-dark;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: $bg-main;
color: $accent;
}
}
&__close-icon {
width: 0.16rem;
height: 0.16rem;
}
&__title {
font-size: 0.18rem;
font-weight: 700;
color: $text-dark;
margin: 0;
}
// ---- 可滚动内容区 ----
&__body {
flex: 1;
overflow-y: auto;
padding: 0.2rem 0.24rem 0.4rem;
}
// ---- 模块标题 ----
&__module-title {
font-size: 0.16rem;
font-weight: 700;
color: $text-dark;
margin: 0.24rem 0 0.12rem 0;
&:first-child {
margin-top: 0;
}
}
// ---- 单条经历记录 ----
&__record {
margin-bottom: 0.2rem;
}
&__record-label {
font-size: 0.13rem;
font-weight: 600;
color: $text-dark;
margin: 0 0 0.08rem 0;
}
// ---- 卡片容器 ----
&__card {
background: $bg-white;
border-radius: 0.1rem;
padding: 0.16rem;
border: 1px solid $border-color;
&--ai {
border: 1px solid rgba($accent, 0.3);
background: rgba($accent, 0.03);
}
}
// ---- 原文编辑 textarea ----
&__textarea {
width: 100%;
min-height: 0.6rem;
font-size: 0.13rem;
color: $text-dark;
line-height: 1.8;
border: none;
outline: none;
resize: none;
overflow: hidden;
background: transparent;
font-family: inherit;
box-sizing: border-box;
padding: 0;
field-sizing: content;
&::placeholder {
color: $text-light;
}
}
// ---- AI 润色结果区 ----
&__ai-section {
margin-top: 0.12rem;
}
&__ai-label {
font-size: 0.13rem;
font-weight: 600;
color: $accent;
margin: 0 0 0.08rem 0;
}
&__ai-text {
font-size: 0.13rem;
color: $text-dark;
line-height: 1.8;
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
// ---- AI 操作按钮 ----
&__ai-actions {
display: flex;
align-items: center;
gap: 0.12rem;
margin-top: 0.1rem;
}
&__action-btn {
font-size: 0.12rem;
font-weight: 500;
border: 1px solid $accent;
border-radius: 0.14rem;
padding: 0.04rem 0.12rem;
cursor: pointer;
transition: all 0.2s;
&--replace {
color: $accent;
background: $bg-white;
&:hover {
background: rgba($accent, 0.08);
}
}
&--regenerate {
color: $accent;
background: $bg-white;
&:hover {
background: rgba($accent, 0.08);
}
}
}
// ---- 底部按钮区 ----
&__footer {
padding: 0.16rem 0.24rem;
display: flex;
justify-content: center;
align-items: center;
gap: 0.2rem;
flex-shrink: 0;
background: $bg-white;
border-top: 1px solid $border-color;
}
&__footer-btn {
padding: 0.1rem 0.3rem;
font-size: 0.14rem;
font-weight: 600;
border-radius: 0.22rem;
cursor: pointer;
transition: all 0.2s;
&--use-ai {
color: $accent;
background: $bg-white;
border: 1px solid $accent;
&:hover {
background: rgba($accent, 0.08);
}
}
&--save {
color: $bg-white;
background: $btn-dark;
border: 1px solid $btn-dark;
&:hover {
background: $btn-dark-hover;
}
}
}
}
// 抽屉滑入动画
.fix-all-drawer-slide-enter-active,
.fix-all-drawer-slide-leave-active {
transition: transform 0.3s ease;
}
.fix-all-drawer-slide-enter-from,
.fix-all-drawer-slide-leave-to {
transform: translateX(100%);
}
+1
View File
@@ -27,6 +27,7 @@
@use './components/job-resume-custom-edit-panel.scss';
@use './components/resume-analysis-report-drawer.scss';
@use './components/resume-issue-fix-drawer.scss';
@use './components/resume-issue-fix-all-drawer.scss';
@use './components/resume-edit-name-dialog.scss';
@use './components/agent-chat-job-list.scss';
@use './components/agent-match-job-add.scss';
+310 -187
View File
@@ -97,7 +97,7 @@
// 左侧说明引导区域
&__left {
width: 4.2rem;
width: 3.0rem;
flex-shrink: 0;
}
@@ -111,7 +111,7 @@
&__intro-card {
background: $bg-white;
border-radius: 0.12rem;
padding: 0.3rem;
padding: 0.38rem;
}
// 卡片头部:图标 + 标题
@@ -214,23 +214,15 @@
&__profile-wrapper {
background: $bg-main;
border-radius: 0.12rem;
border: 1px solid $border-color;
padding: 0.24rem;
max-height: calc(var(--app-height, 100vh) - 1.4rem);
overflow-y: auto;
display: flex;
flex-direction: column;
scrollbar-width: none; // Firefox
-ms-overflow-style: none; // IE/Edge
// 自定义滚动条
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: $border-color;
border-radius: 2px;
}
&::-webkit-scrollbar-track {
background: transparent;
display: none; // Chrome/Safari
}
}
@@ -599,191 +591,309 @@
&:active { transform: scale(0.98); }
}
// ==================== 第3步:网申常见问题 ====================
// 第3步整体容器 — 左右两栏布局
&__step3 {
display: flex;
gap: 0.24rem;
align-items: flex-start;
width: 100%;
position: relative;
padding-top: 0.36rem;
// ==================== 第2步新增:求职偏好区域 ====================
&__pref-section {
background: $bg-white;
border-radius: 0.12rem;
padding: 0.24rem;
margin-bottom: 0.2rem;
border: 1px solid $border-color;
}
// 右侧表单包裹
&__form-wrapper {
&__pref-section-title {
font-size: 0.18rem;
font-weight: 600;
color: $text-dark;
margin-bottom: 0.1rem;
}
&__pref-section-desc {
font-size: 0.13rem;
color: $text-middle;
margin-bottom: 0.16rem;
line-height: 1.5;
}
&__pref-filters {
display: flex;
flex-wrap: wrap;
gap: 0.1rem;
align-items: center;
}
&__pref-type-tag {
padding: 0.06rem 0.16rem;
background: $bg-main;
border-radius: 0.04rem;
font-size: 0.13rem;
color: $text-dark;
border: 1px solid $border-color;
cursor: default;
}
// 招聘分类下拉选择
&__pref-type-dropdown {
position: relative;
padding: 0.1rem 0.28rem;
background: #F5FAF9;
border-radius: 0.24rem;
font-size: 0.14rem;
color: $text-dark;
border: 1px solid $border-color;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.04rem;
min-width: 0.9rem;
height: 0.38rem;
box-sizing: border-box;
user-select: none;
transition: border-color 0.2s ease;
&:hover { border-color: $accent; }
}
&__pref-type-arrow {
width: 0.12rem;
height: 0.12rem;
color: $text-middle;
}
&__pref-type-menu {
position: absolute;
top: calc(100% + 0.04rem);
left: 0;
min-width: 1.2rem;
background: $bg-white;
border: 1px solid $border-color;
border-radius: 0.06rem;
box-shadow: 0 0.02rem 0.08rem rgba(0, 0, 0, 0.1);
z-index: 10;
padding: 0.04rem 0;
}
&__pref-type-menu-item {
padding: 0.08rem 0.14rem;
font-size: 0.13rem;
color: $text-dark;
cursor: pointer;
transition: background 0.15s ease;
&:hover { background: $bg-main; }
&--active {
color: $accent;
font-weight: 500;
}
}
// ==================== 第2步新增:简历设置区域 ====================
&__resume-section {
background: $bg-white;
border-radius: 0.12rem;
padding: 0.24rem;
border: 1px solid $border-color;
}
&__resume-sub-label {
font-size: 0.13rem;
color: $text-middle;
margin-bottom: 0.12rem;
}
&__resume-select-row {
display: flex;
align-items: center;
gap: 0.1rem;
margin-bottom: 0.2rem;
padding: 0.1rem 0.14rem;
border: 1px solid $border-color;
border-radius: 0.08rem;
}
&__resume-check-icon {
flex-shrink: 0;
width: 0.16rem;
height: 0.16rem;
color: $text-middle;
svg {
width: 100%;
height: 100%;
}
}
&__resume-select {
flex: 1;
.el-input__wrapper {
box-shadow: none !important;
padding: 0;
}
}
&__resume-switch-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.1rem 0;
& + & {
border-top: none;
}
}
&__resume-switch-text {
font-size: 0.13rem;
color: $text-dark;
}
// ==================== 第3步:安装插件 ====================
// 安装教程整体容器
&__install-guide {
background: $bg-white;
border-radius: 0.12rem;
padding: 0.28rem;
border: 1px solid $border-color;
}
// 表单标题
&__form-title {
font-size: 0.15rem;
// 安装教程标题
&__install-guide-title {
font-size: 0.18rem;
font-weight: 700;
color: $text-dark;
margin-bottom: 0.24rem;
padding-bottom: 0.12rem;
border-bottom: 1px solid $border-color;
margin-bottom: 0.28rem;
}
// 表单分组
&__form-group {
margin-bottom: 0.28rem;
// 每个安装步骤
&__install-step {
margin-bottom: 0.3rem;
&:last-child { margin-bottom: 0; }
}
// 表单标签
&__form-label {
// 步骤文字说明
&__install-step-text {
font-size: 0.14rem;
font-weight: 600;
color: $text-dark;
margin-bottom: 0.12rem;
text-align: center;
margin-bottom: 0.16rem;
line-height: 1.5;
}
// 选项按钮
&__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;
// 下载按钮
&__install-download-btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 100%;
gap: 0.08rem;
padding: 0.08rem 0.2rem;
background: rgba($accent, 0.1);
border: none;
border-radius: 0.2rem;
color: $accent;
font-size: 0.13rem;
cursor: pointer;
transition: all 0.2s ease;
&:hover { background: rgba($accent, 0.2); }
svg { flex-shrink: 0; }
}
// 步骤2的内容区(图片+浏览器链接并排)
&__install-step-content {
display: flex;
gap: 0.24rem;
align-items: flex-start;
}
// 步骤中的图片区
&__install-step-img {
flex: 1;
min-width: 0;
background: $bg-main;
border-radius: 0.08rem;
padding: 0.12rem;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 0.08rem;
width: 100%;
height: auto;
border-radius: 0.06rem;
}
}
// 浏览器链接区域
&__install-browser-links {
flex-shrink: 0;
width: 2rem;
}
&__install-browser-item {
display: flex;
align-items: center;
gap: 0.08rem;
margin-bottom: 0.06rem;
}
&__install-browser-icon {
width: 0.2rem;
height: 0.2rem;
border-radius: 50%;
object-fit: contain;
}
&__install-browser-name {
font-size: 0.13rem;
font-weight: 500;
color: $text-dark;
}
&__install-browser-url {
display: inline-flex;
align-items: center;
gap: 0.04rem;
font-size: 0.13rem;
color: $accent;
text-decoration: none;
cursor: pointer;
margin-bottom: 0.16rem;
&:hover { text-decoration: underline; }
svg { flex-shrink: 0; color: $text-middle; }
}
// 步骤3/4的图片展示行
&__install-step-img-row {
display: flex;
gap: 0.16rem;
}
&__install-step-img-item {
flex: 1;
background: $bg-main;
border-radius: 0.08rem;
padding: 0.12rem;
img {
width: 100%;
height: auto;
border-radius: 0.06rem;
}
}
// 右侧卡片中的安装步骤列表
&__install-steps-list {
font-size: 0.13rem;
color: $text-middle;
line-height: 2;
margin-bottom: 0.1rem;
p { margin: 0; }
}
// ==================== 设置完成页 ====================
// 完成页容器 — 居中布局
@@ -845,28 +955,34 @@
.agent-main {
height: 100%;
display: flex;
justify-content: center;
// 有右侧面板时取消居中
&--with-panel {
//justify-content: flex-start;
}
// 左侧主区域
// 左侧主区域(60%
&__left {
flex: 1;
width: 60%;
height: calc(var(--app-height, 100vh) - 0.6rem);
display: flex;
flex-direction: column;
max-width: 8rem;
}
// 右侧匹配岗位面板
// 右侧面板40%,常驻设置面板 + 覆盖层)
&__right {
flex: 1;
width: 40%;
height: calc(var(--app-height, 100vh) - 0.6rem);
margin-left: 0.2rem;
max-width: 8rem;
position: relative;
overflow: hidden;
}
// 右侧覆盖层 — 绝对定位覆盖在设置面板之上
&__right-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
background: $bg-white;
border-radius: 0.12rem;
overflow-y: auto;
}
@@ -878,7 +994,6 @@
height: 100%;
overflow-y: auto;
// 自定义滚动条
&::-webkit-scrollbar {
width: 4px;
}
@@ -1142,7 +1257,7 @@
}
}
// ==================== 中间聊天记录区域 ====================
// ==================== 中间内容区域 ====================
&__chat-area {
flex: 1;
overflow-y: auto;
@@ -1164,6 +1279,14 @@
}
}
// 投递进度列表
&__progress-list {
margin-top: 0.16rem;
display: flex;
flex-direction: column;
gap: 0.12rem;
}
// ==================== 对话消息样式 ====================
// 对话行
+2 -1
View File
@@ -8,7 +8,7 @@
padding: 0.12rem 0.36rem;
height: var(--app-height, 100vh);
box-sizing: border-box;
overflow-y: auto;
overflow: hidden;
background: $bg-main;
display: flex;
flex-direction: column;
@@ -50,6 +50,7 @@
gap: 0.24rem;
flex: 1;
min-height: 0;
overflow: hidden;
}
// 右侧边栏
+294 -67
View File
@@ -5,7 +5,7 @@
&__content {
margin-left: 2rem;
flex: 1;
padding: 0.12rem 0.36rem;
padding-bottom: 0.24rem;
height: var(--app-height, 100vh);
box-sizing: border-box;
overflow-y: auto;
@@ -16,10 +16,14 @@
}
&__page-title {
margin: 0 0 0.16rem 0;
padding: 0 0.24rem;
>h2{
font-size: 0.24rem;
font-weight: 700;
color: $text-dark;
margin: 0 0 0.16rem 0;
}
}
// ---- 顶部操作栏 ----
@@ -27,7 +31,8 @@
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.16rem;
margin-bottom: 0.15rem;
padding: 0 0.24rem;
}
&__toolbar-left {
@@ -37,22 +42,18 @@
}
&__back-btn {
width: 0.32rem;
height: 0.32rem;
border-radius: 50%;
border: 1px solid $border-color;
background: $bg-white;
background: none;
border: none;
color: $text-dark;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
font-size: 0.13rem;
&:hover {
background: $theme-color;
border-color: $accent;
color: $accent;
color: #000;
}
}
@@ -83,8 +84,8 @@
gap: 0.04rem;
background: none;
border: none;
color: $text-light;
font-size: 0.12rem;
color: $text-dark;
font-size: 0.13rem;
cursor: pointer;
padding: 0.05rem 0.1rem;
border-radius: 0.06rem;
@@ -92,8 +93,8 @@
white-space: nowrap;
&:hover {
color: $text-dark;
background: $bg-white;
//color: $text-dark;
//background: $bg-white;
}
&--danger:hover {
@@ -115,40 +116,79 @@
background: $bg-white;
border-radius: 0.12rem;
padding: 0.18rem 0.24rem;
padding-top: 0.4rem;
margin-bottom: 0.16rem;
/* B/C/D 评级渐变背景 */
&--grade-bcd {
background: linear-gradient(to bottom, #FFF5E9, #FFF);
}
/* A 评级渐变背景 */
&--grade-a {
background: linear-gradient(to bottom, #ECFDFC, #FFF);
}
}
&__score-left {
display: flex;
align-items: center;
gap: 0.12rem;
gap: 0.2rem;
}
&__score-avatar {
width: 0.4rem;
height: 0.4rem;
border-radius: 50%;
/* 星星+底座容器 */
&__star-wrapper {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 0.7rem;
flex-shrink: 0;
}
/* SVG 五角星 */
&__star-svg {
width: 0.63rem;
height: 0.63rem;
position: relative;
z-index: 1;
}
/* 底座长方形 */
&__star-base {
position: relative;
z-index: 2;
margin-top: -0.08rem;
width: 0.52rem;
height: 0.22rem;
border: 1.5px solid #858585;
border-radius: 0.03rem;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
color: $text-dark;
font-size: 0.18rem;
font-weight: 700;
flex-shrink: 0;
background: $bg-main;
> span {
font-size: 0.12rem;
font-weight: 600;
}
}
&__score-badge {
font-size: 0.11rem;
font-weight: 600;
color: $bg-white;
background: $accent;
border-radius: 0.1rem;
padding: 0.03rem 0.1rem;
/* 右侧操作按钮区 */
&__score-actions {
display: flex;
align-items: center;
gap: 0.16rem;
}
/* 暂无等级提示文字 */
&__score-hint {
font-size: 0.13rem;
color: $text-light;
}
&__score-link {
font-size: 0.12rem;
font-size: 0.13rem;
color: $text-light;
background: none;
border: none;
@@ -169,65 +209,69 @@
&__score-item {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.02rem;
gap: 0.06rem;
}
&__score-num {
font-size: 0.22rem;
font-size: 0.30rem;
font-weight: 700;
color: $text-dark;
line-height: 1.2;
}
&__score-label {
font-size: 0.1rem;
font-size: 0.13rem;
color: $text-light;
white-space: nowrap;
}
&__diagnose-btn {
background: $btn-dark;
margin-left: 0.16rem;
color: $bg-white;
border: none;
border-radius: 0.06rem;
padding: 0.08rem 0.18rem;
border-radius: 0.18rem;
width: 0.93rem;
height: 0.36rem;
line-height: 0.37rem;
font-size: 0.13rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
box-sizing: border-box;
//transition: background 0.2s;
white-space: nowrap;
&:hover {
background: $btn-dark-hover;
}
// 开始诊断按钮 — 稍大一些,更醒目
&--start {
padding: 0.1rem 0.28rem;
font-size: 0.14rem;
border-radius: 0.08rem;
background: $gradient-bg;
&:hover {
background: $accent-hover;
}
}
}
// ---- 简历主体 ----
&__body {
display: flex;
flex-direction: column;
gap: 0.16rem;
padding-bottom: 0.4rem;
padding-bottom: 0.3rem;
border-radius: 0.16rem;
background: #fff;
margin: 0 0.24rem;
}
&__card {
background: $bg-white;
border-radius: 0.12rem;
padding: 0.24rem;
border-radius: 0.1rem;
margin: 0.4rem 0.18rem;
}
&__card-hover {
padding: 0.1rem 0.14rem;
&:hover {
background: #F7F7F7;
}
}
// ---- 个人信息卡片 ----
@@ -316,8 +360,9 @@
&__section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.12rem;
margin-bottom: 0.16rem;
padding: 0.1rem 0 0.1rem 0.14rem;
}
&__section-title {
@@ -325,6 +370,24 @@
font-weight: 700;
color: $text-dark;
margin: 0 0 0.16rem 0;
// 底部加粗装饰线(层级低于文字,部分遮挡文字底部)
&--underline {
position: relative;
padding-bottom: 0.02rem;
background: transparent;
&::after {
content: '';
position: absolute;
left: 0;
bottom: 0.02rem;
width: 100%;
height: 0.06rem;
background: #12C7BE;
border-radius: 0.02rem;
z-index: -1;
}
}
}
&__section-header &__section-title {
@@ -343,6 +406,7 @@
align-items: center;
transition: all 0.2s;
flex-shrink: 0;
margin-left: auto;
&:hover {
color: $accent;
@@ -355,6 +419,63 @@
height: 0.15rem;
}
// ---- 内联编辑按钮(品牌色图标+文字,hover卡片时才显示) ----
&__edit-btn-inline {
background: none;
border: none;
color: $accent;
cursor: pointer;
padding: 0.04rem 0.08rem;
border-radius: 0.04rem;
display: flex;
align-items: center;
gap: 0.04rem;
transition: all 0.2s;
flex-shrink: 0;
margin-left: auto;
font-size: 0.13rem;
font-weight: 500;
// 默认隐藏,hover卡片时显示
opacity: 0;
pointer-events: none;
.el-icon {
font-size: 0.15rem;
}
&:hover {
background: $theme-color;
}
}
// 卡片hover时显示编辑按钮(个人信息、个人概述)
&__card-hover:hover &__edit-btn-inline {
opacity: 1;
pointer-events: auto;
}
// 经历模块标题栏hover时显示添加按钮
&__section-header:hover &__edit-btn-inline {
opacity: 1;
pointer-events: auto;
}
// 经历模块标题栏hover时背景色
&__section-header:hover {
background: #F7F7F7;
border-radius: 0.06rem;
}
// 经历条目内的编辑按钮(hover exp-item 时显示)
&__exp-edit-btn {
margin-left: auto;
}
&__exp-item:hover &__exp-edit-btn {
opacity: 1;
pointer-events: auto;
}
// ---- 教育背景 ----
&__edu-item {
margin-top: 0.16rem;
@@ -381,13 +502,13 @@
// ---- 工作经历 ----
&__exp-item {
margin-top: 0.16rem;
margin-left: 0.1rem;
padding-left: 0.1rem;
border-left: 2px solid $border-color;
&:not(:last-child) {
//border-bottom: 1px solid $border-color;
padding: 0.1rem 0.14rem;
border-radius: 0.09rem;
&:hover {
background: #F7F7F7;
}
}
&__exp-title {
@@ -403,6 +524,112 @@
margin-bottom: 0.06rem;
}
// ---- 经历第一行(名称 + 身份 + 时间 + 修复标记) ----
&__exp-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.12rem;
margin-bottom: 0.06rem;
}
&__exp-name {
font-size: 0.14rem;
font-weight: 600;
color: $text-dark;
}
&__exp-role {
font-size: 0.13rem;
font-weight: 400;
color: $text-dark;
}
&__exp-date {
font-size: 0.12rem;
font-weight: 400;
color: $text-middle;
}
// ---- 修复建议标记 ----
&__fix-tag {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.05rem;
font-size: 0.12rem;
font-weight: 500;
color: #FF8D0B;
background: #FFF5E9;
border-radius: 0.04rem;
padding: 0.04rem 0.12rem;
cursor: default;
transition: all 0.2s;
white-space: nowrap;
&:hover {
background: #FFEDDA;
}
// hover 时显示气泡
&:hover .resume-detail__fix-tooltip {
opacity: 1;
visibility: visible;
}
}
&__fix-tag-icon {
width: 0.14rem;
height: 0.14rem;
flex-shrink: 0;
}
// ---- 诊断详情 hover 气泡 ----
&__fix-tooltip {
position: absolute;
top: 50%;
left: calc(100% + 0.12rem);
transform: translateY(-50%);
min-width: 2.4rem;
max-width: 3.5rem;
padding: 0.14rem 0.18rem;
background: linear-gradient(to bottom,#FFF5E9,#FFFFFF);
color: $text-dark;
font-size: 0.13rem;
font-weight: 400;
line-height: 1.7;
white-space: normal;
word-break: break-all;
border-radius: 0.1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
border: 1px solid #FF8D0B;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
z-index: 100;
pointer-events: none;
// 左侧尖角 — 旋转方块方案,边框粗细与气泡一致
&::before {
content: '';
position: absolute;
top: 50%;
left: -0.05rem;
margin-top: -0.05rem;
width: 0.09rem;
height: 0.09rem;
background: #FFF8F2;
border-left: 1px solid #FF8D0B;
border-bottom: 1px solid #FF8D0B;
transform: rotate(45deg);
}
// 不再需要内层伪元素
&::after {
display: none;
}
}
&__exp-desc {
font-size: 0.12rem;
color: $text-dark;
@@ -419,9 +646,9 @@
&__skill-tag {
font-size: 0.12rem;
color: $text-dark;
background: $bg-main;
border: 1px solid $border-color;
color: $accent;
background: $bg-white;
border: 1px solid $accent;
border-radius: 0.04rem;
padding: 0.05rem 0.14rem;
transition: all 0.2s;
@@ -429,7 +656,7 @@
&:hover {
border-color: $accent;
color: $accent;
background: $theme-color;
background: $bg-white;
}
}
+64 -37
View File
@@ -74,7 +74,7 @@
white-space: nowrap;
&--action {
width: 0.5rem;
width: 2rem;
}
}
@@ -107,7 +107,7 @@
&__name-cell {
display: flex;
align-items: center;
gap: 0.1rem;
gap: 0.06rem;
}
&__avatar {
@@ -128,16 +128,71 @@
}
&__default-tag {
font-size: 0.1rem;
color: $text-light;
background: $bg-main;
border: 1px solid $border-color;
border-radius: 0.04rem;
padding: 0.02rem 0.06rem;
font-size: 0.09rem;
color: $accent;
background: #D9F5F0;
border-radius: 0.14rem;
padding: 0.01rem 0.06rem;
padding-bottom: 0.02rem;
white-space: nowrap;
}
// 更多按钮
// 编辑名称岗位图标按钮
&__edit-btn {
background: none;
border: none;
color: $text-dark;
cursor: pointer;
padding: 0.02rem;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s;
&:hover {
opacity: 0.6;
}
}
&__edit-icon {
font-size: 0.13rem;
}
// 操作按钮容器
&__actions {
display: flex;
align-items: center;
gap: 0.16rem;
justify-content: flex-end;
}
// 行内操作按钮
&__action-btn {
background: none;
border: none;
font-size: 0.12rem;
color: $text-dark;
cursor: pointer;
padding: 0.04rem 0.08rem;
border-radius: 0.04rem;
white-space: nowrap;
transition: all 0.15s;
&:hover {
color: $accent;
}
// 删除按钮红色
&--delete {
color: #FD6464;
&:hover {
color: #e04545;
}
}
}
// 更多按钮(已移除,保留兼容)
&__more-btn {
background: none;
border: none;
@@ -168,34 +223,6 @@
color: $text-light;
font-size: 0.14rem;
}
// 弹出菜单
&__popup {
position: absolute;
right: 0.46rem;
top: 0.18rem;
background: $bg-white;
border: 1px solid $border-color;
border-radius: 0.1rem;
padding: 0.06rem 0;
box-shadow: 0 0.06rem 0.2rem rgba(0, 0, 0, 0.1);
z-index: 10;
min-width: 1.2rem;
}
&__popup-item {
padding: 0.08rem 0.2rem;
font-size: 0.12rem;
color: $text-dark;
cursor: pointer;
white-space: nowrap;
transition: all 0.15s;
&:hover {
background: $theme-color;
color: $accent-hover;
}
}
}
// ==================== 上传简历加载提示 ====================
+7 -5
View File
@@ -28,10 +28,11 @@ $text-middle: #777777;
// 强调色 / 品牌色
//$accent: #4FC2C9;
$accent: #52CAD1;
//$accent: #52CAD1;
$accent: #12C7BE;
// 强调色悬停态
$accent-hover: #42A8B3;
$accent-hover: #11b5ac;
// 危险色 / 警告色(删除、错误等)
$danger: #E85635;
@@ -43,10 +44,10 @@ $border-color: #E8E8E8;
$overlay-bg: rgba(0, 0, 0, 0.5);
// 主按钮颜色背景(确认提交等)
$btn-dark: #52CAD1;
$btn-dark: #12C7BE;
// 主按钮颜色悬停态
$btn-dark-hover: #42A8B3;
$btn-dark-hover: #11b5ac;
// 次要深色按钮背景(除非特意指定使用否则不用这个按钮色)
//$btn-dark: #1A1A2E;
@@ -55,4 +56,5 @@ $btn-dark-hover: #42A8B3;
//$btn-dark-hover: #2E3142;
// 渐变色背景
$gradient-bg: linear-gradient(to right, #52CAD1, #42A8B3);
//$gradient-bg: linear-gradient(to right, #52CAD1, #42A8B3);
$gradient-bg: linear-gradient(to right, #12C7BE, #11b5ac);
+85 -41
View File
@@ -1,46 +1,23 @@
<template>
<!-- Agent会话岗位列表组件 聊天区域中显示推荐岗位卡片 -->
<!-- Agent推荐岗位列表组件 示推荐岗位卡片 -->
<div class="agent-chat-job-list">
<!-- 标题 -->
<div class="agent-chat-job-list__header">为你推荐的岗位</div>
<!-- 推荐说明文字 -->
<div class="agent-chat-job-list__summary">{{ summary }}</div>
<div class="agent-chat-job-list__summary">{{ summary || '根据求职目标和简历信息智能推荐' }}</div>
<!-- 岗位列表显示前3个 -->
<!-- 岗位列表显示前3个 -->
<div
v-for="job in displayJobs"
:key="job.id"
class="agent-chat-job-list__item"
v-if="displayJobs.length>0"
@click="handleClickJob(job)"
>
<!-- 左侧公司图标 + 岗位信息 -->
<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__info" @click="handleClickJob(job)">
<!-- 匹配度环形 -->
<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"
@@ -52,14 +29,54 @@
</svg>
<span class="agent-chat-job-list__score-text">{{ job.matchScore || 0 }}%</span>
</div>
<!-- 岗位详情 -->
<div class="agent-chat-job-list__detail">
<div class="agent-chat-job-list__title-row">
<span class="agent-chat-job-list__title">{{ job.title }}</span>
<span v-if="job.companyShortName || job.companyName" class="agent-chat-job-list__company-dot"></span>
<span class="agent-chat-job-list__company">{{ job.companyShortName || job.companyName }}</span>
</div>
<div class="color-9" v-if="displayJobs.length==0">
<!-- 标签 -->
<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__actions">
<!-- 不再推荐 -->
<span class="agent-chat-job-list__dismiss-btn" @click.stop="handleDismiss(job)">不再推荐</span>
<!-- 添加/已加入按钮 -->
<button
v-if="job.applicationStatus === -1"
class="agent-chat-job-list__add-btn agent-chat-job-list__add-btn--added"
disabled
>已加入</button>
<button
v-else
class="agent-chat-job-list__add-btn"
:disabled="loadingJobIds.includes(job.id)"
@click.stop="handleAdd(job)"
>+ 加入投递</button>
</div>
</div>
<!-- 空状态 -->
<div class="color-9" v-if="displayJobs.length === 0">
暂无相关职位推荐
</div>
<!-- 查看全部岗位按钮 -->
<!-- 底部按钮区域 -->
<div v-if="displayJobs.length > 0" class="agent-chat-job-list__footer">
<button class="agent-chat-job-list__view-all-btn" @click="handleViewAll">查看全部岗位</button>
<!-- 换一批按钮 -->
<button class="agent-chat-job-list__refresh-btn" :disabled="refreshing" @click="handleRefresh">
{{ refreshing ? '加载中...' : '换一批' }}
</button>
<!-- 全部添加按钮 -->
<button class="agent-chat-job-list__add-all-btn" @click="handleAddAll">全部添加</button>
</div>
</div>
</template>
@@ -72,30 +89,57 @@ import type { AgentRecommendJob } from '@/api/agent'
const props = defineProps<{
/** 推荐说明文字 */
summary: string
/** 完整岗位列表数据 */
/** 当前展示的岗位列表数据(父组件已随机抽取10个) */
jobs: AgentRecommendJob[]
/** 正在请求中的岗位 ID 列表 */
loadingJobIds?: number[]
/** 是否正在换一批加载中 */
refreshing?: boolean
}>()
/** 事件 */
const emit = defineEmits<{
/** 点击查看全部岗位 */
/** 点击查看全部岗位(打开右侧面板) */
(e: 'viewAll'): void
/** 点击岗位查看详情 */
(e: 'clickJob', job: AgentRecommendJob): void
/** 添加单个岗位到待投递 */
(e: 'add', job: AgentRecommendJob): void
/** 全部添加 */
(e: 'addAll'): void
/** 换一批 */
(e: 'refresh'): void
/** 不再推荐 */
(e: 'dismiss', job: AgentRecommendJob): void
}>()
/** 只显示前3个岗位 */
const displayJobs = computed(() => props.jobs.slice(0, 3))
/** 点击查看全部岗位 */
function handleViewAll() {
emit('viewAll')
}
/** 点击岗位 — 通知父组件打开岗位预览 */
function handleClickJob(job: AgentRecommendJob) {
emit('clickJob', job)
}
/** 添加岗位到待投递 */
function handleAdd(job: AgentRecommendJob) {
emit('add', job)
}
/** 全部添加 */
function handleAddAll() {
emit('addAll')
}
/** 换一批 */
function handleRefresh() {
emit('refresh')
}
/** 不再推荐 */
function handleDismiss(job: AgentRecommendJob) {
emit('dismiss', job)
}
</script>
<style scoped lang="scss">
+193 -544
View File
@@ -1,207 +1,81 @@
<template>
<!-- Agent设置面板 右侧面板 -->
<!-- Agent设置面板 右侧常驻面板精简版 -->
<div class="agent-setting-panel">
<!-- 顶部标题栏 -->
<div class="agent-setting-panel__header">
<span class="agent-setting-panel__title">设置</span>
<!-- 关闭按钮 -->
<button class="agent-setting-panel__close-btn" @click="emit('close')">
<svg viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" fill="#1A1A2E" />
<path d="M8 8l8 8M16 8l-8 8" stroke="#fff" stroke-width="1.5" stroke-linecap="round" />
</svg>
</button>
</div>
<!-- 内容区域可滚动 -->
<div class="agent-setting-panel__body">
<!-- ========== 个人资料设置项 ========== -->
<!-- ========== 个人资料 ========== -->
<div class="agent-setting-panel__section">
<!-- 个人资料标题行标题 + 编辑按钮含展开/收起箭头 -->
<div class="agent-setting-panel__section-header" @click="toggleProfileExpand">
<div class="agent-setting-panel__section-header">
<span class="agent-setting-panel__section-title">个人资料</span>
<div class="agent-setting-panel__section-action">
<span class="agent-setting-panel__section-action-text">{{ profileExpanded ? '收起' : '编辑' }}</span>
<!-- 方向箭头展开时朝上收起时朝下 -->
<svg
viewBox="0 0 16 16"
fill="none"
class="agent-setting-panel__arrow"
:class="{ 'agent-setting-panel__arrow--up': profileExpanded }"
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="agent-setting-panel__edit-btn" @click="handleEditProfile">编辑 </span>
</div>
<!-- 简要信息展示 -->
<div class="agent-setting-panel__profile-info">
<div class="agent-setting-panel__profile-name">{{ profile.name || '未填写' }}</div>
<div class="agent-setting-panel__profile-sub">{{ profileSubText }}</div>
</div>
</div>
<!-- 收起时的简要信息预览 -->
<div v-if="!profileExpanded" class="agent-setting-panel__section-summary">
<span v-if="profile.name">{{ profile.name }}</span>
<span v-if="profile.phone"> · {{ profile.phone }}</span>
<span v-if="profile.email"> · {{ profile.email }}</span>
<span v-if="!profile.name && !profile.phone && !profile.email" class="agent-setting-panel__section-empty">暂未填写个人资料</span>
</div>
<!-- 展开时显示 ProfilePageContent 组件 -->
<div v-if="profileExpanded" class="agent-setting-panel__section-content">
<ProfilePageContent :profile="profile" @edit="handleEdit" />
</div>
</div>
<!-- ========== 其他设置项待设计稿补充 ========== -->
<!-- ========== 求职目标 ========== -->
<div class="agent-setting-panel__section">
<!-- 求职目标标题行 + 编辑按钮 -->
<div class="agent-setting-panel__section-header" @click="showJobGoalDialog = true">
<div class="agent-setting-panel__section-header">
<span class="agent-setting-panel__section-title">求职目标</span>
<div class="agent-setting-panel__section-action">
<span class="agent-setting-panel__section-action-text">编辑</span>
<svg viewBox="0 0 16 16" fill="none" class="agent-setting-panel__edit-icon">
<path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
<span class="agent-setting-panel__edit-btn" @click="showJobGoalDialog = true">编辑 </span>
</div>
</div>
<!-- 求职意向标签展示 -->
<!-- 求职意向标签 -->
<div class="agent-setting-panel__goal-tags">
<span v-for="name in intentionCategoryNames" :key="'cat-' + name" class="agent-setting-panel__goal-tag">{{ name }}</span>
<span v-for="name in intentionIndustryNames" :key="'ind-' + name" class="agent-setting-panel__goal-tag">{{ name }}</span>
<span v-for="name in intentionRegionNames" :key="'reg-' + name" class="agent-setting-panel__goal-tag">{{ name }}</span>
<span v-if="intentionEmploymentLabel" class="agent-setting-panel__goal-tag">{{ intentionEmploymentLabel }}</span>
<!-- 无意向时的空状态 -->
<span v-for="name in intentionTags" :key="name" class="agent-setting-panel__goal-tag">{{ name }}</span>
<span v-if="intentionTags.length === 0" class="agent-setting-panel__empty-text">暂未设置</span>
</div>
</div>
<!-- ========== 投递简历 ========== -->
<div class="agent-setting-panel__section">
<div class="agent-setting-panel__section-header">
<span class="agent-setting-panel__section-title">浏览器插件</span>
</div>
<!-- 浏览器按钮列表 -->
<div class="agent-setting-panel__browser-btns">
<button
v-for="b in browserList"
:key="b.key"
class="agent-setting-panel__browser-btn"
@click="openBrowserGuide(b.key)"
>{{ b.label }}</button>
</div>
<!-- 下载插件按钮 -->
<div class="agent-setting-panel__download-wrap">
<button class="agent-setting-panel__download-btn" @click="downloadExtension">下载插件</button>
</div>
</div>
<div class="agent-setting-panel__section">
<div class="agent-setting-panel__section-header">
<span class="agent-setting-panel__section-title">求职助手配置</span>
</div>
<!-- 网申常见问题 -->
<div class="agent-setting-panel__config-group">
<div class="agent-setting-panel__config-label">是否愿意接受部门调剂</div>
<div class="agent-setting-panel__config-options">
<button class="agent-setting-panel__config-btn" :class="{ 'agent-setting-panel__config-btn--active': configForm.acceptDeptTransfer === '是,服从调剂' }" @click="configForm.acceptDeptTransfer = '是,服从调剂'">服从调剂</button>
<button class="agent-setting-panel__config-btn" :class="{ 'agent-setting-panel__config-btn--active': configForm.acceptDeptTransfer === '否,不调剂' }" @click="configForm.acceptDeptTransfer = '否,不调剂'">不调剂</button>
</div>
</div>
<div class="agent-setting-panel__config-group">
<div class="agent-setting-panel__config-label">是否接受地点调剂</div>
<div class="agent-setting-panel__config-options">
<button class="agent-setting-panel__config-btn" :class="{ 'agent-setting-panel__config-btn--active': configForm.acceptLocationTransfer === '是' }" @click="configForm.acceptLocationTransfer = '是'"></button>
<button class="agent-setting-panel__config-btn" :class="{ 'agent-setting-panel__config-btn--active': configForm.acceptLocationTransfer === '否' }" @click="configForm.acceptLocationTransfer = '否'"></button>
</div>
</div>
<div class="agent-setting-panel__config-group">
<div class="agent-setting-panel__config-label">可以参加面试的方式</div>
<div class="agent-setting-panel__config-options">
<button class="agent-setting-panel__config-btn" :class="{ 'agent-setting-panel__config-btn--active': configForm.interviewType.includes('线下面试') }" @click="toggleInterviewType('线下面试')">线下面试</button>
<button class="agent-setting-panel__config-btn" :class="{ 'agent-setting-panel__config-btn--active': configForm.interviewType.includes('线上远程') }" @click="toggleInterviewType('线上远程')">线上远程</button>
</div>
</div>
<div class="agent-setting-panel__config-group">
<div class="agent-setting-panel__config-label">你的语言能力</div>
<div class="agent-setting-panel__config-selects">
<el-select v-model="configForm.languages[0].language" placeholder="语种" class="agent-setting-panel__config-select">
<el-option v-for="lang in languageOptions" :key="lang" :label="lang" :value="lang" />
</el-select>
<el-select v-model="configForm.languages[0].proficiency" placeholder="掌握程度" class="agent-setting-panel__config-select">
<el-option v-for="p in proficiencyOptions" :key="p" :label="p" :value="p" />
</el-select>
</div>
</div>
<div class="agent-setting-panel__config-group">
<div class="agent-setting-panel__config-label">预计到岗时间</div>
<div class="agent-setting-panel__config-selects">
<el-select v-model="configForm.availableDate" placeholder="请选择" class="agent-setting-panel__config-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-setting-panel__config-group">
<div class="agent-setting-panel__config-label">每周可实习天数</div>
<div class="agent-setting-panel__config-selects">
<el-select v-model="configForm.internDaysPerWeek" placeholder="请选择" class="agent-setting-panel__config-select">
<el-option v-for="d in internDaysOptions" :key="d" :label="d" :value="d" />
</el-select>
</div>
</div>
<div class="agent-setting-panel__config-group">
<div class="agent-setting-panel__config-label">预计实习时长</div>
<div class="agent-setting-panel__config-selects">
<el-select v-model="configForm.internDuration" placeholder="请选择" class="agent-setting-panel__config-select">
<el-option v-for="d in internDurationOptions" :key="d" :label="d" :value="d" />
</el-select>
</div>
</div>
</template>
<!-- 简历设置 -->
<div class="agent-setting-panel__config-group">
<div class="agent-setting-panel__config-label">
简历设置
<el-tooltip content="选择投递时使用的默认简历" placement="top">
<span class="agent-setting-panel__tip"></span>
</el-tooltip>
</div>
<div class="agent-setting-panel__config-sub">设置默认简历</div>
<div class="agent-setting-panel__resume-row">
<svg viewBox="0 0 16 16" fill="none" class="agent-setting-panel__resume-icon">
<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>
<el-select v-model="selectedResumeId" placeholder="请选择简历" class="agent-setting-panel__config-select agent-setting-panel__config-select--full">
<div class="agent-setting-panel__section-title">投递简历</div>
<el-select v-model="selectedResumeId" placeholder="请选择简历" class="agent-setting-panel__resume-select" @change="handleResumeChange">
<el-option v-for="r in resumeList" :key="r.id" :label="r.resumeName" :value="r.id || ''" />
</el-select>
</div>
</div>
<!-- 自动优化简历开关 -->
<div class="agent-setting-panel__config-group">
<div class="agent-setting-panel__switch-row">
<div class="agent-setting-panel__switch-text">
<span>在投递时帮我针对岗位自动优化简历</span>
<el-tooltip content="MVP只补充缺少技能" placement="top">
<span class="agent-setting-panel__tip"></span>
</el-tooltip>
<br/>
<span class="agent-setting-panel__switch-sub">MVP只补充缺少技能</span>
<!-- ========== 投递偏好 ========== -->
<div class="agent-setting-panel__section">
<div class="agent-setting-panel__section-title">投递偏好</div>
<!-- 简历针对性优化 -->
<div class="agent-setting-panel__pref-item">
<div class="agent-setting-panel__pref-left">
<span class="agent-setting-panel__pref-check" :class="{ 'agent-setting-panel__pref-check--active': autoOptimizeSwitch }">
<svg v-if="autoOptimizeSwitch" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/><path d="M5 8l2 2 4-4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<svg v-else viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/></svg>
</span>
<div class="agent-setting-panel__pref-text">
<span class="agent-setting-panel__pref-label">开启简历针对性优化</span>
<span class="agent-setting-panel__pref-desc">结合岗位要求自动优化简历表达</span>
</div>
<el-switch v-model="autoOptimizeSwitch" active-color="#4FC2C9" />
</div>
<el-switch v-model="autoOptimizeSwitch" active-color="#4FC2C9" @change="handleSavePreference" />
</div>
<!-- 深度人岗匹配分析 -->
<div class="agent-setting-panel__pref-item">
<div class="agent-setting-panel__pref-left">
<span class="agent-setting-panel__pref-check" :class="{ 'agent-setting-panel__pref-check--active': autoMatchAnalysisSwitch }">
<svg v-if="autoMatchAnalysisSwitch" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/><path d="M5 8l2 2 4-4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<svg v-else viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/></svg>
</span>
<div class="agent-setting-panel__pref-text">
<span class="agent-setting-panel__pref-label">开启深度人岗匹配分析</span>
<span class="agent-setting-panel__pref-desc">根据岗位要求分析你的匹配优势与差距</span>
</div>
</div>
<el-switch v-model="autoMatchAnalysisSwitch" active-color="#4FC2C9" @change="handleSavePreference" />
</div>
</div>
<!-- 提交设置按钮 -->
<div class="agent-setting-panel__submit-wrap">
<button class="agent-setting-panel__submit-btn" :disabled="configSaving" @click="handleSubmitConfig">
{{ configSaving ? '保存中...' : '保存设置' }}
</button>
<!-- ========== 浏览器插件 ========== -->
<div class="agent-setting-panel__section">
<div class="agent-setting-panel__section-title">浏览器插件</div>
<div class="agent-setting-panel__browser-btns">
<button class="agent-setting-panel__browser-btn" @click="openBrowserGuide('chrome')">Chrome浏览器</button>
<button class="agent-setting-panel__browser-btn" @click="openBrowserGuide('edge')">Edge浏览器</button>
</div>
</div>
</div>
@@ -217,17 +91,6 @@
<!-- 求职目标设置弹窗 -->
<JobGoalDialog v-model="showJobGoalDialog" />
<!-- 浏览器安装指引弹窗 -->
<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-setting-panel__guide-slide">
<img :src="img" :alt="currentBrowserLabel + ' 安装步骤 ' + (i + 1)" />
</div>
</el-carousel-item>
</el-carousel>
</el-dialog>
</div>
</template>
@@ -235,240 +98,43 @@
import { ref, computed, onMounted } from 'vue'
import { useStore } from 'vuex'
import { formatEmploymentType } from '@/stores/index'
import ProfilePageContent from '@/components/ProfilePageContent.vue'
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
import JobGoalDialog from '@/components/JobGoalDialog.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 { saveProfile, fetchProfile, fetchEducation } from '@/api/profile'
import { resolveRegionName } from '@/utils/region'
import { resolveIndustryName } from '@/utils/industry'
import { resolveJobCategoryName } from '@/utils/jobCategory'
import { fetchResumeList } from '@/api/resume'
import { fetchResumeList, setDefaultResume } from '@/api/resume'
import type { ResumeListItem } from '@/api/resume'
import { fetchAgentConfig, saveAgentConfig } from '@/api/agent'
/** 事件 */
const emit = defineEmits<{
/** 关闭面板 */
(e: 'close'): void
}>()
const store = useStore()
// ==================== 个人资料展开/收起状态 ====================
// ==================== 个人资料 ====================
/** 个人资料区域是否展开 */
const profileExpanded = ref(false)
/** 切换个人资料展开/收起 */
function toggleProfileExpand() {
profileExpanded.value = !profileExpanded.value
}
// ==================== 求职目标 ====================
/** 求职目标设置弹窗显隐 */
const showJobGoalDialog = ref(false)
/** 求职意向 — 岗位类型名称列表 */
const intentionCategoryNames = computed(() => (store.state.jobIntention?.categoryIds || []).map((id: number) => resolveJobCategoryName(id)).filter(Boolean))
/** 求职意向 — 行业名称列表 */
const intentionIndustryNames = computed(() => (store.state.jobIntention?.industryIds || []).map((id: number) => resolveIndustryName(id)).filter(Boolean))
/** 求职意向 — 地区名称列表 */
const intentionRegionNames = computed(() => (store.state.jobIntention?.regionCodes || []).map((code: string) => resolveRegionName(code)).filter(Boolean))
/** 求职意向 — 招聘分类文案 */
const intentionEmploymentLabel = computed(() => formatEmploymentType(store.state.jobIntention?.recruitCategory))
// ==================== 浏览器插件 ====================
/** 插件下载地址 */
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 }
/** 指引图片 OSS 基础路径 */
const guidanceImageBase = 'http://offerpie.oss-cn-guangzhou.aliyuncs.com/extension/guidance_image'
/** 浏览器安装指引弹窗显隐 */
const showBrowserGuide = ref(false)
/** 当前选中的浏览器 key */
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')
}
// ==================== 求职助手配置 ====================
/** 是否为实习类型 */
const isInternship = computed(() => store.state.jobIntention?.recruitCategory === 2)
/** 配置表单数据 */
const configForm = ref({
acceptDeptTransfer: '',
acceptLocationTransfer: '',
interviewType: [] as string[],
languages: [{ language: '', proficiency: '' }] as Array<{ language: string; proficiency: string }>,
availableDate: '',
internDaysPerWeek: '',
internDuration: '',
})
/** 语种选项 */
const languageOptions = ['英语', '日语', '法语', '德语', '韩语', '西班牙语', '俄语']
/** 掌握程度选项 */
const proficiencyOptions = ['入门', '日常会话', '商务会话', '无障碍沟通', '母语']
/** 到岗时间选项 */
const availableDateOptions = ['一周以内', '两周以内', '一个月以内', '一个月以上']
/** 实习天数选项 */
const internDaysOptions = ['3天及以上', '4天及以上', '5天及以上']
/** 实习时长选项 */
const internDurationOptions = ['3个月', '4个月', '5个月', '6个月及以上']
/** 切换面试方式(多选) */
function toggleInterviewType(type: string) {
const idx = configForm.value.interviewType.indexOf(type)
idx >= 0 ? configForm.value.interviewType.splice(idx, 1) : configForm.value.interviewType.push(type)
}
/** 简历列表 */
const resumeList = ref<ResumeListItem[]>([])
/** 当前选中的简历 ID */
const selectedResumeId = ref('')
/** 自动优化简历开关 */
const autoOptimizeSwitch = ref(true)
/** 保存中状态 */
const configSaving = ref(false)
/** 加载简历列表 */
async function loadResumeList() {
try {
const res = await fetchResumeList()
if (res.code === '0' && res.data) {
resumeList.value = res.data
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('[AgentSettingPanel] 加载简历列表失败') }
}
/** 加载已有的求职助手配置 — 填充表单 */
async function loadAgentConfig() {
try {
const res = await fetchAgentConfig()
if (res.code === '0' && res.data) {
const cfg = res.data
configForm.value.acceptDeptTransfer = cfg.acceptDeptTransfer || ''
configForm.value.acceptLocationTransfer = cfg.acceptLocationTransfer || ''
configForm.value.interviewType = cfg.interviewType ? [...cfg.interviewType] : []
configForm.value.languages = cfg.languages?.length
? cfg.languages.map(l => ({ language: l.language || '', proficiency: l.proficiency || '' }))
: [{ language: '', proficiency: '' }]
configForm.value.availableDate = cfg.availableDate || ''
configForm.value.internDaysPerWeek = cfg.internDaysPerWeek || ''
configForm.value.internDuration = cfg.internDuration || ''
if (cfg.autoOptimizeResume !== undefined) autoOptimizeSwitch.value = cfg.autoOptimizeResume === 1
}
} catch { console.error('[AgentSettingPanel] 加载求职助手配置失败') }
}
/** 提交设置 — 调用 saveAgentConfig 接口 */
async function handleSubmitConfig() {
configSaving.value = true
try {
await saveAgentConfig({
acceptDeptTransfer: configForm.value.acceptDeptTransfer,
acceptLocationTransfer: configForm.value.acceptLocationTransfer,
interviewType: configForm.value.interviewType,
languages: configForm.value.languages.filter(l => l.language),
availableDate: configForm.value.availableDate,
internDaysPerWeek: configForm.value.internDaysPerWeek,
internDuration: configForm.value.internDuration,
autoOptimizeResume: autoOptimizeSwitch.value ? 1 : 0,
})
ElMessage.success('设置保存成功')
} catch {
ElMessage.error('设置保存失败,请重试')
} finally {
configSaving.value = false
}
}
// ==================== 个人资料数据 ====================
/** 个人档案响应式数据 */
/** 个人资料数据 */
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 }> }>,
name: '',
phone: '',
email: '',
regionCode: '',
wechat: '',
skills: [] as string[],
certificates: [] as string[],
portfolioUrl: '',
education: [] as Array<{ school: string; major: string; degree: number }>,
})
// ==================== 加载个人资料 ====================
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()
await loadResumeList()
await loadAgentConfig()
/** 个人资料副标题(学校·学历) */
const profileSubText = computed(() => {
if (profile.value.education.length === 0) return ''
const edu = profile.value.education[0]
const degreeMap: Record<number, string> = { 1: '大专', 2: '本科', 3: '硕士', 4: '博士' }
const parts = [edu.school, degreeMap[edu.degree]].filter(Boolean)
return parts.join('·')
})
/** 加载基本信息 */
/** 加载个人资料 */
async function loadProfile() {
try {
const res = await fetchProfile()
@@ -477,181 +143,164 @@ async function loadProfile() {
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('[AgentSettingPanel] 加载个人资料失败') }
} catch { /* 静默 */ }
}
/** 加载教育经历 */
/** 加载教育经历(只取第一条显示) */
async function loadEducation() {
try {
const res = await fetchEducation()
if (res.code === '0' && res.data) {
if (res.code === '0' && res.data && res.data.length > 0) {
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 || '' }))
school: item.school || '',
major: item.major || '',
degree: item.degree ?? 2,
}))
}
} catch { console.error('[AgentSettingPanel] 加载教育经历失败') }
}
/** 加载工作经历 */
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('[AgentSettingPanel] 加载工作经历失败') }
}
/** 加载实习经历 */
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('[AgentSettingPanel] 加载实习经历失败') }
}
/** 加载项目经历 */
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('[AgentSettingPanel] 加载项目经历失败') }
}
/** 加载竞赛经历 */
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('[AgentSettingPanel] 加载竞赛经历失败') }
} catch { /* 静默 */ }
}
// ==================== 编辑抽屉 ====================
/** 编辑抽屉显隐 */
const showEditDrawer = ref(false)
/** 当前编辑的模块名 */
const editModule = ref('info')
/** 编辑抽屉初始数据 */
const editInitialData = ref<Record<string, any>>({})
/** 保存中状态 */
const saving = ref(false)
/** ProfilePageContent 点击编辑 — 打开对应模块的编辑抽屉 */
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 = {}
/** 打开编辑个人资料 */
function handleEditProfile() {
editModule.value = 'info'
editInitialData.value = {
name: profile.value.name,
email: profile.value.email,
phone: profile.value.phone,
location: profile.value.regionCode,
wechat: profile.value.wechat,
}
showEditDrawer.value = true
}
/** 保存编辑数据 — 调用接口持久化 */
/** 保存编辑 */
async function handleSaveEdit(data: Record<string, any>) {
saving.value = true
try {
if (editModule.value === 'info') {
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
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('个人信息保存成功')
} else if (editModule.value === 'education') {
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('教育经历保存成功')
} else if (editModule.value === 'work') {
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('工作经历保存成功')
} else if (editModule.value === 'internship') {
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('实习经历保存成功')
} else if (editModule.value === 'project') {
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('项目经历保存成功')
} else if (editModule.value === 'competition') {
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('竞赛经历保存成功')
} else if (editModule.value === 'portfolio') {
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('作品集保存成功')
} else if (editModule.value === 'skills') {
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('技能保存成功')
} else if (editModule.value === 'certificate') {
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
}
}
// ==================== 求职目标 ====================
const showJobGoalDialog = ref(false)
/** 求职意向标签(汇总岗位+行业+地区+招聘类型) */
const intentionTags = computed(() => {
const tags: string[] = []
const catIds = store.state.jobIntention?.categoryIds || []
const indIds = store.state.jobIntention?.industryIds || []
const regCodes = store.state.jobIntention?.regionCodes || []
catIds.forEach((id: number) => { const n = resolveJobCategoryName(id); if (n) tags.push(n) })
indIds.forEach((id: number) => { const n = resolveIndustryName(id); if (n) tags.push(n) })
regCodes.forEach((code: string) => { const n = resolveRegionName(code); if (n) tags.push(n) })
const empLabel = formatEmploymentType(store.state.jobIntention?.recruitCategory)
if (empLabel) tags.push(empLabel)
return tags
})
// ==================== 投递简历 ====================
const resumeList = ref<ResumeListItem[]>([])
const selectedResumeId = ref('')
/** 加载简历列表 */
async function loadResumeList() {
try {
const res = await fetchResumeList()
if (res.code === '0' && res.data) {
resumeList.value = res.data
const defaultResume = res.data.find(r => r.isDefault === 1)
selectedResumeId.value = defaultResume?.id || (res.data[0]?.id || '')
}
} catch { /* 静默 */ }
}
/** 切换简历时设为默认 */
async function handleResumeChange(newId: string) {
if (!newId) return
try {
await setDefaultResume(newId)
} catch { /* 静默 */ }
}
// ==================== 投递偏好 ====================
const autoOptimizeSwitch = ref(true)
const autoMatchAnalysisSwitch = ref(true)
/** 加载配置 */
async function loadAgentConfigData() {
try {
const res = await fetchAgentConfig()
if (res.code === '0' && res.data) {
if (res.data.autoOptimizeResume !== undefined) autoOptimizeSwitch.value = res.data.autoOptimizeResume === 1
if (res.data.autoMatchAnalysis !== undefined) autoMatchAnalysisSwitch.value = res.data.autoMatchAnalysis === 1
}
} catch { /* 静默 */ }
}
/** 保存偏好设置 */
async function handleSavePreference() {
try {
await saveAgentConfig({
autoOptimizeResume: autoOptimizeSwitch.value ? 1 : 0,
autoMatchAnalysis: autoMatchAnalysisSwitch.value ? 1 : 0,
})
} catch { /* 静默 */ }
}
// ==================== 浏览器插件 ====================
/** 打开浏览器安装指引(简单跳转) */
function openBrowserGuide(browser: string) {
if (browser === 'chrome') {
ElMessage.info('请复制地址 chrome://extensions 到浏览器打开')
} else {
ElMessage.info('请复制地址 edge://extensions/ 到浏览器打开')
}
}
// ==================== 初始化 ====================
onMounted(async () => {
if (!store.state.regions.length) store.dispatch('loadCommonData')
store.dispatch('loadJobIntention')
await loadProfile()
await loadEducation()
await loadResumeList()
await loadAgentConfigData()
})
</script>
<style scoped lang="scss">
+296 -239
View File
@@ -31,183 +31,226 @@
<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>
<div class="agent-page__left">
<div class="agent-page__intro-card">
<div class="agent-page__intro-header por">
<div v-if="false" class="agent-page__intro-icon poa l-22 t0">
<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 mb50 ">请仔细对你的资料评估包括个人信息教育背景工作履历及技能确保资料齐全让我能顺利为你开启求职之旅</p>
<div class="">
<div class="tac"><button class="agent-page__confirm-btn dib" @click="handleNext">确认并进入</button></div>
<div v-if="false" class="tac color-9 mt30 cursor-po" @click="handleBack">返回上一步</div>
</div>
</div>
</div>
</template>
<!-- ========== 第2步确认目标 ========== -->
<template v-if="currentStep === 2">
<div class="agent-page__step2">
<!-- 返回上一步按钮 -->
<button class="agent-page__back-btn" @click="handleBack">
<svg viewBox="0 0 24 24" fill="none" width="16" height="16"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>返回上一步</span>
</button>
<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 class="agent-page__right">
<div class="agent-page__profile-wrapper">
<!-- 求职偏好区域 -->
<div class="agent-page__pref-section">
<h3 class="agent-page__pref-section-title">求职偏好</h3>
<p class="agent-page__pref-section-desc">这些是你之前设定的求职偏好如有需要请随时修改</p>
<div class="agent-page__pref-filters">
<!-- 城市选择 -->
<RegionSelector
:regionCodes="step2RegionCodes"
:level="2"
:maxSelect="3"
:triggerStyle="prefFilterTriggerStyle"
:displayStyle="prefFilterDisplayStyle"
@update:regionCodes="onStep2RegionChange"
/>
<!-- 岗位选择 -->
<JobCategorySelector
:categoryIds="step2CategoryIds"
:maxSelect="3"
:level="3"
:allowParentSelect="true"
:triggerStyle="prefFilterTriggerStyle"
:displayStyle="prefFilterDisplayStyle"
@update:categoryIds="onStep2CategoryChange"
/>
<!-- 行业选择 -->
<IndustrySelector
:industryIds="step2IndustryIds"
:maxSelect="3"
:level="2"
:allowParentSelect="true"
:triggerStyle="prefFilterTriggerStyle"
:displayStyle="prefFilterDisplayStyle"
@update:industryIds="onStep2IndustryChange"
/>
<!-- 招聘分类下拉选择 -->
<div class="agent-page__pref-type-dropdown" @click.stop="showJobTypeDropdown = !showJobTypeDropdown">
<span>{{ intentionEmploymentLabel || '类型' }}</span>
<svg class="agent-page__pref-type-arrow" viewBox="0 0 12 12" fill="none">
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<!-- 下拉菜单 -->
<div v-if="showJobTypeDropdown" class="agent-page__pref-type-menu" @click.stop>
<div
v-for="option in jobTypeOptions"
:key="option.value"
class="agent-page__pref-type-menu-item"
:class="{ 'agent-page__pref-type-menu-item--active': store.state.jobIntention.recruitCategory === option.value }"
@click.stop="selectJobType(option)"
>{{ option.label }}</div>
</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 class="agent-page__resume-section">
<h3 class="agent-page__pref-section-title">简历设置</h3>
<div class="agent-page__resume-sub-label">设置默认简历</div>
<div class="agent-page__resume-select-row">
<span class="agent-page__resume-check-icon">
<svg viewBox="0 0 16 16" fill="none"><rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" stroke-width="1.2"/></svg>
</span>
<el-select v-model="selectedResumeId" placeholder="请选择简历" class="agent-page__resume-select">
<el-option v-for="r in resumeList" :key="r.id" :label="r.resumeName" :value="r.id || ''" />
</el-select>
</div>
<!-- 自动优化简历开关 -->
<div class="agent-page__resume-switch-row">
<span class="agent-page__resume-switch-text">在投递时帮我针对岗位自动优化简历</span>
<el-switch v-model="autoOptimizeSwitch" :active-color="'#4FC2C9'" />
</div>
<!-- 岗位匹配分析开关 -->
<div class="agent-page__resume-switch-row">
<span class="agent-page__resume-switch-text">在投递时帮我进行岗位匹配分析</span>
<el-switch v-model="autoMatchAnalysisSwitch" :active-color="'#4FC2C9'" />
</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 class="agent-page__left">
<div class="agent-page__intro-card">
<div class="agent-page__intro-header por">
<div class="agent-page__intro-icon poa l-22 t0">
<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 mb50">请仔细对你的资料评估包括个人信息教育背景工作履历及技能确保资料齐全让我能顺利为你开启求职之旅</p>
<div class="">
<div class="tac"><button class="agent-page__confirm-btn dib" @click="handleNext">确认并进入</button></div>
<div class="tac color-9 mt30 cursor-po" @click="handleBack">返回上一步</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步网申常见问题 + 插件安装 ========== -->
<!-- ========== 第3步安装插件 ========== -->
<template v-if="currentStep === 3">
<!-- 上半部分网申常见问题 -->
<div v-if="step3Sub === 1" class="agent-page__step3">
<!-- 返回上一步按钮 -->
<button class="agent-page__back-btn" @click="handleBack">
<svg viewBox="0 0 24 24" fill="none" width="16" height="16"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>返回上一步</span>
<!-- 左侧安装教程内容 -->
<div class="agent-page__right">
<div class="agent-page__profile-wrapper">
<div class="agent-page__install-guide">
<h3 class="agent-page__install-guide-title">安装自动填写插件</h3>
<!-- 步骤1下载插件包 -->
<div class="agent-page__install-step">
<p class="agent-page__install-step-text">1下载下方的插件包</p>
<div class="agent-page__install-download">
<button class="agent-page__install-download-btn" @click="downloadExtension">
<span>Offer派自动投递助手.crx</span>
<svg viewBox="0 0 16 16" fill="none" width="14" height="14"><path d="M8 2v8M8 10L5 7M8 10l3-3M3 13h10" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
</div>
<!-- 步骤2复制地址进入扩展程序页面 -->
<div class="agent-page__install-step">
<p class="agent-page__install-step-text">2复制右下方的蓝色地址在浏览器地址栏粘贴并进入浏览器的扩展程序页面</p>
<div class="agent-page__install-step-content">
<div class="agent-page__install-step-img">
<img src="@/assets/images/agent-setting/2-1.png" alt="地址栏示例" />
</div>
<div class="agent-page__install-browser-links">
<div class="agent-page__install-browser-item">
<img src="@/assets/images/agent-setting/2-2.png" style="width: 3rem;height: 0.3rem;" alt="Chrome" class="agent-page__install-browser-icon" />
</div>
<a class="agent-page__install-browser-url" href="javascript:void(0)" @click="copyToClipboard('chrome://extensions')">
chrome://extensions
<svg viewBox="0 0 14 14" fill="none" width="12" height="12"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1"/><path d="M10 4V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h1" stroke="currentColor" stroke-width="1"/></svg>
</a>
<div class="agent-page__install-browser-item mt16">
<img src="@/assets/images/agent-setting/2-3.png" style="width: 3rem;height: 0.3rem;" alt="Edge" class="agent-page__install-browser-icon" />
</div>
<a class="agent-page__install-browser-url" href="javascript:void(0)" @click="copyToClipboard('edge://extensions/')">
edge://extensions/
<svg viewBox="0 0 14 14" fill="none" width="12" height="12"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1"/><path d="M10 4V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h1" stroke="currentColor" stroke-width="1"/></svg>
</a>
</div>
</div>
</div>
<!-- 步骤3打开开发人员模式 -->
<div class="agent-page__install-step">
<p class="agent-page__install-step-text">3在浏览器拓展页面中打开开发人员模式</p>
<div class="agent-page__install-step-img-row">
<div class="agent-page__install-step-img-item">
<img src="@/assets/images/agent-setting/3-1.png" alt="打开开发人员模式" />
</div>
</div>
</div>
<!-- 步骤4拖入插件包 -->
<div class="agent-page__install-step">
<p class="agent-page__install-step-text">4将下载好的离线插件包使用鼠标拖入到浏览器的扩展程序页面</p>
<div class="agent-page__install-step-img-row">
<div class="agent-page__install-step-img-item">
<img src="@/assets/images/agent-setting/4-1.png" alt="拖入插件包" />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧确认卡片 -->
<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 class="agent-page__intro-header por">
<div class="agent-page__intro-icon poa l-22 t0">
<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>
<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>
<p class="agent-page__intro-desc mb20">简单四步安装 Chrome 扩展程序<br/>实现自动填表并一站式追踪所有申请进度</p>
<div class="agent-page__install-steps-list">
<p>1. 下载offer派自动投递助手插件包</p>
<p>2. 进入浏览器扩展程序界面</p>
<p>3. 打开扩展程序开发人员模式</p>
<p>4.拖入安装包完成安装</p>
</div>
<div class="mt30">
<div class="tac"><button class="agent-page__confirm-btn dib" @click="handleNext">我已安装</button></div>
<div class="tac color-9 mt30 cursor-po" @click="handleBack">返回上一步</div>
</div>
</div>
</div>
<!-- 下半部分插件安装 -->
<div v-if="step3Sub === 2" class="agent-page__step3">
<!-- 返回上一阶段按钮 -->
<button class="agent-page__back-btn" @click="handleBack">
<svg viewBox="0 0 24 24" fill="none" width="16" height="16"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>返回上一步</span>
</button>
<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步配置求职助手 ========== -->
<!-- ========== 第4步完成配置 ========== -->
<template v-if="currentStep === 4">
<div v-if="!setupComplete" class="agent-page__step3">
<!-- 返回上一步按钮 -->
<button class="agent-page__back-btn" @click="handleBack">
<svg viewBox="0 0 24 24" fill="none" width="16" height="16"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>返回上一步</span>
</button>
<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">
<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>
@@ -222,20 +265,19 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { ref, computed, watch, onMounted } from 'vue'
import { useStore } from 'vuex'
import { formatEmploymentType } from '@/stores/index'
import { formatEmploymentType, JOB_TYPE_OPTIONS } from '@/stores/index'
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 RegionSelector from '@/components/tools/RegionSelector.vue'
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
import IndustrySelector from '@/components/tools/IndustrySelector.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 { fetchResumeList, setDefaultResume } from '@/api/resume'
import type { ResumeListItem } from '@/api/resume'
import type { AgentConfig } from '@/api/agent'
const store = useStore()
@@ -253,7 +295,7 @@ const emit = defineEmits<{
}>()
// ==================== 步骤导航 ====================
const steps = ['确认个人资料', '确认目标', '开启自动申请', '配置求职助手']
const steps = ['确认个人资料', '确认目标', '安装插件', '配置求职助手']
const currentStep = ref(1)
/** 进入下一步 */
@@ -261,16 +303,9 @@ function handleNext() {
if (currentStep.value < steps.length) currentStep.value++
}
/** 返回上一步(含子阶段判断) */
/** 返回上一步 */
function handleBack() {
if (currentStep.value === 3 && step3Sub.value === 2) {
// 第3步的第2阶段 → 回到第3步第1阶段
step3Sub.value = 1
} else if (currentStep.value === 4) {
// 第4步 → 回到第3步第2阶段(插件安装)
currentStep.value = 3
step3Sub.value = 2
} else if (currentStep.value > 1) {
if (currentStep.value > 1) {
currentStep.value--
}
}
@@ -400,106 +435,128 @@ async function handleSaveEdit(data: Record<string, any>) {
// ==================== 第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(() => formatEmploymentType(store.state.jobIntention.recruitCategory))
interface MatchedJobItem extends JobListItem { feedback: string }
const matchedJobs = ref<MatchedJobItem[]>([])
const loadingMatchJobs = ref(false)
const showDislikeInput = ref(false)
const dislikeReason = ref('')
/** 招聘分类选项 */
const jobTypeOptions = JOB_TYPE_OPTIONS
/** 招聘分类下拉是否展开 */
const showJobTypeDropdown = ref(false)
watch(showJobGoalDialog, (n, o) => { if (o === true && n === false) loadMatchedJobs() })
watch(currentStep, (val) => { if (val === 2) loadMatchedJobs() })
/** 选中招聘分类 */
function selectJobType(option: { label: string; value: number }) {
showJobTypeDropdown.value = false
store.dispatch('saveJobIntention', {
...store.state.jobIntention,
recruitCategory: option.value,
})
}
async function loadMatchedJobs() {
loadingMatchJobs.value = true; matchedJobs.value = []
/** 第2步:求职偏好筛选组件 — 读写 store.jobIntention */
const step2RegionCodes = computed<string[]>(() => store.state.jobIntention.regionCodes || [])
const step2CategoryIds = computed<number[]>(() => store.state.jobIntention.categoryIds || [])
const step2IndustryIds = computed<number[]>(() => store.state.jobIntention.industryIds || [])
/** 第2步:筛选组件统一触发按钮样式 — 大圆角胶囊按钮 */
const prefFilterTriggerStyle = {
padding: '0.1rem 0.28rem',
borderRadius: '0.24rem',
fontSize: '0.14rem',
background: '#F5FAF9',
border: '1px solid #e0e0e0',
minWidth: '0.9rem',
justifyContent: 'center',
height: '0.38rem',
boxSizing: 'border-box',
}
/** 第2步:筛选组件显示文字样式 */
const prefFilterDisplayStyle = {
maxWidth: 'none',
}
/** 第2步:地区选择变更 */
function onStep2RegionChange(codes: string[]) {
store.dispatch('saveJobIntention', { ...store.state.jobIntention, regionCodes: codes })
}
/** 第2步:岗位选择变更 */
function onStep2CategoryChange(ids: number[]) {
store.dispatch('saveJobIntention', { ...store.state.jobIntention, categoryIds: ids })
}
/** 第2步:行业选择变更 */
function onStep2IndustryChange(ids: number[]) {
store.dispatch('saveJobIntention', { ...store.state.jobIntention, industryIds: ids })
}
/** 第2步:简历列表 */
const resumeList = ref<ResumeListItem[]>([])
/** 选中的简历 ID */
const selectedResumeId = ref('')
/** 自动优化简历开关 */
const autoOptimizeSwitch = ref(true)
/** 岗位匹配分析开关 */
const autoMatchAnalysisSwitch = ref(true)
/** 加载简历列表 */
async function loadResumeList() {
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, recruitCategory: intention.recruitCategory ?? 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: '' }))
const res = await fetchResumeList()
if (res.code === '0' && res.data) {
resumeList.value = res.data
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 (e) { console.error('[AgentSetupWizard] 加载匹配岗位失败', e) }
finally { loadingMatchJobs.value = false }
}
} catch { console.error('[AgentSetupWizard] 加载简历列表失败') }
}
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 }
}
/** 进入第2步时加载简历列表 */
watch(currentStep, (val) => { if (val === 2) loadResumeList() })
// ==================== 第3步:网申常见问题 ====================
const isInternship = computed(() => store.state.jobIntention.recruitCategory === 2)
const step3Sub = ref(1)
const step3Form = reactive({
acceptDeptTransfer: '', acceptLocationTransfer: '',
interviewType: [] as string[],
languages: [{ language: '', proficiency: '' }] as Array<{ language: string; proficiency: string }>,
availableDate: '', internDaysPerWeek: '', internDuration: '',
/** 选择简历后设为默认简历 */
watch(selectedResumeId, async (newId, oldId) => {
// 仅在用户主动切换时调用接口(非初始化加载)
if (!newId || !oldId) return
try {
await setDefaultResume(newId)
} catch { console.error('[AgentSetupWizard] 设置默认简历失败') }
})
/** 监听初始配置数据 — 填充第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步下半部分:插件安装 ====================
// ==================== 第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)
/** 复制文本到剪贴板 */
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text).then(() => {
ElMessage.success('已复制到剪贴板')
}).catch(() => {
ElMessage.error('复制失败,请手动复制')
})
}
/** 设置完成 — 收集第3步+第4步数据,通过 emit 传回父组件 */
function handleStep4Complete() {
const step4Data = settingsPanelRef.value?.getData()
// ==================== 第4步:配置求职助手 ====================
/** 启用求职助手 — 收集第2步数据,通过 emit 传回父组件并触发 launch */
function handleLaunchAgent() {
const allSettings = {
jobType: store.state.jobIntention.recruitCategory === 2 ? 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,
agentMode: 1,
weeklyTarget: 2,
autoOptimizeResume: autoOptimizeSwitch.value ? 1 : 0,
autoMatchAnalysis: autoMatchAnalysisSwitch.value ? 1 : 0,
defaultResumeId: selectedResumeId.value,
status: 1,
}
emit('complete', allSettings)
setupComplete.value = true
emit('launch')
}
/** 启用求职助手 */
function handleLaunchAgent() { emit('launch') }
</script>
<style lang="scss">
+30 -4
View File
@@ -90,7 +90,7 @@
<!-- 用户评价 -->
<div class="member-dialog__testimonial">
<div class="member-dialog__testimonial-avatar">
<img src="@/assets/images/home/avatar-temporary.png" alt="用户头像" />
<img src="@/assets/images/home/alex-avatar.png" alt="用户头像" />
</div>
<div class="member-dialog__testimonial-content">
<div class="member-dialog__testimonial-stars"></div>
@@ -107,7 +107,7 @@
<div v-else-if="currentView === 'order1'" class="member-dialog__order">
<!-- 顶部导航 -->
<div class="member-dialog__order-header">
<span class="member-dialog__order-back" @click="currentView = 'intro2'"> 返回会员介绍</span>
<span class="melmber-dialog__order-back" @click="currentView = 'intro2'"> 返回会员介绍</span>
<!-- 步骤条 二维码弹窗打开时显示第2步 -->
<div class="member-dialog__steps">
<div class="member-dialog__step" :class="showQrCode ? 'member-dialog__step--done' : 'member-dialog__step--active'">
@@ -340,15 +340,23 @@
<h3 class="member-dialog__qrcode-subtitle">{{ selectedPayment === 'alipay' ? '扫码完成支付' : '扫码完成支付' }}</h3>
<p class="member-dialog__qrcode-desc">{{ selectedPayment === 'alipay' ? '请在支付宝页面完成支付,完成后点击下方按钮确认。' : '请使用微信 App 扫描二维码完成支付,完成后此窗口会自动关闭。' }}</p>
<!-- 支付宝iframe 渲染支付表单 -->
<div class="member-dialog__qrcode-image">
<div v-if="selectedPayment === 'alipay' && paymentFormHtml" class="member-dialog__qrcode-image">
<iframe
v-if="paymentFormHtml"
:srcdoc="paymentFormHtml"
frameborder="0"
class="member-dialog__payment-iframe"
scrolling="auto"
></iframe>
</div>
<!-- 微信使用 qrcode.js 渲染二维码 -->
<div v-if="selectedPayment === 'wechat' && wechatCodeUrl" class="member-dialog__qrcode-image">
<iframe
:srcdoc="wechatQrHtml"
frameborder="0"
class="member-dialog__wechat-qr-iframe"
scrolling="no"
></iframe>
</div>
<!-- 金额 -->
<div class="member-dialog__qrcode-amount">¥{{ currentPlan.price }}</div>
</div>
@@ -428,6 +436,15 @@ const showQrCode = ref(false)
/** 支付宝表单 HTML(用于 iframe 渲染) */
const paymentFormHtml = ref('')
/** 微信支付 code_url(用于生成二维码) */
const wechatCodeUrl = ref('')
/** 微信二维码 iframe 的 srcdoc HTML */
const wechatQrHtml = computed(() => {
if (!wechatCodeUrl.value) return ''
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"><\/script><style>body{display:flex;align-items:center;justify-content:center;margin:0;height:100%;}</style></head><body><div id="qrcode"></div><script>new QRCode(document.getElementById("qrcode"),{text:"${wechatCodeUrl.value}",width:200,height:200,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.H});<\/script></body></html>`
})
/** 当前订单ID(用于后续轮询支付状态) */
const orderId = ref('')
@@ -442,6 +459,7 @@ watch(() => props.modelValue, (val) => {
showQrCode.value = false
agreeProtocol.value = false
paymentFormHtml.value = ''
wechatCodeUrl.value = ''
orderId.value = ''
// 清除轮询计时器
if (pollTimer) {
@@ -586,7 +604,15 @@ async function handleShowQrCode() {
})
if (res.data) {
orderId.value = String(res.data.orderId)
if (selectedPayment.value === 'wechat') {
// 微信支付 — payData 为 code_url,用于生成二维码
wechatCodeUrl.value = res.data.payData
paymentFormHtml.value = ''
} else {
// 支付宝 — payData 为 HTML 表单
paymentFormHtml.value = res.data.payData
wechatCodeUrl.value = ''
}
showQrCode.value = true
// 创建订单成功后立即开始轮询支付状态
confirmPayment()
File diff suppressed because it is too large Load Diff
+267 -108
View File
@@ -1,11 +1,25 @@
<template>
<div class="profile-page-content">
<!-- 顶部 Tab 导航栏固定不动 -->
<div class="profile-page-content__tabs">
<span
v-for="tab in tabList"
:key="tab.key"
class="profile-page-content__tab-item"
:class="{ 'profile-page-content__tab-item--active': activeTab === tab.key }"
@click="scrollToSection(tab.key)"
>{{ tab.label }}</span>
</div>
<!-- 内容滚动区域 -->
<div ref="scrollContainerRef" class="profile-page-content__body">
<!-- 个人信息 -->
<div class="profile-page-content__card">
<div class="profile-page-content__card-header">
<div ref="infoRef" class="profile-page-content__card p10" :class="{ 'profile-page-content__card-hover': editingModule !== 'info' }">
<template v-if="editingModule !== 'info'">
<div class="profile-page-content__card-header pl0">
<h3 class="profile-page-content__card-title">个人信息</h3>
<button class="profile-page-content__edit-btn" @click="handleEdit('info')">
<svg viewBox="0 0 16 16" fill="none" class="profile-page-content__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<button class="profile-page-content__edit-btn-inline" @click="startEdit('info')">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
</div>
<div class="profile-page-content__info-name">{{ profile.name }}</div>
@@ -17,216 +31,287 @@
<div class="profile-page-content__info-contacts" v-if="profile.wechat">
<span class="profile-page-content__contact-item">💬 {{ profile.wechat }}</span>
</div>
<!-- 作品集链接显示在个人信息卡片内 -->
<div v-if="profile.portfolioUrl" class="profile-page-content__info-contacts">
<span class="profile-page-content__contact-item">🔗 <a :href="profile.portfolioUrl" target="_blank" rel="noopener noreferrer" class="profile-page-content__portfolio-link">{{ profile.portfolioUrl }}</a></span>
</div>
<!-- 作品集 -->
<div class="profile-page-content__card">
<div class="profile-page-content__card-header">
<h3 class="profile-page-content__card-title">作品集</h3>
<button class="profile-page-content__edit-btn" @click="handleEdit('portfolio')">
<svg viewBox="0 0 16 16" fill="none" class="profile-page-content__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<!-- 有链接时显示可点击链接 -->
<a
v-if="profile.portfolioUrl"
:href="profile.portfolioUrl"
target="_blank"
rel="noopener noreferrer"
class="profile-page-content__portfolio-link"
>{{ profile.portfolioUrl }}</a>
<!-- 无链接时显示占位提示 -->
<span v-else class="profile-page-content__portfolio-empty">暂未添加作品集链接</span>
</template>
<!-- 编辑模式 -->
<ProfileEditDrawer
v-if="editingModule === 'info'"
v-model="editVisible"
module="info"
:initial-data="editInitialData"
@save="handleSave"
@cancel="cancelEdit"
/>
</div>
<!-- 教育经历 -->
<div class="profile-page-content__card">
<div class="profile-page-content__card-header">
<div ref="educationRef" class="profile-page-content__card">
<div class="profile-page-content__card-header profile-page-content__header-hover">
<h3 class="profile-page-content__card-title">教育经历</h3>
<button class="profile-page-content__edit-btn" @click="handleEdit('education')">
<svg viewBox="0 0 16 16" fill="none" class="profile-page-content__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<button class="profile-page-content__edit-btn-inline" @click="startAdd('education')">
<el-icon><Plus /></el-icon><span>添加</span>
</button>
</div>
<!-- 新增 -->
<ResumeExpInlineEdit v-if="addingModule === 'education'" module-type="education" @save="handleExpAdd('education', $event)" @cancel="addingModule = ''" />
<div v-for="(edu, i) in profile.education" :key="i" class="profile-page-content__section-item">
<template v-if="editingExpIndex !== `education_${i}`">
<div class="profile-page-content__item-row">
<div class="profile-page-content__item-left">
<div class="profile-page-content__item-title">{{ edu.school }}</div>
<div class="profile-page-content__item-sub">{{ getDegreeText(edu.studyType, edu.degree) }} · {{ edu.major }}</div>
<div class="profile-page-content__item-date">{{ edu.startDate }} - {{ edu.endDate }}</div>
</div>
<button class="profile-page-content__edit-btn-inline profile-page-content__item-edit-btn" @click="startEditExp('education', i, edu)">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
</div>
<ul class="profile-page-content__item-list" v-if="edu.description?.length && edu.description[0]?.text">
<li v-for="desc in edu.description" :key="desc.id">{{ desc.text }}</li>
</ul>
</template>
<ResumeExpInlineEdit v-else module-type="education" :initial-data="edu" @save="handleExpUpdate('education', i, $event)" @cancel="editingExpIndex = ''" />
</div>
</div>
<!-- 工作经历 -->
<div class="profile-page-content__card">
<div class="profile-page-content__card-header">
<div ref="workRef" class="profile-page-content__card">
<div class="profile-page-content__card-header profile-page-content__header-hover">
<h3 class="profile-page-content__card-title">工作经历</h3>
<button class="profile-page-content__edit-btn" @click="handleEdit('work')">
<svg viewBox="0 0 16 16" fill="none" class="profile-page-content__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<button class="profile-page-content__edit-btn-inline" @click="startAdd('work')">
<el-icon><Plus /></el-icon><span>添加</span>
</button>
</div>
<ResumeExpInlineEdit v-if="addingModule === 'work'" module-type="work" @save="handleExpAdd('work', $event)" @cancel="addingModule = ''" />
<div v-for="(exp, i) in profile.works" :key="i" class="profile-page-content__section-item">
<template v-if="editingExpIndex !== `work_${i}`">
<div class="profile-page-content__item-row">
<div class="profile-page-content__item-left">
<div class="profile-page-content__item-title">{{ exp.companyName }}</div>
<div class="profile-page-content__item-sub">{{ exp.position }}</div>
</div>
<div class="profile-page-content__item-period">{{ exp.startDate }} - {{ exp.endDate || '至今' }}</div>
<button class="profile-page-content__edit-btn-inline profile-page-content__item-edit-btn" @click="startEditExp('work', i, exp)">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
</div>
<ul class="profile-page-content__item-list">
<li v-for="desc in exp.description" :key="desc.id">{{ desc.text }}</li>
</ul>
</template>
<ResumeExpInlineEdit v-else module-type="work" :initial-data="exp" @save="handleExpUpdate('work', i, $event)" @cancel="editingExpIndex = ''" />
</div>
</div>
<!-- 实习经历 -->
<div class="profile-page-content__card">
<div class="profile-page-content__card-header">
<div ref="internshipRef" class="profile-page-content__card">
<div class="profile-page-content__card-header profile-page-content__header-hover">
<h3 class="profile-page-content__card-title">实习经历</h3>
<button class="profile-page-content__edit-btn" @click="handleEdit('internship')">
<svg viewBox="0 0 16 16" fill="none" class="profile-page-content__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<button class="profile-page-content__edit-btn-inline" @click="startAdd('internship')">
<el-icon><Plus /></el-icon><span>添加</span>
</button>
</div>
<ResumeExpInlineEdit v-if="addingModule === 'internship'" module-type="internship" @save="handleExpAdd('internship', $event)" @cancel="addingModule = ''" />
<div v-for="(exp, i) in profile.internships" :key="i" class="profile-page-content__section-item">
<template v-if="editingExpIndex !== `internship_${i}`">
<div class="profile-page-content__item-row">
<div class="profile-page-content__item-left">
<div class="profile-page-content__item-title">{{ exp.companyName }}</div>
<div class="profile-page-content__item-sub">{{ exp.position }}</div>
</div>
<div class="profile-page-content__item-period">{{ exp.startDate }} - {{ exp.endDate }}</div>
<button class="profile-page-content__edit-btn-inline profile-page-content__item-edit-btn" @click="startEditExp('internship', i, exp)">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
</div>
<ul class="profile-page-content__item-list">
<li v-for="desc in exp.description" :key="desc.id">{{ desc.text }}</li>
</ul>
</template>
<ResumeExpInlineEdit v-else module-type="internship" :initial-data="exp" @save="handleExpUpdate('internship', i, $event)" @cancel="editingExpIndex = ''" />
</div>
</div>
<!-- 项目经历 -->
<div class="profile-page-content__card">
<div class="profile-page-content__card-header">
<div ref="projectRef" class="profile-page-content__card">
<div class="profile-page-content__card-header profile-page-content__header-hover">
<h3 class="profile-page-content__card-title">项目经历</h3>
<button class="profile-page-content__edit-btn" @click="handleEdit('project')">
<svg viewBox="0 0 16 16" fill="none" class="profile-page-content__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<button class="profile-page-content__edit-btn-inline" @click="startAdd('project')">
<el-icon><Plus /></el-icon><span>添加</span>
</button>
</div>
<ResumeExpInlineEdit v-if="addingModule === 'project'" module-type="project" @save="handleExpAdd('project', $event)" @cancel="addingModule = ''" />
<div v-for="(proj, i) in profile.projects" :key="i" class="profile-page-content__section-item">
<template v-if="editingExpIndex !== `project_${i}`">
<div class="profile-page-content__item-row">
<div class="profile-page-content__item-left">
<div class="profile-page-content__item-title">{{ proj.projectName }}</div>
<div class="profile-page-content__item-sub">{{ proj.role }}</div>
</div>
<div class="profile-page-content__item-period">{{ proj.startDate }} - {{ proj.endDate }}</div>
<button class="profile-page-content__edit-btn-inline profile-page-content__item-edit-btn" @click="startEditExp('project', i, proj)">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
</div>
<ul class="profile-page-content__item-list">
<li v-for="desc in proj.description" :key="desc.id">{{ desc.text }}</li>
</ul>
</div>
</div>
<!-- 技能 -->
<div class="profile-page-content__card">
<div class="profile-page-content__card-header">
<h3 class="profile-page-content__card-title">技能</h3>
<button class="profile-page-content__edit-btn" @click="handleEdit('skills')">
<svg viewBox="0 0 16 16" fill="none" class="profile-page-content__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<div class="profile-page-content__tags">
<span v-for="(skill, i) in profile.skills" :key="i" class="profile-page-content__tag">{{ skill }}</span>
</template>
<ResumeExpInlineEdit v-else module-type="project" :initial-data="proj" @save="handleExpUpdate('project', i, $event)" @cancel="editingExpIndex = ''" />
</div>
</div>
<!-- 竞赛 -->
<div class="profile-page-content__card">
<div class="profile-page-content__card-header">
<div ref="competitionRef" class="profile-page-content__card">
<div class="profile-page-content__card-header profile-page-content__header-hover">
<h3 class="profile-page-content__card-title">竞赛</h3>
<button class="profile-page-content__edit-btn" @click="handleEdit('competition')">
<svg viewBox="0 0 16 16" fill="none" class="profile-page-content__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<button class="profile-page-content__edit-btn-inline" @click="startAdd('competition')">
<el-icon><Plus /></el-icon><span>添加</span>
</button>
</div>
<ResumeExpInlineEdit v-if="addingModule === 'competition'" module-type="competition" @save="handleExpAdd('competition', $event)" @cancel="addingModule = ''" />
<div v-for="(comp, i) in profile.competitions" :key="i" class="profile-page-content__section-item">
<template v-if="editingExpIndex !== `competition_${i}`">
<div class="profile-page-content__item-row">
<div class="profile-page-content__item-left">
<div class="profile-page-content__item-title">{{ comp.competitionName }} · {{ comp.award }}</div>
</div>
<div class="profile-page-content__item-period">{{ comp.awardDate }}</div>
<button class="profile-page-content__edit-btn-inline profile-page-content__item-edit-btn" @click="startEditExp('competition', i, comp)">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
</div>
<ul class="profile-page-content__item-list" v-if="comp.description[0]?.text">
<li v-for="desc in comp.description" :key="desc.id">{{ desc.text }}</li>
</ul>
</template>
<ResumeExpInlineEdit v-else module-type="competition" :initial-data="comp" @save="handleExpUpdate('competition', i, $event)" @cancel="editingExpIndex = ''" />
</div>
</div>
<!-- 技能 -->
<div ref="skillsRef" class="profile-page-content__card" :class="{ 'profile-page-content__card-hover': editingModule !== 'skills' }">
<template v-if="editingModule !== 'skills'">
<div class="profile-page-content__card-header">
<h3 class="profile-page-content__card-title">技能</h3>
<button class="profile-page-content__edit-btn-inline" @click="startEdit('skills')">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
</div>
<div class="profile-page-content__tags">
<span v-for="(skill, i) in profile.skills" :key="i" class="profile-page-content__tag">{{ skill }}</span>
</div>
</template>
<ProfileEditDrawer
v-if="editingModule === 'skills'"
v-model="editVisible"
module="skills"
:initial-data="editInitialData"
@save="handleSave"
@cancel="cancelEdit"
/>
</div>
<!-- 证书 -->
<div class="profile-page-content__card">
<div ref="certificateRef" class="profile-page-content__card" :class="{ 'profile-page-content__card-hover': editingModule !== 'certificate' }">
<template v-if="editingModule !== 'certificate'">
<div class="profile-page-content__card-header">
<h3 class="profile-page-content__card-title">证书</h3>
<button class="profile-page-content__edit-btn" @click="handleEdit('certificate')">
<svg viewBox="0 0 16 16" fill="none" class="profile-page-content__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<button class="profile-page-content__edit-btn-inline" @click="startEdit('certificate')">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
</div>
<div class="profile-page-content__tags">
<span v-for="(cert, i) in profile.certificates" :key="i" class="profile-page-content__tag">{{ cert }}</span>
</div>
</template>
<ProfileEditDrawer
v-if="editingModule === 'certificate'"
v-model="editVisible"
module="certificate"
:initial-data="editInitialData"
@save="handleSave"
@cancel="cancelEdit"
/>
</div>
</div><!-- 内容滚动区域结束 -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { EditPen, Plus } from '@element-plus/icons-vue'
import { resolveRegionName } from '@/utils/region'
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
import ResumeExpInlineEdit from '@/components/ResumeExpInlineEdit.vue'
/** Tab 导航列表 */
const tabList = [
{ key: 'info', label: '个人信息' },
{ key: 'education', label: '教育经历' },
{ key: 'work', label: '工作经历' },
{ key: 'internship', label: '实习经历' },
{ key: 'project', label: '项目经历' },
{ key: 'skills', label: '技能' },
{ key: 'competition', label: '竞赛' },
{ key: 'certificate', label: '证书' },
]
/** 当前激活的 tab */
const activeTab = ref('info')
/** 各模块的 DOM 引用 */
const scrollContainerRef = ref<HTMLElement | null>(null)
const infoRef = ref<HTMLElement | null>(null)
const educationRef = ref<HTMLElement | null>(null)
const workRef = ref<HTMLElement | null>(null)
const internshipRef = ref<HTMLElement | null>(null)
const projectRef = ref<HTMLElement | null>(null)
const skillsRef = ref<HTMLElement | null>(null)
const competitionRef = ref<HTMLElement | null>(null)
const certificateRef = ref<HTMLElement | null>(null)
/** 模块 key 与 ref 的映射 */
const sectionRefMap: Record<string, typeof infoRef> = {
info: infoRef,
education: educationRef,
work: workRef,
internship: internshipRef,
project: projectRef,
skills: skillsRef,
competition: competitionRef,
certificate: certificateRef,
}
/** 点击 tab 滚动到对应模块 */
function scrollToSection(key: string) {
activeTab.value = key
const targetRef = sectionRefMap[key]
const container = scrollContainerRef.value
if (targetRef?.value && container) {
const offsetTop = targetRef.value.offsetTop - container.offsetTop
container.scrollTo({ top: offsetTop, behavior: 'smooth' })
}
}
/** 个人资料数据结构 */
interface ProfileData {
name: string
phone: string
email: string
idNumber: string
/** 所在城市编码 — 对应接口字段 regionCode */
regionCode: string
/** 作品集链接 — 对应接口字段 portfolioUrl */
portfolioUrl: string
wechat?: string
education: Array<{
school: string
major: string
studyType: number
degree: number
startDate: string
endDate: string
description: Array<{ id: string; text: string }>
}>
works: Array<{
companyName: string
position: string
startDate: string
endDate: string
description: Array<{ id: string; text: string }>
}>
internships: Array<{
companyName: string
position: string
startDate: string
endDate: string
description: Array<{ id: string; text: string }>
}>
projects: Array<{
projectName: string
companyName: string
role: string
startDate: string
endDate: string
description: Array<{ id: string; text: string }>
}>
education: Array<{ school: string; major: string; studyType: number; degree: number; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>
works: Array<{ companyName: string; position: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>
internships: Array<{ companyName: string; position: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>
projects: Array<{ projectName: string; companyName: string; role: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>
skills: string[]
competitions: Array<{
competitionName: string
award: string
awardDate: string
description: Array<{ id: string; text: string }>
}>
competitions: Array<{ competitionName: string; award: string; awardDate: string; description: Array<{ id: string; text: string }> }>
certificates: string[]
}
@@ -235,23 +320,97 @@ const props = defineProps<{
}>()
const emit = defineEmits<{
edit: [section: string]
/** 模块编辑保存事件 */
(e: 'edit', section: string): void
/** 内联保存事件(模块名+数据) */
(e: 'save', section: string, data: Record<string, any>): void
/** 单条经历更新事件 */
(e: 'expUpdate', moduleType: string, index: number, data: Record<string, any>): void
/** 新增经历事件 */
(e: 'expAdd', moduleType: string, data: Record<string, any>): void
}>()
/** 获取学历文本 — 根据学历类型和学历代码生成显示文本 */
/** 当前编辑的模块(info/skills/certificate */
const editingModule = ref('')
/** 编辑可见状态 */
const editVisible = ref(false)
/** 编辑初始数据 */
const editInitialData = ref<Record<string, any>>({})
/** 当前编辑的经历索引 key(格式:moduleType_index */
const editingExpIndex = ref('')
/** 当前添加经历的模块 */
const addingModule = ref('')
/** 获取学历文本 */
function getDegreeText(studyType: number, degree: number): string {
const studyTypeText = studyType === 0 ? '全日制' : '非全日制'
const degreeMap: Record<number, string> = {
1: '大专',
2: '本科',
3: '硕士',
4: '博士',
}
const degreeMap: Record<number, string> = { 1: '大专', 2: '本科', 3: '硕士', 4: '博士' }
return `${studyTypeText}${degreeMap[degree] || ''}`
}
function handleEdit(section: string) {
emit('edit', section)
/** 打开模块编辑(个人信息/技能/证书) */
function startEdit(module: string) {
editingModule.value = module
editVisible.value = true
editingExpIndex.value = ''
addingModule.value = ''
if (module === 'info') {
editInitialData.value = {
name: props.profile.name || '',
email: props.profile.email || '',
phone: props.profile.phone || '',
location: props.profile.regionCode || '',
wechat: props.profile.wechat || '',
portfolioUrl: props.profile.portfolioUrl || '',
}
} else if (module === 'skills') {
editInitialData.value = { skills: [...(props.profile.skills || [])] }
} else if (module === 'certificate') {
editInitialData.value = { certificates: [...(props.profile.certificates || [])] }
}
}
/** 取消编辑 */
function cancelEdit() {
editingModule.value = ''
editVisible.value = false
}
/** 保存编辑(个人信息/技能/证书) */
function handleSave(data: Record<string, any>) {
emit('save', editingModule.value, data)
editingModule.value = ''
editVisible.value = false
}
/** 开始添加经历 */
function startAdd(moduleType: string) {
addingModule.value = moduleType
editingExpIndex.value = ''
editingModule.value = ''
}
/** 开始编辑单条经历 */
function startEditExp(_moduleType: string, index: number, _record: any) {
editingExpIndex.value = `${_moduleType}_${index}`
addingModule.value = ''
editingModule.value = ''
}
/** 保存经历更新 */
function handleExpUpdate(moduleType: string, index: number, data: Record<string, any>) {
emit('expUpdate', moduleType, index, data)
editingExpIndex.value = ''
}
/** 保存新增经历 */
function handleExpAdd(moduleType: string, data: Record<string, any>) {
emit('expAdd', moduleType, data)
addingModule.value = ''
}
</script>
+11 -14
View File
@@ -1,21 +1,17 @@
<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">
<!-- 编辑简历名称与目标岗位弹窗 Teleport body 避免缩放影响 -->
<Teleport to="body">
<div v-if="visible" class="resume-edit-name-overlay" @click.self="handleClose">
<div class="resume-edit-name-dialog">
<!-- 关闭按钮 -->
<button class="resume-edit-name-dialog__close" @click="handleClose"></button>
<!-- 简历名称 -->
<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
<input
v-model="form.resumeName"
placeholder="请输入简历名称"
maxlength="50"
@@ -26,7 +22,7 @@
<!-- 目标岗位 -->
<div class="resume-edit-name-dialog__field">
<label class="resume-edit-name-dialog__label">目标岗位</label>
<el-input
<input
v-model="form.targetPosition"
placeholder="请输入岗位名称"
maxlength="50"
@@ -48,7 +44,8 @@
</button>
</div>
</div>
</el-dialog>
</div>
</Teleport>
</template>
<script setup lang="ts">
+228
View File
@@ -0,0 +1,228 @@
<template>
<!-- 单条经历内联编辑组件 -->
<div class="profile-edit-inline">
<div class="profile-edit-inline__body">
<!-- 教育经历 -->
<template v-if="moduleType === 'education'">
<div class="profile-edit-inline__fields-row">
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">学校</label><input class="profile-edit-inline__input" placeholder="请输入学校名称" v-model="form.school" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">专业</label><input class="profile-edit-inline__input" placeholder="请输入专业名称" v-model="form.major" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">学历</label><select class="profile-edit-inline__input" v-model="form.degree"><option value="大专">大专</option><option value="本科">本科</option><option value="硕士">硕士</option><option value="博士">博士</option></select></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">学历类型</label><select class="profile-edit-inline__input" v-model="form.studyType"><option value="全日制">全日制</option><option value="非全日制">非全日制</option></select></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">就读时间</label>
<div class="profile-edit-inline__date-range">
<el-date-picker v-model="form.startDate" type="month" placeholder="开始" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
<span class="profile-edit-inline__date-sep">-</span>
<el-date-picker v-model="form.endDate" type="month" placeholder="结束" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
</div>
</div>
</div>
</template>
<!-- 工作经历 -->
<template v-else-if="moduleType === 'work'">
<div class="profile-edit-inline__fields-row">
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">公司名称</label><input class="profile-edit-inline__input" placeholder="请输入公司名称" v-model="form.companyName" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">职位</label><input class="profile-edit-inline__input" placeholder="请输入职位" v-model="form.position" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">时间</label>
<div class="profile-edit-inline__date-range">
<el-date-picker v-model="form.startDate" type="month" placeholder="开始" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
<span class="profile-edit-inline__date-sep">-</span>
<el-date-picker v-model="form.endDate" type="month" placeholder="结束" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
</div>
</div>
</div>
</template>
<!-- 实习经历 -->
<template v-else-if="moduleType === 'internship'">
<div class="profile-edit-inline__fields-row">
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">公司名称</label><input class="profile-edit-inline__input" placeholder="请输入公司名称" v-model="form.companyName" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">职位</label><input class="profile-edit-inline__input" placeholder="请输入职位" v-model="form.position" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">时间</label>
<div class="profile-edit-inline__date-range">
<el-date-picker v-model="form.startDate" type="month" placeholder="开始" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
<span class="profile-edit-inline__date-sep">-</span>
<el-date-picker v-model="form.endDate" type="month" placeholder="结束" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
</div>
</div>
</div>
</template>
<!-- 项目经历 -->
<template v-else-if="moduleType === 'project'">
<div class="profile-edit-inline__fields-row">
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">项目名称</label><input class="profile-edit-inline__input" placeholder="请输入项目名称" v-model="form.projectName" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">项目角色</label><input class="profile-edit-inline__input" placeholder="请输入项目角色" v-model="form.role" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">时间</label>
<div class="profile-edit-inline__date-range">
<el-date-picker v-model="form.startDate" type="month" placeholder="开始" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
<span class="profile-edit-inline__date-sep">-</span>
<el-date-picker v-model="form.endDate" type="month" placeholder="结束" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
</div>
</div>
</div>
</template>
<!-- 竞赛经历 -->
<template v-else-if="moduleType === 'competition'">
<div class="profile-edit-inline__fields-row">
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">竞赛名称</label><input class="profile-edit-inline__input" placeholder="请输入竞赛名称" v-model="form.competitionName" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">获奖名次</label><input class="profile-edit-inline__input" placeholder="请输入获奖名次" v-model="form.award" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">获奖时间</label><el-date-picker v-model="form.awardDate" type="month" placeholder="请选择" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" /></div>
</div>
</template>
<!-- 经历描述 + AI润色 -->
<div class="profile-edit-inline__summary-header">
<label class="profile-edit-inline__label">经历描述</label>
<button class="profile-edit-inline__ai-polish-btn" @click="handleAiPolish" :disabled="aiPolishing">
<span> AI润色</span>
</button>
</div>
<div class="profile-edit-inline__field">
<textarea class="profile-edit-inline__textarea" placeholder="请输入经历描述" v-model="descriptionText" rows="4"></textarea>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="profile-edit-inline__footer">
<button class="profile-edit-inline__save-btn" @click="handleSave">保存</button>
<button class="profile-edit-inline__cancel-btn" @click="handleCancel">取消</button>
</div>
<!-- AI润色结果保存取消下方 -->
<div v-if="aiPolishResult" class="profile-edit-inline__ai-result">
<p class="profile-edit-inline__ai-result-label"> AI润色结果</p>
<div class="profile-edit-inline__ai-result-card">
<p class="profile-edit-inline__ai-result-text">{{ aiPolishResult }}</p>
</div>
<div class="profile-edit-inline__ai-result-actions">
<button class="profile-edit-inline__ai-replace-btn" @click="handleReplaceWithAi">+ 替换为此内容</button>
<button class="profile-edit-inline__ai-regenerate-btn" @click="handleAiPolish" :disabled="aiPolishing"> 重新生成</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElMessage, ElLoading } from 'element-plus'
import { polishDiagnosisIssue } from '@/api/resume'
/** 描述段落结构 */
interface DescriptionParagraph {
id: string
text: string
}
/** Props */
const props = defineProps<{
/** 经历模块类型 */
moduleType: string
/** 初始数据(编辑已有经历时传入,新增时为空) */
initialData?: Record<string, any>
}>()
/** Emits */
const emit = defineEmits<{
(e: 'save', data: Record<string, any>): void
(e: 'cancel'): void
}>()
/** 表单数据 */
const form = ref<Record<string, any>>({})
/** 经历描述文本(多段用换行拼接) */
const descriptionText = ref('')
/** AI润色结果 */
const aiPolishResult = ref('')
/** AI润色中 */
const aiPolishing = ref(false)
/** 生成短ID */
const generateId = (): string => Math.random().toString(36).substring(2, 8)
/** 初始化表单 */
function initForm() {
aiPolishResult.value = ''
if (props.initialData) {
form.value = { ...props.initialData }
// 将 description 数组拼接为文本
if (props.initialData.description?.length) {
descriptionText.value = props.initialData.description.map((d: DescriptionParagraph) => d.text).join('\n')
} else {
descriptionText.value = ''
}
} else {
// 新增模式 — 空表单
form.value = {}
descriptionText.value = ''
}
}
// 立即初始化
initForm()
// 监听 initialData 变化
watch(() => props.initialData, () => { initForm() }, { deep: true })
/** AI润色 */
async function handleAiPolish() {
const text = descriptionText.value.trim()
if (!text) {
ElMessage.warning('请先输入经历描述')
return
}
aiPolishing.value = true
const loading = ElLoading.service({ lock: true, text: 'AI润色中…', background: 'rgba(0,0,0,0.5)' })
try {
const content = text.split('\n').filter(s => s.trim())
const res = await polishDiagnosisIssue(content)
if (res.code === 0 && res.data?.content) {
aiPolishResult.value = res.data.content.join('\n')
ElMessage.success('AI润色完成')
} else {
ElMessage.error(res.msg || '润色失败')
}
} catch {
ElMessage.error('请求失败')
} finally {
aiPolishing.value = false
loading.close()
}
}
/** 替换为AI润色内容 */
function handleReplaceWithAi() {
if (aiPolishResult.value) {
descriptionText.value = aiPolishResult.value
}
}
/** 保存 */
function handleSave() {
// 将描述文本按换行拆分为 description 数组
const lines = descriptionText.value.split('\n').filter(s => s.trim())
const description: DescriptionParagraph[] = lines.map(text => ({
id: generateId(),
text,
}))
// 如果有原始 description,保留原有的 id
if (props.initialData?.description?.length) {
props.initialData.description.forEach((d: DescriptionParagraph, i: number) => {
if (i < description.length) {
description[i].id = d.id
}
})
}
emit('save', { ...form.value, description })
}
/** 取消 */
function handleCancel() {
emit('cancel')
}
</script>
+403
View File
@@ -0,0 +1,403 @@
<template>
<!-- 一键修复抽屉 从右侧滑入展示所有模块的诊断修复 -->
<Teleport to="body">
<!-- 遮罩层 -->
<Transition name="fix-all-drawer-overlay">
<div
v-if="modelValue"
class="fix-all-drawer-overlay"
@click="handleClose"
/>
</Transition>
<!-- 抽屉主体 -->
<Transition name="fix-all-drawer-slide">
<div v-if="modelValue" class="fix-all-drawer" @click.stop>
<!-- 顶部栏 -->
<div class="fix-all-drawer__header">
<button class="fix-all-drawer__close-btn" @click="handleClose" aria-label="收起">
<svg viewBox="0 0 16 16" fill="none" class="fix-all-drawer__close-icon">
<path d="M10 3l-5 5 5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<h3 class="fix-all-drawer__title">一键修复</h3>
</div>
<!-- 可滚动内容区 -->
<div class="fix-all-drawer__body">
<!-- 遍历每个有诊断数据的模块 -->
<template v-for="module in moduleDataList" :key="module.moduleType">
<!-- 模块标题 -->
<h4 class="fix-all-drawer__module-title">{{ moduleTitleMap[module.moduleType] }}</h4>
<!-- 遍历该模块下每条经历/记录 -->
<div
v-for="record in module.records"
:key="record.issueId"
class="fix-all-drawer__record"
>
<!-- 经历标识经历类模块显示标题summary不显示 -->
<p v-if="record.recordLabel" class="fix-all-drawer__record-label">{{ record.recordLabel }}</p>
<!-- 原文编辑区 -->
<div class="fix-all-drawer__card">
<textarea
v-model="record.editText"
class="fix-all-drawer__textarea"
placeholder="原文内容…"
@input="autoResizeTextarea($event)"
/>
</div>
<!-- AI 润色结果区 -->
<div class="fix-all-drawer__ai-section">
<p class="fix-all-drawer__ai-label"> AI润色结果</p>
<div class="fix-all-drawer__card fix-all-drawer__card--ai">
<p class="fix-all-drawer__ai-text">{{ record.polishText }}</p>
</div>
<!-- 操作按钮 -->
<div class="fix-all-drawer__ai-actions">
<button class="fix-all-drawer__action-btn fix-all-drawer__action-btn--replace" @click="handleReplaceToOriginal(record)">+替换为此内容</button>
<button class="fix-all-drawer__action-btn fix-all-drawer__action-btn--regenerate" @click="handleRegenerate(record)"> 重新生成</button>
</div>
</div>
</div>
</template>
</div>
<!-- 底部按钮区 -->
<div class="fix-all-drawer__footer">
<button class="fix-all-drawer__footer-btn fix-all-drawer__footer-btn--use-ai" @click="handleUseAllAi">一键使用AI润色结果</button>
<button class="fix-all-drawer__footer-btn fix-all-drawer__footer-btn--save" @click="handleSaveCurrent">保存当前修改</button>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import type { DiagnosisIssue, DescriptionParagraph } from '@/api/resume'
import { polishDiagnosisIssue, resolveDiagnosisIssue } from '@/api/resume'
// ==================== Props ====================
/** 单条经历记录的原始数据(父组件传入) */
export interface OriginalRecordData {
/** 模块类型 */
moduleType: string
/** 经历记录 IDsummary 时为 resumeId */
recordId: string
/** 经历显示标题(如"北京大学-经历描述" */
recordLabel: string
/** 原文内容:summary 为字符串,经历为 DescriptionParagraph[] */
originalContent: string | DescriptionParagraph[]
}
const props = defineProps<{
/** 控制抽屉显示/隐藏 */
modelValue: boolean
/** 所有诊断问题列表 */
issues: DiagnosisIssue[]
/** 所有模块的原始经历数据 */
originalRecords: OriginalRecordData[]
}>()
/** 提交结果的单条数据结构 */
export interface FixAllSubmitItem {
/** 模块类型 */
moduleType: string
/** 经历记录 ID */
recordId: string
/** 新的内容:summary 为字符串,经历为 DescriptionParagraph[] */
newContent: string | DescriptionParagraph[]
/** 关联的 issue ID */
issueId: string
}
const emit = defineEmits<{
(e: 'update:modelValue', val: boolean): void
/** 保存当前修改:携带有变化的模块数据 */
(e: 'save', items: FixAllSubmitItem[]): void
/** 一键使用AI润色结果:携带所有模块数据(强制更新) */
(e: 'useAllAi', items: FixAllSubmitItem[]): void
}>()
// ==================== 内部数据结构 ====================
/** 单条记录的内部状态 */
interface RecordState {
/** 关联的 issue ID */
issueId: string
/** 模块类型 */
moduleType: string
/** 经历记录 ID */
recordId: string
/** 经历显示标题 */
recordLabel: string
/** 原文编辑框内容(用户可编辑) */
editText: string
/** AI 润色结果文本 */
polishText: string
/** 原始内容(用于对比是否有变化) */
originalText: string
}
/** 模块分组数据 */
interface ModuleGroup {
/** 模块类型 */
moduleType: string
/** 该模块下所有记录 */
records: RecordState[]
}
// ==================== 模块标题映射 ====================
/** 模块类型到中文标题的映射 */
const moduleTitleMap: Record<string, string> = {
summary: '个人概述',
education: '教育背景',
work: '工作经历',
internship: '实习经历',
project: '项目经历',
competition: '竞赛经历',
}
/** 模块顺序 */
const moduleOrder = ['summary', 'education', 'work', 'internship', 'project', 'competition']
// ==================== 响应式数据 ====================
/** 所有模块分组后的数据列表 */
const moduleDataList = ref<ModuleGroup[]>([])
// ==================== 工具方法 ====================
/** 生成 8 位随机字母数字 ID */
function generateId(): string {
return Math.random().toString(36).substring(2, 10)
}
/** 将 DescriptionParagraph[] 拼接为字符串(每段文字后加换行) */
function descriptionToText(desc: DescriptionParagraph[]): string {
return desc.map(d => d.text || '').join('\n')
}
/** 将文本按换行拆分为 DescriptionParagraph[](每行生成新 id */
function textToDescription(text: string): DescriptionParagraph[] {
const lines = text.split('\n').filter(line => line.trim() !== '')
return lines.map(line => ({ id: generateId(), text: line }))
}
/** 获取 issue 的 optimizedContent 转为文本 */
function getOptimizedText(issue: DiagnosisIssue): string {
const oc = issue.optimizedContent
if (!oc) return ''
// summaryoptimizedContent 是字符串
if (typeof oc === 'string') return oc
// 经历模块:数组对象,取每个子元素的 optimizedContent 或 text 拼接
if (Array.isArray(oc)) {
return oc.map((item: any) => item.optimizedContent || item.text || '').join('\n')
}
return ''
}
// ==================== 初始化逻辑 ====================
/** 抽屉打开时初始化数据 */
watch(() => props.modelValue, (val) => {
if (val) {
initData()
}
})
/** 根据 props 中的 issues 和 originalRecords 初始化内部数据 */
function initData() {
const groupMap: Record<string, RecordState[]> = {}
// 遍历每个 issue,找到对应的原始记录数据
for (const issue of props.issues) {
if (!issue.id || !issue.moduleType || issue.status !== 0) continue
const moduleType = issue.moduleType
const recordId = issue.moduleRecordId || ''
// 找到对应的原始记录
const originalRecord = props.originalRecords.find(
r => r.moduleType === moduleType && r.recordId === recordId
)
// 原文转文本
let originalText = ''
if (originalRecord) {
if (typeof originalRecord.originalContent === 'string') {
originalText = originalRecord.originalContent
} else if (Array.isArray(originalRecord.originalContent)) {
originalText = descriptionToText(originalRecord.originalContent)
}
}
// AI 润色结果:初始用 optimizedContent
const polishText = getOptimizedText(issue)
const state: RecordState = {
issueId: issue.id,
moduleType,
recordId,
recordLabel: originalRecord?.recordLabel || '',
editText: originalText,
polishText,
originalText,
}
if (!groupMap[moduleType]) {
groupMap[moduleType] = []
}
groupMap[moduleType].push(state)
}
// 按模块顺序排列
moduleDataList.value = moduleOrder
.filter(mt => groupMap[mt]?.length)
.map(mt => ({ moduleType: mt, records: groupMap[mt] }))
// 等 DOM 渲染完成后自动调整 textarea 高度
nextTick(() => {
const textareas = document.querySelectorAll('.fix-all-drawer__textarea')
textareas.forEach(el => {
const ta = el as HTMLTextAreaElement
ta.style.height = 'auto'
ta.style.height = ta.scrollHeight + 'px'
})
})
}
// ==================== 事件处理 ====================
/** textarea 自适应高度 */
function autoResizeTextarea(event: Event) {
const el = event.target as HTMLTextAreaElement
el.style.height = 'auto'
el.style.height = el.scrollHeight + 'px'
}
/** 替换为此内容 — 将 AI 润色结果替换到原文编辑框 */
function handleReplaceToOriginal(record: RecordState) {
record.editText = record.polishText
// 触发 textarea 重新计算高度
nextTick(() => {
const textareas = document.querySelectorAll('.fix-all-drawer__textarea')
textareas.forEach(el => {
const ta = el as HTMLTextAreaElement
ta.style.height = 'auto'
ta.style.height = ta.scrollHeight + 'px'
})
})
}
/** 重新生成 — 调用 AI 润色接口获取新结果 */
async function handleRegenerate(record: RecordState) {
if (!record.issueId) return
// 收集当前原文编辑框内容作为输入
let content: string[]
if (record.moduleType === 'summary') {
content = [record.editText.trim()]
} else {
content = record.editText.split('\n').filter(line => line.trim() !== '')
}
if (!content.length) {
ElMessage.warning('原文内容为空,无法生成')
return
}
const loading = ElLoading.service({
lock: true,
text: 'AI 重新生成中…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
try {
const res = await polishDiagnosisIssue(content)
if (res.code === 0 && res.data?.content) {
// 将返回的数组内容用换行拼接
record.polishText = res.data.content.join('\n')
ElMessage.success('重新生成完成')
} else {
ElMessage.error(res.msg || '生成失败,请稍后重试')
}
} catch {
ElMessage.error('请求失败,请稍后重试')
} finally {
loading.close()
}
}
/** 一键使用AI润色结果 — 所有模块强制使用 AI 润色结果更新 */
function handleUseAllAi() {
const items: FixAllSubmitItem[] = []
for (const module of moduleDataList.value) {
for (const record of module.records) {
let newContent: string | DescriptionParagraph[]
if (record.moduleType === 'summary') {
// summary 直接用润色文本
newContent = record.polishText
} else {
// 经历模块:按换行拆分为 DescriptionParagraph[]
newContent = textToDescription(record.polishText)
}
items.push({
moduleType: record.moduleType,
recordId: record.recordId,
newContent,
issueId: record.issueId,
})
}
}
emit('useAllAi', items)
emit('update:modelValue', false)
}
/** 保存当前修改 — 只提交内容有变化的模块 */
function handleSaveCurrent() {
const items: FixAllSubmitItem[] = []
for (const module of moduleDataList.value) {
for (const record of module.records) {
// 对比当前编辑内容和原始内容是否有变化
if (record.editText.trim() === record.originalText.trim()) continue
let newContent: string | DescriptionParagraph[]
if (record.moduleType === 'summary') {
newContent = record.editText
} else {
newContent = textToDescription(record.editText)
}
items.push({
moduleType: record.moduleType,
recordId: record.recordId,
newContent,
issueId: record.issueId,
})
}
}
if (!items.length) {
ElMessage.info('没有检测到内容变化')
return
}
emit('save', items)
emit('update:modelValue', false)
}
/** 关闭抽屉 */
function handleClose() {
emit('update:modelValue', false)
}
</script>
+1 -1
View File
@@ -290,7 +290,7 @@ async function handleAiPolish() {
})
try {
const res = await polishDiagnosisIssue(issueId, content)
const res = await polishDiagnosisIssue(content)
if (res.code === 0 && res.data?.content) {
polishResult.value = res.data.content
ElMessage.success('润色完成')
+365 -857
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -449,7 +449,7 @@ function fillJobData(data: JobDetailData) {
job.title = data.jobTitle || ''
job.location = data.regionName || ''
job.experience = formatExperience(data.minExperience)
job.type = formatEmploymentType(data.employmentType)
job.type = formatEmploymentType(data.recruitCategory)
job.education = formatEducation(data.education)
job.salary = data.salary || ''
job.matchScore = data.matchScore ?? 0
+71 -19
View File
@@ -10,7 +10,7 @@
@save="handleSaveEdit"
/>
<!-- 欢迎上传简历弹窗无个人资料时自动弹出 -->
<!-- 欢迎上传简历弹窗无个人资料主表记录时自动弹出 -->
<ResumeUploadDialog v-model="showWelcomeDialog" @confirm="handleWelcomeUpload" />
<!-- 页面标题 -->
@@ -27,27 +27,12 @@
<ProfilePageContent
:profile="profile"
@edit="handleEdit"
@save="handleInlineSave"
@exp-update="handleExpUpdate"
@exp-add="handleExpAdd"
/>
<!-- 右侧入口 -->
<div class="profile-page__sidebar">
<!-- 提示卡片 -->
<div class="profile-page__tip-card">
<p class="profile-page__tip-text">
非常棒你的个人资料已经被完善了自动填写信息也已变换现在可以轻松开始投递简历啦
</p>
<button class="profile-page__tip-btn" @click="goToJobs">去投递</button>
</div>
<!-- 管理简历入口 -->
<div class="profile-page__nav-entry" @click="goToResume">
<span class="profile-page__nav-entry-icon">📄</span>
<span class="profile-page__nav-entry-text">管理我的简历</span>
<svg viewBox="0 0 16 16" fill="none" class="profile-page__nav-arrow">
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
</div>
</div>
</div>
@@ -658,4 +643,71 @@ function goToJobs() {
function goToResume() {
router.push('/resume')
}
/** 内联保存处理(个人信息/技能/证书模块从组件内直接保存) */
async function handleInlineSave(section: string, data: Record<string, any>) {
// 复用已有的 handleSaveEdit 逻辑
editModule.value = section
await handleSaveEdit(data)
}
/** 单条经历更新 — 将编辑后的数据替换到完整列表中再整体保存 */
async function handleExpUpdate(moduleType: string, index: number, data: Record<string, any>) {
try {
if (moduleType === 'education') {
const list = profile.value.education.map((item, i) => i === index ? data : item) as any[]
await saveEducation(list)
await loadEducation()
} else if (moduleType === 'work') {
const list = profile.value.works.map((item, i) => i === index ? data : item) as any[]
await saveWork(list)
await loadWork()
} else if (moduleType === 'internship') {
const list = profile.value.internships.map((item, i) => i === index ? data : item) as any[]
await saveInternship(list)
await loadInternship()
} else if (moduleType === 'project') {
const list = profile.value.projects.map((item, i) => i === index ? data : item) as any[]
await saveProject(list)
await loadProject()
} else if (moduleType === 'competition') {
const list = profile.value.competitions.map((item, i) => i === index ? data : item) as any[]
await saveCompetition(list)
await loadCompetition()
}
ElMessage.success('保存成功')
} catch {
ElMessage.error('保存失败')
}
}
/** 新增经历 */
async function handleExpAdd(moduleType: string, data: Record<string, any>) {
try {
if (moduleType === 'education') {
const list = [...profile.value.education, data] as any[]
await saveEducation(list)
await loadEducation()
} else if (moduleType === 'work') {
const list = [...profile.value.works, data] as any[]
await saveWork(list)
await loadWork()
} else if (moduleType === 'internship') {
const list = [...profile.value.internships, data] as any[]
await saveInternship(list)
await loadInternship()
} else if (moduleType === 'project') {
const list = [...profile.value.projects, data] as any[]
await saveProject(list)
await loadProject()
} else if (moduleType === 'competition') {
const list = [...profile.value.competitions, data] as any[]
await saveCompetition(list)
await loadCompetition()
}
ElMessage.success('添加成功')
} catch {
ElMessage.error('添加失败')
}
}
</script>
+56 -62
View File
@@ -25,7 +25,6 @@
<th class="resume-page__th">简历</th>
<th class="resume-page__th">目标岗位</th>
<th class="resume-page__th">最近修改</th>
<th class="resume-page__th">创建时间</th>
<th class="resume-page__th resume-page__th--action"></th>
</tr>
</thead>
@@ -38,38 +37,39 @@
>
<td class="resume-page__td">
<div class="resume-page__name-cell">
<span class="resume-page__avatar" :style="{ background: item.avatarColor }">
{{ item.avatarLetter }}
</span>
<!-- <span class="resume-page__avatar" :style="{ background: item.avatarColor }">-->
<!-- {{ item.avatarLetter }}-->
<!-- </span>-->
<span class="resume-page__name">{{ item.name }}</span>
<span v-if="item.isDefault" class="resume-page__default-tag">默认</span>
<!-- 编辑名称岗位图标 -->
<button
class="resume-page__edit-btn"
aria-label="编辑名称岗位"
@click.stop="handleEditName(item)"
>
<el-icon class="resume-page__edit-icon"><EditPen /></el-icon>
</button>
</div>
</td>
<td class="resume-page__td">{{ item.targetJob }}</td>
<td class="resume-page__td">{{ item.updatedAt }}</td>
<td class="resume-page__td">{{ item.createdAt }}</td>
<!-- 操作按钮列 -->
<td class="resume-page__td resume-page__td--action">
<div class="resume-page__actions">
<button
class="resume-page__more-btn"
aria-label="更多操作"
@click.stop="toggleMenu(item.id)"
>
<svg viewBox="0 0 16 16" fill="currentColor" class="resume-page__more-svg">
<circle cx="3" cy="8" r="1.5"/>
<circle cx="8" cy="8" r="1.5"/>
<circle cx="13" cy="8" r="1.5"/>
</svg>
</button>
<!-- 弹出菜单 -->
<div v-if="activeMenuId === item.id" class="resume-page__popup" @click.stop>
<div
v-for="action in popupActions"
:key="action"
class="resume-page__popup-item"
@click="handleAction(action, item.id)"
>
{{ action }}
</div>
v-if="!item.isDefault"
class="resume-page__action-btn"
@click.stop="handleSetDefault(item.id)"
>设为默认</button>
<button
class="resume-page__action-btn"
@click.stop="handleExport(item)"
>导出</button>
<button
class="resume-page__action-btn resume-page__action-btn--delete"
@click.stop="handleDelete(item.id)"
>删除</button>
</div>
</td>
</tr>
@@ -104,13 +104,14 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { EditPen } from '@element-plus/icons-vue'
import SideNav from '@/components/SideNav.vue'
import ResumeEditNameDialog from '@/components/ResumeEditNameDialog.vue'
import ResumeExportDialog from '@/components/ResumeExportDialog.vue'
import ResumeUploadDialog from '@/components/ResumeUploadDialog.vue'
import { uploadResume } from '@/utils/aiRequest'
import {
fetchResumeList, deleteResume, type ResumeListItem,
fetchResumeList, deleteResume, setDefaultResume, type ResumeListItem,
} from '@/api/resume'
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
// 命令式调用的组件,按需引入插件不会自动加载其样式,需手动引入
@@ -203,9 +204,6 @@ onMounted(() => {
// ==================== 页面状态 ====================
/** 当前打开弹出菜单的简历 ID */
const activeMenuId = ref<string | null>(null)
// ==================== 编辑简历名称弹窗状态 ====================
/** 编辑弹窗是否可见 */
@@ -231,62 +229,58 @@ const exportResumeName = ref('')
/** 上传弹窗是否可见 */
const uploadDialogVisible = ref(false)
/** 弹出菜单操作项 */
const popupActions = ['设为默认简历', '编辑名称岗位', '导出简历', '删除']
// ==================== 事件处理 ====================
/** 切换弹出菜单的显示/隐藏 */
function toggleMenu(id: string) {
activeMenuId.value = activeMenuId.value === id ? null : id
/** 点击编辑图标 — 打开编辑名称岗位弹窗 */
function handleEditName(item: ResumeItem) {
editResumeId.value = item.id
editResumeName.value = item.name
editTargetPosition.value = item.targetJob
editNameVisible.value = true
}
/** 弹出菜单操作点击 */
async function handleAction(action: string, id: string) {
activeMenuId.value = null
/** 设为默认简历 */
async function handleSetDefault(id: string) {
try {
const res = await setDefaultResume(id)
if (res.code === '0') {
ElMessage.success('已设为默认简历')
loadResumeList()
} else {
ElMessage.error(res.msg || '设置失败')
}
} catch {
ElMessage.error('设置失败,请稍后重试')
}
}
if (action === '删除') {
// 二次确认删除
/** 导出简历 */
function handleExport(item: ResumeItem) {
exportResumeId.value = item.id
exportResumeName.value = item.name || '简历'
exportDialogVisible.value = true
}
/** 删除简历 */
async function handleDelete(id: string) {
try {
await ElMessageBox.confirm('确定要删除这份简历吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
// 调用删除接口
const res = await deleteResume(id)
if (res.code === '0') {
ElMessage.success('删除成功')
// 刷新列表
loadResumeList()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (error) {
// 用户取消删除或接口报错
if (error !== 'cancel') {
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 {
console.log(action, id)
}
}
/** 上传简历 — 打开上传弹窗 */
+573 -214
View File
@@ -1,9 +1,10 @@
<template>
<template>
<div class="resume-detail dflex">
<SideNav />
<div class="resume-detail__content">
<!-- 个人资料编辑抽屉 -->
<!-- 个人资料编辑组件非内联模块时使用全局实例 -->
<ProfileEditDrawer
v-if="editModule !== 'info' && editModule !== 'summary' && editModule !== 'skills' && editModule !== 'certificate'"
v-model="showEditDrawer"
:module="editModule"
:initial-data="editInitialData"
@@ -29,6 +30,15 @@
@submit="handleFixSubmit"
/>
<!-- 一键修复抽屉全模块 -->
<ResumeIssueFixAllDrawer
v-model="showFixAllDrawer"
:issues="diagnosisIssues"
:original-records="allOriginalRecords"
@save="handleFixAllSave"
@use-all-ai="handleFixAllUseAi"
/>
<!-- 导出简历弹窗组件 -->
<ResumeExportDialog
v-model="exportDialogVisible"
@@ -46,92 +56,94 @@
/>
<!-- 顶部标题 -->
<h2 class="resume-detail__page-title">我的简历</h2>
<div class="resume-detail__page-title"><h2>我的简历</h2></div>
<!-- 顶部操作栏 -->
<div class="resume-detail__toolbar">
<div class="resume-detail__toolbar-left">
<button class="resume-detail__back-btn" @click="goBack" aria-label="返回">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__back-icon">
<path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<el-icon class="fs14 color-2"><ArrowLeft /></el-icon>返回
</button>
<span class="resume-detail__tab-name">{{ resumeMain.resumeName || '未命名简历' }}</span>
</div>
<div class="resume-detail__toolbar-right">
<button class="resume-detail__tool-btn" @click="handleEdit">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__tool-icon">
<path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
编辑简历信息
</button>
<button class="resume-detail__tool-btn" @click="handleExport">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__tool-icon">
<path d="M3 10v3h10v-3M8 2v8M5 5l3-3 3 3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
导出
</button>
<button class="resume-detail__tool-btn resume-detail__tool-btn--danger" @click="handleDelete">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__tool-icon">
<path d="M4 4h8l-.5 9H4.5L4 4zM6 4V2.5h4V4M2.5 4h11" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
删除
导出简历
</button>
</div>
</div>
<!-- 简历评分区域 -->
<div class="resume-detail__score-bar">
<div class="resume-detail__score-left">
<!-- 评级 -->
<span v-if="hasDiagnosis" class="resume-detail__score-avatar">
{{ diagnosisReport.grade }}
</span>
<!-- 有诊断报告时显示评级和查看链接 -->
<template v-if="hasDiagnosis">
<span class="resume-detail__score-badge">{{ gradeLabel }}</span>
<button class="resume-detail__score-link" @click="handleViewReport">查看评估报告 ></button>
</template>
</div>
<div class="resume-detail__score-right">
<!-- 有诊断报告时显示三项计数和重新诊断按钮 -->
<template v-if="hasDiagnosis">
<div class="resume-detail__score-item">
<!-- issues status===0 的子项汇总report 数据修复后未更新暂不使用 diagnosisReport.urgentTotal -->
<span class="resume-detail__score-num">{{ issuesUrgentTotal }}</span>
<span class="resume-detail__score-label">紧急修复</span>
</div>
<div class="resume-detail__score-item">
<!-- issues status===0 的子项汇总report 数据修复后未更新暂不使用 diagnosisReport.importantTotal -->
<span class="resume-detail__score-num">{{ issuesImportantTotal }}</span>
<span class="resume-detail__score-label">重点优化</span>
</div>
<div class="resume-detail__score-item">
<!-- issues status===0 的子项汇总report 数据修复后未更新暂不使用 diagnosisReport.expressionTotal -->
<span class="resume-detail__score-num">{{ issuesExpressionTotal }}</span>
<span class="resume-detail__score-label">表达提升</span>
</div>
<button class="resume-detail__diagnose-btn" @click="handleDiagnose">重新诊断</button>
</template>
<!-- 没有诊断报告时显示开始诊断按钮 -->
<template v-else>
<button class="resume-detail__diagnose-btn resume-detail__diagnose-btn--start" @click="handleDiagnose">开始诊断</button>
</template>
</div>
</div>
<!-- 简历主体内容 -->
<div class="resume-detail__body">
<!-- 简历评分区域 -->
<div class="resume-detail__score-bar" :class="scoreBarClass">
<div class="resume-detail__score-left">
<!-- 星星+底座组合 -->
<div class="resume-detail__star-wrapper">
<!-- SVG 五角星胖型短角 -->
<svg class="resume-detail__star-svg" viewBox="0 0 54 53" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M24.9956 1.45584C25.7174 0.181609 27.553 0.181662 28.2749 1.45584L35.4253 14.0769C35.8275 14.7869 36.5116 15.2941 37.3081 15.4715L51.2944 18.5847C52.6882 18.8952 53.2438 20.5831 52.3071 21.6609L42.7036 32.7117C42.1833 33.3104 41.9326 34.0971 42.0103 34.8865L43.4556 49.5652C43.5975 51.0087 42.1235 52.0649 40.8022 51.4666L27.8257 45.5867C27.0692 45.2441 26.2013 45.244 25.4448 45.5867L12.4692 51.4666C11.1479 52.0653 9.67395 51.0089 9.81592 49.5652L11.2612 34.8865C11.3389 34.0971 11.0882 33.3104 10.5679 32.7117L0.964355 21.6609C0.0276138 20.583 0.583121 18.8951 1.97705 18.5847L15.9624 15.4715C16.759 15.2941 17.444 14.787 17.8462 14.0769L24.9956 1.45584Z"
:fill="starFillColor"
:stroke="starStrokeColor"
stroke-width="1.3"
stroke-linejoin="round"
/>
<!-- 星星中间的文字等级字母或问号 -->
<text x="26" y="37" text-anchor="middle" :fill="starStrokeColor" font-size="24" font-weight="500">
{{ hasDiagnosis ? diagnosisReport.grade : '?' }}
</text>
</svg>
<!-- 底座长方形遮住星星下方两角 -->
<div class="resume-detail__star-base" :style="{ borderColor: starStrokeColor }">
<span :style="{ color: starStrokeColor }">{{ hasDiagnosis ? gradeLabel : '未知' }}</span>
</div>
</div>
<!-- 右侧操作按钮区 -->
<div class="resume-detail__score-actions">
<template v-if="hasDiagnosis">
<button class="resume-detail__score-link" @click="handleViewReport">查看评估报告</button>
<button class="resume-detail__score-link" @click="handleDiagnose">重新诊断</button>
</template>
<template v-else>
<span class="resume-detail__score-hint">暂无等级点击开始诊断简历</span>
</template>
</div>
</div>
<div class="resume-detail__score-right">
<!-- 有诊断报告时显示计数和重新诊断按钮 -->
<template v-if="hasDiagnosis">
<div class="resume-detail__score-item">
<span class="resume-detail__score-num" :style="{ color: starStrokeColor }">{{ issuesUrgentTotal+issuesImportantTotal+issuesExpressionTotal }}</span>
<span class="resume-detail__score-label" :style="{ color: starStrokeColor }">项待修复</span>
<button class="resume-detail__diagnose-btn" :style="{ background: starStrokeColor }" @click="handleFixAll">一键修复</button>
</div>
</template>
<!-- 没有诊断报告时显示开始诊断按钮 -->
<template v-else>
<button class="resume-detail__diagnose-btn" @click="handleDiagnose">开始诊断</button>
</template>
</div>
</div>
<!-- 个人信息卡片 -->
<div class="resume-detail__card">
<div class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'info' || !showEditDrawer }">
<!-- 展示模式 -->
<template v-if="editModule !== 'info' || !showEditDrawer">
<div class="resume-detail__card-header">
<div>
<h3 class="resume-detail__user-name">{{ resumeMain.name || '未填写姓名' }}</h3>
<p class="resume-detail__user-title">{{ resumeMain.targetPosition || '' }}</p>
</div>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('info')" aria-label="编辑个人信息">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<!-- 编辑按钮 hover时才显示品牌色图标+文字 -->
<button class="resume-detail__edit-btn-inline" @click="openEditDrawer('info')" aria-label="编辑个人信息">
<el-icon><EditPen /></el-icon>
<span>编辑</span>
</button>
</div>
<div class="resume-detail__contact">
@@ -166,226 +178,249 @@
</svg>
{{ resumeMain.wechatNumber }}
</span>
<!-- 作品集链接 -->
<span v-if="resumeMain.portfolioUrl" class="resume-detail__contact-item">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
<path d="M3 8a5 5 0 015-5h1a5 5 0 010 10H8a5 5 0 01-5-5z" stroke="currentColor" stroke-width="1.2"/>
<path d="M7 6l2 2-2 2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<a :href="resumeMain.portfolioUrl" target="_blank" rel="noopener noreferrer" class="resume-detail__portfolio-link">{{ resumeMain.portfolioUrl }}</a>
</span>
</div>
</template>
<!-- 编辑模式 内联编辑表单 -->
<ProfileEditDrawer
v-if="editModule === 'info' && showEditDrawer"
v-model="showEditDrawer"
:module="editModule"
:initial-data="editInitialData"
:use-education-text-value="true"
@save="handleSaveEdit"
@cancel="showEditDrawer = false"
/>
</div>
<!-- 个人概述 -->
<div v-if="resumeMain.summary" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">个人概述</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('summary')" aria-label="编辑个人概述">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<div class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'summary' || !showEditDrawer }">
<!-- 展示模式 -->
<template v-if="editModule !== 'summary' || !showEditDrawer">
<div class="resume-detail__section-header pl0">
<h3 class="resume-detail__section-title resume-detail__section-title--underline">个人概述</h3>
<!-- 有修复建议时显示标记hover 显示诊断详情气泡 -->
<span v-if="hasDiagnosis && hasModuleIssue('summary')" class="resume-detail__fix-tag">
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
建议使用AI润色修复 &gt;
<!-- hover 气泡 -->
<span class="resume-detail__fix-tooltip">{{ getSummaryFinding() }}</span>
</span>
<!-- 编辑按钮 hover时显示 -->
<button class="resume-detail__edit-btn-inline" @click="openEditDrawer('summary')" aria-label="编辑个人概述">
<el-icon><EditPen /></el-icon>
<span>编辑</span>
</button>
</div>
<p class="resume-detail__summary-text">{{ resumeMain.summary }}</p>
<!-- 个人概述的问题操作区仅诊断数据中有 summary 类型且 status===0 时显示 -->
<div v-for="issue in getIssuesByModule('summary')" :key="issue.id" v-show="issue.status === 0" class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getIssueTypeCounts(issue)" :key="t.value"
class="resume-detail__issue-type-btn resume-detail__issue-type-btn--active"
:class="{ 'resume-detail__issue-type-btn--active': getActiveType('summary', resumeId) === t.value }"
@click="setActiveType('summary', resumeId, t.value)"
>{{ t.count }} {{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('summary')">修复</button>
</div>
</div>
<!-- 作品集链接 -->
<div v-if="resumeMain.portfolioUrl" class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">作品集</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('portfolio')" aria-label="编辑作品集">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
</button>
</div>
<a :href="resumeMain.portfolioUrl" target="_blank" rel="noopener noreferrer" class="resume-detail__portfolio-link">
{{ resumeMain.portfolioUrl }}
</a>
</template>
<!-- 编辑模式 内联编辑表单 -->
<ProfileEditDrawer
v-if="editModule === 'summary' && showEditDrawer"
v-model="showEditDrawer"
:module="editModule"
:initial-data="editInitialData"
:use-education-text-value="true"
@save="handleSaveEdit"
@cancel="showEditDrawer = false"
/>
</div>
<!-- 教育背景经历 -->
<div v-if="educationList.length" class="resume-detail__card">
<div class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">教育背景</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('education')" aria-label="编辑教育背景">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<h3 class="resume-detail__section-title resume-detail__section-title--underline">教育背景</h3>
<!-- 添加按钮 hover模块时显示 -->
<button class="resume-detail__edit-btn-inline" @click="startAddExp('education')" aria-label="添加教育经历">
<el-icon><Plus /></el-icon>
<span>添加</span>
</button>
</div>
<!-- 新增经历编辑区显示在第一段经历前面 -->
<ResumeExpInlineEdit
v-if="expAddingModule === 'education'"
:module-type="'education'"
@save="handleExpAdd('education', $event)"
@cancel="expAddingModule = ''"
/>
<div v-for="edu in educationList" :key="edu.id" class="resume-detail__exp-item">
<!-- 展示模式 -->
<template v-if="expEditingId !== edu.id">
<div class="resume-detail__exp-row">
<span class="resume-detail__exp-name">{{ edu.school }}</span>
<span v-if="edu.major || edu.degree" class="resume-detail__exp-role">{{ edu.major }}{{ edu.degree ? '·' + edu.degree : '' }}</span>
<span v-if="edu.startDate || edu.endDate" class="resume-detail__exp-date">{{ edu.startDate }}<span v-if="edu.startDate && edu.endDate">-</span>{{ edu.endDate }}</span>
<span v-if="hasDiagnosis && getIssueByRecord('education', edu.id!) && getIssueByRecord('education', edu.id!)!.status === 0" class="resume-detail__fix-tag">
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
建议使用AI润色修复 &gt;&gt;
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('education', edu.id!)!.finding }}</span>
</span>
<!-- 编辑按钮 hover时显示 -->
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('education', edu)" aria-label="编辑">
<el-icon><EditPen /></el-icon>
<span>编辑</span>
</button>
</div>
<div v-for="edu in educationList" :key="edu.id" class="resume-detail__edu-item">
<div class="resume-detail__edu-degree">{{ edu.degree }} · {{ edu.major }}</div>
<div class="resume-detail__edu-meta">{{ edu.school }} · {{ edu.studyType }} · {{ edu.startDate }} - {{ edu.endDate }}</div>
<div v-if="edu.description?.length" class="resume-detail__desc-list">
<p v-for="p in edu.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
</div>
<!-- 每段教育经历的问题操作区status===0 待处理才显示 -->
<template v-if="hasDiagnosis && getIssueByRecord('education', edu.id!) && getIssueByRecord('education', edu.id!)!.status === 0">
<div class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getIssueTypeCounts(getIssueByRecord('education', edu.id!)!)" :key="t.value"
class="resume-detail__issue-type-btn resume-detail__issue-type-btn--active"
:class="{ 'resume-detail__issue-type-btn--active': getActiveType('education', edu.id!) === t.value }"
@click="setActiveType('education', edu.id!, t.value)"
>{{ t.count }} {{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('education', edu.id)">修复</button>
</div>
</template>
<!-- 编辑模式 -->
<ResumeExpInlineEdit
v-else
:module-type="'education'"
:initial-data="edu"
@save="handleExpUpdate('education', edu.id!, $event)"
@cancel="expEditingId = ''"
/>
</div>
</div>
<!-- 工作经历 -->
<div v-if="workList.length" class="resume-detail__card">
<div class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">工作经历</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('work')" aria-label="编辑工作经历">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<h3 class="resume-detail__section-title resume-detail__section-title--underline">工作经历</h3>
<button class="resume-detail__edit-btn-inline" @click="startAddExp('work')" aria-label="添加工作经历">
<el-icon><Plus /></el-icon>
<span>添加</span>
</button>
</div>
<ResumeExpInlineEdit v-if="expAddingModule === 'work'" :module-type="'work'" @save="handleExpAdd('work', $event)" @cancel="expAddingModule = ''" />
<div v-for="exp in workList" :key="exp.id" class="resume-detail__exp-item">
<div class="resume-detail__exp-title dflex">
<span>{{ exp.companyName }}</span>
<span v-if="exp.startDate||exp.endDate" class="color-5 fw400 fs12 pt5">{{ exp.startDate }} <span v-if="exp.startDate&&exp.endDate"> - </span> {{ exp.endDate }}</span>
<template v-if="expEditingId !== exp.id">
<div class="resume-detail__exp-row">
<span class="resume-detail__exp-name">{{ exp.companyName }}</span>
<span v-if="exp.position" class="resume-detail__exp-role">{{ exp.position }}</span>
<span v-if="exp.startDate || exp.endDate" class="resume-detail__exp-date">{{ exp.startDate }}<span v-if="exp.startDate && exp.endDate">-</span>{{ exp.endDate }}</span>
<span v-if="hasDiagnosis && getIssueByRecord('work', exp.id!) && getIssueByRecord('work', exp.id!)!.status === 0" class="resume-detail__fix-tag">
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
建议使用AI润色修复 &gt;&gt;
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('work', exp.id!)!.finding }}</span>
</span>
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('work', exp)" aria-label="编辑">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
</div>
<div class="resume-detail__exp-meta">{{ exp.position }} </div>
<ul v-if="exp.description?.length" class="resume-detail__desc-list pl16">
<li v-for="p in exp.description" :key="p.id" class="resume-detail__desc-item fs12 color-5">{{ p.text }}</li>
<li v-for="p in exp.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</li>
</ul>
<!-- 每段工作经历的问题操作区status===0 待处理才显示 -->
<template v-if="hasDiagnosis && getIssueByRecord('work', exp.id!) && getIssueByRecord('work', exp.id!)!.status === 0">
<div class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getIssueTypeCounts(getIssueByRecord('work', exp.id!)!)" :key="t.value"
class="resume-detail__issue-type-btn resume-detail__issue-type-btn--active"
:class="{ 'resume-detail__issue-type-btn--active': getActiveType('work', exp.id!) === t.value }"
@click="setActiveType('work', exp.id!, t.value)"
>{{ t.count }} {{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('work', exp.id)">修复</button>
</div>
</template>
<ResumeExpInlineEdit v-else :module-type="'work'" :initial-data="exp" @save="handleExpUpdate('work', exp.id!, $event)" @cancel="expEditingId = ''" />
</div>
</div>
<!-- 实习经历 -->
<div v-if="internshipList.length" class="resume-detail__card">
<div class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">实习经历</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('internship')" aria-label="编辑实习经历">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<h3 class="resume-detail__section-title resume-detail__section-title--underline">实习经历</h3>
<button class="resume-detail__edit-btn-inline" @click="startAddExp('internship')" aria-label="添加实习经历">
<el-icon><Plus /></el-icon><span>添加</span>
</button>
</div>
<ResumeExpInlineEdit v-if="expAddingModule === 'internship'" :module-type="'internship'" @save="handleExpAdd('internship', $event)" @cancel="expAddingModule = ''" />
<div v-for="intern in internshipList" :key="intern.id" class="resume-detail__exp-item">
<div class="resume-detail__exp-title dflex">
<span>{{ intern.position }}</span>
<span v-if="intern.startDate||intern.endDate" class="color-5 fw400 fs12 pt5">{{ intern.startDate }} <span v-if="intern.startDate&&intern.endDate"> - </span> {{ intern.endDate }}</span>
<template v-if="expEditingId !== intern.id">
<div class="resume-detail__exp-row">
<span class="resume-detail__exp-name">{{ intern.companyName }}</span>
<span v-if="intern.position" class="resume-detail__exp-role">{{ intern.position }}</span>
<span v-if="intern.startDate || intern.endDate" class="resume-detail__exp-date">{{ intern.startDate }}<span v-if="intern.startDate && intern.endDate">-</span>{{ intern.endDate }}</span>
<span v-if="hasDiagnosis && getIssueByRecord('internship', intern.id!) && getIssueByRecord('internship', intern.id!)!.status === 0" class="resume-detail__fix-tag">
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
建议使用AI润色修复 &gt;&gt;
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('internship', intern.id!)!.finding }}</span>
</span>
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('internship', intern)" aria-label="编辑">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
</div>
<div class="resume-detail__exp-meta">{{ intern.companyName }}</div>
<div v-if="intern.description?.length" class="resume-detail__desc-list">
<p v-for="p in intern.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
</div>
<!-- 每段实习经历的问题操作区status===0 待处理才显示 -->
<template v-if="hasDiagnosis && getIssueByRecord('internship', intern.id!) && getIssueByRecord('internship', intern.id!)!.status === 0">
<div class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getIssueTypeCounts(getIssueByRecord('internship', intern.id!)!)" :key="t.value"
class="resume-detail__issue-type-btn resume-detail__issue-type-btn--active"
:class="{ 'resume-detail__issue-type-btn--active': getActiveType('internship', intern.id!) === t.value }"
@click="setActiveType('internship', intern.id!, t.value)"
>{{ t.count }} {{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('internship', intern.id)">修复</button>
</div>
</template>
<ResumeExpInlineEdit v-else :module-type="'internship'" :initial-data="intern" @save="handleExpUpdate('internship', intern.id!, $event)" @cancel="expEditingId = ''" />
</div>
</div>
<!-- 项目经历 -->
<div v-if="projectList.length" class="resume-detail__card">
<div class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">项目经历</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('project')" aria-label="编辑项目经历">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<h3 class="resume-detail__section-title resume-detail__section-title--underline">项目经历</h3>
<button class="resume-detail__edit-btn-inline" @click="startAddExp('project')" aria-label="添加项目经历">
<el-icon><Plus /></el-icon><span>添加</span>
</button>
</div>
<ResumeExpInlineEdit v-if="expAddingModule === 'project'" :module-type="'project'" @save="handleExpAdd('project', $event)" @cancel="expAddingModule = ''" />
<div v-for="proj in projectList" :key="proj.id" class="resume-detail__exp-item">
<div class="resume-detail__exp-title dflex">
<span>{{ proj.projectName }}</span>
<span v-if="proj.startDate||proj.endDate" class="color-5 fw400 fs12 pt5">{{ proj.startDate }} <span v-if="proj.startDate&&proj.endDate"> - </span> {{ proj.endDate }}</span>
<template v-if="expEditingId !== proj.id">
<div class="resume-detail__exp-row">
<span class="resume-detail__exp-name">{{ proj.projectName }}</span>
<span v-if="proj.role" class="resume-detail__exp-role">{{ proj.role }}</span>
<span v-if="proj.startDate || proj.endDate" class="resume-detail__exp-date">{{ proj.startDate }}<span v-if="proj.startDate && proj.endDate">-</span>{{ proj.endDate }}</span>
<span v-if="hasDiagnosis && getIssueByRecord('project', proj.id!) && getIssueByRecord('project', proj.id!)!.status === 0" class="resume-detail__fix-tag">
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
建议使用AI润色修复 &gt;&gt;
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('project', proj.id!)!.finding }}</span>
</span>
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('project', proj)" aria-label="编辑">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
</div>
<div class="resume-detail__exp-meta">{{ proj.companyName }} · {{ proj.role }}</div>
<div v-if="proj.description?.length" class="resume-detail__desc-list">
<p v-for="p in proj.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
</div>
<!-- 每段项目经历的问题操作区status===0 待处理才显示 -->
<template v-if="hasDiagnosis && getIssueByRecord('project', proj.id!) && getIssueByRecord('project', proj.id!)!.status === 0">
<div class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getIssueTypeCounts(getIssueByRecord('project', proj.id!)!)" :key="t.value"
class="resume-detail__issue-type-btn resume-detail__issue-type-btn--active"
:class="{ 'resume-detail__issue-type-btn--active': getActiveType('project', proj.id!) === t.value }"
@click="setActiveType('project', proj.id!, t.value)"
>{{ t.count }} {{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('project', proj.id)">修复</button>
</div>
</template>
<ResumeExpInlineEdit v-else :module-type="'project'" :initial-data="proj" @save="handleExpUpdate('project', proj.id!, $event)" @cancel="expEditingId = ''" />
</div>
</div>
<!-- 竞赛经历 -->
<div v-if="competitionList.length" class="resume-detail__card">
<div class="resume-detail__card">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">竞赛经历</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('competition')" aria-label="编辑竞赛经历">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<h3 class="resume-detail__section-title resume-detail__section-title--underline">竞赛经历</h3>
<button class="resume-detail__edit-btn-inline" @click="startAddExp('competition')" aria-label="添加竞赛经历">
<el-icon><Plus /></el-icon><span>添加</span>
</button>
</div>
<ResumeExpInlineEdit v-if="expAddingModule === 'competition'" :module-type="'competition'" @save="handleExpAdd('competition', $event)" @cancel="expAddingModule = ''" />
<div v-for="comp in competitionList" :key="comp.id" class="resume-detail__exp-item">
<div class="resume-detail__exp-title dflex">
<span>{{ comp.competitionName }}</span>
<span v-if="comp.awardDate" class="color-5 fw400 fs12 pt5">{{ comp.awardDate }}</span>
<template v-if="expEditingId !== comp.id">
<div class="resume-detail__exp-row">
<span class="resume-detail__exp-name">{{ comp.competitionName }}</span>
<span v-if="comp.award" class="resume-detail__exp-role">{{ comp.award }}</span>
<span v-if="comp.awardDate" class="resume-detail__exp-date">{{ comp.awardDate }}</span>
<span v-if="hasDiagnosis && getIssueByRecord('competition', comp.id!) && getIssueByRecord('competition', comp.id!)!.status === 0" class="resume-detail__fix-tag">
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
建议使用AI润色修复 &gt;&gt;
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('competition', comp.id!)!.finding }}</span>
</span>
<button class="resume-detail__edit-btn-inline resume-detail__exp-edit-btn" @click="startEditExp('competition', comp)" aria-label="编辑">
<el-icon><EditPen /></el-icon><span>编辑</span>
</button>
</div>
<div class="resume-detail__exp-meta">{{ comp.award }}</div>
<div v-if="comp.description?.length" class="resume-detail__desc-list">
<p v-for="p in comp.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
</div>
<!-- 每段竞赛经历的问题操作区status===0 待处理才显示 -->
<template v-if="hasDiagnosis && getIssueByRecord('competition', comp.id!) && getIssueByRecord('competition', comp.id!)!.status === 0">
<div class="resume-detail__card-issue">
<div class="resume-detail__issue-type-group">
<button
v-for="t in getIssueTypeCounts(getIssueByRecord('competition', comp.id!)!)" :key="t.value"
class="resume-detail__issue-type-btn resume-detail__issue-type-btn--active"
:class="{ 'resume-detail__issue-type-btn--active': getActiveType('competition', comp.id!) === t.value }"
@click="setActiveType('competition', comp.id!, t.value)"
>{{ t.count }} {{ t.label }}</button>
</div>
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('competition', comp.id)">修复</button>
</div>
</template>
<ResumeExpInlineEdit v-else :module-type="'competition'" :initial-data="comp" @save="handleExpUpdate('competition', comp.id!, $event)" @cancel="expEditingId = ''" />
</div>
</div>
<!-- 技能 -->
<div v-if="resumeMain.skills?.length" class="resume-detail__card">
<div class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'skills' || !showEditDrawer }">
<template v-if="editModule !== 'skills' || !showEditDrawer">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">技能</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('skills')" aria-label="编辑技能">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<!-- 编辑按钮 hover时显示 -->
<button class="resume-detail__edit-btn-inline" @click="openEditDrawer('skills')" aria-label="编辑技能">
<el-icon><EditPen /></el-icon>
<span>编辑</span>
</button>
</div>
<div class="resume-detail__skills">
@@ -393,15 +428,28 @@
{{ skill }}
</span>
</div>
</template>
<!-- 编辑模式 -->
<ProfileEditDrawer
v-if="editModule === 'skills' && showEditDrawer"
v-model="showEditDrawer"
:module="editModule"
:initial-data="editInitialData"
:use-education-text-value="true"
@save="handleSaveEdit"
@cancel="showEditDrawer = false"
/>
</div>
<!-- 证书 -->
<div v-if="resumeMain.certificates?.length" class="resume-detail__card">
<div class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'certificate' || !showEditDrawer }">
<template v-if="editModule !== 'certificate' || !showEditDrawer">
<div class="resume-detail__section-header">
<h3 class="resume-detail__section-title">证书</h3>
<!-- 编辑按钮 -->
<button class="resume-detail__edit-btn" @click="openEditDrawer('certificate')" aria-label="编辑证书">
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
<!-- 编辑按钮 hover时显示 -->
<button class="resume-detail__edit-btn-inline" @click="openEditDrawer('certificate')" aria-label="编辑证书">
<el-icon><EditPen /></el-icon>
<span>编辑</span>
</button>
</div>
<div class="resume-detail__skills">
@@ -409,6 +457,17 @@
{{ cert }}
</span>
</div>
</template>
<!-- 编辑模式 -->
<ProfileEditDrawer
v-if="editModule === 'certificate' && showEditDrawer"
v-model="showEditDrawer"
:module="editModule"
:initial-data="editInitialData"
:use-education-text-value="true"
@save="handleSaveEdit"
@cancel="showEditDrawer = false"
/>
</div>
</div>
</div>
@@ -425,8 +484,11 @@ import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import SideNav from '@/components/SideNav.vue'
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
import ResumeExpInlineEdit from '@/components/ResumeExpInlineEdit.vue'
import ResumeAnalysisReportDrawer from '@/components/ResumeAnalysisReportDrawer.vue'
import ResumeIssueFixDrawer from '@/components/ResumeIssueFixDrawer.vue'
import ResumeIssueFixAllDrawer from '@/components/ResumeIssueFixAllDrawer.vue'
import type { FixAllSubmitItem, OriginalRecordData } from '@/components/ResumeIssueFixAllDrawer.vue'
import ResumeExportDialog from '@/components/ResumeExportDialog.vue'
import ResumeEditNameDialog from '@/components/ResumeEditNameDialog.vue'
import MemberAccessDialog from '@/components/MemberAccessDialog.vue'
@@ -458,6 +520,7 @@ import {
updateResumeCompetition,
deleteResumeCompetition,
deleteResume,
resolveDiagnosisIssue,
type ResumeMainData,
type ResumeEducation,
type ResumeWork,
@@ -468,6 +531,7 @@ import {
type DiagnosisIssue,
type DescriptionParagraph,
} from '@/api/resume'
import {ArrowLeft, EditPen, Plus} from "@element-plus/icons-vue";
// ==================== 诊断数据相关 ====================
@@ -494,6 +558,27 @@ const gradeLabel = computed(() => {
return map[diagnosisReport.value.grade || ''] || '未评级'
})
/** 星星边框/线条颜色 */
const starStrokeColor = computed(() => {
if (!hasDiagnosis.value) return '#858585'
if (diagnosisReport.value.grade === 'A') return '#12C7BE'
return '#FF8D0B'
})
/** 星星填充背景色 */
const starFillColor = computed(() => {
if (!hasDiagnosis.value) return '#F4F4F4'
if (diagnosisReport.value.grade === 'A') return '#ECFDFC'
return '#FFF5E9'
})
/** 评分区域外层 class(控制背景渐变) */
const scoreBarClass = computed(() => {
if (!hasDiagnosis.value) return ''
if (diagnosisReport.value.grade === 'A') return 'resume-detail__score-bar--grade-a'
return 'resume-detail__score-bar--grade-bcd'
})
/**
* 从 issues 中 status===0 的子项汇总紧急修复数
* (report 数据修复后未实时更新,暂用 issues 汇总代替 diagnosisReport.urgentTotal,等后面接口改了用回来)
@@ -532,6 +617,17 @@ function getIssuesByModule(moduleType: string): DiagnosisIssue[] {
return diagnosisIssues.value.filter(i => i.moduleType === moduleType)
}
/** 判断某个模块是否存在待处理的诊断问题 */
function hasModuleIssue(moduleType: string): boolean {
return diagnosisIssues.value.some(i => i.moduleType === moduleType && i.status === 0)
}
/** 获取个人概述的 finding 文字(用于 hover 气泡) */
function getSummaryFinding(): string {
const issue = diagnosisIssues.value.find(i => i.moduleType === 'summary' && i.status === 0)
return issue?.finding || ''
}
/** 根据模块类型和列表索引获取对应的 issue(用于 ID 不匹配的模块) */
function getIssueByIndex(moduleType: string, index: number): DiagnosisIssue | undefined {
return getIssuesByModule(moduleType)[index]
@@ -847,6 +943,212 @@ function getOriginalContent(moduleType: string, recordId: string): string | Desc
return record?.description || []
}
// ==================== 一键修复抽屉(全模块)相关 ====================
/** 一键修复抽屉显示状态 */
const showFixAllDrawer = ref(false)
/** 构建所有模块的原始经历数据(传给一键修复组件) */
const allOriginalRecords = computed<OriginalRecordData[]>(() => {
const records: OriginalRecordData[] = []
// summary
if (resumeMain.value.summary) {
records.push({
moduleType: 'summary',
recordId: resumeId,
recordLabel: '',
originalContent: resumeMain.value.summary,
})
}
// 教育经历
for (const edu of educationList.value) {
if (!edu.id) continue
records.push({
moduleType: 'education',
recordId: edu.id,
recordLabel: `${edu.school || ''}${edu.major ? '-' + edu.major : ''}-经历描述`,
originalContent: edu.description || [],
})
}
// 工作经历
for (const work of workList.value) {
if (!work.id) continue
records.push({
moduleType: 'work',
recordId: work.id,
recordLabel: `${work.companyName || ''}${work.position ? '-' + work.position : ''}-经历描述`,
originalContent: work.description || [],
})
}
// 实习经历
for (const intern of internshipList.value) {
if (!intern.id) continue
records.push({
moduleType: 'internship',
recordId: intern.id,
recordLabel: `${intern.companyName || ''}${intern.position ? '-' + intern.position : ''}-经历描述`,
originalContent: intern.description || [],
})
}
// 项目经历
for (const proj of projectList.value) {
if (!proj.id) continue
records.push({
moduleType: 'project',
recordId: proj.id,
recordLabel: `${proj.projectName || ''}${proj.role ? '-' + proj.role : ''}-经历描述`,
originalContent: proj.description || [],
})
}
// 竞赛经历
for (const comp of competitionList.value) {
if (!comp.id) continue
records.push({
moduleType: 'competition',
recordId: comp.id,
recordLabel: `${comp.competitionName || ''}${comp.award ? '-' + comp.award : ''}-经历描述`,
originalContent: comp.description || [],
})
}
return records
})
/** 点击一键修复按钮 — 打开全模块修复抽屉 */
function handleFixAll() {
showFixAllDrawer.value = true
}
/** 将 DescriptionParagraph[] 中所有 text 拼接为字符串,用于内容对比 */
function joinDescriptionText(desc: DescriptionParagraph[]): string {
return desc.map(d => d.text || '').join('\n')
}
/** 一键修复 - 保存当前修改(只更新有变化的模块) */
async function handleFixAllSave(items: FixAllSubmitItem[]) {
if (!items.length) return
const loading = ElLoading.service({
lock: true,
text: '正在保存修改…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
try {
for (const item of items) {
// 检查内容是否与简历原内容一致
const hasChanged = checkContentChanged(item)
if (!hasChanged) continue
// 更新简历内容
await updateModuleContent(item)
// 上报 resolve
await resolveDiagnosisIssue(item.issueId)
}
// 刷新数据
await Promise.all([loadResumeDetail(), loadDiagnosis()])
ElMessage.success('修改已保存')
} catch {
ElMessage.error('保存失败,请稍后重试')
} finally {
loading.close()
}
}
/** 一键使用AI润色结果 — 所有模块强制更新 */
async function handleFixAllUseAi(items: FixAllSubmitItem[]) {
if (!items.length) return
const loading = ElLoading.service({
lock: true,
text: '正在应用AI润色结果…',
background: 'rgba(0, 0, 0, 0.5)',
customClass: 'resume-upload-loading',
})
try {
for (const item of items) {
// 强制更新简历内容
await updateModuleContent(item)
// 所有 issue 都要上报 resolve
await resolveDiagnosisIssue(item.issueId)
}
// 刷新数据
await Promise.all([loadResumeDetail(), loadDiagnosis()])
ElMessage.success('AI润色结果已全部应用')
} catch {
ElMessage.error('操作失败,请稍后重试')
} finally {
loading.close()
}
}
/** 检查提交的内容是否与简历当前内容不同 */
function checkContentChanged(item: FixAllSubmitItem): boolean {
if (item.moduleType === 'summary') {
// summary 直接对比字符串
const current = resumeMain.value.summary || ''
return current.trim() !== (item.newContent as string).trim()
}
// 经历模块:找到对应记录,对比 description 文本
const listMap: Record<string, any[]> = {
education: educationList.value,
work: workList.value,
internship: internshipList.value,
project: projectList.value,
competition: competitionList.value,
}
const list = listMap[item.moduleType]
if (!list) return false
const record = list.find((r: any) => r.id === item.recordId)
if (!record?.description) return true
const currentText = joinDescriptionText(record.description)
const newText = joinDescriptionText(item.newContent as DescriptionParagraph[])
return currentText.trim() !== newText.trim()
}
/** 根据模块类型更新简历内容 */
async function updateModuleContent(item: FixAllSubmitItem) {
if (item.moduleType === 'summary') {
// 更新个人概述
const newSummary = item.newContent as string
resumeMain.value.summary = newSummary
await saveResumeMain({ resumeId, summary: newSummary })
return
}
// 经历模块:找到记录并更新 description
const listMap: Record<string, any[]> = {
education: educationList.value,
work: workList.value,
internship: internshipList.value,
project: projectList.value,
competition: competitionList.value,
}
const list = listMap[item.moduleType]
if (!list) return
const record = list.find((r: any) => r.id === item.recordId)
if (record) {
// 替换 description
record.description = item.newContent as DescriptionParagraph[]
// 调用对应模块的编辑接口
await updateSingleRecord(item.moduleType, record)
}
}
/** 修复抽屉提交新版本 — 更新本地数据、调用保存接口、刷新页面 */
async function handleFixSubmit(content: string[]) {
const loading = ElLoading.service({
@@ -952,6 +1254,60 @@ async function updateSingleRecord(moduleType: string, record: any) {
}
}
// ==================== 单条经历内联编辑相关 ====================
/** 当前正在编辑的经历记录 ID */
const expEditingId = ref('')
/** 当前正在新增经历的模块类型 */
const expAddingModule = ref('')
/** 开始编辑某条经历 */
function startEditExp(_moduleType: string, record: any) {
expEditingId.value = record.id
expAddingModule.value = ''
}
/** 开始添加新经历 */
function startAddExp(moduleType: string) {
expAddingModule.value = moduleType
expEditingId.value = ''
}
/** 保存编辑的经历(更新) */
async function handleExpUpdate(moduleType: string, recordId: string, data: Record<string, any>) {
try {
await updateSingleRecord(moduleType, { ...data, id: recordId })
await loadResumeDetail()
expEditingId.value = ''
ElMessage.success('保存成功')
} catch {
ElMessage.error('保存失败')
}
}
/** 保存新增的经历 */
async function handleExpAdd(moduleType: string, data: Record<string, any>) {
try {
const addMap: Record<string, (id: string, d: any) => Promise<any>> = {
education: addResumeEducation,
work: addResumeWork,
internship: addResumeInternship,
project: addResumeProject,
competition: addResumeCompetition,
}
const addFn = addMap[moduleType]
if (addFn) {
await addFn(resumeId, data)
await loadResumeDetail()
expAddingModule.value = ''
ElMessage.success('添加成功')
}
} catch {
ElMessage.error('添加失败')
}
}
// ==================== 编辑抽屉相关 ====================
/** 编辑抽屉的显示状态 */
@@ -989,6 +1345,7 @@ function openEditDrawer(section: string) {
phone: resumeMain.value.mobileNumber || '',
location: '',
wechat: resumeMain.value.wechatNumber || '',
portfolioUrl: resumeMain.value.portfolioUrl || '',
}
} else if (section === 'summary') {
editInitialData.value = {
@@ -1082,7 +1439,7 @@ function openEditDrawer(section: string) {
async function handleSaveEdit(data: Record<string, any>) {
try {
if (editModule.value === 'info') {
// 保存基本信息到简历主表
// 保存基本信息到简历主表(含作品集链接)
await saveResumeMain({
resumeId,
name: data.name,
@@ -1090,12 +1447,14 @@ async function handleSaveEdit(data: Record<string, any>) {
mobileNumber: data.phone,
city: data.locationName || data.location || '',
wechatNumber: data.wechat,
portfolioUrl: data.portfolioUrl,
})
resumeMain.value.name = data.name
resumeMain.value.email = data.email
resumeMain.value.mobileNumber = data.phone
resumeMain.value.city = data.locationName || data.location || ''
resumeMain.value.wechatNumber = data.wechat
resumeMain.value.portfolioUrl = data.portfolioUrl
} else if (editModule.value === 'summary') {
// 保存个人概述到简历主表
await saveResumeMain({