Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dbeddcb8f | |||
| f52a7c56f7 | |||
| a9638fc7ec | |||
| 91992a2443 | |||
| b666f0047d | |||
| 42768cc4c5 | |||
| 833533f780 | |||
| 8b6d424b2f | |||
| 4b16341f92 | |||
| 12baa93e58 | |||
| a3740688ab |
@@ -1,5 +1,3 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# ==================== 第一阶段:构建 ====================
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /build
|
||||
|
||||
Vendored
+90
-96
@@ -1,15 +1,8 @@
|
||||
/**
|
||||
* OfferPie Web 前端蓝绿部署流水线
|
||||
*
|
||||
* 工作目录说明:
|
||||
* - Jenkins 会自动为每个项目创建独立的工作空间:/var/jenkins_home/workspace/<项目名>/
|
||||
* - docker-compose.yml、Dockerfile、nginx.conf 都在项目根目录,直接执行即可
|
||||
* - nginx.conf 首次部署时 docker cp 进 nginx 容器
|
||||
*
|
||||
* 内层 Nginx 职责:
|
||||
* - 蓝绿切换(前端静态文件 serve)
|
||||
* - /api/ 代理到 Java 后端(10202)
|
||||
* - /ai-api/ 代理到 Python AI(10502),rewrite 去掉前缀
|
||||
* 架构:Jenkins 本地编译 → scp 镜像到目标机 → SSH 远程蓝绿切换
|
||||
* 目标机目录:/opt/offerpie/web/
|
||||
*/
|
||||
pipeline {
|
||||
agent any
|
||||
@@ -19,14 +12,28 @@ pipeline {
|
||||
}
|
||||
|
||||
environment {
|
||||
CONTAINER_PREFIX = 'offerpie-web' // 容器名前缀,拼接 -blue / -green / -nginx
|
||||
HEALTH_URL = 'http://localhost:80/' // 前端静态页面健康检查
|
||||
// 目标服务器配置
|
||||
DEPLOY_HOST = '8.148.237.97'
|
||||
DEPLOY_PORT = '22'
|
||||
DEPLOY_USER = 'root'
|
||||
DEPLOY_PASS = 'Mzpy520@126.com'
|
||||
|
||||
// 项目配置
|
||||
IMAGE_NAME = 'offerpie-web'
|
||||
IMAGE_TAG = 'latest'
|
||||
CONTAINER_PREFIX = 'offerpie-web'
|
||||
REMOTE_DIR = '/opt/offerpie/web'
|
||||
HEALTH_URL = 'http://localhost:80/'
|
||||
|
||||
// SSH 命令前缀
|
||||
SSH_CMD = "sshpass -p '${DEPLOY_PASS}' ssh -o StrictHostKeyChecking=no -p ${DEPLOY_PORT} ${DEPLOY_USER}@${DEPLOY_HOST}"
|
||||
SCP_CMD = "sshpass -p '${DEPLOY_PASS}' scp -o StrictHostKeyChecking=no -P ${DEPLOY_PORT}"
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('开始提示') {
|
||||
stage('环境检查') {
|
||||
steps {
|
||||
echo "OfferPie Web 前端开始构建"
|
||||
sh 'sshpass -V'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,23 +41,41 @@ pipeline {
|
||||
steps {
|
||||
echo "拉取 ${params.BRANCH} 分支代码"
|
||||
git branch: "${params.BRANCH}",
|
||||
credentialsId: 'ef5fffc1-9b35-403d-9ca6-e1b73eb0e45a',
|
||||
url: 'https://codeup.aliyun.com/5f0ed3b9769820a3e817dee2/offerpie/offerpie_web.git'
|
||||
credentialsId: 'gitea-fab089c1-b55d-4b58-9fad',
|
||||
url: 'http://git.jianshixingqiu.com/offerpai/offerpai_web.git'
|
||||
}
|
||||
}
|
||||
|
||||
stage('本地编译') {
|
||||
steps {
|
||||
echo "开始构建镜像"
|
||||
sh "docker build -t ${IMAGE_NAME}:${IMAGE_TAG} ."
|
||||
echo "导出镜像"
|
||||
sh "docker save -o ${IMAGE_NAME}.tar ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
}
|
||||
}
|
||||
|
||||
stage('文件传输') {
|
||||
steps {
|
||||
echo "传输文件到目标服务器"
|
||||
sh "${SSH_CMD} 'mkdir -p ${REMOTE_DIR}'"
|
||||
sh "${SCP_CMD} ${IMAGE_NAME}.tar ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/"
|
||||
sh "${SCP_CMD} proxy_nginx.conf ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/proxy_nginx.conf"
|
||||
sh "${SCP_CMD} docker-compose.yml ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/docker-compose.yml"
|
||||
}
|
||||
}
|
||||
|
||||
stage('加载镜像') {
|
||||
steps {
|
||||
sh "${SSH_CMD} 'docker load < ${REMOTE_DIR}/${IMAGE_NAME}.tar'"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测部署目标
|
||||
* - 检查 blue 和 green 容器的运行状态
|
||||
* - blue 在运行 → 部署 green;green 在运行 → 部署 blue
|
||||
* - 都不在或都在 → 默认部署 blue
|
||||
*/
|
||||
stage('检测部署目标') {
|
||||
steps {
|
||||
echo "检查当前容器状态"
|
||||
script {
|
||||
def blueRunning = sh(script: "docker ps -q -f name=${CONTAINER_PREFIX}-blue", returnStdout: true).trim()
|
||||
def greenRunning = sh(script: "docker ps -q -f name=${CONTAINER_PREFIX}-green", returnStdout: true).trim()
|
||||
def blueRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-blue'", returnStdout: true).trim()
|
||||
def greenRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-green'", returnStdout: true).trim()
|
||||
|
||||
env.DEPLOY_TARGET = ''
|
||||
|
||||
@@ -71,70 +96,42 @@ pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建新版本
|
||||
* - 先清理目标颜色的旧容器(如果残留)
|
||||
* - docker-compose build 构建新镜像(含 pnpm build)
|
||||
*/
|
||||
stage('构建新版本') {
|
||||
steps {
|
||||
script {
|
||||
def existingContainer = sh(script: "docker ps -aq -f name=${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}", returnStdout: true).trim()
|
||||
if (existingContainer) {
|
||||
echo "清理已存在的容器: ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}"
|
||||
sh "docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}"
|
||||
}
|
||||
sh "docker-compose build ${env.DEPLOY_TARGET}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Nginx
|
||||
* - 检查 nginx 容器是否存在
|
||||
* - 不存在则启动并复制配置文件(首次部署)
|
||||
* - 存在则确保容器运行中
|
||||
*/
|
||||
stage('检查Nginx') {
|
||||
steps {
|
||||
script {
|
||||
def nginxExists = sh(script: "docker ps -aq -f name=${CONTAINER_PREFIX}-nginx", returnStdout: true).trim()
|
||||
if (!nginxExists) {
|
||||
echo "首次部署,初始化 Nginx 容器"
|
||||
sh "docker-compose up -d nginx"
|
||||
sh "sleep 3"
|
||||
// 复制配置文件到容器内
|
||||
sh "docker cp proxy_nginx.conf ${CONTAINER_PREFIX}-nginx:/etc/nginx/nginx.conf"
|
||||
sh "docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload"
|
||||
} else {
|
||||
def nginxRunning = sh(script: "docker ps -q -f name=${CONTAINER_PREFIX}-nginx", returnStdout: true).trim()
|
||||
if (!nginxRunning) {
|
||||
echo "Nginx 容器已停止,重新启动"
|
||||
sh "docker start ${CONTAINER_PREFIX}-nginx"
|
||||
sh "sleep 2"
|
||||
}
|
||||
echo "Nginx 容器已存在且运行中"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('启动新版本') {
|
||||
steps {
|
||||
script {
|
||||
sh "docker-compose up -d ${env.DEPLOY_TARGET}"
|
||||
// 等待 Nginx 容器启动
|
||||
def existingContainer = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'", returnStdout: true).trim()
|
||||
if (existingContainer) {
|
||||
sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'"
|
||||
}
|
||||
sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d ${env.DEPLOY_TARGET}'"
|
||||
sh 'sleep 5'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
* - 进入容器检测静态页面是否正常返回
|
||||
* - 最多重试 3 次,每次间隔 5 秒
|
||||
* - 全部失败则终止部署流程
|
||||
*/
|
||||
stage('检查Nginx') {
|
||||
steps {
|
||||
script {
|
||||
def nginxExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim()
|
||||
if (!nginxExists) {
|
||||
echo "首次部署,初始化 Nginx 容器"
|
||||
sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d nginx'"
|
||||
sh 'sleep 3'
|
||||
} else {
|
||||
def nginxRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim()
|
||||
if (!nginxRunning) {
|
||||
echo "Nginx 容器已停止,重新启动"
|
||||
sh "${SSH_CMD} 'docker start ${CONTAINER_PREFIX}-nginx'"
|
||||
sh 'sleep 2'
|
||||
}
|
||||
echo "Nginx 容器已存在且运行中"
|
||||
}
|
||||
// docker cp proxy_nginx.conf 进容器
|
||||
sh "${SSH_CMD} 'docker cp ${REMOTE_DIR}/proxy_nginx.conf ${CONTAINER_PREFIX}-nginx:/etc/nginx/nginx.conf'"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('健康检查') {
|
||||
steps {
|
||||
script {
|
||||
@@ -144,7 +141,7 @@ pipeline {
|
||||
|
||||
while (retryCount < maxRetries && !healthy) {
|
||||
try {
|
||||
sh "docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}"
|
||||
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}'"
|
||||
healthy = true
|
||||
} catch (Exception e) {
|
||||
retryCount++
|
||||
@@ -163,38 +160,34 @@ pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换流量
|
||||
* - 修改 nginx 配置中的 proxy_pass 指向新版本
|
||||
* - nginx 不挂载宿主机文件,直接 sed -i 修改容器内配置
|
||||
* - reload 使新配置生效,实现零停机切换
|
||||
*/
|
||||
stage('切换流量') {
|
||||
steps {
|
||||
script {
|
||||
sh "docker exec ${CONTAINER_PREFIX}-nginx sed -i 's/proxy_pass http:\\/\\/\\(blue\\|green\\);/proxy_pass http:\\/\\/${env.DEPLOY_TARGET};/' /etc/nginx/nginx.conf"
|
||||
sh "docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload"
|
||||
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx sed -i \"s/proxy_pass http:\\/\\/\\(blue\\|green\\):80;/proxy_pass http:\\/\\/${env.DEPLOY_TARGET}:80;/\" /etc/nginx/nginx.conf'"
|
||||
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload'"
|
||||
echo "✅ 流量已切换到 ${env.DEPLOY_TARGET}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止并删除旧版本
|
||||
* - 不能用 docker-compose down,会删除所有服务(包括 nginx)
|
||||
* - 用 docker rm -f 只删除指定的旧颜色容器
|
||||
*/
|
||||
stage('删除旧版本') {
|
||||
steps {
|
||||
script {
|
||||
def otherExists = sh(script: "docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}", returnStdout: true).trim()
|
||||
def otherExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}'", returnStdout: true).trim()
|
||||
if (otherExists) {
|
||||
sh "docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}"
|
||||
sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}'"
|
||||
echo "旧版本 ${env.OTHER_TARGET} 已删除"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('清理') {
|
||||
steps {
|
||||
sh "${SSH_CMD} 'rm -f ${REMOTE_DIR}/${IMAGE_NAME}.tar'"
|
||||
sh "rm -f ${IMAGE_NAME}.tar"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
@@ -203,6 +196,7 @@ pipeline {
|
||||
}
|
||||
failure {
|
||||
echo '❌ 部署失败,请检查日志'
|
||||
sh "rm -f ${IMAGE_NAME}.tar || true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -12,8 +12,12 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AgentApplyProgress: typeof import('./src/components/AgentApplyProgress.vue')['default']
|
||||
AgentApplyProgressPanel: typeof import('./src/components/AgentApplyProgressPanel.vue')['default']
|
||||
AgentChatJobList: typeof import('./src/components/AgentChatJobList.vue')['default']
|
||||
AgentJobPreviewPanel: typeof import('./src/components/AgentJobPreviewPanel.vue')['default']
|
||||
AgentMatchJobAdd: typeof import('./src/components/AgentMatchJobAdd.vue')['default']
|
||||
AgentPendingJobListPanel: typeof import('./src/components/AgentPendingJobListPanel.vue')['default']
|
||||
AgentSettingPanel: typeof import('./src/components/AgentSettingPanel.vue')['default']
|
||||
AgentSettingsPanel: typeof import('./src/components/AgentSettingsPanel.vue')['default']
|
||||
AgentSetupWizard: typeof import('./src/components/AgentSetupWizard.vue')['default']
|
||||
AgentTaskListDropdown: typeof import('./src/components/AgentTaskListDropdown.vue')['default']
|
||||
@@ -53,6 +57,7 @@ declare module 'vue' {
|
||||
ResumeEditNameDialog: typeof import('./src/components/ResumeEditNameDialog.vue')['default']
|
||||
ResumeExportDialog: typeof import('./src/components/ResumeExportDialog.vue')['default']
|
||||
ResumeIssueFixDrawer: typeof import('./src/components/ResumeIssueFixDrawer.vue')['default']
|
||||
ResumeUploadDialog: typeof import('./src/components/ResumeUploadDialog.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SettingsDeleteAccountDialog: typeof import('./src/components/SettingsDeleteAccountDialog.vue')['default']
|
||||
|
||||
+4
-7
@@ -5,9 +5,6 @@ services:
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "10302:80"
|
||||
depends_on:
|
||||
- blue
|
||||
- green
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost/health"]
|
||||
interval: 30s
|
||||
@@ -16,11 +13,11 @@ services:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 128M
|
||||
cpus: '0.25'
|
||||
memory: 256M
|
||||
cpus: '0.5'
|
||||
|
||||
blue:
|
||||
build: .
|
||||
image: offerpie-web:latest
|
||||
container_name: offerpie-web-blue
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
@@ -41,7 +38,7 @@ services:
|
||||
cpus: '0.25'
|
||||
|
||||
green:
|
||||
build: .
|
||||
image: offerpie-web:latest
|
||||
container_name: offerpie-web-green
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
|
||||
@@ -19,12 +19,9 @@
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prerenderer/renderer-puppeteer": "^1.2.4",
|
||||
"@prerenderer/rollup-plugin": "^0.3.12",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"puppeteer": "^24.37.5",
|
||||
"typescript": "~5.9.3",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
|
||||
Generated
-1554
File diff suppressed because it is too large
Load Diff
@@ -2,5 +2,4 @@ allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
core-js: true
|
||||
esbuild: true
|
||||
puppeteer: true
|
||||
vue-demi: true
|
||||
|
||||
+3
-12
@@ -23,22 +23,13 @@ http {
|
||||
# 请求体大小限制
|
||||
client_max_body_size 20m;
|
||||
|
||||
# 蓝绿 upstream
|
||||
upstream blue {
|
||||
server offerpie-web-blue:80;
|
||||
}
|
||||
|
||||
upstream green {
|
||||
server offerpie-web-green:80;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# Java 后端 API 代理
|
||||
location /api/ {
|
||||
proxy_pass http://8.138.5.14:10202/api/;
|
||||
proxy_pass http://172.17.0.1:10202/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -54,7 +45,7 @@ http {
|
||||
|
||||
# Python AI 代理(去掉 /ai-api 前缀)
|
||||
location /ai-api/ {
|
||||
proxy_pass http://8.138.5.14:10502/;
|
||||
proxy_pass http://172.17.0.1:10502/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -74,7 +65,7 @@ http {
|
||||
|
||||
# 前端静态文件(蓝绿切换,默认 blue,部署时 sed 切换)
|
||||
location / {
|
||||
proxy_pass http://blue;
|
||||
proxy_pass http://blue:80;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
+2
-2
@@ -148,9 +148,9 @@ export function applyJob(params: JobApplyParams) {
|
||||
/**
|
||||
* 取消投递 / 从待投递移除
|
||||
* DELETE /job/apply?jobId=xxx
|
||||
* @param jobId 岗位 ID
|
||||
* @param jobId 岗位 ID(字符串,避免大整数精度丢失)
|
||||
*/
|
||||
export function cancelApplyJob(jobId: number) {
|
||||
export function cancelApplyJob(jobId: string | number) {
|
||||
return request.delete<any, ApiResult>('/job/apply', {
|
||||
params: { jobId },
|
||||
})
|
||||
|
||||
@@ -206,6 +206,8 @@ export interface ApplyListParams {
|
||||
pageSize?: number
|
||||
/** 投递状态筛选(0=已投递 1=面试中 2=有Offer 3=未通过 4=已结束) */
|
||||
status?: number | null
|
||||
/** 搜索关键词 */
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,6 +219,7 @@ export function fetchApplyList(params: ApplyListParams = {}) {
|
||||
pageNum: params.pageNum ?? 1,
|
||||
pageSize: params.pageSize ?? 10,
|
||||
...(params.status !== null && params.status !== undefined ? { status: params.status } : {}),
|
||||
...(params.keyword ? { keyword: params.keyword } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
/* 申请进度面板样式 */
|
||||
.agent-apply-progress-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: $bg-white;
|
||||
border-radius: 0.2rem;
|
||||
overflow: hidden;
|
||||
|
||||
/* 顶部标题栏 */
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.16rem 0.2rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 0.16rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
&__close-btn {
|
||||
width: 0.28rem;
|
||||
height: 0.28rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
&__filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.1rem;
|
||||
padding: 0 0.2rem 0.14rem;
|
||||
}
|
||||
|
||||
/* 下拉选择容器 */
|
||||
&__select-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__select {
|
||||
padding: 0.08rem 0.12rem;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 0.06rem;
|
||||
font-size: 0.13rem;
|
||||
color: $text-dark;
|
||||
background: $bg-white;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
min-width: 1rem;
|
||||
|
||||
&:focus {
|
||||
border-color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
/* 搜索框 */
|
||||
&__search {
|
||||
flex: 1;
|
||||
padding: 0.08rem 0.12rem;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 0.06rem;
|
||||
font-size: 0.13rem;
|
||||
color: $text-dark;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
/* 列表区域(可滚动) */
|
||||
&__list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 0.2rem 0.2rem;
|
||||
}
|
||||
|
||||
/* 单个岗位项 */
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.14rem 0.16rem;
|
||||
background: $bg-main;
|
||||
border-radius: 0.12rem;
|
||||
margin-bottom: 0.1rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 左侧信息区域 */
|
||||
&__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.12rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 公司 Logo */
|
||||
&__logo {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
border-radius: 0.08rem;
|
||||
background: $bg-middle;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
width: 0.22rem;
|
||||
height: 0.22rem;
|
||||
color: $text-middle;
|
||||
}
|
||||
}
|
||||
|
||||
/* 岗位详情 */
|
||||
&__detail {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__company {
|
||||
font-size: 0.13rem;
|
||||
font-weight: 500;
|
||||
color: $text-dark;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__position {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
margin-top: 0.02rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 标签 */
|
||||
&__tags {
|
||||
display: flex;
|
||||
gap: 0.06rem;
|
||||
margin-top: 0.06rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
font-size: 0.11rem;
|
||||
color: $text-middle;
|
||||
background: $bg-white;
|
||||
padding: 0.02rem 0.08rem;
|
||||
border-radius: 0.04rem;
|
||||
}
|
||||
|
||||
/* 右侧区域 */
|
||||
&__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 匹配度环 */
|
||||
&__score {
|
||||
position: relative;
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
|
||||
&__ring {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__score-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 0.1rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
/* 状态下拉 */
|
||||
&__status-select {
|
||||
padding: 0.05rem 0.1rem;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
color: $text-dark;
|
||||
background: $bg-white;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
/* 删除按钮 */
|
||||
&__delete-btn {
|
||||
width: 0.2rem;
|
||||
height: 0.2rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&:hover svg circle {
|
||||
fill: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载中 */
|
||||
&__loading {
|
||||
text-align: center;
|
||||
padding: 0.16rem 0;
|
||||
font-size: 0.12rem;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
/* 暂无更多数据 */
|
||||
&__no-more {
|
||||
text-align: center;
|
||||
padding: 0.16rem 0;
|
||||
font-size: 0.12rem;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
&__empty {
|
||||
text-align: center;
|
||||
padding: 0.4rem 0;
|
||||
font-size: 0.13rem;
|
||||
color: $text-light;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
border-radius: 0.1rem;
|
||||
padding: 0.14rem 0.16rem;
|
||||
margin-bottom: 0.08rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
/* Agent岗位预览面板样式 */
|
||||
.agent-job-preview-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: $bg-white;
|
||||
border-radius: 0.2rem;
|
||||
overflow: hidden;
|
||||
|
||||
/* 顶部标题栏 */
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.16rem 0.2rem;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
/* 返回按钮 */
|
||||
&__back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.06rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.15rem;
|
||||
font-weight: 500;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
&__back-icon {
|
||||
width: 0.16rem;
|
||||
height: 0.16rem;
|
||||
}
|
||||
|
||||
/* 添加按钮 */
|
||||
&__add-btn {
|
||||
padding: 0.06rem 0.16rem;
|
||||
background: $btn-dark;
|
||||
color: $bg-white;
|
||||
border: none;
|
||||
border-radius: 0.06rem;
|
||||
font-size: 0.13rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: $btn-dark-hover;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移出按钮 */
|
||||
&__remove-btn {
|
||||
padding: 0.06rem 0.16rem;
|
||||
background: $bg-white;
|
||||
color: $text-middle;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 0.06rem;
|
||||
font-size: 0.13rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: $bg-main;
|
||||
color: $danger;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
/* 内容区域(可滚动) */
|
||||
&__body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
/* 岗位卡片 */
|
||||
&__card {
|
||||
background: $bg-main;
|
||||
border-radius: 0.12rem;
|
||||
padding: 0.2rem;
|
||||
margin-bottom: 0.16rem;
|
||||
}
|
||||
|
||||
&__card-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&__card-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 公司行 */
|
||||
&__company-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
margin-bottom: 0.08rem;
|
||||
}
|
||||
|
||||
&__company-icon {
|
||||
width: 0.28rem;
|
||||
height: 0.28rem;
|
||||
border-radius: 0.06rem;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $bg-middle;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 0.18rem;
|
||||
height: 0.18rem;
|
||||
color: $text-middle;
|
||||
}
|
||||
}
|
||||
|
||||
&__company-name {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 500;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 0.12rem;
|
||||
color: $text-light;
|
||||
margin-left: 0.06rem;
|
||||
}
|
||||
|
||||
/* 岗位标题 */
|
||||
&__job-title {
|
||||
font-size: 0.17rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
margin: 0.06rem 0 0.1rem;
|
||||
}
|
||||
|
||||
/* 元信息 */
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
}
|
||||
|
||||
&__meta-icon {
|
||||
width: 0.14rem;
|
||||
height: 0.14rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 匹配度环 */
|
||||
&__match {
|
||||
position: relative;
|
||||
width: 0.6rem;
|
||||
height: 0.6rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__ring-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__match-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 0.13rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
/* 内容段落 */
|
||||
&__section {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
&__section-content {
|
||||
font-size: 0.13rem;
|
||||
color: $text-middle;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,7 @@
|
||||
gap: 0.12rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// 公司 Logo
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
/* 待投递岗位列表面板样式 */
|
||||
.agent-pending-job-list-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: $bg-white;
|
||||
border-radius: 0.2rem;
|
||||
overflow: hidden;
|
||||
|
||||
/* 顶部标题栏 */
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.16rem 0.2rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 0.16rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
&__close-btn {
|
||||
width: 0.28rem;
|
||||
height: 0.28rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 列表区域(可滚动) */
|
||||
&__list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 0.2rem 0.2rem;
|
||||
}
|
||||
|
||||
/* 单个岗位项 */
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.14rem 0.16rem;
|
||||
background: $bg-main;
|
||||
border-radius: 0.12rem;
|
||||
margin-bottom: 0.1rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 左侧信息区域 */
|
||||
&__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.12rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 公司 Logo */
|
||||
&__logo {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
border-radius: 0.08rem;
|
||||
background: $bg-middle;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
width: 0.22rem;
|
||||
height: 0.22rem;
|
||||
color: $text-middle;
|
||||
}
|
||||
}
|
||||
|
||||
/* 岗位详情 */
|
||||
&__detail {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__company {
|
||||
font-size: 0.13rem;
|
||||
font-weight: 500;
|
||||
color: $text-dark;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__position {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
margin-top: 0.02rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 标签 */
|
||||
&__tags {
|
||||
display: flex;
|
||||
gap: 0.06rem;
|
||||
margin-top: 0.06rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
font-size: 0.11rem;
|
||||
color: $text-middle;
|
||||
background: $bg-white;
|
||||
padding: 0.02rem 0.08rem;
|
||||
border-radius: 0.04rem;
|
||||
}
|
||||
|
||||
/* 右侧区域 */
|
||||
&__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.14rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 匹配度环 */
|
||||
&__score {
|
||||
position: relative;
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
|
||||
&__ring {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__score-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 0.1rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
/* 移出按钮 */
|
||||
&__remove-btn {
|
||||
padding: 0.06rem 0.16rem;
|
||||
background: $text-dark;
|
||||
color: $bg-white;
|
||||
border: none;
|
||||
border-radius: 0.2rem;
|
||||
font-size: 0.12rem;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载中 */
|
||||
&__loading {
|
||||
text-align: center;
|
||||
padding: 0.16rem 0;
|
||||
font-size: 0.12rem;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
/* 暂无更多数据 */
|
||||
&__no-more {
|
||||
text-align: center;
|
||||
padding: 0.16rem 0;
|
||||
font-size: 0.12rem;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
&__empty {
|
||||
text-align: center;
|
||||
padding: 0.4rem 0;
|
||||
font-size: 0.13rem;
|
||||
color: $text-light;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
/* Agent设置面板样式 */
|
||||
.agent-setting-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: $bg-white;
|
||||
border-radius: 0.2rem;
|
||||
overflow: hidden;
|
||||
|
||||
/* 顶部标题栏 */
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.16rem 0.2rem;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 0.16rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
&__close-btn {
|
||||
width: 0.28rem;
|
||||
height: 0.28rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 区块标题行 */
|
||||
&__section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
padding: 0.04rem 0;
|
||||
}
|
||||
|
||||
/* 区块标题 */
|
||||
&__section-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
/* 编辑按钮区域(文字 + 箭头) */
|
||||
&__section-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
}
|
||||
|
||||
&__section-action-text {
|
||||
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;
|
||||
}
|
||||
|
||||
/* 展开时的内容区域 */
|
||||
&__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;
|
||||
}
|
||||
|
||||
&__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;
|
||||
background: $bg-white;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 0.2rem;
|
||||
font-size: 0.12rem;
|
||||
color: $text-dark;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
|
||||
&: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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,11 @@
|
||||
&:hover {
|
||||
background: darken(#F3F4F5, 3%);
|
||||
}
|
||||
|
||||
// 拖拽悬停时背景色变化
|
||||
&.is-dragover {
|
||||
background: darken(#F3F4F5, 6%);
|
||||
}
|
||||
}
|
||||
|
||||
// 上传图标
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
// ==================== 简历上传弹窗 ====================
|
||||
.resume-upload-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: $overlay-bg;
|
||||
z-index: 2200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.resume-upload-dialog {
|
||||
width: 5.6rem;
|
||||
background: $bg-white;
|
||||
border-radius: 0.12rem;
|
||||
padding: 0.32rem 0.36rem;
|
||||
box-shadow: 0 0.04rem 0.2rem rgba(0, 0, 0, 0.15);
|
||||
|
||||
// 标题行
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.08rem;
|
||||
}
|
||||
|
||||
// 标题
|
||||
&__title {
|
||||
font-size: 0.2rem;
|
||||
font-weight: 700;
|
||||
color: $text-dark;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// 关闭按钮
|
||||
&__close {
|
||||
width: 0.28rem;
|
||||
height: 0.28rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: $text-middle;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.04rem;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: $bg-main;
|
||||
}
|
||||
}
|
||||
|
||||
// 副标题说明
|
||||
&__desc {
|
||||
font-size: 0.13rem;
|
||||
color: $accent;
|
||||
margin: 0 0 0.2rem 0;
|
||||
}
|
||||
|
||||
// 拖拽上传区域
|
||||
&__drop-zone {
|
||||
border: 1px dashed $border-color;
|
||||
border-radius: 0.1rem;
|
||||
padding: 0.5rem 0.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
background: $theme-color;
|
||||
margin-bottom: 0.28rem;
|
||||
|
||||
&:hover {
|
||||
border-color: $accent;
|
||||
}
|
||||
|
||||
// 拖拽悬停状态 — 背景色加深
|
||||
&.is-dragover {
|
||||
background: darken(#F6FCFC, 4%);
|
||||
border-color: $accent;
|
||||
}
|
||||
|
||||
// 已选择文件状态
|
||||
&.has-file {
|
||||
padding: 0.3rem 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
// 上传图标(加号圆圈)
|
||||
&__drop-icon {
|
||||
width: 0.52rem;
|
||||
height: 0.52rem;
|
||||
border-radius: 50%;
|
||||
background: $bg-white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $accent;
|
||||
margin-bottom: 0.14rem;
|
||||
box-shadow: 0 0.02rem 0.06rem rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
// 主提示文字
|
||||
&__drop-text {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
margin: 0 0 0.06rem 0;
|
||||
}
|
||||
|
||||
// 格式提示
|
||||
&__drop-hint {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// 已选文件信息
|
||||
&__file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
}
|
||||
|
||||
&__file-icon {
|
||||
font-size: 0.2rem;
|
||||
}
|
||||
|
||||
&__file-name {
|
||||
font-size: 0.14rem;
|
||||
color: $text-dark;
|
||||
font-weight: 500;
|
||||
max-width: 3.6rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__file-remove {
|
||||
border: none;
|
||||
background: none;
|
||||
color: $text-middle;
|
||||
font-size: 0.14rem;
|
||||
cursor: pointer;
|
||||
padding: 0.02rem 0.04rem;
|
||||
border-radius: 0.03rem;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部按钮区域
|
||||
&__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// 按钮通用
|
||||
&__btn {
|
||||
height: 0.42rem;
|
||||
border-radius: 0.21rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
border: none;
|
||||
|
||||
// 取消按钮
|
||||
&--cancel {
|
||||
width: 1.4rem;
|
||||
background: $bg-main;
|
||||
color: $text-dark;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
// 确认上传按钮(渐变色)
|
||||
&--confirm {
|
||||
width: 1.8rem;
|
||||
background: $gradient-bg;
|
||||
color: $bg-white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@
|
||||
@use './components/settings-delete-account-dialog.scss';
|
||||
@use './components/profile-welcome-dialog.scss';
|
||||
@use './components/settings-invite-dialog.scss';
|
||||
@use './components/resume-upload-dialog.scss';
|
||||
|
||||
// 全局样式(优先级最高)
|
||||
@use './auto.scss';
|
||||
@@ -67,6 +68,67 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Element Plus MessageBox rem 适配修正 ====================
|
||||
.el-overlay {
|
||||
// 确保遮罩层 z-index 足够高
|
||||
z-index: 2100 !important;
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
width: 420px !important;
|
||||
padding: 20px !important;
|
||||
border-radius: 8px !important;
|
||||
font-size: 14px !important;
|
||||
|
||||
&__header {
|
||||
padding: 0 0 12px 0 !important;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
|
||||
&__headerbtn {
|
||||
top: 20px !important;
|
||||
right: 20px !important;
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: 12px 0 !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
&__message {
|
||||
font-size: 14px !important;
|
||||
line-height: 1.5 !important;
|
||||
|
||||
p {
|
||||
font-size: 14px !important;
|
||||
line-height: 1.5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
&__btns {
|
||||
padding: 8px 0 0 0 !important;
|
||||
|
||||
.el-button {
|
||||
font-size: 14px !important;
|
||||
padding: 8px 20px !important;
|
||||
height: 32px !important;
|
||||
border-radius: 4px !important;
|
||||
min-width: 60px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Element Plus Loading rem 适配修正 + 品牌色覆盖 ====================
|
||||
.el-loading-mask {
|
||||
// 全屏 loading 遮罩层 z-index 确保最高
|
||||
|
||||
@@ -246,11 +246,40 @@
|
||||
|
||||
// ==================== 第2步:确认目标 ====================
|
||||
|
||||
// 返回上一步按钮 — 位于每步容器左上角
|
||||
&__back-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
padding: 0.06rem 0.12rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: $text-middle;
|
||||
font-size: 0.13rem;
|
||||
cursor: pointer;
|
||||
border-radius: 0.04rem;
|
||||
transition: color 0.2s ease, background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: $text-dark;
|
||||
background: $bg-middle;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 第2步整体容器
|
||||
&__step2 {
|
||||
width: 100%;
|
||||
max-width: 7rem;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
padding-top: 0.36rem;
|
||||
}
|
||||
|
||||
// 对话区域
|
||||
@@ -578,6 +607,8 @@
|
||||
gap: 0.24rem;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-top: 0.36rem;
|
||||
}
|
||||
|
||||
// 右侧表单包裹
|
||||
@@ -1085,7 +1116,7 @@
|
||||
|
||||
// 工具按钮
|
||||
&__tool-btn {
|
||||
width: 0.28rem;
|
||||
width: 0.38rem;
|
||||
height: 0.28rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
@@ -1097,6 +1128,8 @@
|
||||
color: $text-middle;
|
||||
border-radius: 0.06rem;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 600;
|
||||
|
||||
|
||||
svg {
|
||||
width: 0.18rem;
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<!-- 申请进度面板 — 右侧面板显示已投递岗位列表(带筛选、搜索、滚动分页) -->
|
||||
<div class="agent-apply-progress-panel">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="agent-apply-progress-panel__header">
|
||||
<span class="agent-apply-progress-panel__title">申请进度</span>
|
||||
<!-- 关闭按钮 -->
|
||||
<button class="agent-apply-progress-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-apply-progress-panel__filter">
|
||||
<!-- 状态下拉选择 -->
|
||||
<div class="agent-apply-progress-panel__select-wrap">
|
||||
<select v-model="selectedStatus" class="agent-apply-progress-panel__select" @change="handleFilterChange">
|
||||
<option :value="null">全部</option>
|
||||
<option :value="0">已投递</option>
|
||||
<option :value="1">面试中</option>
|
||||
<option :value="2">有Offer</option>
|
||||
<option :value="3">未通过</option>
|
||||
<option :value="4">已结束</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 搜索框 -->
|
||||
<input
|
||||
v-model="keyword"
|
||||
class="agent-apply-progress-panel__search"
|
||||
placeholder="搜索岗位/公司"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 列表内容(可滚动,触底加载更多) -->
|
||||
<div ref="listRef" class="agent-apply-progress-panel__list" @scroll="handleScroll">
|
||||
<!-- 岗位项 -->
|
||||
<div
|
||||
v-for="job in jobList"
|
||||
:key="job.id"
|
||||
class="agent-apply-progress-panel__item"
|
||||
>
|
||||
<!-- 左侧信息区域 -->
|
||||
<div class="agent-apply-progress-panel__info">
|
||||
<!-- 公司 Logo -->
|
||||
<div class="agent-apply-progress-panel__logo">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="7" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M7 7V5a2 2 0 012-2h6a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 岗位详情 -->
|
||||
<div class="agent-apply-progress-panel__detail">
|
||||
<div class="agent-apply-progress-panel__company">{{ job.companyShortName || job.companyName }}</div>
|
||||
<div class="agent-apply-progress-panel__position">{{ job.title }}</div>
|
||||
<!-- 标签 -->
|
||||
<div class="agent-apply-progress-panel__tags">
|
||||
<span v-if="job.regionName" class="agent-apply-progress-panel__tag">{{ job.regionName }}</span>
|
||||
<span v-if="job.categoryName" class="agent-apply-progress-panel__tag">{{ job.categoryName }}</span>
|
||||
<span v-for="tag in (job.tags || []).slice(0, 2)" :key="tag" class="agent-apply-progress-panel__tag">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:匹配度 + 状态下拉 + 删除按钮 -->
|
||||
<div class="agent-apply-progress-panel__right">
|
||||
<!-- 匹配度环形 -->
|
||||
<div class="agent-apply-progress-panel__score">
|
||||
<svg class="agent-apply-progress-panel__ring" viewBox="0 0 44 44">
|
||||
<circle cx="22" cy="22" r="18" fill="none" stroke="#E8E8E8" stroke-width="3" />
|
||||
<circle
|
||||
cx="22" cy="22" r="18" fill="none"
|
||||
stroke="#4FC2C9" stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
:stroke-dasharray="2 * Math.PI * 18"
|
||||
:stroke-dashoffset="2 * Math.PI * 18 * (1 - (job.matchScore || 0) / 100)"
|
||||
transform="rotate(-90 22 22)"
|
||||
/>
|
||||
</svg>
|
||||
<span class="agent-apply-progress-panel__score-text">{{ job.matchScore || 0 }}%</span>
|
||||
</div>
|
||||
<!-- 状态下拉 -->
|
||||
<select
|
||||
class="agent-apply-progress-panel__status-select"
|
||||
:value="job.status"
|
||||
@change="handleStatusChange(job, $event)"
|
||||
>
|
||||
<option :value="0">已投递</option>
|
||||
<option :value="1">面试中</option>
|
||||
<option :value="2">有Offer</option>
|
||||
<option :value="3">未通过</option>
|
||||
<option :value="4">已结束</option>
|
||||
</select>
|
||||
<!-- 删除按钮 -->
|
||||
<button class="agent-apply-progress-panel__delete-btn" @click="handleDelete(job)">
|
||||
<svg viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="8" cy="8" r="7" fill="#BFBFBF" />
|
||||
<path d="M5.5 5.5l5 5M10.5 5.5l-5 5" stroke="#fff" stroke-width="1.2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<div v-if="loadingMore" class="agent-apply-progress-panel__loading">加载中...</div>
|
||||
<!-- 没有更多数据 -->
|
||||
<div v-if="noMore && jobList.length > 0" class="agent-apply-progress-panel__no-more">暂无更多数据</div>
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && jobList.length === 0 && !loadingMore" class="agent-apply-progress-panel__empty">暂无数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { fetchApplyList } from '@/api/jobs'
|
||||
import { applyJob, cancelApplyJob } from '@/api/agent'
|
||||
import type { JobListItem } from '@/api/jobs'
|
||||
|
||||
/** 事件 */
|
||||
const emit = defineEmits<{
|
||||
/** 关闭面板 */
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
/** 岗位列表数据 */
|
||||
const jobList = ref<JobListItem[]>([])
|
||||
|
||||
/** 当前页码 */
|
||||
const currentPage = ref(1)
|
||||
|
||||
/** 每页条数 */
|
||||
const pageSize = 30
|
||||
|
||||
/** 是否正在加载首页 */
|
||||
const loading = ref(false)
|
||||
|
||||
/** 是否正在加载更多 */
|
||||
const loadingMore = ref(false)
|
||||
|
||||
/** 是否没有更多数据 */
|
||||
const noMore = ref(false)
|
||||
|
||||
/** 当前选中的状态筛选 */
|
||||
const selectedStatus = ref<number | null>(null)
|
||||
|
||||
/** 搜索关键词 */
|
||||
const keyword = ref('')
|
||||
|
||||
/** 列表容器 ref */
|
||||
const listRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/** 加载投递列表 */
|
||||
async function loadList(page: number) {
|
||||
if (page === 1) {
|
||||
loading.value = true
|
||||
} else {
|
||||
loadingMore.value = true
|
||||
}
|
||||
try {
|
||||
const res = await fetchApplyList({
|
||||
pageNum: page,
|
||||
pageSize,
|
||||
status: selectedStatus.value,
|
||||
keyword: keyword.value || undefined,
|
||||
})
|
||||
if (res.code === '0' && res.data) {
|
||||
const list = res.data.list || []
|
||||
if (page === 1) {
|
||||
jobList.value = list
|
||||
} else {
|
||||
jobList.value.push(...list)
|
||||
}
|
||||
/* 判断是否还有更多数据 */
|
||||
const total = Number(res.data.total || 0)
|
||||
if (jobList.value.length >= total || list.length < pageSize) {
|
||||
noMore.value = true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.error('[AgentApplyProgressPanel] 加载投递列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置列表并重新加载 */
|
||||
function resetAndLoad() {
|
||||
currentPage.value = 1
|
||||
jobList.value = []
|
||||
noMore.value = false
|
||||
loadList(1)
|
||||
}
|
||||
|
||||
/** 筛选状态变更 */
|
||||
function handleFilterChange() {
|
||||
resetAndLoad()
|
||||
}
|
||||
|
||||
/** 搜索框回车 */
|
||||
function handleSearch() {
|
||||
resetAndLoad()
|
||||
}
|
||||
|
||||
/** 滚动触底加载更多 */
|
||||
function handleScroll() {
|
||||
if (!listRef.value || loadingMore.value || noMore.value) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = listRef.value
|
||||
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
||||
currentPage.value++
|
||||
loadList(currentPage.value)
|
||||
}
|
||||
}
|
||||
|
||||
/** 修改单个岗位的投递状态 */
|
||||
async function handleStatusChange(job: JobListItem, event: Event) {
|
||||
const newStatus = Number((event.target as HTMLSelectElement).value)
|
||||
try {
|
||||
await applyJob({ jobId: job.id, status: newStatus })
|
||||
job.status = newStatus
|
||||
ElMessage.success('状态已更新')
|
||||
} catch {
|
||||
ElMessage.error('状态更新失败')
|
||||
/* 恢复原值 — 触发视图刷新 */
|
||||
;(event.target as HTMLSelectElement).value = String(job.status)
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除(取消投递) */
|
||||
async function handleDelete(job: JobListItem) {
|
||||
try {
|
||||
await cancelApplyJob(job.id)
|
||||
const idx = jobList.value.findIndex(j => j.id === job.id)
|
||||
if (idx !== -1) {
|
||||
jobList.value.splice(idx, 1)
|
||||
}
|
||||
ElMessage.success('已删除')
|
||||
} catch {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadList(1)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '../assets/styles/components/agent-apply-progress-panel';
|
||||
</style>
|
||||
@@ -10,6 +10,7 @@
|
||||
:key="job.id"
|
||||
class="agent-chat-job-list__item"
|
||||
v-if="displayJobs.length>0"
|
||||
@click="handleClickJob(job)"
|
||||
>
|
||||
<!-- 左侧:公司图标 + 岗位信息 -->
|
||||
<div class="agent-chat-job-list__info">
|
||||
@@ -79,6 +80,8 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
/** 点击查看全部岗位 */
|
||||
(e: 'viewAll'): void
|
||||
/** 点击岗位查看详情 */
|
||||
(e: 'clickJob', job: AgentRecommendJob): void
|
||||
}>()
|
||||
|
||||
/** 只显示前3个岗位 */
|
||||
@@ -88,6 +91,11 @@ const displayJobs = computed(() => props.jobs.slice(0, 3))
|
||||
function handleViewAll() {
|
||||
emit('viewAll')
|
||||
}
|
||||
|
||||
/** 点击岗位 — 通知父组件打开岗位预览 */
|
||||
function handleClickJob(job: AgentRecommendJob) {
|
||||
emit('clickJob', job)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<!-- Agent岗位预览面板 — 右侧面板显示岗位详情 -->
|
||||
<div class="agent-job-preview-panel" v-loading="loading" element-loading-text="加载中...">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="agent-job-preview-panel__header">
|
||||
<div class="agent-job-preview-panel__back" @click="handleBack">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="agent-job-preview-panel__back-icon">
|
||||
<path d="M10 3L5 8l5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>岗位详情</span>
|
||||
</div>
|
||||
<!-- 添加按钮 — 仅未投递时显示 -->
|
||||
<button
|
||||
v-if="jobDetail && (jobDetail.applicationStatus === null || jobDetail.applicationStatus === undefined)"
|
||||
class="agent-job-preview-panel__add-btn"
|
||||
:disabled="addLoading"
|
||||
@click="handleAdd"
|
||||
>+ 添加</button>
|
||||
<!-- 移出按钮 — 仅待投递状态时显示 -->
|
||||
<button
|
||||
v-else-if="jobDetail && jobDetail.applicationStatus === -1"
|
||||
class="agent-job-preview-panel__remove-btn"
|
||||
:disabled="addLoading"
|
||||
@click="handleRemove"
|
||||
>移出</button>
|
||||
</div>
|
||||
|
||||
<!-- 岗位详情内容 -->
|
||||
<div v-if="jobDetail" class="agent-job-preview-panel__body">
|
||||
<!-- 岗位卡片 -->
|
||||
<div class="agent-job-preview-panel__card">
|
||||
<div class="agent-job-preview-panel__card-top">
|
||||
<!-- 左侧信息 -->
|
||||
<div class="agent-job-preview-panel__card-left">
|
||||
<div class="agent-job-preview-panel__company-row">
|
||||
<div class="agent-job-preview-panel__company-icon">
|
||||
<img v-if="jobDetail.companyLogoUrl" :src="jobDetail.companyLogoUrl" :alt="jobDetail.companyShortName" />
|
||||
<svg v-else viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="7" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M7 7V5a2 2 0 012-2h6a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="agent-job-preview-panel__company-name">{{ jobDetail.companyShortName || jobDetail.companyName }}</span>
|
||||
<span class="agent-job-preview-panel__time">{{ formatTime }}</span>
|
||||
</div>
|
||||
<h3 class="agent-job-preview-panel__job-title">{{ jobDetail.jobTitle }}</h3>
|
||||
<!-- 岗位元信息 -->
|
||||
<div class="agent-job-preview-panel__meta">
|
||||
<span v-if="jobDetail.regionName" class="agent-job-preview-panel__meta-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="agent-job-preview-panel__meta-icon">
|
||||
<circle cx="8" cy="6.5" r="2.5" stroke="currentColor" stroke-width="1.2"/>
|
||||
<path d="M8 14s-5-4-5-7.5a5 5 0 0110 0C13 10 8 14 8 14z" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
{{ jobDetail.regionName }}
|
||||
</span>
|
||||
<span class="agent-job-preview-panel__meta-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="agent-job-preview-panel__meta-icon">
|
||||
<rect x="2" y="3" width="12" height="11" rx="1.5" stroke="currentColor" stroke-width="1.2"/>
|
||||
<path d="M2 6.5h12" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
{{ formatEmploymentType(jobDetail.employmentType) }}
|
||||
</span>
|
||||
<span v-if="jobDetail.salary" class="agent-job-preview-panel__meta-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="agent-job-preview-panel__meta-icon">
|
||||
<path d="M8 1v14M4 4h8M3 8h10M5 12h6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
{{ jobDetail.salary }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧匹配度环 -->
|
||||
<div class="agent-job-preview-panel__match">
|
||||
<svg viewBox="0 0 60 60" class="agent-job-preview-panel__ring-svg">
|
||||
<circle cx="30" cy="30" r="25" stroke-width="4" stroke="#E8E8E8" fill="none" />
|
||||
<circle cx="30" cy="30" r="25" stroke-width="4" fill="none"
|
||||
stroke="#4FC2C9" stroke-linecap="round"
|
||||
:stroke-dasharray="2 * Math.PI * 25"
|
||||
:stroke-dashoffset="2 * Math.PI * 25 * (1 - (jobDetail.matchScore || 0) / 100)"
|
||||
transform="rotate(-90 30 30)"
|
||||
/>
|
||||
</svg>
|
||||
<span class="agent-job-preview-panel__match-text">{{ jobDetail.matchScore || 0 }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 岗位职责 -->
|
||||
<div v-if="jobDetail.description" class="agent-job-preview-panel__section">
|
||||
<h4 class="agent-job-preview-panel__section-title">岗位职责</h4>
|
||||
<div class="agent-job-preview-panel__section-content">{{ jobDetail.description }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 任职要求 -->
|
||||
<div v-if="jobDetail.requirement" class="agent-job-preview-panel__section">
|
||||
<h4 class="agent-job-preview-panel__section-title">任职要求</h4>
|
||||
<div class="agent-job-preview-panel__section-content">{{ jobDetail.requirement }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 加分项 -->
|
||||
<div v-if="jobDetail.bonus" class="agent-job-preview-panel__section">
|
||||
<h4 class="agent-job-preview-panel__section-title">加分项</h4>
|
||||
<div class="agent-job-preview-panel__section-content">{{ jobDetail.bonus }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { fetchJobDetail } from '@/api/jobs'
|
||||
import type { JobDetailData } from '@/api/jobs'
|
||||
|
||||
/** 组件 Props */
|
||||
const props = defineProps<{
|
||||
/** 岗位 ID */
|
||||
jobId: string
|
||||
/** 投递状态(用于控制添加按钮显示) */
|
||||
applicationStatus?: number | null
|
||||
}>()
|
||||
|
||||
/** 事件 */
|
||||
const emit = defineEmits<{
|
||||
/** 返回上一个面板模式 */
|
||||
(e: 'back'): void
|
||||
/** 添加岗位到待投递 */
|
||||
(e: 'add', jobId: string): void
|
||||
/** 从待投递移除岗位 */
|
||||
(e: 'remove', jobId: string): void
|
||||
}>()
|
||||
|
||||
/** 加载状态 */
|
||||
const loading = ref(false)
|
||||
|
||||
/** 添加按钮加载状态 */
|
||||
const addLoading = ref(false)
|
||||
|
||||
/** 岗位详情数据(扩展 applicationStatus 字段) */
|
||||
const jobDetail = ref<(JobDetailData & { applicationStatus?: number | null }) | null>(null)
|
||||
|
||||
/** 监听 applicationStatus prop 变化,同步到 jobDetail */
|
||||
watch(() => props.applicationStatus, (newVal) => {
|
||||
if (jobDetail.value) {
|
||||
jobDetail.value.applicationStatus = newVal ?? null
|
||||
}
|
||||
})
|
||||
|
||||
/** 格式化时间显示 */
|
||||
const formatTime = computed(() => {
|
||||
// 暂时不显示具体时间,可后续扩展
|
||||
return ''
|
||||
})
|
||||
|
||||
/** 工作类型映射 */
|
||||
function formatEmploymentType(type: number | undefined): string {
|
||||
const map: Record<number, string> = { 0: '全职', 1: '兼职' }
|
||||
return map[type ?? -1] ?? ''
|
||||
}
|
||||
|
||||
/** 加载岗位详情 */
|
||||
async function loadDetail() {
|
||||
if (!props.jobId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchJobDetail(props.jobId)
|
||||
if (res.code === '0' && res.data) {
|
||||
jobDetail.value = { ...res.data, applicationStatus: props.applicationStatus ?? null }
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[AgentJobPreviewPanel] 加载岗位详情失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回按钮 */
|
||||
function handleBack() {
|
||||
emit('back')
|
||||
}
|
||||
|
||||
/** 添加到待投递 */
|
||||
function handleAdd() {
|
||||
emit('add', props.jobId)
|
||||
}
|
||||
|
||||
/** 从待投递移除 */
|
||||
function handleRemove() {
|
||||
emit('remove', props.jobId)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '../assets/styles/components/agent-job-preview-panel';
|
||||
</style>
|
||||
@@ -27,7 +27,7 @@
|
||||
class="agent-match-job-add__item"
|
||||
>
|
||||
<!-- 左侧:公司图标 + 岗位信息 -->
|
||||
<div class="agent-match-job-add__info">
|
||||
<div class="agent-match-job-add__info" @click="handleClickJob(job)">
|
||||
<!-- 公司 Logo -->
|
||||
<div class="agent-match-job-add__logo">
|
||||
<img v-if="job.companyLogoUrl" :src="job.companyLogoUrl" :alt="job.companyShortName" />
|
||||
@@ -116,6 +116,8 @@ const emit = defineEmits<{
|
||||
(e: 'toggle', job: AgentRecommendJob): void
|
||||
/** 全部添加操作 */
|
||||
(e: 'addAll'): void
|
||||
/** 点击岗位查看详情 */
|
||||
(e: 'clickJob', job: AgentRecommendJob): void
|
||||
}>()
|
||||
|
||||
/** 点击添加/移除 — 通知父组件处理 */
|
||||
@@ -123,6 +125,11 @@ function handleToggle(job: AgentRecommendJob) {
|
||||
emit('toggle', job)
|
||||
}
|
||||
|
||||
/** 点击岗位 — 通知父组件打开岗位预览 */
|
||||
function handleClickJob(job: AgentRecommendJob) {
|
||||
emit('clickJob', job)
|
||||
}
|
||||
|
||||
/** 全部添加 — 通知父组件处理(只传 applicationStatus 为 null 的岗位) */
|
||||
function handleAddAll() {
|
||||
emit('addAll')
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<!-- 待投递岗位列表面板 — 右侧面板显示全部待投递岗位(滚动分页) -->
|
||||
<div class="agent-pending-job-list-panel">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="agent-pending-job-list-panel__header">
|
||||
<span class="agent-pending-job-list-panel__title">待投递岗位列表</span>
|
||||
<!-- 关闭按钮 -->
|
||||
<button class="agent-pending-job-list-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 ref="listRef" class="agent-pending-job-list-panel__list" @scroll="handleScroll">
|
||||
<!-- 岗位项 -->
|
||||
<div
|
||||
v-for="job in jobList"
|
||||
:key="job.id"
|
||||
class="agent-pending-job-list-panel__item"
|
||||
>
|
||||
<!-- 左侧信息区域 -->
|
||||
<div class="agent-pending-job-list-panel__info">
|
||||
<!-- 公司 Logo -->
|
||||
<div class="agent-pending-job-list-panel__logo">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="7" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M7 7V5a2 2 0 012-2h6a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 岗位详情 -->
|
||||
<div class="agent-pending-job-list-panel__detail">
|
||||
<div class="agent-pending-job-list-panel__company">{{ job.companyShortName || job.companyName }}</div>
|
||||
<div class="agent-pending-job-list-panel__position">{{ job.title }}</div>
|
||||
<!-- 标签 -->
|
||||
<div class="agent-pending-job-list-panel__tags">
|
||||
<span v-if="job.regionName" class="agent-pending-job-list-panel__tag">{{ job.regionName }}</span>
|
||||
<span v-if="job.categoryName" class="agent-pending-job-list-panel__tag">{{ job.categoryName }}</span>
|
||||
<span v-for="tag in (job.tags || []).slice(0, 2)" :key="tag" class="agent-pending-job-list-panel__tag">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:匹配度 + 移出按钮 -->
|
||||
<div class="agent-pending-job-list-panel__right">
|
||||
<!-- 匹配度环形 -->
|
||||
<div class="agent-pending-job-list-panel__score">
|
||||
<svg class="agent-pending-job-list-panel__ring" viewBox="0 0 44 44">
|
||||
<circle cx="22" cy="22" r="18" fill="none" stroke="#E8E8E8" stroke-width="3" />
|
||||
<circle
|
||||
cx="22" cy="22" r="18" fill="none"
|
||||
stroke="#4FC2C9" stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
:stroke-dasharray="2 * Math.PI * 18"
|
||||
:stroke-dashoffset="2 * Math.PI * 18 * (1 - (job.matchScore || 0) / 100)"
|
||||
transform="rotate(-90 22 22)"
|
||||
/>
|
||||
</svg>
|
||||
<span class="agent-pending-job-list-panel__score-text">{{ job.matchScore || 0 }}%</span>
|
||||
</div>
|
||||
<!-- 移出按钮 -->
|
||||
<button
|
||||
class="agent-pending-job-list-panel__remove-btn"
|
||||
:disabled="removingIds.includes(job.id)"
|
||||
@click="handleRemove(job)"
|
||||
>移出</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<div v-if="loadingMore" class="agent-pending-job-list-panel__loading">加载中...</div>
|
||||
<!-- 没有更多数据 -->
|
||||
<div v-if="noMore && jobList.length > 0" class="agent-pending-job-list-panel__no-more">暂无更多数据</div>
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && jobList.length === 0" class="agent-pending-job-list-panel__empty">暂无待投递岗位</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { fetchAgentTaskList } from '@/api/jobs'
|
||||
import { cancelApplyJob } from '@/api/agent'
|
||||
import type { JobListItem } from '@/api/jobs'
|
||||
|
||||
/** 事件 */
|
||||
const emit = defineEmits<{
|
||||
/** 关闭面板 */
|
||||
(e: 'close'): void
|
||||
/** 移除岗位后通知父组件 */
|
||||
(e: 'removed', jobId: string): void
|
||||
}>()
|
||||
|
||||
/** 岗位列表数据 */
|
||||
const jobList = ref<JobListItem[]>([])
|
||||
|
||||
/** 当前页码 */
|
||||
const currentPage = ref(1)
|
||||
|
||||
/** 每页条数 */
|
||||
const pageSize = 30
|
||||
|
||||
/** 是否正在加载首页 */
|
||||
const loading = ref(false)
|
||||
|
||||
/** 是否正在加载更多 */
|
||||
const loadingMore = ref(false)
|
||||
|
||||
/** 是否没有更多数据 */
|
||||
const noMore = ref(false)
|
||||
|
||||
/** 正在移除中的岗位 ID 列表 */
|
||||
const removingIds = ref<string[]>([])
|
||||
|
||||
/** 列表容器 ref */
|
||||
const listRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/** 加载待投递列表 */
|
||||
async function loadList(page: number) {
|
||||
if (page === 1) {
|
||||
loading.value = true
|
||||
} else {
|
||||
loadingMore.value = true
|
||||
}
|
||||
try {
|
||||
const res = await fetchAgentTaskList({ pageNum: page, pageSize, tab: 1 })
|
||||
if (res.code === '0' && res.data) {
|
||||
const list = res.data.list || []
|
||||
if (page === 1) {
|
||||
jobList.value = list
|
||||
} else {
|
||||
jobList.value.push(...list)
|
||||
}
|
||||
/* 判断是否还有更多数据 */
|
||||
const total = Number(res.data.total || 0)
|
||||
if (jobList.value.length >= total || list.length < pageSize) {
|
||||
noMore.value = true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.error('[AgentPendingJobListPanel] 加载待投递列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 滚动触底加载更多 */
|
||||
function handleScroll() {
|
||||
if (!listRef.value || loadingMore.value || noMore.value) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = listRef.value
|
||||
/* 距离底部 50px 时触发加载 */
|
||||
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
||||
currentPage.value++
|
||||
loadList(currentPage.value)
|
||||
}
|
||||
}
|
||||
|
||||
/** 移出岗位 */
|
||||
async function handleRemove(job: JobListItem) {
|
||||
if (removingIds.value.includes(job.id)) return
|
||||
removingIds.value.push(job.id)
|
||||
try {
|
||||
await cancelApplyJob(job.id)
|
||||
/* 从列表中移除 */
|
||||
const idx = jobList.value.findIndex(j => j.id === job.id)
|
||||
if (idx !== -1) {
|
||||
jobList.value.splice(idx, 1)
|
||||
}
|
||||
emit('removed', job.id)
|
||||
ElMessage.success('已移除')
|
||||
} catch {
|
||||
ElMessage.error('移除失败,请重试')
|
||||
} finally {
|
||||
removingIds.value = removingIds.value.filter(id => id !== job.id)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadList(1)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '../assets/styles/components/agent-pending-job-list-panel';
|
||||
</style>
|
||||
@@ -0,0 +1,658 @@
|
||||
<template>
|
||||
<!-- 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">
|
||||
<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>
|
||||
</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">
|
||||
<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>
|
||||
</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 class="agent-setting-panel__goal-tag">{{ intentionEmploymentLabel }}</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">
|
||||
<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>
|
||||
<el-switch v-model="autoOptimizeSwitch" active-color="#4FC2C9" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交设置按钮 -->
|
||||
<div class="agent-setting-panel__submit-wrap">
|
||||
<button class="agent-setting-panel__submit-btn" :disabled="configSaving" @click="handleSubmitConfig">
|
||||
{{ configSaving ? '保存中...' : '保存设置' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 个人资料编辑抽屉 -->
|
||||
<ProfileEditDrawer
|
||||
v-model="showEditDrawer"
|
||||
:module="editModule"
|
||||
:initial-data="editInitialData"
|
||||
:saving="saving"
|
||||
@save="handleSaveEdit"
|
||||
/>
|
||||
|
||||
<!-- 求职目标设置弹窗 -->
|
||||
<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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import ProfilePageContent from '@/components/ProfilePageContent.vue'
|
||||
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
|
||||
import JobGoalDialog from '@/components/JobGoalDialog.vue'
|
||||
import {
|
||||
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 { resolveRegionName } from '@/utils/region'
|
||||
import { resolveIndustryName } from '@/utils/industry'
|
||||
import { resolveJobCategoryName } from '@/utils/jobCategory'
|
||||
import { fetchResumeList } 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(() => store.state.jobIntention?.employmentType === 1 ? '实习' : '全职')
|
||||
|
||||
// ==================== 浏览器插件 ====================
|
||||
|
||||
/** 插件下载地址 */
|
||||
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?.employmentType === 1)
|
||||
|
||||
/** 配置表单数据 */
|
||||
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 }> }>,
|
||||
})
|
||||
|
||||
// ==================== 加载个人资料 ====================
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
/** 加载基本信息 */
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const res = await fetchProfile()
|
||||
if (res.code === '0' && res.data) {
|
||||
const d = res.data
|
||||
profile.value.name = d.name || ''
|
||||
profile.value.phone = d.mobileNumber || ''
|
||||
profile.value.email = d.email || ''
|
||||
profile.value.idNumber = d.idCard || ''
|
||||
profile.value.regionCode = d.regionCode || ''
|
||||
profile.value.wechat = d.wechatNumber || ''
|
||||
profile.value.skills = d.skills || []
|
||||
profile.value.certificates = d.certificates || []
|
||||
profile.value.portfolioUrl = d.portfolioUrl || ''
|
||||
}
|
||||
} catch { console.error('[AgentSettingPanel] 加载个人资料失败') }
|
||||
}
|
||||
|
||||
/** 加载教育经历 */
|
||||
async function loadEducation() {
|
||||
try {
|
||||
const res = await fetchEducation()
|
||||
if (res.code === '0' && res.data) {
|
||||
profile.value.education = res.data.map(item => ({
|
||||
school: item.school || '', major: item.major || '', studyType: item.studyType ?? 0,
|
||||
degree: item.degree ?? 2, startDate: item.startDate || '', endDate: item.endDate || '',
|
||||
description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' }))
|
||||
}))
|
||||
}
|
||||
} catch { console.error('[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] 加载竞赛经历失败') }
|
||||
}
|
||||
|
||||
// ==================== 编辑抽屉 ====================
|
||||
|
||||
/** 编辑抽屉显隐 */
|
||||
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 = {}
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '../assets/styles/components/agent-setting-panel';
|
||||
</style>
|
||||
@@ -62,6 +62,11 @@
|
||||
<!-- ========== 第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>
|
||||
@@ -119,6 +124,11 @@
|
||||
<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>
|
||||
</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>
|
||||
@@ -143,6 +153,11 @@
|
||||
</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>
|
||||
@@ -173,6 +188,11 @@
|
||||
<!-- ========== 第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>
|
||||
@@ -240,6 +260,20 @@ 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) {
|
||||
currentStep.value--
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 编辑抽屉状态 ====================
|
||||
const showEditDrawer = ref(false)
|
||||
const editModule = ref('info')
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</template>
|
||||
</div>
|
||||
<!-- 查看全部 -->
|
||||
<div class="agent-main__task-view-all" @click="emit('viewAll')">查看全部</div>
|
||||
<div class="agent-main__task-view-all" @click="handleViewAll">查看全部</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -66,8 +66,10 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
/** 移除岗位后通知父组件刷新列表 */
|
||||
(e: 'removed', jobId: string): void
|
||||
/** 查看全部 */
|
||||
/** 查看全部(进行中 tab) */
|
||||
(e: 'viewAll'): void
|
||||
/** 查看全部(已完成 tab — 申请进度) */
|
||||
(e: 'viewAllCompleted'): void
|
||||
}>()
|
||||
|
||||
// ==================== 状态 ====================
|
||||
@@ -102,7 +104,7 @@ function switchTab(tab: number) {
|
||||
async function loadCompletedList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchAgentTaskList({ pageNum: 1, pageSize: 100, tab: 2 })
|
||||
const res = await fetchAgentTaskList({ pageNum: 1, pageSize: 30, tab: 2 })
|
||||
if (res.code === '0' && res.data) {
|
||||
completedList.value = res.data.list || []
|
||||
}
|
||||
@@ -116,11 +118,20 @@ async function loadCompletedList() {
|
||||
/** 移除岗位 — 调用 cancelApplyJob 接口并通知父组件 */
|
||||
async function removeJob(job: JobListItem) {
|
||||
try {
|
||||
await cancelApplyJob(Number(job.id))
|
||||
await cancelApplyJob(job.id)
|
||||
emit('removed', job.id)
|
||||
ElMessage.success('已移除')
|
||||
} catch {
|
||||
ElMessage.error('移除失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看全部 — 根据当前 tab 触发不同事件 */
|
||||
function handleViewAll() {
|
||||
if (activeTab.value === 1) {
|
||||
emit('viewAll')
|
||||
} else {
|
||||
emit('viewAllCompleted')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,15 @@
|
||||
<!-- 简历上传区域 -->
|
||||
<div class="profile-welcome-dialog__form">
|
||||
<div class="profile-welcome-dialog__label">*简历</div>
|
||||
<div class="profile-welcome-dialog__upload-area" @click="handleUploadClick">
|
||||
<div
|
||||
class="profile-welcome-dialog__upload-area"
|
||||
:class="{ 'is-dragover': isDragover }"
|
||||
@click="handleUploadClick"
|
||||
@dragover.prevent="isDragover = true"
|
||||
@dragenter.prevent="isDragover = true"
|
||||
@dragleave.prevent="isDragover = false"
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<!-- 未上传状态 -->
|
||||
<template v-if="!uploadedFileName">
|
||||
<div class="profile-welcome-dialog__upload-icon">
|
||||
@@ -18,7 +26,8 @@
|
||||
<path d="M12 14V4m0 0l-4 4m4-4l4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="profile-welcome-dialog__upload-tip">支持上传PDF、WORD格式,文件大小不超过10M</p>
|
||||
<p class="profile-welcome-dialog__upload-tip fs16 color-3 fw600">点击或拖拽文件到这里上传</p>
|
||||
<p class="profile-welcome-dialog__upload-tip">支持 PDF / DOC / DOCX,单个文件不超过 10MB</p>
|
||||
</template>
|
||||
<!-- 已上传状态 -->
|
||||
<template v-else>
|
||||
@@ -66,6 +75,9 @@ const route = useRoute()
|
||||
/** 已上传的文件名 */
|
||||
const uploadedFileName = ref('')
|
||||
|
||||
/** 拖拽悬停状态 */
|
||||
const isDragover = ref(false)
|
||||
|
||||
/** 内部控制弹窗显隐(用于上传时临时隐藏) */
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
@@ -87,61 +99,84 @@ watch(() => props.modelValue, (val) => {
|
||||
document.body.style.overflow = val ? 'hidden' : ''
|
||||
})
|
||||
|
||||
/** 允许的文件 MIME 类型 */
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
]
|
||||
|
||||
/** 处理文件校验与上传(点击和拖拽共用) */
|
||||
async function processFile(file: File) {
|
||||
// 格式校验
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
ElMessage.error('仅支持上传PDF、WORD格式文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 文件大小校验(10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
ElMessage.error('文件大小不能超过10M')
|
||||
return
|
||||
}
|
||||
|
||||
// 标记上传中,降低弹窗层级让 loading 显示在上面
|
||||
uploading.value = true
|
||||
|
||||
// 全屏加载
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '简历解析中,请耐心等待…',
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
customClass: 'profile-welcome-loading',
|
||||
})
|
||||
|
||||
try {
|
||||
// 上传简历文件
|
||||
const res = await uploadResume(file)
|
||||
if (res.code === 0 && res.data?.resumeId) {
|
||||
resumeId.value = String(res.data.resumeId)
|
||||
uploadedFileName.value = file.name
|
||||
|
||||
// 继续调用同步个人资料接口,加载特效延续
|
||||
loading.setText('正在同步个人资料…')
|
||||
await syncProfileFromResume(resumeId.value)
|
||||
loading.close()
|
||||
uploading.value = false
|
||||
ElMessage.success('简历上传并同步成功')
|
||||
} else {
|
||||
loading.close()
|
||||
uploading.value = false
|
||||
ElMessage.error(res.msg || '上传失败')
|
||||
}
|
||||
} catch {
|
||||
loading.close()
|
||||
uploading.value = false
|
||||
ElMessage.error('上传失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击上传区域 — 弹出文件选择 */
|
||||
function handleUploadClick() {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = '.pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
|
||||
input.onchange = async () => {
|
||||
input.onchange = () => {
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
// 文件大小校验(10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
ElMessage.error('文件大小不能超过10M')
|
||||
return
|
||||
}
|
||||
|
||||
// 标记上传中,降低弹窗层级让 loading 显示在上面
|
||||
uploading.value = true
|
||||
|
||||
// 全屏加载
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '简历解析中,请耐心等待…',
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
customClass: 'profile-welcome-loading',
|
||||
})
|
||||
|
||||
try {
|
||||
// 上传简历文件
|
||||
const res = await uploadResume(file)
|
||||
if (res.code === 0 && res.data?.resumeId) {
|
||||
resumeId.value = String(res.data.resumeId)
|
||||
uploadedFileName.value = file.name
|
||||
|
||||
// 继续调用同步个人资料接口,加载特效延续
|
||||
loading.setText('正在同步个人资料…')
|
||||
await syncProfileFromResume(resumeId.value)
|
||||
loading.close()
|
||||
uploading.value = false
|
||||
ElMessage.success('简历上传并同步成功')
|
||||
} else {
|
||||
loading.close()
|
||||
uploading.value = false
|
||||
ElMessage.error(res.msg || '上传失败')
|
||||
}
|
||||
} catch {
|
||||
loading.close()
|
||||
uploading.value = false
|
||||
ElMessage.error('上传失败,请稍后重试')
|
||||
}
|
||||
if (file) processFile(file)
|
||||
}
|
||||
|
||||
input.click()
|
||||
}
|
||||
|
||||
/** 拖拽放下文件 — 取第一个文件进行上传 */
|
||||
function handleDrop(e: DragEvent) {
|
||||
isDragover.value = false
|
||||
const file = e.dataTransfer?.files[0]
|
||||
if (file) processFile(file)
|
||||
}
|
||||
|
||||
/** 点击开始匹配按钮 */
|
||||
function handleStart() {
|
||||
emit('update:modelValue', false)
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<!-- 简历上传弹窗 — 支持点击选择和拖拽上传,确认后返回 File 对象 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="modelValue" class="resume-upload-overlay" @click.self="handleCancel">
|
||||
<div class="resume-upload-dialog">
|
||||
<!-- 标题行 -->
|
||||
<div class="resume-upload-dialog__header">
|
||||
<h2 class="resume-upload-dialog__title">上传简历</h2>
|
||||
<button class="resume-upload-dialog__close" aria-label="关闭" @click="handleCancel">
|
||||
<svg viewBox="0 0 16 16" fill="none" width="16" height="16">
|
||||
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 副标题说明 -->
|
||||
<p class="resume-upload-dialog__desc">上传新的简历文件,系统会自动解析内容并同步到简历中心。</p>
|
||||
|
||||
<!-- 上传区域(点击 + 拖拽) -->
|
||||
<div
|
||||
class="resume-upload-dialog__drop-zone"
|
||||
:class="{ 'is-dragover': isDragover, 'has-file': !!selectedFile }"
|
||||
@click="triggerFileInput"
|
||||
@dragover.prevent="isDragover = true"
|
||||
@dragenter.prevent="isDragover = true"
|
||||
@dragleave.prevent="isDragover = false"
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<!-- 未选择文件状态 -->
|
||||
<template v-if="!selectedFile">
|
||||
<div class="resume-upload-dialog__drop-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" width="28" height="28">
|
||||
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="resume-upload-dialog__drop-text">点击或拖拽文件到这里上传</p>
|
||||
<p class="resume-upload-dialog__drop-hint">支持 PDF / DOC / DOCX,单个文件不超过 10MB</p>
|
||||
</template>
|
||||
<!-- 已选择文件状态 -->
|
||||
<template v-else>
|
||||
<div class="resume-upload-dialog__file-info">
|
||||
<span class="resume-upload-dialog__file-icon">📄</span>
|
||||
<span class="resume-upload-dialog__file-name">{{ selectedFile.name }}</span>
|
||||
<button class="resume-upload-dialog__file-remove" aria-label="移除文件" @click.stop="removeFile">✕</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<div class="resume-upload-dialog__footer">
|
||||
<button class="resume-upload-dialog__btn resume-upload-dialog__btn--cancel" @click="handleCancel">取消</button>
|
||||
<button
|
||||
class="resume-upload-dialog__btn resume-upload-dialog__btn--confirm"
|
||||
:disabled="!selectedFile"
|
||||
@click="handleConfirm"
|
||||
>确认上传</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
/** 组件 Props */
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
|
||||
/** 组件 Emits */
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm', file: File): void
|
||||
}>()
|
||||
|
||||
/** 拖拽悬停状态 */
|
||||
const isDragover = ref(false)
|
||||
|
||||
/** 已选择的文件 */
|
||||
const selectedFile = ref<File | null>(null)
|
||||
|
||||
/** 允许的文件 MIME 类型 */
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
]
|
||||
|
||||
/** 弹窗打开时重置状态 */
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val) {
|
||||
selectedFile.value = null
|
||||
isDragover.value = false
|
||||
}
|
||||
})
|
||||
|
||||
/** 校验文件格式和大小 */
|
||||
function validateFile(file: File): boolean {
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
ElMessage.error('仅支持上传 PDF、DOC、DOCX 格式文件')
|
||||
return false
|
||||
}
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
ElMessage.error('文件大小不能超过 10MB')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** 点击区域触发文件选择 */
|
||||
function triggerFileInput() {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = '.pdf,.doc,.docx'
|
||||
input.onchange = () => {
|
||||
const file = input.files?.[0]
|
||||
if (file && validateFile(file)) {
|
||||
selectedFile.value = file
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
/** 拖拽放下文件 */
|
||||
function handleDrop(e: DragEvent) {
|
||||
isDragover.value = false
|
||||
const file = e.dataTransfer?.files[0]
|
||||
if (file && validateFile(file)) {
|
||||
selectedFile.value = file
|
||||
}
|
||||
}
|
||||
|
||||
/** 移除已选文件 */
|
||||
function removeFile() {
|
||||
selectedFile.value = null
|
||||
}
|
||||
|
||||
/** 取消关闭弹窗 */
|
||||
function handleCancel() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
/** 确认上传 — 将 File 对象传给父组件 */
|
||||
function handleConfirm() {
|
||||
if (selectedFile.value) {
|
||||
emit('confirm', selectedFile.value)
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -155,7 +155,7 @@ import { cancelAccount } from '@/api/auth'
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
|
||||
/** 组件 Emits */
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void; (e: 'deleted'): void }>()
|
||||
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
@@ -225,9 +225,10 @@ const handleConfirmDelete = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 完成 — 关闭弹窗并退出登录 */
|
||||
/** 完成 — 关闭弹窗,通知父组件注销完成,跳转首页 */
|
||||
const handleFinish = () => {
|
||||
emit('update:modelValue', false)
|
||||
emit('deleted')
|
||||
store.commit('SET_AUTHENTICATED', false)
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
<JobGoalDialog v-model="showGoalDialog" />
|
||||
|
||||
<!-- 注销账号弹窗 -->
|
||||
<SettingsDeleteAccountDialog v-model="showDeleteAccount" />
|
||||
<SettingsDeleteAccountDialog v-model="showDeleteAccount" @deleted="onAccountDeleted" />
|
||||
|
||||
<!-- 邀请注册送会员弹窗 -->
|
||||
<SettingsInviteDialog v-model="showInviteDialog" />
|
||||
@@ -385,6 +385,11 @@ const handleDeleteAccount = () => {
|
||||
showDeleteAccount.value = true
|
||||
}
|
||||
|
||||
/** 注销完成回调 — 关闭设置弹窗 */
|
||||
const onAccountDeleted = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
/** 管理订阅 */
|
||||
const handleManageSubscription = () => {
|
||||
ElMessage.info('管理订阅功能开发中')
|
||||
|
||||
+204
-10
@@ -50,15 +50,13 @@
|
||||
v-if="showTaskListDropdown"
|
||||
:pending-list="applyJobList"
|
||||
@removed="handleTaskRemoved"
|
||||
@view-all="showTaskListDropdown = false"
|
||||
@view-all="handleOpenPendingListPanel"
|
||||
@view-all-completed="handleOpenApplyProgressPanel"
|
||||
/>
|
||||
<!-- 齿轮按钮 -->
|
||||
<!-- <button class="agent-main__tool-btn" title="配置">-->
|
||||
<!-- <svg viewBox="0 0 24 24" fill="none">-->
|
||||
<!-- <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5" />-->
|
||||
<!-- <path d="M12 1v2m0 18v2m-9-11h2m18 0h2m-3.3-7.7-1.4 1.4M4.7 19.3l1.4-1.4m0-11.8L4.7 4.7m14.6 14.6-1.4-1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />-->
|
||||
<!-- </svg>-->
|
||||
<!-- </button>-->
|
||||
<!-- 设置按钮 -->
|
||||
<button class="agent-main__tool-btn" title="配置" @click="handleOpenSettingsPanel">
|
||||
设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,6 +81,7 @@
|
||||
:summary="getRecommendSummaryFromExtra(msg.extra)"
|
||||
:jobs="getRecommendJobsFromExtra(msg.extra)"
|
||||
@view-all="handleOpenRecommendPanel(msg.id)"
|
||||
@click-job="handleChatJobClick"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -138,6 +137,7 @@
|
||||
@view-more="handleViewMoreJobs"
|
||||
@toggle="handleToggleJobApply"
|
||||
@add-all="handleAddAllJobs"
|
||||
@click-job="handleMatchJobClick"
|
||||
/>
|
||||
<!-- 模式2:简历生成进度 -->
|
||||
<div v-else-if="rightPanelMode === 'generating'" class="agent-resume-generating">
|
||||
@@ -154,6 +154,32 @@
|
||||
<div v-else-if="rightPanelMode === 'resume'" class="agent-main__right-resume">
|
||||
<JobResumeTemplate :resume-data="applyResumeData" />
|
||||
</div>
|
||||
<!-- 模式4:岗位预览 -->
|
||||
<AgentJobPreviewPanel
|
||||
v-else-if="rightPanelMode === 'jobPreview'"
|
||||
:job-id="previewJobId"
|
||||
:application-status="previewJobApplicationStatus"
|
||||
@back="handleJobPreviewBack"
|
||||
@add="handleJobPreviewAdd"
|
||||
@remove="handleJobPreviewRemove"
|
||||
/>
|
||||
<!-- 模式5:全部的待投递列表 -->
|
||||
<AgentPendingJobListPanel
|
||||
v-else-if="rightPanelMode === 'pendingList'"
|
||||
@close="handleClosePendingListPanel"
|
||||
@removed="handlePendingListRemoved"
|
||||
/>
|
||||
<!-- 模式6:全部的已投递进度 -->
|
||||
<AgentApplyProgressPanel
|
||||
v-else-if="rightPanelMode === 'applyProgress'"
|
||||
@close="handleCloseApplyProgressPanel"
|
||||
/>
|
||||
<!-- 模式7:设置面板 -->
|
||||
<AgentSettingPanel
|
||||
v-else-if="rightPanelMode === 'settings'"
|
||||
@close="handleCloseSettingsPanel"
|
||||
/>
|
||||
<!-- 模式7:AI助手设置 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,6 +198,10 @@ import AgentMatchJobAdd from '@/components/AgentMatchJobAdd.vue'
|
||||
import AgentApplyProgress from '@/components/AgentApplyProgress.vue'
|
||||
import AgentTaskListDropdown from '@/components/AgentTaskListDropdown.vue'
|
||||
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
||||
import AgentJobPreviewPanel from '@/components/AgentJobPreviewPanel.vue'
|
||||
import AgentPendingJobListPanel from '@/components/AgentPendingJobListPanel.vue'
|
||||
import AgentApplyProgressPanel from '@/components/AgentApplyProgressPanel.vue'
|
||||
import AgentSettingPanel from '@/components/AgentSettingPanel.vue'
|
||||
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
||||
import { fetchAgentConfig, saveAgentConfig, fetchAgentRecommend, fetchAgentChatMessages, addAgentChatMessage, applyJob, cancelApplyJob, fetchApplyByJobIds, sendAgentChat, optimizeAgentResume } from '@/api/agent'
|
||||
import type { AgentConfig, AgentRecommendJob, AgentChatMessage, AgentChatHistoryItem } from '@/api/agent'
|
||||
@@ -370,6 +400,11 @@ async function loadApplyList() {
|
||||
|
||||
/** 切换待投递列表下拉弹窗显隐 */
|
||||
function toggleTaskListDropdown() {
|
||||
/* 投递流程进行中时禁止打开待投递列表 */
|
||||
if (isApplying.value) {
|
||||
ElMessage.warning('请完成或取消投递流程后再打开编辑待投递列表')
|
||||
return
|
||||
}
|
||||
showTaskListDropdown.value = !showTaskListDropdown.value
|
||||
if (showTaskListDropdown.value) {
|
||||
loadApplyList()
|
||||
@@ -564,6 +599,11 @@ function handleViewMoreJobs() {
|
||||
|
||||
/** 打开右侧匹配岗位面板 — 先查询投递记录更新岗位状态再显示 */
|
||||
async function handleOpenRecommendPanel(msgId: number) {
|
||||
/* 投递流程进行中(模式2简历生成 / 模式3简历预览)时禁止打开推荐面板 */
|
||||
if (rightPanelMode.value === 'generating' || rightPanelMode.value === 'resume') {
|
||||
ElMessage.warning('请完成或退出投递流程后再添加岗位')
|
||||
return
|
||||
}
|
||||
activeRecommendMsgId.value = msgId
|
||||
showRightPanel.value = true
|
||||
panelLoading.value = true
|
||||
@@ -618,10 +658,137 @@ function handleCloseRecommendPanel() {
|
||||
rightPanelMode.value = 'recommend'
|
||||
}
|
||||
|
||||
// ==================== 岗位预览(模式4) ====================
|
||||
|
||||
/** 当前预览的岗位 ID */
|
||||
const previewJobId = ref('')
|
||||
|
||||
/** 当前预览岗位的投递状态 */
|
||||
const previewJobApplicationStatus = ref<number | null>(null)
|
||||
|
||||
/** 进入岗位预览前记录的面板模式(用于返回) */
|
||||
const prevPanelMode = ref<'recommend' | 'generating' | 'resume' | 'jobPreview' | 'pendingList' | 'applyProgress' | 'settings'>('recommend')
|
||||
|
||||
/** 打开岗位预览面板 */
|
||||
function handleOpenJobPreview(jobId: string | number, applicationStatus?: number | null) {
|
||||
previewJobId.value = String(jobId)
|
||||
previewJobApplicationStatus.value = applicationStatus ?? null
|
||||
prevPanelMode.value = rightPanelMode.value
|
||||
rightPanelMode.value = 'jobPreview'
|
||||
showRightPanel.value = true
|
||||
}
|
||||
|
||||
/** 岗位预览返回 — 回到之前的面板模式 */
|
||||
function handleJobPreviewBack() {
|
||||
/* 如果之前是 recommend 模式且有选中的消息,回到推荐面板 */
|
||||
if (prevPanelMode.value === 'recommend' && activeRecommendMsgId.value !== null) {
|
||||
rightPanelMode.value = 'recommend'
|
||||
} else {
|
||||
/* 否则关闭面板 */
|
||||
showRightPanel.value = false
|
||||
rightPanelMode.value = 'recommend'
|
||||
}
|
||||
}
|
||||
|
||||
/** 岗位预览中点击添加 — 添加到待投递 */
|
||||
async function handleJobPreviewAdd(jobId: string) {
|
||||
try {
|
||||
await applyJob({ jobId, status: -1 })
|
||||
/* 更新对话消息中的岗位状态 */
|
||||
updateJobStatusInMessages(Number(jobId), -1)
|
||||
previewJobApplicationStatus.value = -1
|
||||
await loadApplyList()
|
||||
ElMessage.success('岗位已添加到待投递')
|
||||
} catch {
|
||||
ElMessage.error('操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
/** 岗位预览中点击移出 — 从待投递移除 */
|
||||
async function handleJobPreviewRemove(jobId: string) {
|
||||
try {
|
||||
await cancelApplyJob(jobId)
|
||||
/* 更新对话消息中的岗位状态 */
|
||||
updateJobStatusInMessages(Number(jobId), null)
|
||||
previewJobApplicationStatus.value = null
|
||||
await loadApplyList()
|
||||
ElMessage.success('岗位已从待投递移除')
|
||||
} catch {
|
||||
ElMessage.error('操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
/** AgentMatchJobAdd 中点击岗位 — 打开岗位预览 */
|
||||
function handleMatchJobClick(job: AgentRecommendJob) {
|
||||
handleOpenJobPreview(job.id, job.applicationStatus)
|
||||
}
|
||||
|
||||
/** AgentChatJobList 中点击岗位 — 打开岗位预览 */
|
||||
function handleChatJobClick(job: AgentRecommendJob) {
|
||||
/* 投递流程进行中时禁止打开岗位详情 */
|
||||
if (isApplying.value) {
|
||||
ElMessage.warning('请完成或取消投递流程后再查看岗位详情')
|
||||
return
|
||||
}
|
||||
handleOpenJobPreview(job.id, job.applicationStatus)
|
||||
}
|
||||
|
||||
// ==================== 待投递列表面板(模式5) ====================
|
||||
|
||||
/** 打开待投递列表面板 */
|
||||
function handleOpenPendingListPanel() {
|
||||
showTaskListDropdown.value = false
|
||||
rightPanelMode.value = 'pendingList'
|
||||
showRightPanel.value = true
|
||||
}
|
||||
|
||||
/** 关闭待投递列表面板 */
|
||||
function handleClosePendingListPanel() {
|
||||
showRightPanel.value = false
|
||||
rightPanelMode.value = 'recommend'
|
||||
}
|
||||
|
||||
/** 待投递列表面板中移除岗位后同步父组件数据 */
|
||||
function handlePendingListRemoved(jobId: string) {
|
||||
const idx = applyJobList.value.findIndex(j => j.id === jobId)
|
||||
if (idx !== -1) {
|
||||
applyJobList.value.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 申请进度面板(模式6) ====================
|
||||
|
||||
/** 打开申请进度面板 */
|
||||
function handleOpenApplyProgressPanel() {
|
||||
showTaskListDropdown.value = false
|
||||
rightPanelMode.value = 'applyProgress'
|
||||
showRightPanel.value = true
|
||||
}
|
||||
|
||||
/** 关闭申请进度面板 */
|
||||
function handleCloseApplyProgressPanel() {
|
||||
showRightPanel.value = false
|
||||
rightPanelMode.value = 'recommend'
|
||||
}
|
||||
|
||||
// ==================== 设置面板(模式7) ====================
|
||||
|
||||
/** 打开设置面板 */
|
||||
function handleOpenSettingsPanel() {
|
||||
rightPanelMode.value = 'settings'
|
||||
showRightPanel.value = true
|
||||
}
|
||||
|
||||
/** 关闭设置面板 */
|
||||
function handleCloseSettingsPanel() {
|
||||
showRightPanel.value = false
|
||||
rightPanelMode.value = 'recommend'
|
||||
}
|
||||
|
||||
// ==================== 投递进度流程 ====================
|
||||
|
||||
/** 右侧面板显示模式:recommend-岗位推荐 / generating-简历生成中 / resume-简历预览 */
|
||||
const rightPanelMode = ref<'recommend' | 'generating' | 'resume'>('recommend')
|
||||
/** 右侧面板显示模式 */
|
||||
const rightPanelMode = ref<'recommend' | 'generating' | 'resume' | 'jobPreview' | 'pendingList' | 'applyProgress' | 'settings'>('recommend')
|
||||
|
||||
/** 简历生成进度百分比(0-100) */
|
||||
const generateProgress = ref(0)
|
||||
@@ -770,6 +937,7 @@ async function handleStartApply() {
|
||||
// 打开右侧面板,显示生成进度
|
||||
rightPanelMode.value = 'generating'
|
||||
showRightPanel.value = true
|
||||
applyCancelled.value = false
|
||||
startProgressSimulation()
|
||||
|
||||
try {
|
||||
@@ -778,12 +946,18 @@ async function handleStartApply() {
|
||||
jobId: targetJob.id,
|
||||
})
|
||||
|
||||
// 如果在等待期间已取消,直接退出
|
||||
if (applyCancelled.value) return
|
||||
|
||||
// 接口返回后涨满进度条
|
||||
finishProgress()
|
||||
|
||||
// 等待 1 秒让用户看到 100%
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 再次检查取消状态
|
||||
if (applyCancelled.value) return
|
||||
|
||||
// 解析简历数据 — 映射接口返回格式到 ResumeTemplateData
|
||||
const apiData = res?.data || res
|
||||
if (apiData) {
|
||||
@@ -801,6 +975,8 @@ async function handleStartApply() {
|
||||
rightPanelMode.value = 'resume'
|
||||
}
|
||||
} catch (e) {
|
||||
// 取消导致的异常不提示
|
||||
if (applyCancelled.value) return
|
||||
console.error('[Agent] 优化简历失败', e)
|
||||
ElMessage.error('简历优化失败,请重试')
|
||||
finishProgress()
|
||||
@@ -895,8 +1071,20 @@ function handleTogglePause() {
|
||||
isPaused.value = !isPaused.value
|
||||
}
|
||||
|
||||
/** 标记当前投递流程是否已被取消(防止异步回调覆盖状态) */
|
||||
const applyCancelled = ref(false)
|
||||
|
||||
/** 取消投递流程 */
|
||||
function handleCancelApply(msg: AgentChatMessage) {
|
||||
// 标记已取消,阻止进行中的异步回调继续操作
|
||||
applyCancelled.value = true
|
||||
// 清除进度条定时器
|
||||
if (progressTimer) {
|
||||
clearInterval(progressTimer)
|
||||
progressTimer = null
|
||||
}
|
||||
// 重置暂停状态
|
||||
isPaused.value = false
|
||||
// 从消息列表中移除该条 apply_progress
|
||||
const idx = chatMessages.value.findIndex(m => m.id === msg.id)
|
||||
if (idx !== -1) {
|
||||
@@ -1029,6 +1217,7 @@ async function autoStartNextApply() {
|
||||
// 第1步:优化简历
|
||||
rightPanelMode.value = 'generating'
|
||||
showRightPanel.value = true
|
||||
applyCancelled.value = false
|
||||
startProgressSimulation()
|
||||
|
||||
try {
|
||||
@@ -1037,9 +1226,13 @@ async function autoStartNextApply() {
|
||||
jobId: targetJob.id,
|
||||
})
|
||||
|
||||
if (applyCancelled.value) return
|
||||
|
||||
finishProgress()
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
if (applyCancelled.value) return
|
||||
|
||||
const apiData = res?.data || res
|
||||
if (apiData) {
|
||||
const resumeResult = mapOptimizeResumeToTemplate(apiData)
|
||||
@@ -1053,6 +1246,7 @@ async function autoStartNextApply() {
|
||||
rightPanelMode.value = 'resume'
|
||||
}
|
||||
} catch (e) {
|
||||
if (applyCancelled.value) return
|
||||
console.error('[Agent] 优化简历失败', e)
|
||||
ElMessage.error('简历优化失败,请重试')
|
||||
finishProgress()
|
||||
|
||||
@@ -564,8 +564,6 @@ function goSearchJobs() {
|
||||
onMounted(() => {
|
||||
// 加载公共工具数据(行业分类、岗位分类、地区分类等)
|
||||
store.dispatch('loadCommonData')
|
||||
// 触发预渲染事件 — 配合 vite.config.ts 中 PrerenderPlugin 的 renderAfterDocumentEvent 设置
|
||||
document.dispatchEvent(new Event('prerender-trigger'))
|
||||
})
|
||||
|
||||
/** 点击登陆按钮 — 打开登录弹窗,登录成功后跳转到 jobs 页面 */
|
||||
|
||||
+6
-29
@@ -259,8 +259,7 @@
|
||||
<!-- 职位问题反馈弹窗 -->
|
||||
<JobFeedbackDialog ref="feedbackDialogRef" v-model="showFeedbackDialog" :job-id="feedbackJobId" />
|
||||
|
||||
<!-- 欢迎上传简历弹窗 -->
|
||||
<ProfileWelcomeDialog v-model="showWelcomeDialog" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -276,8 +275,8 @@ import JobFeedbackDialog from '@/components/JobFeedbackDialog.vue'
|
||||
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
||||
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
||||
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||||
import ProfileWelcomeDialog from '@/components/ProfileWelcomeDialog.vue'
|
||||
import { fetchProfile } from '@/api/profile'
|
||||
|
||||
|
||||
import { fetchJobList, fetchFavoriteList, toggleJobFavorite, removeJobFavorite, fetchFavoriteCount, fetchApplyList, fetchApplyCount, removeJobFromList } from '@/api/jobs'
|
||||
import type { JobListItem, JobListParams, FavoriteListParams, ApplyListParams, ApplyCountData } from '@/api/jobs'
|
||||
|
||||
@@ -332,8 +331,7 @@ const feedbackJobId = ref<string | null>(null)
|
||||
/** 当前问助手的岗位 ID(传给 AiChat 组件) */
|
||||
const currentAskJobId = ref<string>('')
|
||||
|
||||
/** 欢迎弹窗的显示状态 */
|
||||
const showWelcomeDialog = ref(false)
|
||||
|
||||
|
||||
/** 点击"问助手"按钮,传入岗位 ID 给 AiChat */
|
||||
function askAssistant(job: JobItem) {
|
||||
@@ -552,15 +550,7 @@ onMounted(async () => {
|
||||
// 加载用户个人信息到全局 store
|
||||
store.dispatch('loadUserInfo')
|
||||
|
||||
// 检查个人资料是否存在,不存在则弹出欢迎弹窗
|
||||
// try {
|
||||
// const profileRes = await fetchProfile()
|
||||
// if (profileRes.code === '0' && (!profileRes.data || profileRes.data === null)) {
|
||||
// showWelcomeDialog.value = true
|
||||
// }
|
||||
// } catch {
|
||||
// // 接口异常不阻塞页面加载
|
||||
// }
|
||||
|
||||
|
||||
// 加载收藏统计(用于 Tab 标签显示)
|
||||
loadFavoriteCount()
|
||||
@@ -907,20 +897,7 @@ watch(
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 监听登录状态变化 — 登录成功后检查个人资料是否存在
|
||||
watch(isAuthenticated, async (newVal, oldVal) => {
|
||||
if (newVal && !oldVal) {
|
||||
// 从未登录变为已登录,检查个人资料
|
||||
try {
|
||||
const profileRes = await fetchProfile()
|
||||
if (profileRes.code === '0' && (!profileRes.data || profileRes.data === null)) {
|
||||
showWelcomeDialog.value = true
|
||||
}
|
||||
} catch {
|
||||
// 接口异常不阻塞
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 监听 Tab 切换,重置列表状态并加载对应数据
|
||||
watch(activeTab, (newTab, oldTab) => {
|
||||
|
||||
+36
-4
@@ -10,8 +10,8 @@
|
||||
@save="handleSaveEdit"
|
||||
/>
|
||||
|
||||
<!-- 欢迎上传简历弹窗 -->
|
||||
<ProfileWelcomeDialog v-model="showWelcomeDialog" />
|
||||
<!-- 欢迎上传简历弹窗(首次无个人资料时自动弹出) -->
|
||||
<ResumeUploadDialog v-model="showWelcomeDialog" @confirm="handleWelcomeUpload" />
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<div class="profile-page__header">
|
||||
@@ -60,8 +60,11 @@ import { useStore } from 'vuex'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
|
||||
import ProfilePageContent from '@/components/ProfilePageContent.vue'
|
||||
import ProfileWelcomeDialog from '@/components/ProfileWelcomeDialog.vue'
|
||||
import { saveProfile, fetchProfile, fetchEducation, saveEducation, fetchWork, saveWork, fetchInternship, saveInternship, fetchProject, saveProject, fetchCompetition, saveCompetition } from '@/api/profile'
|
||||
import ResumeUploadDialog from '@/components/ResumeUploadDialog.vue'
|
||||
import { saveProfile, fetchProfile, fetchEducation, saveEducation, fetchWork, saveWork, fetchInternship, saveInternship, fetchProject, saveProject, fetchCompetition, saveCompetition, syncProfileFromResume } from '@/api/profile'
|
||||
import { uploadResume } from '@/utils/aiRequest'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
import 'element-plus/es/components/loading/style/css'
|
||||
import type { SaveEducationItem, SaveWorkItem, SaveProjectItem, SaveCompetitionItem } from '@/api/profile'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -217,6 +220,35 @@ const showEditDrawer = ref(false)
|
||||
/** 欢迎弹窗的显示状态 */
|
||||
const showWelcomeDialog = ref(false)
|
||||
|
||||
/** 欢迎弹窗确认上传回调 — 上传简历并同步个人资料,完成后刷新页面 */
|
||||
async function handleWelcomeUpload(file: File) {
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '简历解析中,请耐心等待…',
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
customClass: 'profile-welcome-loading',
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await uploadResume(file)
|
||||
if (res.code === 0 && res.data?.resumeId) {
|
||||
// 继续同步个人资料
|
||||
loading.setText('正在同步个人资料…')
|
||||
await syncProfileFromResume(String(res.data.resumeId))
|
||||
loading.close()
|
||||
ElMessage.success('简历上传并同步成功')
|
||||
// 刷新页面数据
|
||||
router.go(0)
|
||||
} else {
|
||||
loading.close()
|
||||
ElMessage.error(res.msg || '上传失败')
|
||||
}
|
||||
} catch {
|
||||
loading.close()
|
||||
ElMessage.error('上传失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
/** 登录状态 */
|
||||
const isAuthenticated = computed(() => store.state.isAuthenticated)
|
||||
|
||||
|
||||
+40
-35
@@ -92,6 +92,12 @@
|
||||
:resume-id="exportResumeId"
|
||||
:resume-name="exportResumeName"
|
||||
/>
|
||||
|
||||
<!-- 上传简历弹窗组件 -->
|
||||
<ResumeUploadDialog
|
||||
v-model="uploadDialogVisible"
|
||||
@confirm="handleUploadConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -101,13 +107,15 @@ import { useRouter } from 'vue-router'
|
||||
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,
|
||||
} from '@/api/resume'
|
||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
|
||||
// ElLoading.service() 是命令式调用,按需引入插件不会自动加载其样式,需手动引入
|
||||
// 命令式调用的组件,按需引入插件不会自动加载其样式,需手动引入
|
||||
import 'element-plus/es/components/loading/style/css'
|
||||
import 'element-plus/es/components/message-box/style/css'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -218,6 +226,11 @@ const exportResumeId = ref('')
|
||||
/** 当前导出的简历名称(用于文件名) */
|
||||
const exportResumeName = ref('')
|
||||
|
||||
// ==================== 上传简历弹窗状态 ====================
|
||||
|
||||
/** 上传弹窗是否可见 */
|
||||
const uploadDialogVisible = ref(false)
|
||||
|
||||
/** 弹出菜单操作项 */
|
||||
const popupActions = ['设为默认简历', '编辑名称岗位', '导出简历', '删除']
|
||||
|
||||
@@ -276,46 +289,38 @@ async function handleAction(action: string, id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 上传简历 — 弹出文件选择,选择后调用 AI 接口上传 */
|
||||
/** 上传简历 — 打开上传弹窗 */
|
||||
function handleUpload() {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
// 限定 pdf 和 word 格式
|
||||
input.accept = '.pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
uploadDialogVisible.value = true
|
||||
}
|
||||
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
/** 上传弹窗确认回调 — 接收 File 对象,调用接口上传 */
|
||||
async function handleUploadConfirm(file: File) {
|
||||
// 全屏加载提示,AI 接口响应较慢
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '简历解析中,请耐心等待…',
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
customClass: 'resume-upload-loading',
|
||||
})
|
||||
|
||||
// 全屏加载提示,AI 接口响应较慢
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '简历解析中,请耐心等待…',
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
customClass: 'resume-upload-loading',
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await uploadResume(file)
|
||||
if (res.code === 0) {
|
||||
// 上传成功,刷新列表
|
||||
loadResumeList()
|
||||
// 等待让后端异步处理数据,再关闭加载动画并跳转详情页
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
loading.close()
|
||||
// resumeId 已经是字符串(transformResponse 处理过),直接使用
|
||||
goDetail(res.data.resumeId)
|
||||
} else {
|
||||
loading.close()
|
||||
ElMessage.error(res.msg || '上传失败')
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
const res = await uploadResume(file)
|
||||
if (res.code === 0) {
|
||||
// 上传成功,刷新列表
|
||||
loadResumeList()
|
||||
// 等待让后端异步处理数据,再关闭加载动画并跳转详情页
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
loading.close()
|
||||
ElMessage.error('上传失败,请稍后重试')
|
||||
goDetail(res.data.resumeId)
|
||||
} else {
|
||||
loading.close()
|
||||
ElMessage.error(res.msg || '上传失败')
|
||||
}
|
||||
} catch {
|
||||
loading.close()
|
||||
ElMessage.error('上传失败,请稍后重试')
|
||||
}
|
||||
|
||||
input.click()
|
||||
}
|
||||
|
||||
/** 跳转到简历详情页 */
|
||||
|
||||
Reference in New Issue
Block a user