imeeting/frontend/src/pages/Tenants.tsx

332 lines
9.9 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,
Drawer,
Form,
Input,
message,
Popconfirm,
Space,
Table,
Tag,
Typography,
DatePicker,
Row,
Col,
Select
} from "antd";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { createTenant, deleteTenant, listTenants, updateTenant } from "../api";
import { usePermission } from "../hooks/usePermission";
import { useDict } from "../hooks/useDict";
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined,
ReloadOutlined,
ShopOutlined,
CalendarOutlined,
PhoneOutlined,
UserOutlined
} from "@ant-design/icons";
import type { SysTenant } from "../types";
import PageHeader from "../components/shared/PageHeader";
import dayjs from "dayjs";
const { Title, Text } = Typography;
export default function Tenants() {
const { t } = useTranslation();
const { can } = usePermission();
// Dictionaries
const { items: statusDict } = useDict("sys_common_status");
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [data, setData] = useState<SysTenant[]>([]);
const [total, setTotal] = useState(0);
const [params, setParams] = useState({
current: 1,
size: 10,
name: "",
code: ""
});
const [drawerOpen, setDrawerOpen] = useState(false);
const [editing, setEditing] = useState<SysTenant | null>(null);
const [form] = Form.useForm();
const loadData = async (currentParams = params) => {
setLoading(true);
try {
const result = await listTenants(currentParams);
setData(result.records || []);
setTotal(result.total || 0);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, [params.current, params.size]);
const handleSearch = () => {
setParams({ ...params, current: 1 });
loadData({ ...params, current: 1 });
};
const handleReset = () => {
const resetParams = {
current: 1,
size: 10,
name: "",
code: ""
};
setParams(resetParams);
loadData(resetParams);
};
const openCreate = () => {
setEditing(null);
form.resetFields();
form.setFieldsValue({ status: 1 });
setDrawerOpen(true);
};
const openEdit = (record: SysTenant) => {
setEditing(record);
form.setFieldsValue({
...record,
expireTime: record.expireTime ? dayjs(record.expireTime) : null
});
setDrawerOpen(true);
};
const handleDelete = async (id: number) => {
try {
await deleteTenant(id);
message.success(t('common.success'));
loadData();
} catch (e) {
// Handled by interceptor
}
};
const submit = async () => {
try {
const values = await form.validateFields();
setSaving(true);
const payload = {
...values,
expireTime: values.expireTime ? values.expireTime.format("YYYY-MM-DD HH:mm:ss") : null
};
if (editing) {
await updateTenant(editing.id, payload);
message.success(t('common.success'));
} else {
await createTenant(payload);
message.success(t('common.success'));
}
setDrawerOpen(false);
loadData();
} catch (e) {
// Handled by interceptor
} finally {
setSaving(false);
}
};
const columns = [
{
title: t('tenants.tenantInfo'),
key: "tenant",
render: (_: any, record: SysTenant) => (
<Space>
<div style={{ width: 40, height: 40, background: '#f0f5ff', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#1890ff', fontSize: 20 }}>
<ShopOutlined aria-hidden="true" />
</div>
<div>
<div style={{ fontWeight: 600, color: '#262626' }}>{record.tenantName}</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }} className="tabular-nums">{record.tenantCode}</div>
</div>
</Space>
),
},
{
title: t('tenants.contact'),
key: "contact",
render: (_: any, record: SysTenant) => (
<div>
<div><UserOutlined style={{ marginRight: 4, color: '#8c8c8c' }} />{record.contactName || "-"}</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }} className="tabular-nums"><PhoneOutlined style={{ marginRight: 4 }} />{record.contactPhone || "-"}</div>
</div>
)
},
{
title: t('common.status'),
dataIndex: "status",
width: 100,
render: (status: number) => {
const item = statusDict.find(i => i.itemValue === String(status));
return (
<Tag color={status === 1 ? "green" : "red"}>
{item ? item.itemLabel : (status === 1 ? "正常" : "禁用")}
</Tag>
);
},
},
{
title: t('tenants.expireTime'),
dataIndex: "expireTime",
width: 180,
render: (text: string) => (
<Space>
<CalendarOutlined style={{ color: '#8c8c8c' }} />
<Text className="tabular-nums">{text ? text.substring(0, 10) : t('tenants.forever')}</Text>
</Space>
)
},
{
title: t('common.action'),
key: "action",
width: 120,
fixed: "right" as const,
render: (_: any, record: SysTenant) => (
<Space>
{can("sys_tenant:update") && (
<Button
type="text"
icon={<EditOutlined aria-hidden="true" />}
onClick={() => openEdit(record)}
aria-label={t('common.edit')}
/>
)}
{can("sys_tenant:delete") && (
<Popconfirm title={`确定删除租户 "${record.tenantName}" 吗?`} onConfirm={() => handleDelete(record.id)}>
<Button type="text" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={t('common.delete')} />
</Popconfirm>
)}
</Space>
),
},
];
return (
<div className="p-6">
<PageHeader
title={t('tenants.title')}
subtitle={t('tenants.subtitle')}
extra={can("sys_tenant:create") && (
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
{t('tenants.drawerTitleCreate')}
</Button>
)}
/>
<Card className="shadow-sm mb-4">
<Space wrap size="middle">
<Input
placeholder={t('tenants.tenantName')}
prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />}
style={{ width: 200 }}
value={params.name}
onChange={e => setParams({ ...params, name: e.target.value })}
allowClear
/>
<Input
placeholder={t('tenants.tenantCode')}
style={{ width: 180 }}
value={params.code}
onChange={e => setParams({ ...params, code: e.target.value })}
allowClear
/>
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch}>{t('common.search')}</Button>
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={handleReset}>{t('common.reset')}</Button>
</Space>
</Card>
<Card className="shadow-sm" styles={{ body: { padding: 0 } }}>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
size="middle"
pagination={{
current: params.current,
pageSize: params.size,
total: total,
showSizeChanger: true,
onChange: (page, size) => setParams({ ...params, current: page, size }),
showTotal: (total) => t('common.total', { total })
}}
/>
</Card>
<Drawer
title={
<Space>
<ShopOutlined aria-hidden="true" />
<span>{editing ? t('tenants.drawerTitleEdit') : t('tenants.drawerTitleCreate')}</span>
</Space>
}
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
width={480}
destroyOnClose
footer={
<div className="flex justify-end gap-2 p-2">
<Button onClick={() => setDrawerOpen(false)}>{t('common.cancel')}</Button>
<Button type="primary" loading={saving} onClick={submit}>{t('common.confirm')}</Button>
</div>
}
>
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col span={12}>
<Form.Item label={t('tenants.tenantName')} name="tenantName" rules={[{ required: true, message: t('tenants.tenantName') }]}>
<Input placeholder="例如:云合智慧" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label={t('tenants.tenantCode')} name="tenantCode" rules={[{ required: true, message: t('tenants.tenantCode') }]}>
<Input placeholder="例如UNIS" disabled={!!editing} className="tabular-nums" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label={t('tenants.contactName')} name="contactName">
<Input placeholder="姓名" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label={t('tenants.contactPhone')} name="contactPhone">
<Input placeholder="手机或座机" className="tabular-nums" />
</Form.Item>
</Col>
</Row>
<Form.Item label={t('tenants.expireTime')} name="expireTime">
<DatePicker style={{ width: "100%" }} placeholder={t('tenants.forever')} />
</Form.Item>
<Form.Item label={t('common.status')} name="status" initialValue={1}>
<Select options={statusDict.map(i => ({ label: i.itemLabel, value: Number(i.itemValue) }))} />
</Form.Item>
<Form.Item label={t('common.remark')} name="remark">
<Input.TextArea rows={3} placeholder="选填,租户详细背景说明…" />
</Form.Item>
</Form>
</Drawer>
</div>
);
}