2026-04-20 03:30:26 +00:00
|
|
|
import { Button, Card, Col, Drawer, Empty, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Table, Tag, Typography, message } from "antd";
|
2026-03-17 07:31:09 +00:00
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
import { BookOutlined, DeleteOutlined, EditOutlined, PlusOutlined, ProfileOutlined, SearchOutlined } from "@ant-design/icons";
|
|
|
|
|
import { createDictItem, createDictType, deleteDictItem, deleteDictType, fetchDictItems, fetchDictTypes, updateDictItem, updateDictType } from "@/api";
|
|
|
|
|
import { useDict } from "@/hooks/useDict";
|
|
|
|
|
import { usePermission } from "@/hooks/usePermission";
|
|
|
|
|
import PageHeader from "@/components/shared/PageHeader";
|
2026-05-09 02:17:46 +00:00
|
|
|
import PageContainer from "@/components/shared/PageContainer";
|
2026-03-17 07:31:09 +00:00
|
|
|
import { getStandardPagination } from "@/utils/pagination";
|
|
|
|
|
import type { SysDictItem, SysDictType } from "@/types";
|
|
|
|
|
import "./index.less";
|
|
|
|
|
|
|
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
|
|
|
|
export default function Dictionaries() {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const { can } = usePermission();
|
|
|
|
|
const { items: statusDict } = useDict("sys_common_status");
|
|
|
|
|
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);
|
|
|
|
|
const [typeTotal, setTypeTotal] = useState(0);
|
|
|
|
|
const [typeParams, setTypeParams] = useState({ current: 1, size: 10, typeCode: "", typeName: "" });
|
|
|
|
|
const [typeKeyword, setTypeKeyword] = useState("");
|
|
|
|
|
const [typeDrawerVisible, setTypeDrawerVisible] = useState(false);
|
|
|
|
|
const [editingType, setEditingType] = useState<SysDictType | null>(null);
|
|
|
|
|
const [typeForm] = Form.useForm();
|
|
|
|
|
const [itemDrawerVisible, setItemDrawerVisible] = useState(false);
|
|
|
|
|
const [editingItem, setEditingItem] = useState<SysDictItem | null>(null);
|
|
|
|
|
const [itemForm] = Form.useForm();
|
|
|
|
|
|
|
|
|
|
const loadTypes = useCallback(async (params = typeParams) => {
|
|
|
|
|
setLoadingTypes(true);
|
|
|
|
|
try {
|
|
|
|
|
const data = await fetchDictTypes(params);
|
|
|
|
|
setTypes(data.records || []);
|
|
|
|
|
setTypeTotal(data.total || 0);
|
|
|
|
|
if (data.records?.length && !selectedType) {
|
|
|
|
|
setSelectedType(data.records[0]);
|
|
|
|
|
} else if (selectedType) {
|
|
|
|
|
const updatedSelected = data.records.find((type: SysDictType) => type.dictTypeId === selectedType.dictTypeId);
|
|
|
|
|
if (updatedSelected) setSelectedType(updatedSelected);
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setLoadingTypes(false);
|
|
|
|
|
}
|
|
|
|
|
}, [selectedType, typeParams]);
|
|
|
|
|
|
|
|
|
|
const loadItems = async (typeCode: string) => {
|
|
|
|
|
setLoadingItems(true);
|
|
|
|
|
try {
|
|
|
|
|
const data = await fetchDictItems(typeCode);
|
|
|
|
|
setItems(data || []);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoadingItems(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadTypes();
|
|
|
|
|
}, [typeParams.current, typeParams.size]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (selectedType) {
|
|
|
|
|
loadItems(selectedType.typeCode);
|
|
|
|
|
} else {
|
|
|
|
|
setItems([]);
|
|
|
|
|
}
|
|
|
|
|
}, [selectedType]);
|
|
|
|
|
|
|
|
|
|
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(t("common.success"));
|
|
|
|
|
loadTypes();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleTypeSubmit = async () => {
|
|
|
|
|
const values = await typeForm.validateFields();
|
|
|
|
|
if (editingType) {
|
|
|
|
|
await updateDictType(editingType.dictTypeId, values);
|
|
|
|
|
} else {
|
|
|
|
|
await createDictType(values);
|
|
|
|
|
}
|
|
|
|
|
message.success(t("common.success"));
|
|
|
|
|
setTypeDrawerVisible(false);
|
|
|
|
|
loadTypes();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleTypeSearch = (value: string) => {
|
|
|
|
|
const next = { ...typeParams, current: 1, typeName: value };
|
|
|
|
|
setTypeParams(next);
|
|
|
|
|
loadTypes(next);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleTypeReset = () => {
|
|
|
|
|
setTypeKeyword("");
|
|
|
|
|
const next = { ...typeParams, current: 1, typeName: "" };
|
|
|
|
|
setTypeParams(next);
|
|
|
|
|
loadTypes(next);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAddItem = () => {
|
|
|
|
|
if (!selectedType) {
|
|
|
|
|
message.warning(t("dicts.selectType"));
|
|
|
|
|
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(t("common.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(t("common.success"));
|
|
|
|
|
setItemDrawerVisible(false);
|
|
|
|
|
if (selectedType) loadItems(selectedType.typeCode);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-09 02:17:46 +00:00
|
|
|
<PageContainer
|
|
|
|
|
title={t("dicts.title")}
|
|
|
|
|
subtitle={t("dicts.subtitle")}
|
|
|
|
|
>
|
|
|
|
|
<Row gutter={24} style={{ flex: 1, minHeight: 0, overflow: "hidden" }}>
|
2026-03-17 07:31:09 +00:00
|
|
|
<Col span={8} className="h-full flex flex-col overflow-hidden">
|
2026-05-28 06:11:20 +00:00
|
|
|
<Card title={<Space><BookOutlined aria-hidden="true" /><span>{t("dicts.dictType")}</span></Space>} className="app-page__panel-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: "12px", flex: 1, display: "flex", flexDirection: "column", overflow: "hidden",maxHeight:"calc(100vh - 360px)" } }} extra={can("sys_dict:type:create") && <Button type="primary" size="small" icon={<PlusOutlined aria-hidden="true" />} onClick={handleAddType}>{t("common.create")}</Button>}>
|
2026-03-17 07:31:09 +00:00
|
|
|
<div style={{ marginBottom: 12 }} className="flex-shrink-0">
|
|
|
|
|
<Space.Compact style={{ width: "100%" }}>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder={t("dictsExt.searchTypes")}
|
|
|
|
|
allowClear
|
|
|
|
|
value={typeKeyword}
|
|
|
|
|
onChange={(event) => setTypeKeyword(event.target.value)}
|
|
|
|
|
onPressEnter={() => handleTypeSearch(typeKeyword)}
|
|
|
|
|
/>
|
|
|
|
|
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={() => handleTypeSearch(typeKeyword)}>{t("common.search")}</Button>
|
|
|
|
|
<Button onClick={handleTypeReset}>{t("common.reset")}</Button>
|
|
|
|
|
</Space.Compact>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 min-h-0">
|
|
|
|
|
<Table
|
|
|
|
|
rowKey="dictTypeId"
|
|
|
|
|
loading={loadingTypes}
|
|
|
|
|
dataSource={types}
|
2026-04-20 03:30:26 +00:00
|
|
|
pagination={{
|
|
|
|
|
...getStandardPagination(typeTotal, typeParams.current, typeParams.size, (page, size) => setTypeParams({ ...typeParams, current: page, size })),
|
|
|
|
|
simple: true,
|
|
|
|
|
size: "small",
|
|
|
|
|
position: ["bottomCenter"]
|
|
|
|
|
}}
|
2026-03-17 07:31:09 +00:00
|
|
|
size="small"
|
|
|
|
|
showHeader={false}
|
|
|
|
|
scroll={{ y: "calc(100vh - 480px)" }}
|
|
|
|
|
onRow={(record) => ({ onClick: () => setSelectedType(record), className: `cursor-pointer dict-type-row ${selectedType?.dictTypeId === record.dictTypeId ? "dict-type-row-selected" : ""}` })}
|
|
|
|
|
columns={[
|
|
|
|
|
{
|
|
|
|
|
render: (_: any, record: SysDictType) => (
|
|
|
|
|
<div className="dict-type-item flex justify-between items-center p-1">
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<div className="dict-type-name font-medium truncate" style={{ fontSize: "14px" }}>{record.typeName}</div>
|
|
|
|
|
<div className="dict-type-code text-xs text-gray-400 truncate tabular-nums">{record.typeCode}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="dict-type-actions flex gap-1">
|
|
|
|
|
{can("sys_dict:type:update") && <Button type="text" size="small" icon={<EditOutlined aria-hidden="true" style={{ fontSize: "12px" }} />} onClick={(event) => { event.stopPropagation(); handleEditType(record); }} />}
|
|
|
|
|
{can("sys_dict:type:delete") && <Popconfirm title={t("dictsExt.deleteType", { name: record.typeName })} okText={t("common.confirm")} cancelText={t("common.cancel")} onConfirm={(event) => { event?.stopPropagation(); handleDeleteType(record.dictTypeId); }}><Button type="text" size="small" danger icon={<DeleteOutlined aria-hidden="true" style={{ fontSize: "12px" }} />} onClick={(event) => event.stopPropagation()} /></Popconfirm>}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
|
|
<Col span={16} className="h-full flex flex-col overflow-hidden">
|
|
|
|
|
<Card title={<Space><ProfileOutlined aria-hidden="true" /><span>{t("dicts.dictItem")}{selectedType ? ` - ${selectedType.typeName}` : ""}</span></Space>} className="app-page__panel-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }} extra={can("sys_dict:item:create") && <Button type="primary" size="small" icon={<PlusOutlined aria-hidden="true" />} onClick={handleAddItem} disabled={!selectedType}>{t("common.create")}</Button>}>
|
|
|
|
|
{selectedType ? (
|
|
|
|
|
<div className="flex-1 overflow-hidden">
|
|
|
|
|
<Table
|
|
|
|
|
rowKey="dictItemId"
|
|
|
|
|
loading={loadingItems}
|
|
|
|
|
dataSource={items}
|
|
|
|
|
pagination={false}
|
|
|
|
|
size="middle"
|
|
|
|
|
scroll={{ y: "calc(100vh - 320px)" }}
|
|
|
|
|
columns={[
|
|
|
|
|
{ title: t("dicts.itemLabel"), dataIndex: "itemLabel", render: (text: string) => <Text strong>{text}</Text> },
|
|
|
|
|
{ title: t("dicts.itemValue"), dataIndex: "itemValue", className: "tabular-nums" },
|
|
|
|
|
{ title: t("dicts.sort"), dataIndex: "sortOrder", width: 80, className: "tabular-nums" },
|
|
|
|
|
{
|
|
|
|
|
title: t("common.status"),
|
|
|
|
|
dataIndex: "status",
|
|
|
|
|
width: 100,
|
|
|
|
|
render: (value: number) => {
|
|
|
|
|
const item = statusDict.find((dictItem) => dictItem.itemValue === String(value));
|
|
|
|
|
return <Tag color={value === 1 ? "green" : "red"}>{item ? item.itemLabel : value === 1 ? t("dictsExt.enabled") : t("dictsExt.disabled")}</Tag>;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: t("common.action"),
|
|
|
|
|
width: 120,
|
|
|
|
|
fixed: "right" as const,
|
|
|
|
|
render: (_: any, record: SysDictItem) => (
|
|
|
|
|
<Space>
|
|
|
|
|
{can("sys_dict:item:update") && <Button type="text" size="small" icon={<EditOutlined aria-hidden="true" />} onClick={() => handleEditItem(record)} aria-label={t("common.edit")} />}
|
|
|
|
|
{can("sys_dict:item:delete") && <Popconfirm title={t("dictsExt.deleteItem", { name: record.itemLabel })} okText={t("common.confirm")} cancelText={t("common.cancel")} onConfirm={() => handleDeleteItem(record.dictItemId)}><Button type="text" size="small" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={t("common.delete")} /></Popconfirm>}
|
|
|
|
|
</Space>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex items-center justify-center h-full"><Empty description={t("dicts.selectType")} /></div>
|
|
|
|
|
)}
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
|
2026-04-20 03:30:26 +00:00
|
|
|
<Drawer title={<Space><BookOutlined aria-hidden="true" /><span>{editingType ? t("dicts.drawerTitleTypeEdit") : t("dicts.drawerTitleTypeCreate")}</span></Space>} open={typeDrawerVisible} onClose={() => setTypeDrawerVisible(false)} width={400} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setTypeDrawerVisible(false)}>{t("common.cancel")}</Button><Button type="primary" onClick={handleTypeSubmit}>{t("common.save")}</Button></div>}>
|
2026-03-17 07:31:09 +00:00
|
|
|
<Form form={typeForm} layout="vertical">
|
|
|
|
|
<Form.Item label={t("dicts.typeCode")} name="typeCode" rules={[{ required: true, message: t("dicts.typeCode") }]}>
|
2026-04-17 02:08:40 +00:00
|
|
|
<Input disabled={!!editingType} placeholder={t("dictsExt.typeCodePlaceholder")} />
|
2026-03-17 07:31:09 +00:00
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item label={t("dicts.typeName")} name="typeName" rules={[{ required: true, message: t("dicts.typeName") }]}>
|
2026-04-17 02:08:40 +00:00
|
|
|
<Input placeholder={t("dictsExt.typeNamePlaceholder")} />
|
2026-03-17 07:31:09 +00:00
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item label={t("common.remark")} name="remark">
|
|
|
|
|
<Input.TextArea placeholder={t("dictsExt.typeRemarkPlaceholder")} rows={3} />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Drawer>
|
|
|
|
|
|
2026-04-17 02:08:40 +00:00
|
|
|
<Drawer title={<Space><ProfileOutlined aria-hidden="true" /><span>{editingItem ? t("dicts.drawerTitleItemEdit") : t("dicts.drawerTitleItemCreate")}</span></Space>} open={itemDrawerVisible} onClose={() => setItemDrawerVisible(false)} width={400} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setItemDrawerVisible(false)}>{t("common.cancel")}</Button><Button type="primary" onClick={handleItemSubmit}>{t("common.save")}</Button></div>}>
|
2026-03-17 07:31:09 +00:00
|
|
|
<Form form={itemForm} layout="vertical">
|
|
|
|
|
<Form.Item label={t("dicts.typeCode")} name="typeCode"><Input disabled className="tabular-nums" /></Form.Item>
|
|
|
|
|
<Form.Item label={t("dicts.itemLabel")} name="itemLabel" rules={[{ required: true, message: t("dicts.itemLabel") }]}><Input placeholder={t("dictsExt.itemLabelPlaceholder")} /></Form.Item>
|
|
|
|
|
<Form.Item label={t("dicts.itemValue")} name="itemValue" rules={[{ required: true, message: t("dicts.itemValue") }]}><Input placeholder={t("dictsExt.itemValuePlaceholder")} className="tabular-nums" /></Form.Item>
|
|
|
|
|
<Form.Item label={t("dicts.sort")} name="sortOrder" initialValue={0}><InputNumber className="w-full tabular-nums" /></Form.Item>
|
|
|
|
|
<Form.Item label={t("common.status")} name="status" initialValue={1}><Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} /></Form.Item>
|
|
|
|
|
<Form.Item label={t("common.remark")} name="remark"><Input.TextArea placeholder={t("dictsExt.itemRemarkPlaceholder")} rows={3} /></Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Drawer>
|
2026-05-09 02:17:46 +00:00
|
|
|
</PageContainer>
|
2026-03-17 07:31:09 +00:00
|
|
|
);
|
2026-04-15 09:47:31 +00:00
|
|
|
}
|