refactor : 岗位列表-申请人-查看竞争力分析
This commit is contained in:
10
src/app.tsx
10
src/app.tsx
@@ -218,12 +218,12 @@ export async function render(oldRender: () => void) {
|
|||||||
const checkRegion = 5 * 60 * 1000;
|
const checkRegion = 5 * 60 * 1000;
|
||||||
export const request = {
|
export const request = {
|
||||||
...errorConfig,
|
...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: 'http://39.98.44.136:8080',
|
||||||
baseURL:
|
// baseURL:
|
||||||
process.env.NODE_ENV === 'development'
|
// process.env.NODE_ENV === 'development'
|
||||||
? 'http://10.213.6.207:19010'
|
// ? 'http://10.213.6.207:19010'
|
||||||
: 'http://10.213.6.207:19010/api',
|
// : 'http://10.213.6.207:19010/api',
|
||||||
requestInterceptors: [
|
requestInterceptors: [
|
||||||
(url: any, options: { headers: any; data?: any; params?: any; method?: string }) => {
|
(url: any, options: { headers: any; data?: any; params?: any; method?: string }) => {
|
||||||
const headers = options.headers ? options.headers : {};
|
const headers = options.headers ? options.headers : {};
|
||||||
|
|||||||
375
src/pages/Management/List/SeeMatching/detail copy.tsx
Normal file
375
src/pages/Management/List/SeeMatching/detail copy.tsx
Normal file
@@ -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<API.MobileUser.ListRow>;
|
||||||
|
competitivenessData?: API.ManagementList.CompetitivenessData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Detail: React.FC<ListFormProps> = (props) => {
|
||||||
|
const { open, values, competitivenessData } = props;
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const [currentStep, setCurrentStep] = useState<number>(0);
|
||||||
|
const [isStyleInjected, setIsStyleInjected] = useState<boolean>(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 (
|
||||||
|
<Modal
|
||||||
|
title="竞争力分析"
|
||||||
|
width={600}
|
||||||
|
open={open}
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => props.onClose()}
|
||||||
|
maskClosable={true}
|
||||||
|
>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{/* 统计数据 */}
|
||||||
|
<Col span={24}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="总申请人数"
|
||||||
|
value={competitivenessData?.totalApplicants || 0}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="匹配分数"
|
||||||
|
value={competitivenessData?.matchScore || 0}
|
||||||
|
suffix="分"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="排名"
|
||||||
|
value={competitivenessData?.rank || 0}
|
||||||
|
suffix="位"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="超过百分比"
|
||||||
|
value={competitivenessData?.percentile || 0}
|
||||||
|
suffix="%"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* 用户基本信息 */}
|
||||||
|
{values && (
|
||||||
|
<Col span={24}>
|
||||||
|
<Card title="用户基本信息" size="small">
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={6}>
|
||||||
|
<div>用户名:{values.name}</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<div>
|
||||||
|
期望薪资:{values.salaryMin}-{values.salaryMax}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<div>学历:{values.education}</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<div>区域:{values.area}</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 竞争力分析 */}
|
||||||
|
<Col span={24}>
|
||||||
|
<Card title="竞争力分析" size="small">
|
||||||
|
<div style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#333',
|
||||||
|
lineHeight: '1.6',
|
||||||
|
padding: '10px 0'
|
||||||
|
}}>
|
||||||
|
三个月内共{competitivenessData?.totalApplicants || 0}位求职者申请,
|
||||||
|
你的简历匹配度为{competitivenessData?.matchScore || 0}分,
|
||||||
|
排名位于第{competitivenessData?.rank || 0}位,
|
||||||
|
超过{competitivenessData?.percentile || 0}%的竞争者,处在优秀位置。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 雷达图 */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px 0' }}>
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{
|
||||||
|
border: 'none', // 移除边框,与移动端保持一致
|
||||||
|
width: '300px',
|
||||||
|
height: '250px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 匹配度进度条(使用DOM渲染,和移动端一样) */}
|
||||||
|
<div style={{ padding: '0 20px', marginTop: '10px' }}>
|
||||||
|
<div style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#000000',
|
||||||
|
marginBottom: '10px'
|
||||||
|
}}>
|
||||||
|
你与职位的匹配度
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 进度条容器 */}
|
||||||
|
<div className="progress-container">
|
||||||
|
{matchingDegree.map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`progress-item ${getProgressClass(index)}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 进度条文字 */}
|
||||||
|
<div className="progress-text">
|
||||||
|
{matchingDegree.map((text, index) => (
|
||||||
|
<div key={index} style={{ width: '25%' }}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Detail;
|
||||||
@@ -1,74 +1,333 @@
|
|||||||
import { Modal } from 'antd';
|
import { Modal, Row, Col, Card, Statistic } from 'antd';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import * as echarts from 'echarts';
|
|
||||||
|
|
||||||
export type ListFormProps = {
|
export type ListFormProps = {
|
||||||
onClose: (flag?: boolean, formVals?: unknown) => void;
|
onClose: (flag?: boolean, formVals?: unknown) => void;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
values?: Partial<API.MobileUser.ListRow>;
|
loading: boolean;
|
||||||
matching?: any;
|
competitivenessData?: API.ManagementList.CompetitivenessData;
|
||||||
};
|
};
|
||||||
|
|
||||||
const listEdit: React.FC<ListFormProps> = (props) => {
|
const Detail: React.FC<ListFormProps> = (props) => {
|
||||||
const { open, values, matching } = props;
|
const { open, loading, competitivenessData } = props;
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const [currentStep, setCurrentStep] = useState<number>(0);
|
||||||
|
const [isStyleInjected, setIsStyleInjected] = useState<boolean>(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(() => {
|
useEffect(() => {
|
||||||
console.log(matching);
|
if (competitivenessData?.matchScore !== undefined) {
|
||||||
if (matching && open) {
|
// 计算匹配度
|
||||||
const myCharts = echarts.init(mapRef.current);
|
const score = competitivenessData.matchScore;
|
||||||
const { educationMatch, maxSimilarity, salaryMatch, areaMatch } = matching;
|
const step = score * 0.04;
|
||||||
// educationMatch
|
setCurrentStep(step);
|
||||||
// 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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [matching]);
|
}, [competitivenessData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && competitivenessData && canvasRef.current) {
|
||||||
|
// 初始化Canvas
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
canvas.width = 400;
|
||||||
|
canvas.height = 320;
|
||||||
|
drawRadarChart();
|
||||||
|
}
|
||||||
|
}, [open, competitivenessData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="匹配详情"
|
title="竞争力分析"
|
||||||
width={400}
|
width={700}
|
||||||
open={props.open}
|
open={open}
|
||||||
footer={''}
|
footer={null}
|
||||||
onCancel={() => props.onClose()}
|
onCancel={() => props.onClose()}
|
||||||
maskClosable={true}
|
maskClosable={true}
|
||||||
>
|
loading={loading}
|
||||||
<div
|
styles={{
|
||||||
style={{
|
body: {
|
||||||
display: 'flex',
|
minHeight: '600px',
|
||||||
alignItems: 'center',
|
},
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ width: '300px', height: '300px' }} ref={mapRef}></div>
|
<Row gutter={[16, 16]}>
|
||||||
|
{/* 统计数据 */}
|
||||||
|
<Col span={24}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic title="总申请人数" value={competitivenessData?.totalApplicants || 0} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="匹配分数"
|
||||||
|
value={competitivenessData?.matchScore || 0}
|
||||||
|
suffix="分"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic title="排名" value={competitivenessData?.rank || 0} suffix="位" />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="超过百分比"
|
||||||
|
value={competitivenessData?.percentile || 0}
|
||||||
|
suffix="%"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* 雷达图 */}
|
||||||
|
<Col span={24}>
|
||||||
|
<Card title="雷达图" size="small">
|
||||||
|
{/* 雷达图 */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 匹配度进度条*/}
|
||||||
|
<div style={{ padding: '0 20px', marginTop: '10px' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#000000',
|
||||||
|
marginBottom: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
职位匹配度
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 进度条容器 */}
|
||||||
|
<div className="progress-container">
|
||||||
|
{matchingDegree.map((_, index) => (
|
||||||
|
<div key={index} className={`progress-item ${getProgressClass(index)}`} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 进度条文字 */}
|
||||||
|
<div className="progress-text">
|
||||||
|
{matchingDegree.map((text, index) => (
|
||||||
|
<div key={index} style={{ width: '25%' }}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default listEdit;
|
export default Detail;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FormOutlined, PlusOutlined } from '@ant-design/icons';
|
|||||||
import { getDictValueEnum } from '@/services/system/dict';
|
import { getDictValueEnum } from '@/services/system/dict';
|
||||||
import DictTag from '@/components/DictTag';
|
import DictTag from '@/components/DictTag';
|
||||||
import { exportCmsAppUserExport } from '@/services/mobileusers/list';
|
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 similarityJobs from '@/utils/similarity_Job';
|
||||||
import Detail from './detail';
|
import Detail from './detail';
|
||||||
|
|
||||||
@@ -26,7 +26,6 @@ const handleExport = async (values: API.MobileUser.ListParams) => {
|
|||||||
|
|
||||||
function ManagementList() {
|
function ManagementList() {
|
||||||
const access = useAccess();
|
const access = useAccess();
|
||||||
|
|
||||||
const formTableRef = useRef<FormInstance>();
|
const formTableRef = useRef<FormInstance>();
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
|
|
||||||
@@ -35,14 +34,15 @@ function ManagementList() {
|
|||||||
const [experienceEnum, setExperienceEnum] = useState<any>([]);
|
const [experienceEnum, setExperienceEnum] = useState<any>([]);
|
||||||
const [areaEnum, setAreaEnum] = useState<any>([]);
|
const [areaEnum, setAreaEnum] = useState<any>([]);
|
||||||
const [sexEnum, setSexEnum] = useState<any>([]);
|
const [sexEnum, setSexEnum] = useState<any>([]);
|
||||||
const [hotEnum, setHotEnum] = useState<any>([]);
|
|
||||||
const [politicalEnum, setPoliticalEnum] = useState<any>([]);
|
const [politicalEnum, setPoliticalEnum] = useState<any>([]);
|
||||||
const [currentRow, setCurrentRow] = useState<API.MobileUser.ListRow>();
|
const [currentRow, setCurrentRow] = useState<API.MobileUser.ListRow>();
|
||||||
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||||
const [jobInfo, setJobInfo] = useState({});
|
const [jobInfo, setJobInfo] = useState({});
|
||||||
const [matchingDegree, setMatchingDegree] = useState<any>(null);
|
const [competitivenessData, setCompetitivenessData] = useState<API.ManagementList.CompetitivenessData>({});
|
||||||
|
const [competitivenessLoading, setCompetitivenessLoading] = useState<boolean>(false);
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = params.id || '0';
|
const id = params.id || '0';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (jobId !== id) {
|
if (jobId !== id) {
|
||||||
setJobId(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(() => {
|
useEffect(() => {
|
||||||
getDictValueEnum('education', true, true).then((data) => {
|
getDictValueEnum('education', true, true).then((data) => {
|
||||||
setEducationEnum(data);
|
setEducationEnum(data);
|
||||||
@@ -143,14 +162,11 @@ function ManagementList() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '匹配度',
|
title: '投递时间',
|
||||||
dataIndex: 'minSalary',
|
dataIndex: 'applyDate',
|
||||||
valueType: 'text',
|
valueType: 'text',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (_, record) => (
|
|
||||||
<>{similarityJobs.calculationMatchingDegreeJob(record).overallMatch}</>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -158,21 +174,22 @@ function ManagementList() {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
dataIndex: 'jobId',
|
dataIndex: 'jobId',
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (jobId, record) => [
|
render: (_, record) => [
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
key="edit"
|
key="edit"
|
||||||
icon={<FormOutlined />}
|
icon={<FormOutlined />}
|
||||||
hidden={!access.hasPerms('area:business:List.update')}
|
hidden={!access.hasPerms('area:business:List.update')}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
setCurrentRow(record);
|
const data = await fetchCompetitivenessData(record.userId, jobId);
|
||||||
const val = similarityJobs.calculationMatchingDegreeJob(record);
|
if (data) {
|
||||||
setMatchingDegree(val);
|
setCompetitivenessData(data);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
查看匹配详情
|
查看竞争力分析
|
||||||
</Button>,
|
</Button>,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -182,8 +199,6 @@ function ManagementList() {
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<div style={{ width: '100%', float: 'right' }}>
|
<div style={{ width: '100%', float: 'right' }}>
|
||||||
<ProTable<API.MobileUser.ListRow>
|
<ProTable<API.MobileUser.ListRow>
|
||||||
// params 是需要自带的参数
|
|
||||||
// 这个参数优先级更高,会覆盖查询表单的参数
|
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
formRef={formTableRef}
|
formRef={formTableRef}
|
||||||
rowKey="jobId"
|
rowKey="jobId"
|
||||||
@@ -191,8 +206,6 @@ function ManagementList() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
request={(params) =>
|
request={(params) =>
|
||||||
exportCmsJobCandidates(jobId).then((res) => {
|
exportCmsJobCandidates(jobId).then((res) => {
|
||||||
// const v = similarityJobs.calculationMatchingDegreeJob(res.rows[0]);
|
|
||||||
// console.log(v);
|
|
||||||
const result = {
|
const result = {
|
||||||
data: res.rows,
|
data: res.rows,
|
||||||
total: res.total,
|
total: res.total,
|
||||||
@@ -217,14 +230,13 @@ function ManagementList() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Detail
|
<Detail
|
||||||
values={currentRow}
|
|
||||||
open={modalVisible}
|
open={modalVisible}
|
||||||
matching={matchingDegree}
|
loading={competitivenessLoading}
|
||||||
|
competitivenessData={competitivenessData}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
console.log('close');
|
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
setCurrentRow(undefined);
|
setCurrentRow(undefined);
|
||||||
setMatchingDegree(null);
|
setCompetitivenessData({});
|
||||||
}}
|
}}
|
||||||
></Detail>
|
></Detail>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,3 +64,10 @@ export async function cmsfileupload(params) {
|
|||||||
data: params
|
data: params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getJobCompetitiveness(params: { userId: number | string; jobId: string }) {
|
||||||
|
return request<API.ManagementList.CompetitivenessResult>(`/api/cms/job/competitiveness`, {
|
||||||
|
method: 'GET',
|
||||||
|
params: params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
26
src/types/Management/list.d.ts
vendored
26
src/types/Management/list.d.ts
vendored
@@ -72,3 +72,29 @@ declare namespace API.ManagementList {
|
|||||||
rows: Array<Manage>;
|
rows: Array<Manage>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user