From 388dc6c9fac7e5cbb7d07562b902930ead85b8a8 Mon Sep 17 00:00:00 2001 From: bin <719488417@qq.com> Date: Tue, 9 Dec 2025 14:44:09 +0800 Subject: [PATCH] =?UTF-8?q?refactor=20:=20=E5=B2=97=E4=BD=8D=E5=88=97?= =?UTF-8?q?=E8=A1=A8-=E7=94=B3=E8=AF=B7=E4=BA=BA-=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E7=AB=9E=E4=BA=89=E5=8A=9B=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.tsx | 10 +- .../List/SeeMatching/detail copy.tsx | 375 ++++++++++++++++++ .../Management/List/SeeMatching/detail.tsx | 367 ++++++++++++++--- .../Management/List/SeeMatching/index.tsx | 58 +-- src/services/Management/list.ts | 7 + src/types/Management/list.d.ts | 26 ++ 6 files changed, 761 insertions(+), 82 deletions(-) create mode 100644 src/pages/Management/List/SeeMatching/detail copy.tsx diff --git a/src/app.tsx b/src/app.tsx index ecc45a7..04903e1 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -218,12 +218,12 @@ export async function render(oldRender: () => void) { const checkRegion = 5 * 60 * 1000; export const request = { ...errorConfig, - // baseURL: process.env.NODE_ENV === 'development' ? '' : 'https://qd.zhaopinzao8dian.com/api', + baseURL: process.env.NODE_ENV === 'development' ? '' : 'https://qd.zhaopinzao8dian.com/api', // baseURL: 'http://39.98.44.136:8080', - baseURL: - process.env.NODE_ENV === 'development' - ? 'http://10.213.6.207:19010' - : 'http://10.213.6.207:19010/api', + // baseURL: + // process.env.NODE_ENV === 'development' + // ? 'http://10.213.6.207:19010' + // : 'http://10.213.6.207:19010/api', requestInterceptors: [ (url: any, options: { headers: any; data?: any; params?: any; method?: string }) => { const headers = options.headers ? options.headers : {}; diff --git a/src/pages/Management/List/SeeMatching/detail copy.tsx b/src/pages/Management/List/SeeMatching/detail copy.tsx new file mode 100644 index 0000000..7a1cb59 --- /dev/null +++ b/src/pages/Management/List/SeeMatching/detail copy.tsx @@ -0,0 +1,375 @@ +import { Modal, Row, Col, Card, Statistic } from 'antd'; +import React, { useEffect, useRef, useState } from 'react'; + +export type ListFormProps = { + onClose: (flag?: boolean, formVals?: unknown) => void; + open: boolean; + values?: Partial; + competitivenessData?: API.ManagementList.CompetitivenessData; +}; + +const Detail: React.FC = (props) => { + const { open, values, competitivenessData } = props; + const canvasRef = useRef(null); + const [currentStep, setCurrentStep] = useState(0); + const [isStyleInjected, setIsStyleInjected] = useState(false); + + const matchingDegree = ['一般', '良好', '优秀', '极好']; + + // 注入进度条样式 + useEffect(() => { + if (!isStyleInjected) { + const styleId = 'progress-bar-styles'; + if (!document.getElementById(styleId)) { + const style = document.createElement('style'); + style.id = styleId; + + // 生成样式 + let styleContent = ` + .progress-container { + display: flex; + align-items: center; + gap: 4px; + margin-top: 12px; + } + + .progress-text { + margin-top: 4px; + display: flex; + align-items: center; + gap: 4px; + justify-content: space-around; + width: 100%; + font-weight: 400; + font-size: 10px; + color: #999999; + text-align: center; + } + + .progress-item { + width: 25%; + height: 12px; + background-color: #eee; + border-radius: 12px; + overflow: hidden; + position: relative; + transition: background-color 0.3s; + } + + .progress-item.active { + background: linear-gradient(to right, #256bfa, #8c68ff); + } + `; + + // 生成半透明进度样式 + for (let i = 0; i <= 100; i++) { + styleContent += ` + .progress-item.half${i}::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 100%; + background: linear-gradient(to right, #256bfa ${i}%, #eaeaea ${i}%); + border-radius: 12px; + } + `; + } + + style.textContent = styleContent; + document.head.appendChild(style); + } + setIsStyleInjected(true); + } + }, [isStyleInjected]); + + // 绘制雷达图 + const drawRadarChart = () => { + if (!canvasRef.current || !competitivenessData?.radarChart) return; + + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // 清空画布 + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const { skill, experience, education, salary, age, location } = competitivenessData.radarChart; + + // 使用与移动端相同的5个维度(去掉技能) + const labels = ['学历', '年龄', '工作地', '工作经验', '期望薪资']; + const data = [education, age, location, experience, salary].map(item => item * 0.05); + + // 绘制参数 + const width = 120; + const height = 120; + const centerX = canvas.width / 2; + const centerY = 125; // 与移动端保持一致的中心Y坐标 + const colors = ['#F5F5F5', '#F5F5F5', '#F5F5F5', '#F5F5F5', '#F5F5F5']; + const maxScore = 5; + const angleStep = (2 * Math.PI) / labels.length; + + // 1. 绘制背景圆形 + ctx.beginPath(); + ctx.arc(centerX, centerY, width, 0, 2 * Math.PI); + ctx.fillStyle = '#FFFFFF'; + ctx.fill(); + ctx.closePath(); + + // 2. 绘制5层多边形 + ctx.lineWidth = 1; + for (let i = 5; i > 0; i--) { + ctx.strokeStyle = colors[i - 1]; + ctx.beginPath(); + + labels.forEach((_, index) => { + const x = centerX + (width / 5) * i * Math.cos(angleStep * index - Math.PI / 2); + const y = centerY + (height / 5) * i * Math.sin(angleStep * index - Math.PI / 2); + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + + ctx.closePath(); + ctx.stroke(); + } + + // 3. 绘制坐标轴 + ctx.strokeStyle = '#F5F5F5'; + labels.forEach((_, index) => { + ctx.beginPath(); + const x1 = centerX + width * 0.6 * Math.cos(angleStep * index - Math.PI / 2); + const y1 = centerY + height * 0.6 * Math.sin(angleStep * index - Math.PI / 2); + const x = centerX + width * Math.cos(angleStep * index - Math.PI / 2); + const y = centerY + height * Math.sin(angleStep * index - Math.PI / 2); + ctx.moveTo(x1, y1); + ctx.lineTo(x, y); + ctx.closePath(); + ctx.stroke(); + }); + + // 4. 绘制数据区域 + const pointList: Array<{x: number, y: number}> = []; + ctx.strokeStyle = '#256BFA'; + ctx.fillStyle = 'rgba(37,107,250,0.24)'; + ctx.lineWidth = 2; + ctx.beginPath(); + + data.forEach((score, index) => { + const x = centerX + width * (score / maxScore) * Math.cos(angleStep * index - Math.PI / 2); + const y = centerY + height * (score / maxScore) * Math.sin(angleStep * index - Math.PI / 2); + pointList.push({ x, y }); + + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // 5. 绘制数据点 + ctx.fillStyle = '#256BFA'; + pointList.forEach(point => { + ctx.beginPath(); + ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI); + ctx.fill(); + }); + + // 6. 绘制标签 + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = '#000'; + ctx.font = 'bold 12px sans-serif'; + + labels.forEach((label, index) => { + const x = centerX + (width + 30) * Math.cos(angleStep * index - Math.PI / 2); + const y = centerY + (height + 26) * Math.sin(angleStep * index - Math.PI / 2); + ctx.fillText(label, x, y); + }); + }; + + // 获取进度条类名(和移动端逻辑一致) + const getProgressClass = (index: number) => { + if (currentStep === 0) return ''; + + const floorIndex = Math.floor(currentStep); + + if (index < floorIndex) { + return 'active'; + } else if (index === floorIndex) { + const decimal = currentStep % 1; + const percent = Math.round(decimal * 100); + return `half${percent}`; + } else { + return ''; + } + }; + + useEffect(() => { + if (competitivenessData?.matchScore !== undefined) { + // 计算当前步数(匹配分数转换为0-4的进度) + const score = competitivenessData.matchScore; + const step = score * 0.04; // 和移动端一样的计算方式 + setCurrentStep(step); + } + }, [competitivenessData]); + + useEffect(() => { + if (open && competitivenessData && canvasRef.current) { + // 初始化Canvas + const canvas = canvasRef.current; + canvas.width = 400; // 与移动端保持一致 + canvas.height = 350; // 与移动端保持一致 + + drawRadarChart(); + } + }, [open, competitivenessData]); + + return ( + props.onClose()} + maskClosable={true} + > + + {/* 统计数据 */} + + + + + + + + + + + + + + + + + + + + + + + + + + {/* 用户基本信息 */} + {values && ( + + + + +
用户名:{values.name}
+ + +
+ 期望薪资:{values.salaryMin}-{values.salaryMax} +
+ + +
学历:{values.education}
+ + +
区域:{values.area}
+ +
+
+ + )} + + {/* 竞争力分析 */} + + +
+ 三个月内共{competitivenessData?.totalApplicants || 0}位求职者申请, + 你的简历匹配度为{competitivenessData?.matchScore || 0}分, + 排名位于第{competitivenessData?.rank || 0}位, + 超过{competitivenessData?.percentile || 0}%的竞争者,处在优秀位置。 +
+ + {/* 雷达图 */} +
+ +
+ + {/* 匹配度进度条(使用DOM渲染,和移动端一样) */} +
+
+ 你与职位的匹配度 +
+ + {/* 进度条容器 */} +
+ {matchingDegree.map((_, index) => ( +
+ ))} +
+ + {/* 进度条文字 */} +
+ {matchingDegree.map((text, index) => ( +
+ {text} +
+ ))} +
+
+ + + + + ); +}; + +export default Detail; \ No newline at end of file diff --git a/src/pages/Management/List/SeeMatching/detail.tsx b/src/pages/Management/List/SeeMatching/detail.tsx index 1d890ea..6ef7bed 100644 --- a/src/pages/Management/List/SeeMatching/detail.tsx +++ b/src/pages/Management/List/SeeMatching/detail.tsx @@ -1,74 +1,333 @@ -import { Modal } from 'antd'; -import React, { useEffect, useRef } from 'react'; -import * as echarts from 'echarts'; +import { Modal, Row, Col, Card, Statistic } from 'antd'; +import React, { useEffect, useRef, useState } from 'react'; export type ListFormProps = { onClose: (flag?: boolean, formVals?: unknown) => void; open: boolean; - values?: Partial; - matching?: any; + loading: boolean; + competitivenessData?: API.ManagementList.CompetitivenessData; }; -const listEdit: React.FC = (props) => { - const { open, values, matching } = props; +const Detail: React.FC = (props) => { + const { open, loading, competitivenessData } = props; + const canvasRef = useRef(null); + const [currentStep, setCurrentStep] = useState(0); + const [isStyleInjected, setIsStyleInjected] = useState(false); - const mapRef = useRef(null); + const matchingDegree = ['一般', '良好', '优秀', '极好']; + + // 注入进度条样式 + useEffect(() => { + if (!isStyleInjected) { + const styleId = 'progress-bar-styles'; + if (!document.getElementById(styleId)) { + const style = document.createElement('style'); + style.id = styleId; + + // 生成样式 + let styleContent = ` + .progress-container { + display: flex; + align-items: center; + gap: 4px; + margin-top: 12px; + } + + .progress-text { + margin-top: 4px; + display: flex; + align-items: center; + gap: 4px; + justify-content: space-around; + width: 100%; + font-weight: 400; + font-size: 10px; + color: #999999; + text-align: center; + } + + .progress-item { + width: 25%; + height: 12px; + background-color: #eee; + border-radius: 12px; + overflow: hidden; + position: relative; + transition: background-color 0.3s; + } + + .progress-item.active { + background: linear-gradient(to right, #256bfa, #8c68ff); + } + `; + + // 生成半透明进度样式 + for (let i = 0; i <= 100; i++) { + styleContent += ` + .progress-item.half${i}::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 100%; + background: linear-gradient(to right, #256bfa ${i}%, #eaeaea ${i}%); + border-radius: 12px; + } + `; + } + + style.textContent = styleContent; + document.head.appendChild(style); + } + setIsStyleInjected(true); + } + }, [isStyleInjected]); + + // 绘制雷达图 + const drawRadarChart = () => { + if (!canvasRef.current || !competitivenessData?.radarChart) return; + + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // 清空画布 + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const { skill, experience, education, salary, age, location } = competitivenessData.radarChart; + + // + const labels = ['学历', '年龄', '工作地', '工作经验', '期望薪资']; + const data = [education, age, location, experience, salary].map((item) => item * 0.05); + + // 绘制参数 + const width = 120; + const height = 120; + const centerX = canvas.width / 2; + const centerY = canvas.height / 2 + 10; + const colors = ['#e6e6e6', '#e6e6e6', '#e6e6e6', '#e6e6e6', '#e6e6e6']; + const maxScore = 5; + const angleStep = (2 * Math.PI) / labels.length; + + // 1. 绘制背景圆形 + ctx.beginPath(); + ctx.arc(centerX, centerY, width, 0, 2 * Math.PI); + ctx.fillStyle = '#FFFFFF'; + ctx.fill(); + ctx.closePath(); + + // 2. 绘制5层多边形 + ctx.lineWidth = 1; + for (let i = 5; i > 0; i--) { + ctx.strokeStyle = colors[i - 1]; + ctx.beginPath(); + + labels.forEach((_, index) => { + const x = centerX + (width / 5) * i * Math.cos(angleStep * index - Math.PI / 2); + const y = centerY + (height / 5) * i * Math.sin(angleStep * index - Math.PI / 2); + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + + ctx.closePath(); + ctx.stroke(); + } + + // 3. 绘制坐标轴 + ctx.strokeStyle = '#e6e6e6'; + labels.forEach((_, index) => { + ctx.beginPath(); + const x1 = centerX + width * 0.6 * Math.cos(angleStep * index - Math.PI / 2); + const y1 = centerY + height * 0.6 * Math.sin(angleStep * index - Math.PI / 2); + const x = centerX + width * Math.cos(angleStep * index - Math.PI / 2); + const y = centerY + height * Math.sin(angleStep * index - Math.PI / 2); + ctx.moveTo(x1, y1); + ctx.lineTo(x, y); + ctx.closePath(); + ctx.stroke(); + }); + + // 4. 绘制数据区域 + const pointList: Array<{ x: number; y: number }> = []; + ctx.strokeStyle = '#256BFA'; + ctx.fillStyle = 'rgba(37,107,250,0.24)'; + ctx.lineWidth = 2; + ctx.beginPath(); + + data.forEach((score, index) => { + const x = centerX + width * (score / maxScore) * Math.cos(angleStep * index - Math.PI / 2); + const y = centerY + height * (score / maxScore) * Math.sin(angleStep * index - Math.PI / 2); + pointList.push({ x, y }); + + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // 5. 绘制数据点 + ctx.fillStyle = '#256BFA'; + pointList.forEach((point) => { + ctx.beginPath(); + ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI); + ctx.fill(); + }); + + // 6. 绘制标签 + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = '#000'; + ctx.font = 'bold 12px sans-serif'; + + labels.forEach((label, index) => { + const x = centerX + (width + 30) * Math.cos(angleStep * index - Math.PI / 2); + const y = centerY + (height + 26) * Math.sin(angleStep * index - Math.PI / 2); + ctx.fillText(label, x, y); + }); + }; + + // 获取进度条类名 + const getProgressClass = (index: number) => { + if (currentStep === 0) return ''; + + const floorIndex = Math.floor(currentStep); + + if (index < floorIndex) { + return 'active'; + } else if (index === floorIndex) { + const decimal = currentStep % 1; + const percent = Math.round(decimal * 100); + return `half${percent}`; + } else { + return ''; + } + }; useEffect(() => { - console.log(matching); - if (matching && open) { - const myCharts = echarts.init(mapRef.current); - const { educationMatch, maxSimilarity, salaryMatch, areaMatch } = matching; - // educationMatch - // maxSimilarity - // salaryMatch - // areaMatch - myCharts.setOption({ - radar: { - shape: 'circle', - indicator: [ - { name: '学历', max: 1 }, - { name: '薪资', max: 1 }, - { name: '区域', max: 1 }, - { name: '内容', max: 1 }, - ], - }, - series: [ - { - name: '综合匹配度', - type: 'radar', - data: [ - { - value: [educationMatch, salaryMatch, areaMatch, maxSimilarity], - name: 'Allocated Budget', - }, - ], - }, - ], - }); + if (competitivenessData?.matchScore !== undefined) { + // 计算匹配度 + const score = competitivenessData.matchScore; + const step = score * 0.04; + setCurrentStep(step); } - }, [matching]); + }, [competitivenessData]); + + useEffect(() => { + if (open && competitivenessData && canvasRef.current) { + // 初始化Canvas + const canvas = canvasRef.current; + canvas.width = 400; + canvas.height = 320; + drawRadarChart(); + } + }, [open, competitivenessData]); return ( props.onClose()} maskClosable={true} + loading={loading} + styles={{ + body: { + minHeight: '600px', + }, + }} > -
-
-
+ + {/* 统计数据 */} + + + + + + + + + + + + + + + + + + + + + + + + + + {/* 雷达图 */} + + + {/* 雷达图 */} +
+ +
+ + {/* 匹配度进度条*/} +
+
+ 职位匹配度 +
+ + {/* 进度条容器 */} +
+ {matchingDegree.map((_, index) => ( +
+ ))} +
+ + {/* 进度条文字 */} +
+ {matchingDegree.map((text, index) => ( +
+ {text} +
+ ))} +
+
+ + + ); }; -export default listEdit; +export default Detail; diff --git a/src/pages/Management/List/SeeMatching/index.tsx b/src/pages/Management/List/SeeMatching/index.tsx index fa57f76..3b3b35b 100644 --- a/src/pages/Management/List/SeeMatching/index.tsx +++ b/src/pages/Management/List/SeeMatching/index.tsx @@ -6,7 +6,7 @@ import { FormOutlined, PlusOutlined } from '@ant-design/icons'; import { getDictValueEnum } from '@/services/system/dict'; import DictTag from '@/components/DictTag'; import { exportCmsAppUserExport } from '@/services/mobileusers/list'; -import { exportCmsJobCandidates, getCmsJobIds } from '@/services/Management/list'; +import { exportCmsJobCandidates, getCmsJobIds,getJobCompetitiveness } from '@/services/Management/list'; import similarityJobs from '@/utils/similarity_Job'; import Detail from './detail'; @@ -26,7 +26,6 @@ const handleExport = async (values: API.MobileUser.ListParams) => { function ManagementList() { const access = useAccess(); - const formTableRef = useRef(); const actionRef = useRef(); @@ -35,14 +34,15 @@ function ManagementList() { const [experienceEnum, setExperienceEnum] = useState([]); const [areaEnum, setAreaEnum] = useState([]); const [sexEnum, setSexEnum] = useState([]); - const [hotEnum, setHotEnum] = useState([]); const [politicalEnum, setPoliticalEnum] = useState([]); const [currentRow, setCurrentRow] = useState(); const [modalVisible, setModalVisible] = useState(false); const [jobInfo, setJobInfo] = useState({}); - const [matchingDegree, setMatchingDegree] = useState(null); + const [competitivenessData, setCompetitivenessData] = useState({}); + const [competitivenessLoading, setCompetitivenessLoading] = useState(false); const params = useParams(); const id = params.id || '0'; + useEffect(() => { if (jobId !== id) { setJobId(id); @@ -58,6 +58,25 @@ function ManagementList() { } }; + // 获取竞争力分析数据 + const fetchCompetitivenessData = async (userId: string, jobId: string) => { + try { + setCompetitivenessLoading(true) + const res = await getJobCompetitiveness({ userId, jobId }); + if (res.code === 200) { + setCompetitivenessLoading(false) + setCompetitivenessData(res.data); + return res.data; + } + setCompetitivenessLoading(false) + return null; + } catch (error) { + setCompetitivenessLoading(false) + console.error('获取竞争力分析数据失败:', error); + return null; + } + }; + useEffect(() => { getDictValueEnum('education', true, true).then((data) => { setEducationEnum(data); @@ -143,14 +162,11 @@ function ManagementList() { }, }, { - title: '匹配度', - dataIndex: 'minSalary', + title: '投递时间', + dataIndex: 'applyDate', valueType: 'text', hideInSearch: true, align: 'center', - render: (_, record) => ( - <>{similarityJobs.calculationMatchingDegreeJob(record).overallMatch} - ), }, { title: '操作', @@ -158,21 +174,22 @@ function ManagementList() { align: 'center', dataIndex: 'jobId', width: 200, - render: (jobId, record) => [ + render: (_, record) => [ , ], }, @@ -182,8 +199,6 @@ function ManagementList() {
- // params 是需要自带的参数 - // 这个参数优先级更高,会覆盖查询表单的参数 actionRef={actionRef} formRef={formTableRef} rowKey="jobId" @@ -191,8 +206,6 @@ function ManagementList() { columns={columns} request={(params) => exportCmsJobCandidates(jobId).then((res) => { - // const v = similarityJobs.calculationMatchingDegreeJob(res.rows[0]); - // console.log(v); const result = { data: res.rows, total: res.total, @@ -217,14 +230,13 @@ function ManagementList() { ]} /> { - console.log('close'); setModalVisible(false); setCurrentRow(undefined); - setMatchingDegree(null); + setCompetitivenessData({}); }} >
diff --git a/src/services/Management/list.ts b/src/services/Management/list.ts index 0dffb8c..b20a0c4 100644 --- a/src/services/Management/list.ts +++ b/src/services/Management/list.ts @@ -64,3 +64,10 @@ export async function cmsfileupload(params) { data: params }); } + +export async function getJobCompetitiveness(params: { userId: number | string; jobId: string }) { + return request(`/api/cms/job/competitiveness`, { + method: 'GET', + params: params, + }); +} diff --git a/src/types/Management/list.d.ts b/src/types/Management/list.d.ts index 2a84233..0ba0a44 100644 --- a/src/types/Management/list.d.ts +++ b/src/types/Management/list.d.ts @@ -72,3 +72,29 @@ declare namespace API.ManagementList { rows: Array; } } + +declare namespace API.ManagementList { + // 添加竞争力分析接口返回类型 + export interface CompetitivenessData { + totalApplicants?: number; + matchScore?: number; + rank?: number; + percentile?: number; + radarChart?: RadarChart; + } + + export interface RadarChart { + skill?: number; + experience?: number; + education?: number; + salary?: number; + age?: number; + location?: number; + } + + export interface CompetitivenessResult { + code: number; + msg: string; + data: CompetitivenessData; + } +}