imeeting/frontend/src/pages/Dictionaries.tsx

429 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 { useTranslation } from "react-i18next";
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 { t } = useTranslation();
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(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();
};
// Item Actions
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 (
<div className="dictionaries-page p-6">
<div className="dictionaries-header mb-6">
<div>
<Title level={4} className="mb-1">{t('dicts.title')}</Title>
<Text type="secondary">{t('dicts.subtitle')}</Text>
</div>
</div>
<Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}>
<Col span={8} style={{ height: '100%' }}>
<Card
title={
<Space>
<BookOutlined aria-hidden="true" />
<span>{t('dicts.dictType')}</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}
>
{t('common.create')}
</Button>
)
}
>
<div style={{ height: 'calc(100% - 10px)', overflowY: 'auto' }}>
<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 flex justify-between items-center p-2">
<div className="min-w-0 flex-1">
<div className="dict-type-name font-medium truncate">{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" />}
onClick={(e) => {
e.stopPropagation();
handleEditType(record);
}}
/>
)}
{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()}
/>
</Popconfirm>
)}
</div>
</div>
)
}
]}
/>
</div>
</Card>
</Col>
<Col span={16} style={{ height: '100%' }}>
<Card
title={
<Space>
<ProfileOutlined aria-hidden="true" />
<span>{t('dicts.dictItem')}{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}
>
{t('dicts.drawerTitleItemCreate')}
</Button>
)
}
>
{selectedType ? (
<div style={{ height: 'calc(100% - 10px)', overflowY: 'auto' }}>
<Table
rowKey="dictItemId"
loading={loadingItems}
dataSource={items}
pagination={false}
size="middle"
columns={[
{
title: t('dicts.itemLabel'),
dataIndex: "itemLabel",
render: (text) => <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: (v) => (
<Tag color={v === 1 ? "green" : "red"}>
{v === 1 ? "启用" : "禁用"}
</Tag>
)
},
{
title: t('common.action'),
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={t('common.edit')}
/>
)}
{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={t('common.delete')}
/>
</Popconfirm>
)}
</Space>
)
}
]}
/>
</div>
) : (
<div className="flex items-center justify-center h-full">
<Empty description={t('dicts.selectType')} />
</div>
)}
</Card>
</Col>
</Row>
{/* Type Drawer */}
<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="flex justify-end gap-2 p-2">
<Button onClick={() => setTypeDrawerVisible(false)}>{t('common.cancel')}</Button>
<Button type="primary" onClick={handleTypeSubmit}>{t('common.confirm')}</Button>
</div>
}
>
<Form form={typeForm} layout="vertical">
<Form.Item label={t('dicts.typeCode')} name="typeCode" rules={[{ required: true, message: t('dicts.typeCode') }]}>
<Input disabled={!!editingType} placeholder="例如user_status…" />
</Form.Item>
<Form.Item label={t('dicts.typeName')} name="typeName" rules={[{ required: true, message: t('dicts.typeName') }]}>
<Input placeholder="例如:用户状态…" />
</Form.Item>
<Form.Item label={t('common.remark')} name="remark">
<Input.TextArea placeholder="该字典类型的用途描述…" rows={3} />
</Form.Item>
</Form>
</Drawer>
{/* Item 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="flex justify-end gap-2 p-2">
<Button onClick={() => setItemDrawerVisible(false)}>{t('common.cancel')}</Button>
<Button type="primary" onClick={handleItemSubmit}>{t('common.confirm')}</Button>
</div>
}
>
<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="例如:正常、禁用…" />
</Form.Item>
<Form.Item label={t('dicts.itemValue')} name="itemValue" rules={[{ required: true, message: t('dicts.itemValue') }]}>
<Input placeholder="例如1、0…" 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={[
{ label: "启用", value: 1 },
{ label: "禁用", value: 0 }
]}
/>
</Form.Item>
<Form.Item label={t('common.remark')} name="remark">
<Input.TextArea placeholder="可选项,备注详细信息…" rows={3} />
</Form.Item>
</Form>
</Drawer>
</div>
);
}