imeeting/frontend/src/pages/Tenants.tsx

324 lines
9.6 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, useMemo } from "react";
import { createTenant, deleteTenant, listTenants, updateTenant } from "../api";
import { usePermission } from "../hooks/usePermission";
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined,
ReloadOutlined,
ShopOutlined,
CalendarOutlined,
PhoneOutlined,
UserOutlined
} from "@ant-design/icons";
import type { SysTenant } from "../types";
import dayjs from "dayjs";
const { Title, Text } = Typography;
export default function Tenants() {
const { can } = usePermission();
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);
} catch (e) {
message.error("加载租户列表失败");
} 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("租户已删除");
loadData();
} catch (e) {
message.error("删除失败");
}
};
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("租户信息已更新");
} else {
await createTenant(payload);
message.success("租户已成功创建");
}
setDrawerOpen(false);
loadData();
} catch (e) {
if (e instanceof Error && e.message) message.error(e.message);
} finally {
setSaving(false);
}
};
const columns = [
{
title: "租户信息",
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: "联系人",
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: "状态",
dataIndex: "status",
width: 100,
render: (status: number) => (
<Tag color={status === 1 ? "green" : "red"}>
{status === 1 ? "正常" : "禁用"}
</Tag>
),
},
{
title: "过期时间",
dataIndex: "expireTime",
width: 180,
render: (text: string) => (
<Space>
<CalendarOutlined style={{ color: '#8c8c8c' }} />
<Text className="tabular-nums">{text ? text.substring(0, 10) : "永久有效"}</Text>
</Space>
)
},
{
title: "操作",
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={`编辑租户 ${record.tenantName}`}
/>
)}
{can("sys_tenant:delete") && (
<Popconfirm title={`确定删除租户 "${record.tenantName}" 吗?`} onConfirm={() => handleDelete(record.id)}>
<Button type="text" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={`删除租户 ${record.tenantName}`} />
</Popconfirm>
)}
</Space>
),
},
];
return (
<div className="p-6">
<div className="mb-6 flex justify-between items-end">
<div>
<Title level={4} className="mb-1"></Title>
<Text type="secondary"></Text>
</div>
{can("sys_tenant:create") && (
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
</Button>
)}
</div>
<Card className="shadow-sm mb-4">
<Space wrap size="middle">
<Input
placeholder="租户名称…"
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="租户编码…"
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}></Button>
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={handleReset}></Button>
</Space>
</Card>
<Card className="shadow-sm" styles={{ body: { padding: 0 } }}>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
pagination={{
current: params.current,
pageSize: params.size,
total: total,
showSizeChanger: true,
onChange: (page, size) => setParams({ ...params, current: page, size }),
showTotal: (total) => `${total} 条数据`
}}
/>
</Card>
<Drawer
title={
<Space>
<ShopOutlined aria-hidden="true" />
<span>{editing ? "编辑租户信息" : "创建新租户"}</span>
</Space>
}
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
width={480}
destroyOnClose
footer={
<div className="flex justify-end gap-2 p-2">
<Button onClick={() => setDrawerOpen(false)}></Button>
<Button type="primary" loading={saving} onClick={submit}></Button>
</div>
}
>
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col span={12}>
<Form.Item label="租户名称" name="tenantName" rules={[{ required: true, message: "请输入租户名称" }]}>
<Input placeholder="例如:云合智慧" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="租户编码" name="tenantCode" rules={[{ required: true, message: "请输入租户编码" }]}>
<Input placeholder="例如UNIS" disabled={!!editing} className="tabular-nums" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="联系人姓名" name="contactName">
<Input placeholder="姓名" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="联系电话" name="contactPhone">
<Input placeholder="手机或座机" className="tabular-nums" />
</Form.Item>
</Col>
</Row>
<Form.Item label="过期时间" name="expireTime">
<DatePicker style={{ width: "100%" }} placeholder="留空为永久有效" />
</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 rows={3} placeholder="选填,租户详细背景说明…" />
</Form.Item>
</Form>
</Drawer>
</div>
);
}