Files
Neo-ZQYY/apps/admin-web/src/pages/TenantAdmins/index.tsx
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:03:48 +08:00

1135 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 租户管理员管理页面。
*
* 功能:
* - Ant Design Table 展示管理员列表(分页 + 关键词搜索)
* - 2 步创建流程Step1 基本信息 → Step2 简写ID 设置(可跳过)
* - 编辑 Modal修改用户名 / 显示名称 / managedSiteIds / isActive
* - 重置密码 Modal新密码 + 确认密码
* - 删除Popconfirm 二次确认 → 软删除
* - 显示已禁用开关:控制 include_inactive 参数
* - 简写ID 管理按钮(打开 SiteCode 弹窗Task 12.5 实现)
* - 租户列(显示 tenantName
*/
import React, { useEffect, useState, useCallback } from "react";
import {
Table,
Button,
Input,
Space,
Modal,
Form,
Select,
Switch,
Tag,
message,
Typography,
Popconfirm,
Steps,
Spin,
} from "antd";
import {
PlusOutlined,
EditOutlined,
KeyOutlined,
TeamOutlined,
DeleteOutlined,
BarcodeOutlined,
} from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import {
fetchTenantAdmins,
createTenantAdmin,
editTenantAdmin,
resetTenantAdminPassword,
deleteTenantAdmin,
} from "../../api/tenantAdmins";
import type {
TenantAdminItem,
TenantAdminCreatePayload,
TenantAdminEditPayload,
} from "../../api/tenantAdmins";
import {
fetchTenants,
fetchTenantSites,
updateSiteCode,
fetchSiteCodeHistory,
createSite,
deleteSite,
} from "../../api/registry";
import type { TenantItem, SiteItem, SiteCodeHistoryItem } from "../../api/registry";
const { Title } = Typography;
const { Search } = Input;
/* ------------------------------------------------------------------ */
/* 组件 */
/* ------------------------------------------------------------------ */
const TenantAdmins: React.FC = () => {
/* ---- 列表状态 ---- */
const [data, setData] = useState<TenantAdminItem[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [keyword, setKeyword] = useState("");
const [loading, setLoading] = useState(false);
const [includeInactive, setIncludeInactive] = useState(false);
/* ---- Modal 状态 ---- */
const [createOpen, setCreateOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
const [resetPwdOpen, setResetPwdOpen] = useState(false);
const [currentRecord, setCurrentRecord] = useState<TenantAdminItem | null>(null);
const [submitLoading, setSubmitLoading] = useState(false);
/* ---- 2 步创建流程状态 ---- */
const [createStep, setCreateStep] = useState(0);
const [tenants, setTenants] = useState<TenantItem[]>([]);
const [tenantsLoading, setTenantsLoading] = useState(false);
const [tenantSites, setTenantSites] = useState<SiteItem[]>([]);
const [tenantSitesLoading, setTenantSitesLoading] = useState(false);
/** 所有选中租户下的店铺Step 2 展示用) */
const [allTenantSites, setAllTenantSites] = useState<SiteItem[]>([]);
/** Step 2 中每个 siteId 对应的 site_code 输入值 */
const [siteCodes, setSiteCodes] = useState<Record<number, string>>({});
/* ---- SiteCode 弹窗状态 ---- */
const [siteCodeOpen, setSiteCodeOpen] = useState(false);
const [siteCodeRecord, setSiteCodeRecord] = useState<TenantAdminItem | null>(null);
const [siteCodeSites, setSiteCodeSites] = useState<SiteItem[]>([]);
const [siteCodeSitesLoading, setSiteCodeSitesLoading] = useState(false);
const [editingSiteId, setEditingSiteId] = useState<number | null>(null);
const [editingCode, setEditingCode] = useState("");
const [editingSaving, setEditingSaving] = useState(false);
const [siteCodeHistory, setSiteCodeHistory] = useState<SiteCodeHistoryItem[]>([]);
const [historyLoading, setHistoryLoading] = useState(false);
const [selectedSiteId, setSelectedSiteId] = useState<number | null>(null);
/* ---- 测试模式:添加/删除店铺 ---- */
const [devMode, setDevMode] = useState(false);
const [addSiteForm] = Form.useForm();
const [addSiteLoading, setAddSiteLoading] = useState(false);
const [createForm] = Form.useForm();
const [editForm] = Form.useForm();
const [resetPwdForm] = Form.useForm();
/* ---- 数据加载 ---- */
const loadData = useCallback(async () => {
setLoading(true);
try {
const res = await fetchTenantAdmins({
page,
page_size: pageSize,
keyword: keyword || undefined,
include_inactive: includeInactive || undefined,
});
setData(res.items);
setTotal(res.total);
} catch {
message.error("加载租户管理员列表失败");
} finally {
setLoading(false);
}
}, [page, pageSize, keyword, includeInactive]);
useEffect(() => {
loadData();
}, [loadData]);
/* ---- 搜索 ---- */
const handleSearch = (value: string) => {
setKeyword(value);
setPage(1);
};
/* ---- 创建2 步流程) ---- */
/** 打开创建弹窗时加载租户列表 */
const openCreate = async () => {
createForm.resetFields();
setCreateStep(0);
setTenantSites([]);
setAllTenantSites([]);
setSiteCodes({});
setCreateOpen(true);
setTenantsLoading(true);
try {
const list = await fetchTenants();
setTenants(list);
} catch {
message.error("加载租户列表失败");
} finally {
setTenantsLoading(false);
}
};
/** 租户选择变化时加载该租户下的店铺 */
const handleTenantChange = async (tenantId: number) => {
createForm.setFieldValue("managedSiteIds", []);
setTenantSites([]);
if (!tenantId) return;
setTenantSitesLoading(true);
try {
const sites = await fetchTenantSites(tenantId);
setTenantSites(sites);
} catch {
message.error("加载店铺列表失败");
} finally {
setTenantSitesLoading(false);
}
};
/** Step 1 → Step 2校验表单后进入下一步 */
const handleNextStep = async () => {
try {
await createForm.validateFields();
// 加载选中租户下所有店铺用于 Step 2
const tenantId = createForm.getFieldValue("tenantId");
setTenantSitesLoading(true);
try {
const sites = await fetchTenantSites(tenantId);
setAllTenantSites(sites);
// 预填已有 site_code
const codes: Record<number, string> = {};
for (const s of sites) {
if (s.siteCode) codes[s.siteId] = s.siteCode;
}
setSiteCodes(codes);
} catch {
message.error("加载店铺列表失败");
} finally {
setTenantSitesLoading(false);
}
setCreateStep(1);
} catch {
// 表单校验失败,停留在 Step 1
}
};
/** 仅创建管理员(跳过 Step 2 */
const handleCreateSkip = async () => {
try {
const values = createForm.getFieldsValue();
setSubmitLoading(true);
const payload: TenantAdminCreatePayload = {
username: values.username,
password: values.password,
displayName: values.displayName,
tenantId: Number(values.tenantId),
managedSiteIds: (values.managedSiteIds ?? []).map(Number),
};
await createTenantAdmin(payload);
message.success("创建成功");
setCreateOpen(false);
createForm.resetFields();
setCreateStep(0);
loadData();
} catch (err: any) {
if (err?.response?.status === 409) {
message.error("用户名已存在");
} else {
const detail = err?.response?.data?.detail || err?.message || "未知错误";
message.error(`创建失败: ${detail}`);
}
} finally {
setSubmitLoading(false);
}
};
/** 创建管理员 + 保存 site codesStep 2 保存) */
const handleCreateWithCodes = async () => {
try {
const values = createForm.getFieldsValue();
setSubmitLoading(true);
const payload: TenantAdminCreatePayload = {
username: values.username,
password: values.password,
displayName: values.displayName,
tenantId: Number(values.tenantId),
managedSiteIds: (values.managedSiteIds ?? []).map(Number),
};
await createTenantAdmin(payload);
// 保存有变更的 site codes
const codeEntries = Object.entries(siteCodes).filter(([, code]) => code.trim());
let codeErrors = 0;
for (const [siteIdStr, code] of codeEntries) {
const siteId = Number(siteIdStr);
// 跳过已有相同 code 的店铺
const existing = allTenantSites.find((s) => s.siteId === siteId);
if (existing?.siteCode === code) continue;
try {
await updateSiteCode(siteId, code);
} catch {
codeErrors++;
}
}
if (codeErrors > 0) {
message.warning(`管理员创建成功,但 ${codeErrors} 个简写ID 设置失败`);
} else {
message.success("创建成功");
}
setCreateOpen(false);
createForm.resetFields();
setCreateStep(0);
loadData();
} catch (err: any) {
if (err?.response?.status === 409) {
message.error("用户名已存在");
} else {
const detail = err?.response?.data?.detail || err?.message || "未知错误";
message.error(`创建失败: ${detail}`);
}
} finally {
setSubmitLoading(false);
}
};
/** 关闭创建弹窗 */
const handleCreateCancel = () => {
setCreateOpen(false);
setCreateStep(0);
createForm.resetFields();
};
/* ---- 编辑 ---- */
const openEdit = (record: TenantAdminItem) => {
setCurrentRecord(record);
editForm.setFieldsValue({
username: record.username,
displayName: record.displayName,
managedSiteIds: record.managedSiteIds,
isActive: record.isActive,
});
setEditOpen(true);
};
const handleEdit = async () => {
if (!currentRecord) return;
try {
const values = await editForm.validateFields();
setSubmitLoading(true);
const payload: TenantAdminEditPayload = {
username: values.username,
displayName: values.displayName,
managedSiteIds: values.managedSiteIds,
isActive: values.isActive,
};
await editTenantAdmin(currentRecord.id, payload);
message.success("编辑成功");
setEditOpen(false);
loadData();
} catch (err: any) {
if (err?.response?.status === 409) {
message.error("用户名已存在");
} else if (err?.errorFields) {
// 表单校验失败
} else {
message.error("编辑失败");
}
} finally {
setSubmitLoading(false);
}
};
/* ---- 删除 ---- */
const handleDelete = async (id: number) => {
try {
await deleteTenantAdmin(id);
message.success("已删除");
loadData();
} catch {
message.error("删除失败");
}
};
/* ---- 重置密码 ---- */
const openResetPwd = (record: TenantAdminItem) => {
setCurrentRecord(record);
resetPwdForm.resetFields();
setResetPwdOpen(true);
};
const handleResetPwd = async () => {
if (!currentRecord) return;
try {
const values = await resetPwdForm.validateFields();
setSubmitLoading(true);
await resetTenantAdminPassword(currentRecord.id, {
newPassword: values.newPassword,
});
message.success("密码重置成功");
setResetPwdOpen(false);
} catch (err: any) {
if (err?.errorFields) {
// 表单校验失败
} else {
message.error("密码重置失败");
}
} finally {
setSubmitLoading(false);
}
};
/* ---- 简写ID 管理 ---- */
/** site_code 格式校验3 位字母/数字 + 3 位数字 */
const SITE_CODE_RE = /^[A-Z0-9]{3}\d{3}$/;
const openSiteCode = async (record: TenantAdminItem) => {
setSiteCodeRecord(record);
setSiteCodeOpen(true);
setSiteCodeSites([]);
setSiteCodeHistory([]);
setSelectedSiteId(null);
setEditingSiteId(null);
setEditingCode("");
// 加载该管理员租户下的店铺
setSiteCodeSitesLoading(true);
try {
const sites = await fetchTenantSites(record.tenantId);
// devMode 下显示全部店铺,否则仅展示管辖门店
setSiteCodeSites(devMode
? sites
: sites.filter((s) => record.managedSiteIds.includes(s.siteId)),
);
} catch {
message.error("加载店铺列表失败");
} finally {
setSiteCodeSitesLoading(false);
}
};
/** 加载某个店铺的简写ID 变更历史 */
const loadSiteCodeHistory = async (siteId: number) => {
setSelectedSiteId(siteId);
setHistoryLoading(true);
try {
const list = await fetchSiteCodeHistory(siteId);
setSiteCodeHistory(list);
} catch {
message.error("加载变更历史失败");
} finally {
setHistoryLoading(false);
}
};
/** 开始编辑某行 */
const startEditCode = (siteId: number, currentCode: string | null) => {
setEditingSiteId(siteId);
setEditingCode(currentCode ?? "");
};
/** 取消编辑 */
const cancelEditCode = () => {
setEditingSiteId(null);
setEditingCode("");
};
/** 保存简写ID */
const saveEditCode = async (siteId: number) => {
const code = editingCode.toUpperCase().trim();
if (!SITE_CODE_RE.test(code)) {
message.error("简写ID 格式错误,需 6 位(前 3 位字母/数字 + 后 3 位数字,如 LLQ001");
return;
}
setEditingSaving(true);
try {
await updateSiteCode(siteId, code);
message.success("简写ID 修改成功");
cancelEditCode();
// 刷新店铺列表
if (siteCodeRecord) {
const sites = await fetchTenantSites(siteCodeRecord.tenantId);
const managed = sites.filter((s) =>
siteCodeRecord.managedSiteIds.includes(s.siteId),
);
setSiteCodeSites(managed);
}
// 刷新历史
await loadSiteCodeHistory(siteId);
} catch (err: any) {
if (err?.response?.status === 409) {
message.error("简写ID 已被占用");
} else {
message.error("简写ID 修改失败");
}
} finally {
setEditingSaving(false);
}
};
/* ---- 测试模式:添加/删除店铺处理函数 ---- */
const handleAddSite = async () => {
try {
const values = await addSiteForm.validateFields();
setAddSiteLoading(true);
await createSite({
tenantId: siteCodeRecord!.tenantId,
siteId: Number(values.siteId),
siteName: values.siteName,
siteCode: values.siteCode || undefined,
});
message.success("店铺创建成功");
addSiteForm.resetFields();
// 刷新店铺列表(测试模式下显示全部)
const sites = await fetchTenantSites(siteCodeRecord!.tenantId);
setSiteCodeSites(devMode
? sites
: sites.filter((s) => siteCodeRecord!.managedSiteIds.includes(s.siteId)),
);
} catch (err: any) {
if (err?.response?.status === 409) {
message.error(err.response.data?.detail || "唯一约束冲突");
} else if (!err?.errorFields) {
message.error("创建失败");
}
} finally {
setAddSiteLoading(false);
}
};
const handleDeleteSite = async (id: number) => {
try {
await deleteSite(id);
message.success("店铺已删除");
// 刷新店铺列表(测试模式下显示全部)
if (siteCodeRecord) {
const sites = await fetchTenantSites(siteCodeRecord.tenantId);
setSiteCodeSites(devMode
? sites
: sites.filter((s) => siteCodeRecord.managedSiteIds.includes(s.siteId)),
);
}
} catch {
message.error("删除失败");
}
};
/* ---- 表格列定义 ---- */
const columns: ColumnsType<TenantAdminItem> = [
{
title: "用户名",
dataIndex: "username",
width: 140,
},
{
title: "显示名称",
dataIndex: "displayName",
width: 140,
render: (v: string | null) => v || "-",
},
{
title: "租户",
dataIndex: "tenantName",
width: 140,
render: (v: string | null) => v || "-",
},
{
title: "类型",
dataIndex: "adminType",
width: 100,
render: (v: string) =>
v === "site_admin" ? (
<Tag color="orange"></Tag>
) : (
<Tag color="blue"></Tag>
),
},
{
title: "管辖门店",
dataIndex: "managedSiteIds",
width: 200,
render: (ids: number[]) =>
ids && ids.length > 0
? ids.map((id) => (
<Tag key={id} color="blue">
{id}
</Tag>
))
: "-",
},
{
title: "状态",
dataIndex: "isActive",
width: 80,
render: (v: boolean) =>
v ? (
<Tag color="success"></Tag>
) : (
<Tag color="error"></Tag>
),
},
{
title: "创建时间",
dataIndex: "createdAt",
width: 170,
render: (v: string) => (v ? new Date(v).toLocaleString() : "-"),
},
{
title: "最后登录",
dataIndex: "lastLoginAt",
width: 170,
render: (v: string | null) =>
v ? new Date(v).toLocaleString() : "从未登录",
},
{
title: "操作",
key: "actions",
width: 280,
render: (_: unknown, record: TenantAdminItem) => (
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
<Button
type="link"
size="small"
icon={<KeyOutlined />}
onClick={() => openResetPwd(record)}
>
</Button>
<Button
type="link"
size="small"
icon={<BarcodeOutlined />}
onClick={() => openSiteCode(record)}
>
ID
</Button>
<Popconfirm
title="确定要删除此管理员吗?"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
),
},
];
/* ---- 渲染 ---- */
return (
<div>
<Title level={4} style={{ marginBottom: 16 }}>
<TeamOutlined style={{ marginRight: 8 }} />
</Title>
<Space style={{ marginBottom: 16 }}>
<Search
placeholder="搜索用户名或显示名称"
allowClear
onSearch={handleSearch}
style={{ width: 280 }}
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={openCreate}
>
</Button>
<Switch
checked={includeInactive}
onChange={setIncludeInactive}
checkedChildren="显示已禁用"
unCheckedChildren="显示已禁用"
/>
</Space>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
showTotal: (t) => `${t}`,
onChange: (p, ps) => {
setPage(p);
setPageSize(ps);
},
}}
size="middle"
/>
{/* ---- 创建 Modal2 步流程) ---- */}
<Modal
title="创建租户管理员"
open={createOpen}
onCancel={handleCreateCancel}
width={640}
footer={
createStep === 0 ? (
<Space>
<Button onClick={handleCreateCancel}></Button>
<Button type="primary" onClick={handleNextStep}>
</Button>
</Space>
) : (
<Space>
<Button onClick={() => setCreateStep(0)}></Button>
<Button onClick={handleCreateSkip} loading={submitLoading}>
</Button>
<Button
type="primary"
onClick={handleCreateWithCodes}
loading={submitLoading}
>
</Button>
</Space>
)
}
destroyOnClose
>
<Steps
current={createStep}
size="small"
style={{ marginBottom: 24 }}
items={[
{ title: "基本信息" },
{ title: "简写ID 设置" },
]}
/>
{/* Step 1: 基本信息 */}
<div style={{ display: createStep === 0 ? "block" : "none" }}>
<Form form={createForm} layout="vertical">
<Form.Item
name="tenantId"
label="租户"
rules={[{ required: true, message: "请选择租户" }]}
>
<Select
placeholder="选择所属租户"
loading={tenantsLoading}
onChange={handleTenantChange}
showSearch
optionFilterProp="label"
options={tenants.map((t) => ({
label: t.tenantName,
value: t.tenantId,
}))}
/>
</Form.Item>
<Form.Item
name="username"
label="用户名"
rules={[{ required: true, message: "请输入用户名" }]}
>
<Input placeholder="登录用户名" />
</Form.Item>
<Form.Item
name="password"
label="初始密码"
rules={[
{ required: true, message: "请输入初始密码" },
{ min: 6, message: "密码至少 6 位" },
]}
>
<Input.Password placeholder="初始密码" />
</Form.Item>
<Form.Item
name="displayName"
label="显示名称"
rules={[{ required: true, message: "请输入显示名称" }]}
>
<Input placeholder="管理员显示名称" />
</Form.Item>
<Form.Item
name="managedSiteIds"
label="管辖门店"
rules={[{ required: true, message: "请选择管辖门店" }]}
>
<Select
mode="multiple"
placeholder={tenantSitesLoading ? "加载中..." : "选择管辖门店"}
loading={tenantSitesLoading}
optionFilterProp="label"
options={tenantSites.map((s) => ({
label: s.siteName || `店铺 ${s.siteId}`,
value: s.siteId,
}))}
/>
</Form.Item>
</Form>
</div>
{/* Step 2: 简写ID 设置 */}
{createStep === 1 && (
<Spin spinning={tenantSitesLoading}>
<p style={{ marginBottom: 12, color: "#666" }}>
ID6 3+3 LLQ001
</p>
<Table
rowKey="siteId"
dataSource={allTenantSites}
size="small"
pagination={false}
columns={[
{
title: "店铺名称",
dataIndex: "siteName",
width: 200,
render: (v: string | null, r: SiteItem) =>
v || `店铺 ${r.siteId}`,
},
{
title: "当前简写ID",
dataIndex: "siteCode",
width: 120,
render: (v: string | null) => v || "-",
},
{
title: "设置简写ID",
key: "newCode",
width: 180,
render: (_: unknown, record: SiteItem) => (
<Input
size="small"
placeholder="如 LLQ001"
maxLength={6}
value={siteCodes[record.siteId] ?? ""}
onChange={(e) =>
setSiteCodes((prev) => ({
...prev,
[record.siteId]: e.target.value.toUpperCase(),
}))
}
/>
),
},
]}
/>
</Spin>
)}
</Modal>
{/* ---- 编辑 Modal ---- */}
<Modal
title={`编辑管理员 — ${currentRecord?.username ?? ""}`}
open={editOpen}
onOk={handleEdit}
onCancel={() => setEditOpen(false)}
confirmLoading={submitLoading}
destroyOnClose
>
<Form form={editForm} layout="vertical">
<Form.Item
name="username"
label="用户名"
rules={[{ required: true, message: "请输入用户名" }]}
>
<Input placeholder="登录用户名" />
</Form.Item>
<Form.Item
name="displayName"
label="显示名称"
rules={[{ required: true, message: "请输入显示名称" }]}
>
<Input />
</Form.Item>
<Form.Item
name="managedSiteIds"
label="管辖门店"
rules={[{ required: true, message: "请选择管辖门店" }]}
>
<Select
mode="tags"
placeholder="输入门店 ID 后回车"
tokenSeparators={[","]}
/>
</Form.Item>
<Form.Item
name="isActive"
label="账号状态"
valuePropName="checked"
>
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
</Form.Item>
</Form>
</Modal>
{/* ---- 重置密码 Modal ---- */}
<Modal
title={`重置密码 — ${currentRecord?.username ?? ""}`}
open={resetPwdOpen}
onOk={handleResetPwd}
onCancel={() => setResetPwdOpen(false)}
confirmLoading={submitLoading}
destroyOnClose
>
<Form form={resetPwdForm} layout="vertical">
<Form.Item
name="newPassword"
label="新密码"
rules={[
{ required: true, message: "请输入新密码" },
{ min: 6, message: "密码至少 6 位" },
]}
>
<Input.Password placeholder="新密码" />
</Form.Item>
<Form.Item
name="confirmPassword"
label="确认密码"
dependencies={["newPassword"]}
rules={[
{ required: true, message: "请确认密码" },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue("newPassword") === value) {
return Promise.resolve();
}
return Promise.reject(new Error("两次输入的密码不一致"));
},
}),
]}
>
<Input.Password placeholder="再次输入新密码" />
</Form.Item>
</Form>
</Modal>
{/* ---- 简写ID 管理 Modal ---- */}
<Modal
title={`简写ID 管理 — ${siteCodeRecord?.username ?? ""}`}
open={siteCodeOpen}
onCancel={() => {
setSiteCodeOpen(false);
cancelEditCode();
setDevMode(false);
addSiteForm.resetFields();
}}
footer={null}
destroyOnClose
width={640}
>
{/* 测试模式开关 */}
<div style={{ marginBottom: 12, display: "flex", alignItems: "center", gap: 8 }}>
<Switch
checked={devMode}
onChange={async (checked) => {
setDevMode(checked);
// 切换模式时刷新列表
if (siteCodeRecord) {
setSiteCodeSitesLoading(true);
try {
const sites = await fetchTenantSites(siteCodeRecord.tenantId);
setSiteCodeSites(checked
? sites
: sites.filter((s) => siteCodeRecord.managedSiteIds.includes(s.siteId)),
);
} catch {
message.error("加载店铺列表失败");
} finally {
setSiteCodeSitesLoading(false);
}
}
}}
size="small"
/>
<span style={{ color: devMode ? "#fa8c16" : "#999", fontSize: 13 }}>
{devMode ? "(已开启)" : ""}
</span>
</div>
{/* 测试模式:添加店铺表单 */}
{devMode && (
<div style={{ marginBottom: 16, padding: 12, background: "#fffbe6", borderRadius: 6, border: "1px solid #ffe58f" }}>
<Typography.Text strong style={{ display: "block", marginBottom: 8, color: "#fa8c16" }}>
</Typography.Text>
<Form form={addSiteForm} layout="inline" size="small">
<Form.Item name="siteId" rules={[{ required: true, message: "必填" }]}>
<Input placeholder="上游 siteId" style={{ width: 130 }} />
</Form.Item>
<Form.Item name="siteName" rules={[{ required: true, message: "必填" }]}>
<Input placeholder="店铺名称" style={{ width: 130 }} />
</Form.Item>
<Form.Item name="siteCode">
<Input placeholder="简写ID可选" maxLength={6} style={{ width: 120 }} />
</Form.Item>
<Form.Item>
<Button type="primary" loading={addSiteLoading} onClick={handleAddSite}>
</Button>
</Form.Item>
</Form>
</div>
)}
{/* 店铺表格 */}
<Table<SiteItem>
rowKey="siteId"
dataSource={siteCodeSites}
loading={siteCodeSitesLoading}
size="small"
pagination={false}
columns={[
{
title: "店铺名称",
dataIndex: "siteName",
width: 180,
render: (v: string | null, r: SiteItem) =>
v || `店铺 ${r.siteId}`,
},
{
title: "当前简写ID",
dataIndex: "siteCode",
width: 120,
render: (v: string | null) =>
v ? <Tag color="blue">{v}</Tag> : <span style={{ color: "#999" }}></span>,
},
{
title: "操作",
key: "action",
width: devMode ? 300 : 240,
render: (_: unknown, record: SiteItem) => {
if (editingSiteId === record.siteId) {
return (
<Space size="small">
<Input
size="small"
placeholder="如 LLQ001"
maxLength={6}
style={{ width: 100 }}
value={editingCode}
onChange={(e) =>
setEditingCode(e.target.value.toUpperCase())
}
onPressEnter={() => saveEditCode(record.siteId)}
/>
<Button
type="primary"
size="small"
loading={editingSaving}
onClick={() => saveEditCode(record.siteId)}
>
</Button>
<Button size="small" onClick={cancelEditCode}>
</Button>
</Space>
);
}
return (
<Space size="small">
<Button
type="link"
size="small"
onClick={() =>
startEditCode(record.siteId, record.siteCode)
}
>
</Button>
<Button
type="link"
size="small"
onClick={() => loadSiteCodeHistory(record.siteId)}
>
</Button>
{devMode && (
<Popconfirm
title="确定删除此店铺?(硬删除,不可恢复)"
onConfirm={() => handleDeleteSite(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="link" size="small" danger>
</Button>
</Popconfirm>
)}
</Space>
);
},
},
]}
/>
{/* 变更历史区域 */}
{selectedSiteId && (
<div style={{ marginTop: 16 }}>
<Typography.Text strong style={{ display: "block", marginBottom: 8 }}>
ID {selectedSiteId}
</Typography.Text>
<Table<SiteCodeHistoryItem>
rowKey="id"
dataSource={siteCodeHistory}
loading={historyLoading}
size="small"
pagination={false}
columns={[
{
title: "简写ID",
dataIndex: "siteCode",
width: 100,
},
{
title: "状态",
dataIndex: "isCurrent",
width: 80,
render: (v: boolean) =>
v ? (
<Tag color="success"></Tag>
) : (
<Tag color="default">退</Tag>
),
},
{
title: "创建时间",
dataIndex: "createdAt",
width: 160,
render: (v: string) =>
v ? new Date(v).toLocaleString() : "-",
},
{
title: "退役时间",
dataIndex: "retiredAt",
width: 160,
render: (v: string | null) =>
v ? new Date(v).toLocaleString() : "-",
},
]}
/>
</div>
)}
</Modal>
</div>
);
};
export default TenantAdmins;