Files
performance-evaluation-system/frontend/src/pages/gm/ConfigManagement.tsx
2026-04-11 11:51:54 +08:00

218 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;