From b9bbc906bfa6df8941d1778e00e41e1eb3348a01 Mon Sep 17 00:00:00 2001 From: linxd <544554903@qq.com> Date: Mon, 23 Jun 2025 19:15:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E5=8F=91=E5=AF=B9=E6=8E=A5=E4=BE=9B?= =?UTF-8?q?=E5=BA=94=E5=95=86=E8=AF=84=E4=BB=B7=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/proxy.ts | 2 +- config/router.config.ts | 32 + src/baseStyle.less | 70 ++ .../CategorySelector/CategorySelector.less | 22 + .../CategorySelector/CategorySelector.tsx | 193 +++++ src/components/CategorySelector/index.ts | 3 + .../EvaluateTaskPersonnelSelector.less | 104 +++ .../EvaluateTaskPersonnelSelector.tsx | 369 +++++++++ .../EvaluateTemplateTable.less | 10 + .../EvaluateTemplateTable.tsx | 709 ++++++++++++++++++ src/components/EvaluateTemplateTable/index.ts | 3 + src/dicts/supplierEvaluateDict.ts | 56 ++ src/dicts/supplierTemplateDict.ts | 40 +- .../components/GeneralEvaluation.tsx | 21 - .../supplierEvaluateResultByZb.tsx | 206 ++++- .../supplierEvaluateResultInfo.tsx | 5 +- .../supplierEvaluateResultScoreByList.tsx | 296 +++++++- .../supplierEvaluateResultApproval.less | Bin 2 -> 922 bytes .../supplierEvaluateResultApproval.tsx | 318 ++++---- .../supplierEvaluateResultApprovalInfo.tsx | 318 ++++++++ .../components/BasicInfoStep.tsx | 249 ++++++ .../components/EvaluatorSelectStep.tsx | 373 +++++++++ .../components/SupplierSelectStep.tsx | 396 ++++++++++ .../supplierTaskManage.less | 62 +- .../supplierTaskManage/supplierTaskManage.tsx | 18 +- .../supplierTaskManageAdd.less | 83 ++ .../supplierTaskManageAdd.tsx | 188 +++++ .../supplierTemplateManage.less | 38 + .../supplierTemplateManage.tsx | 529 +++---------- .../supplierTemplateManageAdd.tsx | 408 ++++++++++ .../supplierTemplateManageDetail.tsx | 135 ++++ src/servers/api/dicts.ts | 45 ++ src/servers/api/supplierEvaluate.ts | 89 +++ src/servers/api/typings.d.ts | 439 +++++++++++ src/typings.d.ts | 80 +- 35 files changed, 5288 insertions(+), 621 deletions(-) create mode 100644 src/components/CategorySelector/CategorySelector.less create mode 100644 src/components/CategorySelector/CategorySelector.tsx create mode 100644 src/components/CategorySelector/index.ts create mode 100644 src/components/EvaluateTaskPersonnelSelector/EvaluateTaskPersonnelSelector.less create mode 100644 src/components/EvaluateTaskPersonnelSelector/EvaluateTaskPersonnelSelector.tsx create mode 100644 src/components/EvaluateTemplateTable/EvaluateTemplateTable.less create mode 100644 src/components/EvaluateTemplateTable/EvaluateTemplateTable.tsx create mode 100644 src/components/EvaluateTemplateTable/index.ts create mode 100644 src/dicts/supplierEvaluateDict.ts create mode 100644 src/pages/supplierEvaluateManage/supplierEvaluateResultApproval/supplierEvaluateResultApprovalInfo.tsx create mode 100644 src/pages/supplierEvaluateManage/supplierTaskManage/components/BasicInfoStep.tsx create mode 100644 src/pages/supplierEvaluateManage/supplierTaskManage/components/EvaluatorSelectStep.tsx create mode 100644 src/pages/supplierEvaluateManage/supplierTaskManage/components/SupplierSelectStep.tsx create mode 100644 src/pages/supplierEvaluateManage/supplierTaskManage/supplierTaskManageAdd.less create mode 100644 src/pages/supplierEvaluateManage/supplierTaskManage/supplierTaskManageAdd.tsx create mode 100644 src/pages/supplierEvaluateManage/supplierTemplateManage/supplierTemplateManageAdd.tsx create mode 100644 src/pages/supplierEvaluateManage/supplierTemplateManage/supplierTemplateManageDetail.tsx create mode 100644 src/servers/api/dicts.ts create mode 100644 src/servers/api/supplierEvaluate.ts diff --git a/config/proxy.ts b/config/proxy.ts index e6188de..9e663ac 100644 --- a/config/proxy.ts +++ b/config/proxy.ts @@ -2,7 +2,7 @@ export default { dev: { '/api': { // target: 'http://10.242.37.148:18022',// - target: 'http://10.0.0.10:18013',// + target: 'http://10.0.0.10:18012',// changeOrigin: true, pathRewrite: { '^/api': '' }, }, diff --git a/config/router.config.ts b/config/router.config.ts index 2022811..1e9304b 100644 --- a/config/router.config.ts +++ b/config/router.config.ts @@ -120,6 +120,22 @@ export default [ }, component: '@/pages/supplierEvaluateManage/supplierTemplateManage/supplierTemplateManage', }, + { + name: 'supplierTemplateManageAdd', + path: '/supplierTemplateManage/supplierTemplateManageAdd', + meta: { + title: '供应商模板管理新增', + }, + component: '@/pages/supplierEvaluateManage/supplierTemplateManage/supplierTemplateManageAdd', + }, + { + name: 'supplierTemplateManageDetail', + path: '/supplierTemplateManage/supplierTemplateManageDetail', + meta: { + title: '供应商模板管理详情', + }, + component: '@/pages/supplierEvaluateManage/supplierTemplateManage/supplierTemplateManageDetail', + }, { name: 'supplierTaskManage', path: '/supplierTaskManage', @@ -128,6 +144,14 @@ export default [ }, component: '@/pages/supplierEvaluateManage/supplierTaskManage/supplierTaskManage', }, + { + name: 'supplierTaskManageAdd', + path: '/supplierTaskManage/supplierTaskManageAdd', + meta: { + title: '供应商任务管理新增', + }, + component: '@/pages/supplierEvaluateManage/supplierTaskManage/supplierTaskManageAdd', + }, { name: 'supplierEvaluateScore', path: '/supplierEvaluateScore', @@ -184,6 +208,14 @@ export default [ }, component: '@/pages/supplierEvaluateManage/supplierEvaluateResultApproval/supplierEvaluateResultApproval', }, + { + name: 'supplierEvaluateResultApprovalInfo', + path: '/supplierEvaluateResultApproval/supplierEvaluateResultApprovalInfo', + meta: { + title: '供应商评价审批', + }, + component: '@/pages/supplierEvaluateManage/supplierEvaluateResultApproval/supplierEvaluateResultApprovalInfo', + }, { name: 'supplierAnnualTemplateManage', path: '/supplierAnnualTemplateManage', diff --git a/src/baseStyle.less b/src/baseStyle.less index 4459fc1..df231c6 100644 --- a/src/baseStyle.less +++ b/src/baseStyle.less @@ -12,3 +12,73 @@ // 宽度 @width: 1366px; + +// 公共容器样式 +.common-container { + background-color: #f0f2f5; + padding: 24px; + min-height: calc(100vh - 144px); + + .page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + + h2 { + margin-bottom: 0; + } + } + + .inner-card { + margin-bottom: 24px; + background-color: #fff; + border-radius: 2px; + } + + // 步骤表单相关样式 + .steps-container { + margin: 16px 0 24px; + padding: 24px 0; + background-color: #fff; + border-radius: 2px; + } + + .steps-content { + margin-top: 16px; + padding: 24px; + background-color: #fff; + border-radius: 2px; + min-height: 300px; + } + + .steps-action { + margin-top: 24px; + text-align: center; + } + + // 表单相关通用样式 + .required-label::before { + content: '*'; + color: #ff4d4f; + margin-right: 4px; + } + + .select-with-clear { + position: relative; + + .clear-icon { + position: absolute; + top: 50%; + right: 24px; + transform: translateY(-50%); + color: rgba(0, 0, 0, 0.25); + cursor: pointer; + z-index: 1; + + &:hover { + color: rgba(0, 0, 0, 0.45); + } + } + } +} diff --git a/src/components/CategorySelector/CategorySelector.less b/src/components/CategorySelector/CategorySelector.less new file mode 100644 index 0000000..c37f4a6 --- /dev/null +++ b/src/components/CategorySelector/CategorySelector.less @@ -0,0 +1,22 @@ +.category-selector { + .selector-trigger { + display: flex; + align-items: center; + cursor: pointer; + + .selected-categories { + margin-left: 8px; + display: flex; + flex-wrap: wrap; + max-width: 80%; + + .ant-tag { + margin-bottom: 4px; + } + } + } + + .search-container { + margin-bottom: 16px; + } +} diff --git a/src/components/CategorySelector/CategorySelector.tsx b/src/components/CategorySelector/CategorySelector.tsx new file mode 100644 index 0000000..3c7764b --- /dev/null +++ b/src/components/CategorySelector/CategorySelector.tsx @@ -0,0 +1,193 @@ +import React, { useState, useEffect } from 'react'; +import { Button, Modal, Table, Input, Tag, Space } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import type { TableRowSelection } from 'antd/es/table/interface'; +import './CategorySelector.less'; + +interface CategorySelectorProps { + value?: string | string[]; + onChange?: (value: string | string[]) => void; + disabled?: boolean; + multiple?: boolean; // 是否多选,默认为多选 +} + +interface CategoryItem { + id: string; + name: string; + code: string; +} + +const CategorySelector: React.FC = ({ + value = [], + onChange, + disabled = false, + multiple = true +}) => { + const [visible, setVisible] = useState(false); + const [selectedCategories, setSelectedCategories] = useState([]); + const [categoryData, setCategoryData] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [searchValue, setSearchValue] = useState(''); + + // 初始化数据 + useEffect(() => { + // 将输入值转换为数组 + const valueArray = Array.isArray(value) ? value : value ? [value] : []; + + // 只有当value真正变化时才更新状态 + if (JSON.stringify(valueArray) !== JSON.stringify(selectedCategories)) { + setSelectedCategories(valueArray); + // 将选中的品类ID转换为rowKey数组 + const selectedKeys = valueArray.map(categoryId => { + return categoryId; + }).filter(Boolean); + setSelectedRowKeys(selectedKeys); + } + }, [value]); + + // 模拟获取品类数据 + useEffect(() => { + // 模拟API请求 + const mockData: CategoryItem[] = [ + { id: '1', name: '电子', code: 'DZ001' }, + { id: '2', name: '机械', code: 'JX001' }, + { id: '3', name: '化工', code: 'HG001' }, + { id: '4', name: '医药', code: 'YY001' }, + { id: '5', name: '食品', code: 'SP001' }, + { id: '6', name: '服装', code: 'FZ001' }, + { id: '7', name: '建材', code: 'JC001' }, + { id: '8', name: '家电', code: 'JD001' }, + { id: '9', name: '汽车', code: 'QC001' }, + { id: '10', name: '能源', code: 'NY001' }, + ]; + setCategoryData(mockData); + + // 初始化选中的行 + const valueArray = Array.isArray(value) ? value : value ? [value] : []; + if (valueArray.length > 0) { + setSelectedRowKeys(valueArray); + } + }, []); + + // 打开选择弹窗 + const showModal = () => { + if (disabled) return; + setVisible(true); + }; + + // 处理确认选择 + const handleOk = () => { + // 根据selectedRowKeys获取对应的品类数据 + const selectedIds = selectedRowKeys as string[]; + + // 只有当选择真正变化时才触发onChange + if (JSON.stringify(selectedIds) !== JSON.stringify(selectedCategories)) { + setSelectedCategories(selectedIds); + if (onChange) { + // 如果是单选模式,返回第一个选中的值,否则返回数组 + onChange(multiple ? selectedIds : selectedIds[0] || ''); + } + } + + setVisible(false); + }; + + // 处理取消选择 + const handleCancel = () => { + // 恢复到打开弹窗前的选择状态 + const valueArray = Array.isArray(value) ? value : value ? [value] : []; + setSelectedRowKeys(valueArray); + setVisible(false); + }; + + // 处理搜索 + const handleSearch = (e: React.ChangeEvent) => { + setSearchValue(e.target.value); + }; + + // 过滤数据 + const filteredData = categoryData.filter(item => + item.name.toLowerCase().includes(searchValue.toLowerCase()) || + item.code.toLowerCase().includes(searchValue.toLowerCase()) + ); + + // 获取已选择的品类名称 + const getSelectedCategoryNames = () => { + if (!selectedCategories || !Array.isArray(selectedCategories)) { + return []; + } + + return selectedCategories.map(categoryId => { + const category = categoryData.find(item => item.id === categoryId); + return category ? category.name : ''; + }).filter(Boolean); + }; + + const columns = [ + { + title: '品类编码', + dataIndex: 'code', + key: 'code', + }, + { + title: '品类名称', + dataIndex: 'name', + key: 'name', + }, + ]; + + // 表格行选择配置 + const rowSelection = { + selectedRowKeys, + onChange: (newSelectedRowKeys: React.Key[]) => { + // 如果是单选模式,只保留最后选择的一项 + if (!multiple && newSelectedRowKeys.length > 0) { + setSelectedRowKeys([newSelectedRowKeys[newSelectedRowKeys.length - 1]]); + } else { + setSelectedRowKeys(newSelectedRowKeys); + } + }, + type: multiple ? 'checkbox' as const : 'radio' as const, + }; + + return ( +
+
+ +
+ {getSelectedCategoryNames().map((name, index) => ( + {name} + ))} +
+
+ +
+ } + value={searchValue} + onChange={handleSearch} + style={{ marginBottom: 16 }} + /> +
+ + + + ); +}; + +export default CategorySelector; diff --git a/src/components/CategorySelector/index.ts b/src/components/CategorySelector/index.ts new file mode 100644 index 0000000..690e984 --- /dev/null +++ b/src/components/CategorySelector/index.ts @@ -0,0 +1,3 @@ +import CategorySelector from './CategorySelector'; + +export default CategorySelector; diff --git a/src/components/EvaluateTaskPersonnelSelector/EvaluateTaskPersonnelSelector.less b/src/components/EvaluateTaskPersonnelSelector/EvaluateTaskPersonnelSelector.less new file mode 100644 index 0000000..c91455c --- /dev/null +++ b/src/components/EvaluateTaskPersonnelSelector/EvaluateTaskPersonnelSelector.less @@ -0,0 +1,104 @@ +.evaluate-task-personnel-selector { + display: flex; + flex-direction: column; + height: 100%; + + .selector-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + + .search-input { + width: 320px; + } + + .selected-count { + .count-label { + margin-right: 8px; + font-size: 14px; + } + } + } + + .department-tabs { + flex: 1; + overflow-y: auto; + } + + .department-personnel { + padding: 8px 0; + + .personnel-card { + border-radius: 4px; + + .department-header { + padding-bottom: 8px; + + .department-title { + font-weight: 500; + font-size: 15px; + } + + .department-count { + color: rgba(0, 0, 0, 0.45); + font-size: 13px; + } + } + + .personnel-list { + .personnel-item { + transition: all 0.3s; + border: 1px solid #f0f0f0; + + &.selected { + background-color: #e6f7ff; + border-color: #91d5ff; + } + + .personnel-info { + display: flex; + flex-direction: column; + margin-left: 8px; + + .personnel-name { + font-size: 14px; + display: flex; + align-items: center; + + .selected-icon { + color: #1890ff; + margin-left: 4px; + font-size: 12px; + } + } + + .personnel-position { + font-size: 12px; + color: rgba(0, 0, 0, 0.45); + } + } + + .selected-avatar { + background-color: #1890ff; + color: #fff; + } + } + + .no-data { + text-align: center; + color: rgba(0, 0, 0, 0.45); + padding: 32px 0; + } + } + } + } + + .selector-footer { + display: flex; + justify-content: center; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; + } +} diff --git a/src/components/EvaluateTaskPersonnelSelector/EvaluateTaskPersonnelSelector.tsx b/src/components/EvaluateTaskPersonnelSelector/EvaluateTaskPersonnelSelector.tsx new file mode 100644 index 0000000..06caa1c --- /dev/null +++ b/src/components/EvaluateTaskPersonnelSelector/EvaluateTaskPersonnelSelector.tsx @@ -0,0 +1,369 @@ +import React, { useState, useEffect } from 'react'; +import { Input, Button, Badge, Avatar, Space, Tabs, Checkbox, Row, Col, Card, Empty, Divider, Alert } from 'antd'; +import { SearchOutlined, UserOutlined, TeamOutlined, CheckCircleOutlined } from '@ant-design/icons'; +import './EvaluateTaskPersonnelSelector.less'; + +const { TabPane } = Tabs; +const { Search } = Input; + +/** + * 评价任务人员选择组件属性接口 + * @interface EvaluateTaskPersonnelSelectorProps + * @property {Function} onSelect - 选择确认后的回调函数,返回所选人员数组 + * @property {PersonnelItem[]} selectedPersonnel - 已选择的人员列表 + */ +interface EvaluateTaskPersonnelSelectorProps { + onSelect: (personnel: PersonnelItem[]) => void; + selectedPersonnel?: PersonnelItem[]; +} + +/** + * 人员信息接口 + * @interface PersonnelItem + * @property {string} id - 人员唯一标识 + * @property {string} name - 人员姓名 + * @property {string} department - 所属部门 + * @property {string} position - 职位信息 + * @property {boolean} selected - 是否被选中 + */ +export interface PersonnelItem { + id: string; + name: string; + department: string; + position?: string; + selected?: boolean; +} + +/** + * 部门信息接口 + * @interface Department + * @property {string} id - 部门ID + * @property {string} name - 部门名称 + */ +interface Department { + id: string; + name: string; +} + +/** + * 评价任务人员选择组件 + * 用于在评价任务管理中选择评价人员,按部门分类展示 + * + * @component + * @example + * ```jsx + * console.log('已选择人员:', selected)} + * selectedPersonnel={[]} + * /> + * ``` + */ +const EvaluateTaskPersonnelSelector: React.FC = ({ + onSelect, + selectedPersonnel = [] +}) => { + // 搜索关键词 + const [keyword, setKeyword] = useState(''); + // 所有可选人员列表 + const [personnel, setPersonnel] = useState([]); + // 已选择人员ID列表 + const [selectedKeys, setSelectedKeys] = useState([]); + // 当前选中的部门Tab + const [activeTab, setActiveTab] = useState('1'); + + /** + * 部门数据列表 + * 实际项目中可通过API获取 + */ + const departments: Department[] = [ + { id: '1', name: '采购部' }, + { id: '2', name: '技术部' }, + { id: '3', name: '质量部' }, + { id: '4', name: '生产部' }, + { id: '5', name: '财务部' }, + ]; + + /** + * 初始化人员数据和已选状态 + */ + useEffect(() => { + // 加载人员数据(模拟API请求) + fetchPersonnelData(); + + // 设置已选人员 + if (selectedPersonnel && selectedPersonnel.length > 0) { + const selectedIds = selectedPersonnel.map(item => item.id); + setSelectedKeys(selectedIds); + } + }, [selectedPersonnel]); + + /** + * 模拟获取人员数据 + * 实际项目中替换为API调用 + */ + const fetchPersonnelData = () => { + // 模拟API调用获取评价人员 + const mockPersonnel: PersonnelItem[] = [ + { id: '1', name: '张三', department: '采购部', position: '采购经理' }, + { id: '2', name: '李四', department: '技术部', position: '技术专家' }, + { id: '3', name: '王五', department: '质量部', position: '质量工程师' }, + { id: '4', name: '赵六', department: '采购部', position: '高级采购' }, + { id: '5', name: '钱七', department: '技术部', position: '部门经理' }, + { id: '6', name: '孙八', department: '质量部', position: '质量总监' }, + { id: '7', name: '周九', department: '生产部', position: '生产主管' }, + { id: '8', name: '吴十', department: '财务部', position: '财务经理' }, + { id: '9', name: '郑十一', department: '采购部', position: '采购专员' }, + { id: '10', name: '王十二', department: '技术部', position: '工程师' }, + { id: '11', name: '刘十三', department: '财务部', position: '财务专员' }, + { id: '12', name: '陈十四', department: '生产部', position: '技术员' }, + ]; + + setPersonnel(mockPersonnel); + }; + + /** + * 处理搜索 + * @param {string} value - 搜索关键词 + */ + const handleSearch = (value: string) => { + setKeyword(value); + }; + + /** + * 处理Tab切换 + * @param {string} key - 选中的Tab键值 + */ + const handleTabChange = (key: string) => { + setActiveTab(key); + }; + + /** + * 处理单个人员选择 + * @param {string} personnelId - 人员ID + * @param {boolean} checked - 是否选中 + */ + const handleSelect = (personnelId: string, checked: boolean) => { + let newSelectedKeys = [...selectedKeys]; + if (checked) { + newSelectedKeys.push(personnelId); + } else { + newSelectedKeys = newSelectedKeys.filter(id => id !== personnelId); + } + + setSelectedKeys(newSelectedKeys); + }; + + /** + * 处理部门人员全选 + * @param {string} deptId - 部门ID + * @param {boolean} checked - 是否全选 + */ + const handleSelectAll = (deptId: string, checked: boolean) => { + const deptName = departments.find(d => d.id === deptId)?.name || ''; + const deptPersonnel = personnel.filter(p => + p.department === deptName && + (keyword ? p.name.includes(keyword) : true) + ); + const deptPersonnelIds = deptPersonnel.map(p => p.id); + + let newSelectedKeys = [...selectedKeys]; + + if (checked) { + // 添加该部门所有人员 + deptPersonnelIds.forEach(id => { + if (!newSelectedKeys.includes(id)) { + newSelectedKeys.push(id); + } + }); + } else { + // 移除该部门所有人员 + newSelectedKeys = newSelectedKeys.filter(id => !deptPersonnelIds.includes(id)); + } + + setSelectedKeys(newSelectedKeys); + }; + + /** + * 确认选择 + * 将选中的人员ID列表转换为人员对象列表 + */ + const handleConfirm = () => { + const selectedItems = personnel.filter(item => selectedKeys.includes(item.id)); + onSelect(selectedItems); + }; + + /** + * 渲染部门下的人员列表 + * @param {string} deptId - 部门ID + * @returns {React.ReactNode} - 渲染结果 + */ + const renderDepartmentPersonnel = (deptId: string) => { + // 获取部门名称 + const deptName = departments.find(d => d.id === deptId)?.name || ''; + + // 过滤出当前部门且符合搜索条件的人员 + const deptPersonnel = personnel.filter(p => + p.department === deptName && + (keyword ? p.name.includes(keyword) : true) + ); + + // 检查该部门是否全部选中 + const deptPersonnelIds = deptPersonnel.map(p => p.id); + const isAllSelected = deptPersonnelIds.length > 0 && deptPersonnelIds.every(id => selectedKeys.includes(id)); + + // 如果没有符合条件的人员,显示空状态 + if (deptPersonnel.length === 0) { + return ; + } + + return ( +
+ +
+ + + {deptName} + + handleSelectAll(deptId, e.target.checked)} + > + 全选 + + + (共{deptPersonnel.length}人) + + +
+ + + + + {deptPersonnel.map(person => ( +
+ + handleSelect(person.id, e.target.checked)} + > + + } + size="small" + className={selectedKeys.includes(person.id) ? 'selected-avatar' : ''} + /> +
+
+ {person.name} + {selectedKeys.includes(person.id) && ( + + )} +
+
{person.position}
+
+
+
+
+ + ))} + + + + ); + }; + + /** + * 计算各部门已选人数 + * @param {string} deptId - 部门ID + * @returns {number} - 已选人数 + */ + const getSelectedCountByDept = (deptId: string) => { + const deptName = departments.find(d => d.id === deptId)?.name || ''; + const deptPersonnel = personnel.filter(p => p.department === deptName); + const selectedCount = deptPersonnel.filter(p => selectedKeys.includes(p.id)).length; + return selectedCount; + }; + + /** + * 渲染Tab标签 + * @param {Department} dept - 部门信息 + * @returns {React.ReactNode} - 渲染结果 + */ + const renderTabTitle = (dept: Department) => { + const selectedCount = getSelectedCountByDept(dept.id); + return ( + 0 ? selectedCount : 0} + size="small" + offset={[5, -3]} + style={{ backgroundColor: selectedCount > 0 ? '#1890ff' : '#d9d9d9' }} + > + {dept.name} + + ); + }; + + return ( +
+ {/* 顶部搜索和统计区域 */} +
+ 搜索} + onSearch={handleSearch} + onChange={(e) => setKeyword(e.target.value)} + className="search-input" + /> +
+ + 已选人员 + +
+
+ + {/* 显示已选人员数量的提示 */} + {selectedKeys.length > 0 && ( + + )} + + {/* 部门分类标签页 */} + + {departments.map(dept => ( + + {renderDepartmentPersonnel(dept.id)} + + ))} + + + {/* 底部确认按钮区域 */} +
+ +
+
+ ); +}; + +export default EvaluateTaskPersonnelSelector; diff --git a/src/components/EvaluateTemplateTable/EvaluateTemplateTable.less b/src/components/EvaluateTemplateTable/EvaluateTemplateTable.less new file mode 100644 index 0000000..e60b080 --- /dev/null +++ b/src/components/EvaluateTemplateTable/EvaluateTemplateTable.less @@ -0,0 +1,10 @@ +.evaluate-template-table { + margin-top: 16px; + + .action-buttons { + display: flex; + justify-content: space-around; + } + + +} diff --git a/src/components/EvaluateTemplateTable/EvaluateTemplateTable.tsx b/src/components/EvaluateTemplateTable/EvaluateTemplateTable.tsx new file mode 100644 index 0000000..16f9af7 --- /dev/null +++ b/src/components/EvaluateTemplateTable/EvaluateTemplateTable.tsx @@ -0,0 +1,709 @@ +// 供应商评价 模板管理新增中的table +import React, { useState, useEffect } from 'react'; +import { + Table, + Input, + Button, + Select, + Form, + InputNumber, + Upload, + message, + Divider, + Switch, +} from 'antd'; +import { + PlusOutlined, + MinusCircleOutlined, + UploadOutlined, + PlusCircleOutlined, + DeleteOutlined, +} from '@ant-design/icons'; +import { getDictList, DictItem } from '@/servers/api/dicts'; +import { StarLevel, StarLevelText } from '@/dicts/supplierTemplateDict'; +import './EvaluateTemplateTable.less'; + +// 注意:这里我们使用any类型是为了灵活处理多种数据格式 +// 实际上,API的类型定义在 src/servers/api/typings.d.ts 中的 SupplierEvaluate 命名空间下 +// 包含了 IndicatorStItem 和 IndicatorNdItem 类型 + +const { Option } = Select; +const { TextArea } = Input; + +interface EvaluateTemplateTableProps { + value?: any[]; + onChange?: (value: any[]) => void; + isDetail?: boolean; // 是否详情展示用,如果为true则将input都改为text展示并且将操作列隐藏 +} + +// 内部使用的数据结构,扁平化后的行数据 +interface TableRowItem { + key: string; + stId?: string; // 一级指标ID + ndId?: string; // 二级指标ID + baseIndicator: string; + descIndicator?: string; + stScore: string; + indicatorType?: string; + subIndicator: string; + ndScore: string; + isStar: string; + desc?: string; + orderBy?: number; + ndOrderBy?: number; +} + +const EvaluateTemplateTable: React.FC = ({ + value = [], + onChange, + isDetail = false, +}) => { + const [dataSource, setDataSource] = useState([]); + const [form] = Form.useForm(); + const [indicatorTypes, setIndicatorTypes] = useState([]); + const [loadingTypes, setLoadingTypes] = useState(false); + + // 获取指标类型字典 + const fetchIndicatorTypes = async () => { + setLoadingTypes(true); + try { + const res = await getDictList('Indicator_type'); + if (res.success && res.data) { + setIndicatorTypes(res.data); + } else { + message.error('获取指标类型失败'); + } + } catch (error) { + console.error('获取指标类型失败:', error); + message.error('获取指标类型失败'); + } finally { + setLoadingTypes(false); + } + }; + + // 将API数据转换为表格数据 + const convertApiDataToTableData = (apiData: any[]): TableRowItem[] => { + // 检查数据是否已经是扁平化的表格数据格式 + if (apiData.length > 0 && 'subIndicator' in apiData[0]) { + return apiData.map((item, index) => ({ + key: item.id || `row-${index}`, + stId: item.stId, + ndId: item.id, + baseIndicator: item.baseIndicator || '', + descIndicator: item.descIndicator || '', + stScore: item.stScore || '0', + indicatorType: item.indicatorType || '', + subIndicator: item.subIndicator || '', + ndScore: item.score || '0', + isStar: item.isStar || StarLevel.NO, + desc: item.desc || '', + orderBy: typeof item.stOrderBy === 'string' ? parseInt(item.stOrderBy) : item.stOrderBy, + ndOrderBy: typeof item.orderBy === 'string' ? parseInt(item.orderBy) : item.orderBy + })); + } + + // 如果是嵌套结构,需要扁平化处理 + if (apiData.length > 0 && 'indicatorNdList' in apiData[0]) { + const flattenedData: TableRowItem[] = []; + + apiData.forEach((stItem: any) => { + stItem.indicatorNdList.forEach((ndItem: any, ndIndex: number) => { + flattenedData.push({ + key: ndItem.id || `${stItem.id}-${ndIndex}`, + stId: stItem.id, + ndId: ndItem.id, + baseIndicator: stItem.baseIndicator, + descIndicator: stItem.descIndicator, + stScore: stItem.score, + indicatorType: stItem.indicatorType, + subIndicator: ndItem.subIndicator, + ndScore: ndItem.score, + isStar: ndItem.isStar, + desc: ndItem.desc, + orderBy: typeof stItem.orderBy === 'string' ? parseInt(stItem.orderBy) : stItem.orderBy, + ndOrderBy: typeof ndItem.orderBy === 'string' ? parseInt(ndItem.orderBy) : ndItem.orderBy + }); + }); + }); + + return flattenedData; + } + + // 如果是旧格式,尝试兼容处理 + if (apiData.length > 0 && 'level1Name' in apiData[0]) { + return apiData.map((item, index) => ({ + key: item.key || `legacy-${index}`, + stId: item._originalData?.stItem?.id, + ndId: item._originalData?.ndItem?.id, + baseIndicator: item.level1Name || '', + descIndicator: item.level1Description || '', + stScore: item.level1Score?.toString() || '0', + indicatorType: item.indicator_type || '', + subIndicator: item.level2Name || '', + ndScore: item.level2Score?.toString() || '0', + isStar: item.isStar || StarLevel.NO, + desc: item.desc_score || '', + orderBy: typeof item.orderBy === 'string' ? parseInt(item.orderBy) : (item.orderBy || 0), + ndOrderBy: typeof item.ndOrderBy === 'string' ? parseInt(item.ndOrderBy) : (item.ndOrderBy || 0) + })); + } + + return []; + }; + + // 将表格数据转换回API格式 + const convertTableDataToApiData = (tableData: TableRowItem[]): any[] => { + // 按一级指标分组 + const groupedByLevel1 = tableData.reduce((acc: Record, item: TableRowItem) => { + const baseIndicator = item.baseIndicator; + if (!baseIndicator) return acc; + + if (!acc[baseIndicator]) { + acc[baseIndicator] = []; + } + acc[baseIndicator].push(item); + return acc; + }, {}); + + // 转换为API需要的格式 + return Object.keys(groupedByLevel1).map((baseIndicator, stIndex) => { + const level1Items = groupedByLevel1[baseIndicator]; + const firstItem = level1Items[0]; + + // 使用any类型避免类型错误 + const stItem: any = { + id: firstItem.stId, + baseIndicator, + descIndicator: firstItem.descIndicator, + score: firstItem.stScore, + orderBy: firstItem.orderBy || stIndex + 1, + indicatorType: firstItem.indicatorType, + indicatorNdList: level1Items.map((item, ndIndex) => ({ + id: item.ndId, + subIndicator: item.subIndicator, + score: item.ndScore, + isStar: item.isStar, + orderBy: item.ndOrderBy || ndIndex + 1, + desc: item.desc + })) + }; + + return stItem; + }); + }; + + // 初始化数据和获取字典 + useEffect(() => { + // 获取指标类型字典 + fetchIndicatorTypes(); + + // 初始化表格数据 + if (value && value.length > 0) { + const tableData = convertApiDataToTableData(value); + setDataSource(tableData); + } + }, [value]); + + // 更新数据源 + const updateDataSource = (newData: TableRowItem[]) => { + setDataSource(newData); + if (onChange) { + // 转换回API格式再传递给父组件 + const apiData = convertTableDataToApiData(newData); + onChange(apiData); + } + }; + + // 处理输入变化 + const handleInputChange = (val: any, record: TableRowItem, field: string) => { + const newData = [...dataSource]; + const index = newData.findIndex((item) => item.key === record.key); + if (index > -1) { + const item = newData[index]; + + // 特殊处理baseIndicator字段,确保唯一性 + if (field === 'baseIndicator') { + // 如果之前为空,现在有值,则视为新的一级指标 + if (!item.baseIndicator && val) { + // 检查是否有重复的baseIndicator + const existingNames = newData.map((d) => d.baseIndicator).filter(Boolean); + if (existingNames.includes(val)) { + message.warning('指标名称已存在,请使用不同的名称'); + return; + } + } + + // 更新同组内所有行的一级指标信息 + if (item.baseIndicator) { + const oldName = item.baseIndicator; + newData.forEach((row, i) => { + if (row.baseIndicator === oldName) { + newData[i] = { ...row, baseIndicator: val }; + } + }); + } else { + newData[index] = { ...item, [field]: val }; + } + } + // 处理其他一级指标字段,需要同步到同组的所有行 + else if (['descIndicator', 'stScore', 'indicatorType'].includes(field)) { + const baseIndicator = item.baseIndicator; + if (baseIndicator) { + newData.forEach((row, i) => { + if (row.baseIndicator === baseIndicator) { + newData[i] = { ...row, [field]: val }; + } + }); + } else { + newData[index] = { ...item, [field]: val }; + } + } + // 处理二级指标字段,只更新当前行 + else { + newData[index] = { ...item, [field]: val }; + } + + updateDataSource(newData); + } + }; + + // 添加一级指标 + const addLevel1Indicator = (currentRecord?: TableRowItem) => { + const newKey = `level1-${Date.now()}`; + + const newItem: TableRowItem = { + key: newKey, + baseIndicator: '', // 初始为空 + descIndicator: '', + stScore: '0', + indicatorType: '', + subIndicator: '', + ndScore: '0', + isStar: StarLevel.NO, + desc: '', + }; + + // 找到当前记录所在的位置 + const newData = [...dataSource]; + + // 找到当前记录所在的一级指标组的最后一行 + let insertIndex = -1; + if (currentRecord && currentRecord.baseIndicator) { + // 找到相同baseIndicator的最后一个元素 + const sameGroup = newData.filter((item) => item.baseIndicator === currentRecord.baseIndicator); + const lastOfGroup = sameGroup[sameGroup.length - 1]; + insertIndex = newData.findIndex((item) => item.key === lastOfGroup.key); + } else if (currentRecord && currentRecord.key) { + // 如果是新增的空白行,直接在其后插入 + insertIndex = newData.findIndex((item) => item.key === currentRecord.key); + } + + // 如果找到了位置,在该位置后插入,否则添加到末尾 + if (insertIndex !== -1) { + newData.splice(insertIndex + 1, 0, newItem); + } else { + newData.push(newItem); + } + + updateDataSource(newData); + }; + + // 删除一级指标 + const removeLevel1Indicator = (record: TableRowItem) => { + // 如果baseIndicator为空,只删除当前行 + if (!record.baseIndicator) { + const newData = dataSource.filter((item) => item.key !== record.key); + updateDataSource(newData); + return; + } + + // 删除所有具有相同baseIndicator的行 + const newData = dataSource.filter((item) => item.baseIndicator !== record.baseIndicator); + updateDataSource(newData); + }; + + // 添加二级指标 + const addSubIndicator = (parentKey: string) => { + const newKey = `level2-${Date.now()}`; + const parent = dataSource.find((item) => item.key === parentKey); + + if (!parent || !parent.baseIndicator) { + message.warning('请先填写一级指标名称'); + return; + } + + const newItem: TableRowItem = { + key: newKey, + baseIndicator: parent.baseIndicator, + descIndicator: parent.descIndicator, + stScore: parent.stScore, + indicatorType: parent.indicatorType, + subIndicator: '', + ndScore: '0', + isStar: StarLevel.NO, + desc: '', + }; + + // 找到当前记录所在的位置 + const newData = [...dataSource]; + const parentIndex = newData.findIndex((item) => item.key === parentKey); + + // 插入到父级之后 + if (parentIndex !== -1) { + newData.splice(parentIndex + 1, 0, newItem); + } else { + newData.push(newItem); + } + + updateDataSource(newData); + }; + + // 删除二级指标 + const removeSubIndicator = (key: string) => { + const newData = dataSource.filter((item) => item.key !== key); + updateDataSource(newData); + }; + + // 获取一级指标的行数 + const getLevel1RowSpan = (baseIndicator: string) => { + // 如果baseIndicator为空,返回1(不合并) + if (!baseIndicator) { + return 1; + } + return dataSource.filter((item) => item.baseIndicator === baseIndicator).length; + }; + + // 处理合并单元格 + const renderWithRowSpan = ( + content: any, + record: TableRowItem, + render: (cellContent: any, record: TableRowItem) => React.ReactNode, + ) => { + // 如果baseIndicator为空,不进行合并 + if (!record.baseIndicator) { + return { + children: render(content, record), + props: { + rowSpan: 1, + }, + }; + } + + // 查找相同baseIndicator的所有项 + const level1Items = dataSource.filter((item) => item.baseIndicator === record.baseIndicator); + const index = level1Items.findIndex((item) => item.key === record.key); + + if (index === 0) { + return { + children: render(content, record), + props: { + rowSpan: level1Items.length, + }, + }; + } + + return { + props: { + rowSpan: 0, + }, + }; + }; + + const columns = [ + { + title: '一级指标', + children: [ + { + title: '序号', + dataIndex: 'index', + align: 'center', + key: 'index', + width: 50, + render: (_: any, record: TableRowItem, index: number) => { + return renderWithRowSpan(index + 1, record, (cellContent) => cellContent); + }, + }, + { + title: '类型', + dataIndex: 'indicatorType', + align: 'center', + key: 'indicatorType', + width: 120, + render: (text: string, record: TableRowItem) => { + return renderWithRowSpan(text, record, (cellContent) => { + if (isDetail) { + // 在详情模式下,查找并显示字典中对应的名称 + const typeItem = indicatorTypes.find((item) => item.code === cellContent); + return typeItem ? typeItem.dicName : cellContent || '-'; + } + return ( + + ); + }); + }, + }, + { + title: '基本指标', + align: 'center', + dataIndex: 'baseIndicator', + key: 'baseIndicator', + width: 150, + render: (text: string, record: TableRowItem) => { + return renderWithRowSpan(text, record, (cellContent) => { + if (isDetail) { + return cellContent || '-'; + } + return ( + handleInputChange(e.target.value, record, 'baseIndicator')} + placeholder="请输入基本指标" + /> + ); + }); + }, + }, + { + title: '指标说明', + align: 'center', + dataIndex: 'descIndicator', + key: 'descIndicator', + width: 200, + render: (text: string, record: TableRowItem) => { + return renderWithRowSpan(text, record, (cellContent) => { + if (isDetail) { + return cellContent || '-'; + } + return ( + handleInputChange(e.target.value, record, 'descIndicator')} + placeholder="请输入指标说明" + /> + ); + }); + }, + }, + { + title: '分值', + align: 'center', + dataIndex: 'stScore', + key: 'stScore', + width: 80, + render: (text: string, record: TableRowItem) => { + return renderWithRowSpan(text, record, (cellContent) => { + if (isDetail) { + return cellContent || '-'; + } + return ( + handleInputChange(val?.toString() || '0', record, 'stScore')} + style={{ width: '100%' }} + /> + ); + }); + }, + }, + { + title: '操作', + key: 'level1Action', + align: 'center', + width: 100, + render: (_: any, record: TableRowItem) => { + if (isDetail) return null; + + return renderWithRowSpan(null, record, () => ( +
+
+ )); + }, + }, + ].filter((col) => !(isDetail && col.key === 'level1Action')), + }, + { + title: '二级指标', + children: [ + { + title: '细分指标', + dataIndex: 'subIndicator', + key: 'subIndicator', + align: 'center', + width: 200, + render: (text: string, record: TableRowItem) => { + if (isDetail) { + return text || '-'; + } + return ( + handleInputChange(e.target.value, record, 'subIndicator')} + placeholder="请输入细分指标" + /> + ); + }, + }, + { + title: '分值', + dataIndex: 'ndScore', + key: 'ndScore', + width: 80, + align: 'center', + render: (text: string, record: TableRowItem) => { + if (isDetail) { + return text || '-'; + } + return ( + handleInputChange(val?.toString() || '0', record, 'ndScore')} + style={{ width: '100%' }} + /> + ); + }, + }, + { + title: '星号项', + dataIndex: 'isStar', + key: 'isStar', + align: 'center', + width: 80, + render: (text: string, record: TableRowItem) => { + if (isDetail) { + return text === StarLevel.YES ? '*' : ''; + } + return ( + + ); + }, + }, + { + title: '评分说明', + dataIndex: 'desc', + align: 'center', + key: 'desc', + render: (text: string, record: TableRowItem) => { + if (isDetail) { + return text || '-'; + } + return ( +