imeeting/frontend/src/pages/system/dictionaries/index.tsx

281 lines
14 KiB
TypeScript
Raw Normal View History

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";
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 (
<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">
<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}
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>
<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") }]}>
<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") }]}>
<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>
<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>
</PageContainer>
2026-03-17 07:31:09 +00:00
);
}