218 lines
9.6 KiB
TypeScript
218 lines
9.6 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import { Card, Row, Col, Button, Space, Typography, message, Spin, Form, InputNumber, Divider, DatePicker, Modal } from 'antd';
|
||
import { DownloadOutlined, SaveOutlined, ReloadOutlined, SettingOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
||
import dayjs, { Dayjs } from 'dayjs';
|
||
import http from '../../api/http';
|
||
import { useIsMobile } from '../../hooks/useBreakpoint';
|
||
|
||
const { Title, Text } = Typography;
|
||
|
||
interface RuleResponse {
|
||
ruleId: number; ruleKey: string; ruleValue: any;
|
||
description: string | null; effectiveCycle: string | null;
|
||
updatedBy: number | null; updatedAt: string;
|
||
}
|
||
|
||
// 规则字段配置
|
||
const RULE_FIELDS = {
|
||
business: [
|
||
{ name: 'business_completion_weight', label: '工作完成度', max: 70 },
|
||
{ name: 'business_quality_weight', label: '工作质量', max: 70 },
|
||
{ name: 'business_efficiency_weight', label: '工作效率', max: 70 },
|
||
{ name: 'business_skill_weight', label: '专业技能', max: 70 },
|
||
{ name: 'business_innovation_weight', label: '创新能力', max: 70 },
|
||
{ name: 'business_problem_solving_weight', label: '问题解决', max: 70 },
|
||
{ name: 'business_customer_satisfaction_weight', label: '客户满意度', max: 70 },
|
||
{ name: 'business_teamwork_weight', label: '团队协作', max: 70 },
|
||
{ name: 'business_goal_achievement_weight', label: '目标达成', max: 70 },
|
||
],
|
||
comprehensive: [
|
||
{ name: 'comprehensive_responsibility_weight', label: '责任心', max: 30 },
|
||
{ name: 'comprehensive_initiative_weight', label: '主动性', max: 30 },
|
||
{ name: 'comprehensive_learning_weight', label: '学习能力', max: 30 },
|
||
{ name: 'comprehensive_communication_weight', label: '沟通能力', max: 30 },
|
||
{ name: 'comprehensive_execution_weight', label: '执行力', max: 30 },
|
||
{ name: 'comprehensive_discipline_weight', label: '纪律性', max: 30 },
|
||
{ name: 'comprehensive_team_spirit_weight', label: '团队精神', max: 30 },
|
||
{ name: 'comprehensive_attendance_weight', label: '考勤', max: 30 },
|
||
],
|
||
reward: [
|
||
{ name: 'reward_excellent', label: '优秀奖励(≥90分)', unit: '元' },
|
||
{ name: 'punish_qualified_high', label: '合格扣款(80-89分)', unit: '元' },
|
||
{ name: 'punish_qualified_low', label: '合格扣款(70-79分)', unit: '元' },
|
||
{ name: 'punish_need_motivation', label: '需激励扣款(60-69分)', unit: '元' },
|
||
{ name: 'punish_unqualified', label: '不合格扣款(<60分)', unit: '元' },
|
||
],
|
||
attendance: [
|
||
{ name: 'attendance_leave_deduct', label: '事假扣分(每天)', unit: '分' },
|
||
{ name: 'attendance_late_deduct', label: '迟到扣分(每次)', unit: '分' },
|
||
{ name: 'attendance_lack_card_deduct', label: '缺卡扣分(每次)', unit: '分' },
|
||
{ name: 'attendance_base_score', label: '考勤基础分', unit: '分' },
|
||
],
|
||
};
|
||
|
||
const ConfigManagement: React.FC = () => {
|
||
const [loading, setLoading] = useState(false);
|
||
const [saving, setSaving] = useState(false);
|
||
const [exporting, setExporting] = useState(false);
|
||
const [rules, setRules] = useState<RuleResponse[]>([]);
|
||
const [selectedMonth, setSelectedMonth] = useState<Dayjs>(dayjs());
|
||
const [form] = Form.useForm();
|
||
const isMobile = useIsMobile();
|
||
|
||
const loadRules = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const { data } = await http.get('/api/config/rules');
|
||
const ruleList: RuleResponse[] = data.data || data;
|
||
setRules(ruleList);
|
||
const formValues: Record<string, any> = {};
|
||
ruleList.forEach((rule) => { formValues[rule.ruleKey] = rule.ruleValue; });
|
||
form.setFieldsValue(formValues);
|
||
} catch (err: any) {
|
||
message.error(err?.response?.data?.message || '加载配置规则失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => { loadRules(); }, []);
|
||
|
||
const handleSave = async () => {
|
||
try {
|
||
const values = await form.validateFields();
|
||
Modal.confirm({
|
||
title: '确认修改考核规则',
|
||
icon: <ExclamationCircleOutlined />,
|
||
content: '修改后的规则将应用于后续考核周期,是否确认?',
|
||
okText: '确认', cancelText: '取消',
|
||
onOk: async () => {
|
||
setSaving(true);
|
||
try {
|
||
await http.put('/api/config/rules', {
|
||
rules: Object.entries(values).map(([ruleKey, ruleValue]) => ({ ruleKey, ruleValue })),
|
||
});
|
||
message.success('规则更新成功');
|
||
await loadRules();
|
||
} catch (err: any) {
|
||
message.error(err?.response?.data?.message || '规则更新失败');
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
},
|
||
});
|
||
} catch { message.error('请检查表单输入'); }
|
||
};
|
||
|
||
const handleExport = async () => {
|
||
setExporting(true);
|
||
try {
|
||
const month = selectedMonth.format('YYYY-MM');
|
||
const response = await http.get('/api/performance/export', { params: { month, type: 'all' }, responseType: 'blob' });
|
||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.setAttribute('download', `全公司绩效数据_${month}.xlsx`);
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
link.remove();
|
||
window.URL.revokeObjectURL(url);
|
||
message.success('导出成功');
|
||
} catch (err: any) {
|
||
message.error(err?.response?.data?.message || '导出失败');
|
||
} finally {
|
||
setExporting(false);
|
||
}
|
||
};
|
||
|
||
if (loading && rules.length === 0) return <div style={{ textAlign: 'center', padding: 100 }}><Spin size="large" /></div>;
|
||
|
||
// 移动端每行2列,桌面端每行3或4列
|
||
const colSpan = isMobile ? 12 : 8;
|
||
const colSpan4 = isMobile ? 12 : 6;
|
||
|
||
return (
|
||
<div style={{ padding: isMobile ? 0 : 24 }}>
|
||
<Title level={isMobile ? 4 : 3}>系统配置与数据导出</Title>
|
||
|
||
{/* 数据导出 */}
|
||
<Card title={<Space><DownloadOutlined /><span>全量数据导出</span></Space>} style={{ marginBottom: 16 }}
|
||
bodyStyle={{ padding: isMobile ? 12 : 24 }}>
|
||
<Text style={{ display: 'block', marginBottom: 8 }}>选择考核月份,导出全公司所有员工的绩效数据(Excel 格式)</Text>
|
||
<Space wrap>
|
||
<DatePicker picker="month" value={selectedMonth} onChange={(d) => d && setSelectedMonth(d)} format="YYYY-MM" />
|
||
<Button type="primary" icon={<DownloadOutlined />} onClick={handleExport} loading={exporting}>
|
||
导出全量数据
|
||
</Button>
|
||
</Space>
|
||
</Card>
|
||
|
||
{/* 考核规则配置 */}
|
||
<Card
|
||
title={<Space><SettingOutlined /><span>考核规则配置</span></Space>}
|
||
bodyStyle={{ padding: isMobile ? 12 : 24 }}
|
||
extra={
|
||
<Space size={4}>
|
||
<Button icon={<ReloadOutlined />} onClick={loadRules} loading={loading} size={isMobile ? 'small' : 'middle'}>刷新</Button>
|
||
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave} loading={saving} size={isMobile ? 'small' : 'middle'}>保存</Button>
|
||
</Space>
|
||
}
|
||
>
|
||
<Form form={form} layout="vertical">
|
||
<Divider orientation="left" style={{ fontSize: isMobile ? 13 : 14 }}>业务素质考核指标权重(总计70分)</Divider>
|
||
<Row gutter={[8, 0]}>
|
||
{RULE_FIELDS.business.map(f => (
|
||
<Col span={colSpan} key={f.name}>
|
||
<Form.Item label={f.label} name={f.name} rules={[{ required: true, message: '请输入' }]}>
|
||
<InputNumber min={0} max={f.max} style={{ width: '100%' }} addonAfter="分" />
|
||
</Form.Item>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
|
||
<Divider orientation="left" style={{ fontSize: isMobile ? 13 : 14 }}>综合素质考核指标权重(总计30分)</Divider>
|
||
<Row gutter={[8, 0]}>
|
||
{RULE_FIELDS.comprehensive.map(f => (
|
||
<Col span={colSpan} key={f.name}>
|
||
<Form.Item label={f.label} name={f.name} rules={[{ required: true, message: '请输入' }]}>
|
||
<InputNumber min={0} max={f.max} style={{ width: '100%' }} addonAfter="分" />
|
||
</Form.Item>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
|
||
<Divider orientation="left" style={{ fontSize: isMobile ? 13 : 14 }}>奖惩金额配置</Divider>
|
||
<Row gutter={[8, 0]}>
|
||
{RULE_FIELDS.reward.map(f => (
|
||
<Col span={colSpan} key={f.name}>
|
||
<Form.Item label={f.label} name={f.name} rules={[{ required: true, message: '请输入' }]}>
|
||
<InputNumber min={0} style={{ width: '100%' }} addonAfter={f.unit} />
|
||
</Form.Item>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
|
||
<Divider orientation="left" style={{ fontSize: isMobile ? 13 : 14 }}>考勤扣分规则</Divider>
|
||
<Row gutter={[8, 0]}>
|
||
{RULE_FIELDS.attendance.map(f => (
|
||
<Col span={colSpan4} key={f.name}>
|
||
<Form.Item label={f.label} name={f.name} rules={[{ required: true, message: '请输入' }]}>
|
||
<InputNumber min={0} style={{ width: '100%' }} addonAfter={f.unit} />
|
||
</Form.Item>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
</Form>
|
||
|
||
<Card size="small" style={{ marginTop: 12, background: '#fffbe6' }}>
|
||
<Text type="warning" style={{ fontSize: 12 }}>
|
||
<ExclamationCircleOutlined /> 注意:修改考核规则后,将自动应用于后续考核周期。已完成的历史绩效数据不受影响。
|
||
</Text>
|
||
</Card>
|
||
</Card>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ConfigManagement;
|