632 lines
20 KiB
TypeScript
Executable File
632 lines
20 KiB
TypeScript
Executable File
// apps/web/src/components/models/term/term-manager.tsx
|
||
import {
|
||
Button,
|
||
Input,
|
||
Modal,
|
||
Space,
|
||
Table,
|
||
TreeSelect,
|
||
Select,
|
||
Card,
|
||
Row,
|
||
Col,
|
||
Typography,
|
||
Divider,
|
||
} from "antd";
|
||
import { api } from "@nice/client";
|
||
import { useState, useEffect } from "react";
|
||
import {
|
||
PlusOutlined,
|
||
EditOutlined,
|
||
DeleteOutlined,
|
||
SearchOutlined,
|
||
FileTextOutlined,
|
||
} from "@ant-design/icons";
|
||
import { ObjectType } from "@nice/common";
|
||
|
||
const { Title } = Typography;
|
||
|
||
interface TermManagerProps {
|
||
title: string;
|
||
}
|
||
|
||
export default function DeviceManager({ title }: TermManagerProps) {
|
||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||
const [editingTerm, setEditingTerm] = useState<any>(null);
|
||
const [termName, setTermName] = useState("");
|
||
const [parentId, setParentId] = useState<string | null>(null);
|
||
const [taxonomyId, setTaxonomyId] = useState<string | null>(null);
|
||
const [searchValue, setSearchValue] = useState("");
|
||
const [treeData, setTreeData] = useState<any[]>([]);
|
||
const [taxonomySelectDisabled, setTaxonomySelectDisabled] = useState(false);
|
||
const [pageSize, setPageSize] = useState(10);
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
|
||
// ... 保持原有的 API 调用和逻辑 ...
|
||
|
||
// 获取所有taxonomy
|
||
const { data: taxonomies } = api.taxonomy.getAll.useQuery({
|
||
type: ObjectType.DEVICE,
|
||
});
|
||
|
||
// 获取所有故障类型taxonomy
|
||
// 故障网系类别taxonomy
|
||
const { data: systemTypeTaxonomy } = api.taxonomy.findBySlug.useQuery({
|
||
slug: "system_type",
|
||
});
|
||
|
||
// 故障类型taxonomy
|
||
const { data: deviceTypeTaxonomy } = api.taxonomy.findBySlug.useQuery({
|
||
slug: "device_type",
|
||
});
|
||
|
||
// 获取所有网系类别条目
|
||
const { data: systemTypeTerms, refetch: refetchSystemType } =
|
||
api.term.findMany.useQuery({
|
||
where: {
|
||
taxonomy: { slug: "system_type" },
|
||
deletedAt: null,
|
||
},
|
||
include: {
|
||
children: true,
|
||
},
|
||
orderBy: { order: "asc" },
|
||
});
|
||
|
||
// 获取所有故障类型条目
|
||
const { data: deviceTypeTerms, refetch: refetchDeviceType } =
|
||
api.term.findMany.useQuery({
|
||
where: {
|
||
taxonomy: { slug: "device_type" },
|
||
deletedAt: null,
|
||
},
|
||
include: {
|
||
children: true,
|
||
},
|
||
orderBy: { order: "asc" },
|
||
});
|
||
|
||
const handlePageChange = (page: number, size: number) => {
|
||
setCurrentPage(page);
|
||
setPageSize(size);
|
||
};
|
||
|
||
const handlePageSizeChange = (current: number, size: number) => {
|
||
setCurrentPage(1); // 重置到第一页
|
||
setPageSize(size);
|
||
};
|
||
// 构建包含两种分类的树形数据
|
||
useEffect(() => {
|
||
if (systemTypeTerms && deviceTypeTerms) {
|
||
// 先获取顶级网系类别
|
||
const rootTerms = systemTypeTerms.filter((term) => !term.parentId);
|
||
|
||
// 构建树形数据
|
||
const buildTreeData = (items: any[]): any[] => {
|
||
return items.map((item) => {
|
||
// 找到与此网系类别关联的故障类型
|
||
const deviceChildren = deviceTypeTerms.filter(
|
||
(t) => t.parentId === item.id
|
||
);
|
||
|
||
// 为每个故障类型找到其子故障
|
||
const processedDeviceChildren = deviceChildren.map((deviceType) => {
|
||
const deviceItems = deviceTypeTerms.filter(
|
||
(t) => t.parentId === deviceType.id
|
||
);
|
||
|
||
return {
|
||
...deviceType,
|
||
key: deviceType.id,
|
||
children: deviceItems.map((device) => ({
|
||
...device,
|
||
key: device.id,
|
||
})),
|
||
};
|
||
});
|
||
|
||
return {
|
||
...item,
|
||
key: item.id,
|
||
children: processedDeviceChildren,
|
||
};
|
||
});
|
||
};
|
||
|
||
setTreeData(buildTreeData(rootTerms));
|
||
}
|
||
}, [systemTypeTerms, deviceTypeTerms]);
|
||
|
||
// 搜索过滤逻辑
|
||
useEffect(() => {
|
||
if (systemTypeTerms && deviceTypeTerms) {
|
||
if (!searchValue) {
|
||
// 重新构建完整的树形结构
|
||
const rootTerms = systemTypeTerms.filter((term) => !term.parentId);
|
||
const buildTreeData = (items: any[]): any[] => {
|
||
return items.map((item) => {
|
||
const deviceChildren = deviceTypeTerms.filter(
|
||
(t) => t.parentId === item.id
|
||
);
|
||
|
||
const processedDeviceChildren = deviceChildren.map((deviceType) => {
|
||
const deviceItems = deviceTypeTerms.filter(
|
||
(t) => t.parentId === deviceType.id
|
||
);
|
||
|
||
return {
|
||
...deviceType,
|
||
key: deviceType.id,
|
||
children: deviceItems.map((device) => ({
|
||
...device,
|
||
key: device.id,
|
||
})),
|
||
};
|
||
});
|
||
|
||
return {
|
||
...item,
|
||
key: item.id,
|
||
children: processedDeviceChildren,
|
||
};
|
||
});
|
||
};
|
||
|
||
setTreeData(buildTreeData(rootTerms));
|
||
} else {
|
||
// 搜索匹配所有项
|
||
const allTerms = [...systemTypeTerms, ...deviceTypeTerms];
|
||
const filtered = allTerms.filter((term) =>
|
||
term.name.toLowerCase().includes(searchValue.toLowerCase())
|
||
);
|
||
setTreeData(filtered.map((item) => ({ ...item, key: item.id })));
|
||
}
|
||
}
|
||
}, [systemTypeTerms, deviceTypeTerms, searchValue]);
|
||
|
||
const handleSearch = (value: string) => {
|
||
setSearchValue(value);
|
||
};
|
||
|
||
// API调用
|
||
const { mutate: createTerm } = api.term.create.useMutation({
|
||
onSuccess: () => {
|
||
refetchSystemType();
|
||
refetchDeviceType();
|
||
setIsModalVisible(false);
|
||
setTermName("");
|
||
setParentId(null);
|
||
setTaxonomyId(null);
|
||
},
|
||
});
|
||
|
||
const { mutate: updateTerm } = api.term.update.useMutation({
|
||
onSuccess: () => {
|
||
refetchSystemType();
|
||
refetchDeviceType();
|
||
setIsModalVisible(false);
|
||
setEditingTerm(null);
|
||
setTermName("");
|
||
setParentId(null);
|
||
setTaxonomyId(null);
|
||
},
|
||
});
|
||
|
||
const { mutate: softDeleteByIds } = api.term.softDeleteByIds.useMutation({
|
||
onSuccess: () => {
|
||
refetchSystemType();
|
||
refetchDeviceType();
|
||
},
|
||
});
|
||
|
||
// 操作处理函数
|
||
const handleAdd = (parentRecord?: any) => {
|
||
setEditingTerm(null);
|
||
setTermName("");
|
||
setParentId(parentRecord?.id || null);
|
||
refetchSystemType();
|
||
refetchDeviceType();
|
||
// 根据父记录类型自动选择taxonomy
|
||
if (parentRecord) {
|
||
// 如果父类是网系类别,则自动设置为故障类型
|
||
if (parentRecord.taxonomyId === systemTypeTaxonomy?.id) {
|
||
setTaxonomyId(deviceTypeTaxonomy?.id);
|
||
// 可以设置状态来禁用Select组件
|
||
setTaxonomySelectDisabled(true);
|
||
} else {
|
||
setTaxonomyId(parentRecord.taxonomyId);
|
||
setTaxonomySelectDisabled(false);
|
||
}
|
||
} else {
|
||
// 如果是顶级项,默认设为网系类别
|
||
setTaxonomyId(systemTypeTaxonomy?.id || null);
|
||
setTaxonomySelectDisabled(false);
|
||
}
|
||
|
||
setIsModalVisible(true);
|
||
};
|
||
|
||
const handleEdit = (term: any) => {
|
||
setEditingTerm(term);
|
||
setTermName(term.name);
|
||
setParentId(term.parentId);
|
||
setTaxonomyId(term.taxonomyId);
|
||
setIsModalVisible(true);
|
||
refetchSystemType();
|
||
refetchDeviceType();
|
||
};
|
||
|
||
const handleDelete = (term: any) => {
|
||
Modal.confirm({
|
||
title: "确认删除",
|
||
content: `确定要删除"${term.name}"吗?这将同时删除其下所有子项!`,
|
||
onOk: () => softDeleteByIds({ ids: [term.id] }),
|
||
});
|
||
refetchSystemType();
|
||
refetchDeviceType();
|
||
};
|
||
|
||
const handleSave = () => {
|
||
if (!termName.trim() || !taxonomyId) return;
|
||
|
||
if (editingTerm) {
|
||
updateTerm({
|
||
where: { id: editingTerm.id },
|
||
data: {
|
||
name: termName,
|
||
parentId: parentId,
|
||
hasChildren: editingTerm.hasChildren,
|
||
},
|
||
});
|
||
} else {
|
||
createTerm({
|
||
data: {
|
||
name: termName,
|
||
taxonomyId: taxonomyId,
|
||
parentId: parentId,
|
||
},
|
||
});
|
||
}
|
||
refetchSystemType();
|
||
refetchDeviceType();
|
||
};
|
||
|
||
// 构建父级选择器的选项
|
||
const getParentOptions = () => {
|
||
if (!systemTypeTerms || !deviceTypeTerms) return [];
|
||
|
||
const allTerms = [...systemTypeTerms, ...deviceTypeTerms];
|
||
|
||
// 根据编辑对象和当前选择的taxonomy过滤有效的父级选项
|
||
let validParents = allTerms;
|
||
|
||
// 如果是编辑现有项
|
||
if (editingTerm) {
|
||
// 递归查找所有子孙节点ID,避免循环引用
|
||
const findAllDescendantIds = (itemId: string): string[] => {
|
||
const directChildren = allTerms.filter((t) => t.parentId === itemId);
|
||
const descendantIds = directChildren.map((c) => c.id);
|
||
|
||
directChildren.forEach((child) => {
|
||
const childDescendants = findAllDescendantIds(child.id);
|
||
descendantIds.push(...childDescendants);
|
||
});
|
||
|
||
return descendantIds;
|
||
};
|
||
|
||
const invalidIds = [
|
||
editingTerm.id,
|
||
...findAllDescendantIds(editingTerm.id),
|
||
];
|
||
validParents = allTerms.filter((t) => !invalidIds.includes(t.id));
|
||
}
|
||
|
||
// 如果是添加故障类型,只能选择网系类别作为父级
|
||
if (!editingTerm && taxonomyId === deviceTypeTaxonomy?.id) {
|
||
validParents = systemTypeTerms;
|
||
}
|
||
|
||
// 如果是添加具体故障,只能选择故障类型作为父级
|
||
if (
|
||
!editingTerm &&
|
||
taxonomyId &&
|
||
taxonomyId !== systemTypeTaxonomy?.id &&
|
||
taxonomyId !== deviceTypeTaxonomy?.id
|
||
) {
|
||
validParents = deviceTypeTerms;
|
||
}
|
||
|
||
// 转换为TreeSelect需要的格式
|
||
const buildTreeOptions = (items: any[], depth = 0): any[] => {
|
||
return items
|
||
.filter((item) => (depth === 0 ? !item.parentId : true))
|
||
.map((item) => {
|
||
const children = allTerms.filter((t) => t.parentId === item.id);
|
||
return {
|
||
title: "—".repeat(depth) + (depth > 0 ? " " : "") + item.name,
|
||
value: item.id,
|
||
children:
|
||
children.length > 0
|
||
? buildTreeOptions(children, depth + 1)
|
||
: undefined,
|
||
};
|
||
});
|
||
};
|
||
|
||
return buildTreeOptions(validParents);
|
||
};
|
||
|
||
return (
|
||
<div className="p-6 min-h-screen bg-gray-50">
|
||
{/* 主要内容卡片 */}
|
||
<Card className="shadow-sm" bodyStyle={{ padding: 0 }}>
|
||
{/* 统计信息区域 */}
|
||
<div className="p-4 bg-gray-50 border-b border-gray-100">
|
||
<Row gutter={16} justify="center">
|
||
<Col span={8}>
|
||
<div className="text-center">
|
||
<div className="text-2xl font-bold text-blue-600">
|
||
{systemTypeTerms?.length || 0}
|
||
</div>
|
||
<div className="text-sm text-gray-600">网系类别</div>
|
||
</div>
|
||
</Col>
|
||
<Col span={8}>
|
||
<div className="text-center">
|
||
<div className="text-2xl font-bold text-green-600">
|
||
{deviceTypeTerms?.filter((term) =>
|
||
systemTypeTerms?.some(
|
||
(sysType) => sysType.id === term.parentId
|
||
)
|
||
).length || 0}
|
||
</div>
|
||
<div className="text-sm text-gray-600">故障类型</div>
|
||
</div>
|
||
</Col>
|
||
{/* <Col span={8}> */}
|
||
{/* <div className="text-center"> */}
|
||
{/* <div className="text-2xl font-bold text-orange-600">
|
||
{deviceTypeTerms?.filter(term =>
|
||
!systemTypeTerms?.some(sysType => sysType.id === term.parentId) &&
|
||
deviceTypeTerms?.some(devType => devType.id === term.parentId)
|
||
).length || 0}
|
||
</div>
|
||
<div className="text-sm text-gray-600">具体故障</div> */}
|
||
{/* </div>
|
||
</Col> */}
|
||
</Row>
|
||
</div>
|
||
{/* 工具栏区域 */}
|
||
<div className="p-4 border-b border-gray-100 bg-white">
|
||
<Row gutter={[16, 16]} align="middle" justify="space-between">
|
||
<Col xs={24} sm={16} md={12} lg={10}>
|
||
<Input.Search
|
||
placeholder={`搜索${title}名称...`}
|
||
onSearch={handleSearch}
|
||
onChange={(e) => handleSearch(e.target.value)}
|
||
value={searchValue}
|
||
allowClear
|
||
prefix={<SearchOutlined className="text-gray-400" />}
|
||
className="w-full"
|
||
size="middle"
|
||
/>
|
||
</Col>
|
||
<Col xs={24} sm={8} md={12} lg={14} className="flex justify-end">
|
||
<Space>
|
||
<Button
|
||
type="primary"
|
||
icon={<PlusOutlined />}
|
||
onClick={() => handleAdd()}
|
||
size="middle"
|
||
className="shadow-sm"
|
||
>
|
||
添加{title}
|
||
</Button>
|
||
</Space>
|
||
</Col>
|
||
</Row>
|
||
</div>
|
||
|
||
|
||
|
||
{/* 表格区域 */}
|
||
<div className="p-4">
|
||
<Table
|
||
dataSource={treeData}
|
||
expandable={{
|
||
defaultExpandAllRows: false, // 改为不默认展开所有行
|
||
expandRowByClick: true,
|
||
indentSize: 20, // 设置缩进大小
|
||
}}
|
||
columns={[
|
||
{
|
||
title: "名称",
|
||
dataIndex: "name",
|
||
key: "name",
|
||
width: "50%", // 增加名称列的宽度
|
||
render: (text, record, index, ) => (
|
||
<div className="flex items-center gap-2">
|
||
{/* 根据层级添加不同的图标 */}
|
||
{record.taxonomyId === systemTypeTaxonomy?.id ? (
|
||
<span className="text-blue-500">📁</span>
|
||
) : record.taxonomyId === deviceTypeTaxonomy?.id ? (
|
||
<span className="text-green-500">📂</span>
|
||
) : (
|
||
<span className="text-orange-500">📄</span>
|
||
)}
|
||
<span className="font-medium">{text}</span>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: "分类类型",
|
||
key: "taxonomyType",
|
||
width: "25%",
|
||
render: (_, record) => {
|
||
if (record.taxonomyId === systemTypeTaxonomy?.id) {
|
||
return (
|
||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||
网系类别
|
||
</span>
|
||
);
|
||
} else if (record.taxonomyId === deviceTypeTaxonomy?.id) {
|
||
return (
|
||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||
故障类型
|
||
</span>
|
||
);
|
||
} else {
|
||
return (
|
||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
|
||
具体故障
|
||
</span>
|
||
);
|
||
}
|
||
},
|
||
},
|
||
{
|
||
title: "操作",
|
||
key: "action",
|
||
width: "25%",
|
||
render: (_, record: any) => (
|
||
<div className="flex items-center gap-1">
|
||
<Button
|
||
type="text"
|
||
icon={<PlusOutlined />}
|
||
onClick={() => handleAdd(record)}
|
||
className="text-green-600 hover:text-green-700 hover:bg-green-50"
|
||
size="small"
|
||
title="添加子项"
|
||
/>
|
||
<Button
|
||
type="text"
|
||
icon={<EditOutlined />}
|
||
onClick={() => handleEdit(record)}
|
||
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
|
||
size="small"
|
||
title="编辑"
|
||
/>
|
||
<Button
|
||
type="text"
|
||
danger
|
||
icon={<DeleteOutlined />}
|
||
onClick={() => handleDelete(record)}
|
||
size="small"
|
||
title="删除"
|
||
className="hover:bg-red-50"
|
||
/>
|
||
</div>
|
||
),
|
||
},
|
||
]}
|
||
rowKey="id"
|
||
pagination={{
|
||
pageSize: pageSize, // 每页显示数量
|
||
current: currentPage,
|
||
showSizeChanger: true,
|
||
showQuickJumper: true,
|
||
showTotal: (total) => `共 ${total} 条记录`,
|
||
pageSizeOptions: [ "10", "15", "20"],
|
||
onChange: handlePageChange,
|
||
onShowSizeChange: handlePageSizeChange,
|
||
}}
|
||
rowClassName={(record, index) =>
|
||
`hover:bg-blue-50 transition-colors duration-200 ${
|
||
record.taxonomyId === systemTypeTaxonomy?.id
|
||
? "bg-blue-25"
|
||
: record.taxonomyId === deviceTypeTaxonomy?.id
|
||
? "bg-green-25"
|
||
: "bg-orange-25"
|
||
}`
|
||
}
|
||
onHeaderRow={() => ({
|
||
style: {
|
||
backgroundColor: "#f8fafc",
|
||
fontWeight: "600",
|
||
borderBottom: "2px solid #e2e8f0",
|
||
},
|
||
})}
|
||
bordered={true} // 改为有边框
|
||
size="middle"
|
||
locale={{ emptyText: "暂无数据" }}
|
||
className="rounded-lg overflow-hidden shadow-sm"
|
||
showHeader={true}
|
||
scroll={{ x: 800 }} // 添加横向滚动
|
||
/>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 编辑/添加模态框 */}
|
||
<Modal
|
||
title={
|
||
<div className="flex items-center gap-2">
|
||
{editingTerm ? <EditOutlined /> : <PlusOutlined />}
|
||
{editingTerm ? `编辑${title}` : `添加${title}`}
|
||
</div>
|
||
}
|
||
open={isModalVisible}
|
||
onOk={handleSave}
|
||
onCancel={() => setIsModalVisible(false)}
|
||
okText="保存"
|
||
cancelText="取消"
|
||
width={600}
|
||
destroyOnClose
|
||
>
|
||
<Divider className="mt-4 mb-6" />
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
名称 <span className="text-red-500">*</span>
|
||
</label>
|
||
<Input
|
||
placeholder={`请输入${title}名称`}
|
||
value={termName}
|
||
onChange={(e) => setTermName(e.target.value)}
|
||
size="large"
|
||
/>
|
||
</div>
|
||
|
||
{!editingTerm && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
分类类型 <span className="text-red-500">*</span>
|
||
</label>
|
||
<Select
|
||
style={{ width: "100%" }}
|
||
placeholder="请选择分类类型"
|
||
value={taxonomyId}
|
||
onChange={setTaxonomyId}
|
||
disabled={taxonomySelectDisabled}
|
||
size="large"
|
||
options={taxonomies?.map((tax) => ({
|
||
label: tax.name,
|
||
value: tax.id,
|
||
}))}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
上级分类
|
||
</label>
|
||
<TreeSelect
|
||
style={{ width: "100%" }}
|
||
dropdownStyle={{ maxHeight: 400, overflow: "auto" }}
|
||
placeholder="请选择上级分类(可选)"
|
||
allowClear
|
||
treeDefaultExpandAll
|
||
value={parentId}
|
||
onChange={setParentId}
|
||
treeData={getParentOptions()}
|
||
size="large"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
}
|