imeeting/frontend/src/pages/Dictionaries.tsx

431 lines
15 KiB
TypeScript
Raw Normal View History

import {
Button,
Card,
Col,
Drawer,
Form,
Input,
InputNumber,
message,
Popconfirm,
Row,
Select,
Space,
Table,
Tag,
Typography,
Empty
} from "antd";
import { useEffect, useState } from "react";
import {
createDictItem,
createDictType,
deleteDictItem,
deleteDictType,
fetchDictItems,
fetchDictTypes,
updateDictItem,
updateDictType
} from "../api";
import { usePermission } from "../hooks/usePermission";
import { PlusOutlined, EditOutlined, DeleteOutlined, BookOutlined, ProfileOutlined } from "@ant-design/icons";
import type { SysDictItem, SysDictType } from "../types";
import "./Dictionaries.css";
const { Title, Text } = Typography;
export default function Dictionaries() {
const { can } = usePermission();
const [types, setTypes] = useState<SysDictType[]>([]);
const [items, setItems] = useState<SysDictItem[]>([]);
const [selectedType, setSelectedType] = useState<SysDictType | null>(null);
const [loadingTypes, setLoadingTypes] = useState(false);
const [loadingItems, setLoadingItems] = useState(false);
// Type Drawer
const [typeDrawerVisible, setTypeDrawerVisible] = useState(false);
const [editingType, setEditingType] = useState<SysDictType | null>(null);
const [typeForm] = Form.useForm();
// Item Drawer
const [itemDrawerVisible, setItemDrawerVisible] = useState(false);
const [editingItem, setEditingItem] = useState<SysDictItem | null>(null);
const [itemForm] = Form.useForm();
const loadTypes = async () => {
setLoadingTypes(true);
try {
const data = await fetchDictTypes();
setTypes(data || []);
if (data && data.length > 0 && !selectedType) {
setSelectedType(data[0]);
}
} finally {
setLoadingTypes(false);
}
};
const loadItems = async (typeCode: string) => {
setLoadingItems(true);
try {
const data = await fetchDictItems(typeCode);
setItems(data || []);
} finally {
setLoadingItems(false);
}
};
useEffect(() => {
loadTypes();
}, []);
useEffect(() => {
if (selectedType) {
loadItems(selectedType.typeCode);
} else {
setItems([]);
}
}, [selectedType]);
// Type Actions
const handleAddType = () => {
setEditingType(null);
typeForm.resetFields();
setTypeDrawerVisible(true);
};
const handleEditType = (record: SysDictType) => {
setEditingType(record);
typeForm.setFieldsValue(record);
setTypeDrawerVisible(true);
};
const handleDeleteType = async (id: number) => {
await deleteDictType(id);
message.success("类型删除成功");
loadTypes();
};
const handleTypeSubmit = async () => {
const values = await typeForm.validateFields();
if (editingType) {
await updateDictType(editingType.dictTypeId, values);
} else {
await createDictType(values);
}
message.success(editingType ? "类型更新成功" : "类型创建成功");
setTypeDrawerVisible(false);
loadTypes();
};
// Item Actions
const handleAddItem = () => {
if (!selectedType) {
message.warning("请先从左侧选择一个字典类型");
return;
}
setEditingItem(null);
itemForm.resetFields();
itemForm.setFieldsValue({ typeCode: selectedType.typeCode, sortOrder: 0, status: 1 });
setItemDrawerVisible(true);
};
const handleEditItem = (record: SysDictItem) => {
setEditingItem(record);
itemForm.setFieldsValue(record);
setItemDrawerVisible(true);
};
const handleDeleteItem = async (id: number) => {
await deleteDictItem(id);
message.success("字典项删除成功");
if (selectedType) loadItems(selectedType.typeCode);
};
const handleItemSubmit = async () => {
const values = await itemForm.validateFields();
if (editingItem) {
await updateDictItem(editingItem.dictItemId, values);
} else {
await createDictItem(values);
}
message.success(editingItem ? "字典项更新成功" : "字典项创建成功");
setItemDrawerVisible(false);
if (selectedType) loadItems(selectedType.typeCode);
};
return (
<div className="dictionaries-page">
<div className="dictionaries-header">
<div>
<Title level={4} className="dictionaries-title"></Title>
<Text type="secondary"></Text>
</div>
</div>
<Row gutter={24} className="dictionaries-content">
<Col span={8} className="full-height">
<Card
title={
<Space>
<BookOutlined aria-hidden="true" />
<span></span>
</Space>
}
className="full-height-card shadow-sm"
extra={
can("sys_dict:type:create") && (
<Button
type="primary"
size="small"
icon={<PlusOutlined aria-hidden="true" />}
onClick={handleAddType}
aria-label="新增字典类型"
>
</Button>
)
}
>
<div className="scroll-container">
<Table
rowKey="dictTypeId"
loading={loadingTypes}
dataSource={types}
pagination={false}
size="small"
showHeader={false}
onRow={(record) => ({
onClick: () => setSelectedType(record),
className: `cursor-pointer dict-type-row ${selectedType?.dictTypeId === record.dictTypeId ? "dict-type-row-selected" : ""}`
})}
columns={[
{
render: (_, record) => (
<div className="dict-type-item">
<div className="min-w-0 flex-1">
<div className="dict-type-name truncate">{record.typeName}</div>
<div className="dict-type-code truncate tabular-nums">{record.typeCode}</div>
</div>
<div className="dict-type-actions">
{can("sys_dict:type:update") && (
<Button
type="text"
size="small"
icon={<EditOutlined aria-hidden="true" />}
onClick={(e) => {
e.stopPropagation();
handleEditType(record);
}}
aria-label={`编辑类型 ${record.typeName}`}
/>
)}
{can("sys_dict:type:delete") && (
<Popconfirm
title={`确定删除类型 "${record.typeName}" 吗?这会影响关联的字典项。`}
onConfirm={(e) => {
e?.stopPropagation();
handleDeleteType(record.dictTypeId);
}}
>
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined aria-hidden="true" />}
onClick={(e) => e.stopPropagation()}
aria-label={`删除类型 ${record.typeName}`}
/>
</Popconfirm>
)}
</div>
</div>
)
}
]}
/>
</div>
</Card>
</Col>
<Col span={16} className="full-height">
<Card
title={
<Space>
<ProfileOutlined aria-hidden="true" />
<span>{selectedType ? ` - ${selectedType.typeName}` : ""}</span>
</Space>
}
className="full-height-card shadow-sm"
extra={
can("sys_dict:item:create") && (
<Button
type="primary"
size="small"
icon={<PlusOutlined aria-hidden="true" />}
onClick={handleAddItem}
disabled={!selectedType}
aria-label="新增字典项"
>
</Button>
)
}
>
{selectedType ? (
<div className="scroll-container">
<Table
rowKey="dictItemId"
loading={loadingItems}
dataSource={items}
pagination={false}
size="middle"
columns={[
{
title: "展示标签",
dataIndex: "itemLabel",
render: (text) => <Text strong>{text}</Text>
},
{
title: "数据数值",
dataIndex: "itemValue",
className: "tabular-nums"
},
{
title: "排序",
dataIndex: "sortOrder",
width: 80,
className: "tabular-nums"
},
{
title: "状态",
dataIndex: "status",
width: 100,
render: (v) => (
<Tag color={v === 1 ? "green" : "red"}>
{v === 1 ? "启用" : "禁用"}
</Tag>
)
},
{
title: "操作",
width: 120,
fixed: "right" as const,
render: (_, record) => (
<Space>
{can("sys_dict:item:update") && (
<Button
type="text"
size="small"
icon={<EditOutlined aria-hidden="true" />}
onClick={() => handleEditItem(record)}
aria-label={`编辑字典项 ${record.itemLabel}`}
/>
)}
{can("sys_dict:item:delete") && (
<Popconfirm title={`确定删除字典项 "${record.itemLabel}" 吗?`} onConfirm={() => handleDeleteItem(record.dictItemId)}>
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined aria-hidden="true" />}
aria-label={`删除字典项 ${record.itemLabel}`}
/>
</Popconfirm>
)}
</Space>
)
}
]}
/>
</div>
) : (
<div className="flex-center h-full">
<Empty description="请先从左侧选择一个字典类型" />
</div>
)}
</Card>
</Col>
</Row>
{/* Type Drawer */}
<Drawer
title={
<Space>
<BookOutlined aria-hidden="true" />
<span>{editingType ? "编辑字典类型" : "新增字典类型"}</span>
</Space>
}
open={typeDrawerVisible}
onClose={() => setTypeDrawerVisible(false)}
width={400}
destroyOnClose
footer={
<div className="flex justify-end gap-2">
<Button onClick={() => setTypeDrawerVisible(false)}></Button>
<Button type="primary" onClick={handleTypeSubmit}></Button>
</div>
}
>
<Form form={typeForm} layout="vertical">
<Form.Item label="类型编码" name="typeCode" rules={[{ required: true, message: "请输入类型编码" }]}>
<Input disabled={!!editingType} placeholder="例如user_status…" />
</Form.Item>
<Form.Item label="类型名称" name="typeName" rules={[{ required: true, message: "请输入类型名称" }]}>
<Input placeholder="例如:用户状态…" />
</Form.Item>
<Form.Item label="备注说明" name="remark">
<Input.TextArea placeholder="该字典类型的用途描述…" rows={3} />
</Form.Item>
</Form>
</Drawer>
{/* Item Drawer */}
<Drawer
title={
<Space>
<ProfileOutlined aria-hidden="true" />
<span>{editingItem ? "编辑字典项" : "新增字典项"}</span>
</Space>
}
open={itemDrawerVisible}
onClose={() => setItemDrawerVisible(false)}
width={400}
destroyOnClose
footer={
<div className="flex justify-end gap-2">
<Button onClick={() => setItemDrawerVisible(false)}></Button>
<Button type="primary" onClick={handleItemSubmit}></Button>
</div>
}
>
<Form form={itemForm} layout="vertical">
<Form.Item label="所属类型" name="typeCode">
<Input disabled className="tabular-nums" />
</Form.Item>
<Form.Item label="显示标签" name="itemLabel" rules={[{ required: true, message: "请输入展示标签" }]}>
<Input placeholder="例如:正常、禁用…" />
</Form.Item>
<Form.Item label="存储数值" name="itemValue" rules={[{ required: true, message: "请输入数值" }]}>
<Input placeholder="例如1、0…" className="tabular-nums" />
</Form.Item>
<Form.Item label="显示排序" name="sortOrder" initialValue={0}>
<InputNumber className="w-full tabular-nums" />
</Form.Item>
<Form.Item label="当前状态" name="status" initialValue={1}>
<Select
options={[
{ label: "启用", value: 1 },
{ label: "禁用", value: 0 }
]}
/>
</Form.Item>
<Form.Item label="备注说明" name="remark">
<Input.TextArea placeholder="可选项,备注详细信息…" rows={3} />
</Form.Item>
</Form>
</Drawer>
</div>
);
}