/** * 租户管理员管理页面。 * * 功能: * - 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([]); 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(null); const [submitLoading, setSubmitLoading] = useState(false); /* ---- 2 步创建流程状态 ---- */ const [createStep, setCreateStep] = useState(0); const [tenants, setTenants] = useState([]); const [tenantsLoading, setTenantsLoading] = useState(false); const [tenantSites, setTenantSites] = useState([]); const [tenantSitesLoading, setTenantSitesLoading] = useState(false); /** 所有选中租户下的店铺(Step 2 展示用) */ const [allTenantSites, setAllTenantSites] = useState([]); /** Step 2 中每个 siteId 对应的 site_code 输入值 */ const [siteCodes, setSiteCodes] = useState>({}); /* ---- SiteCode 弹窗状态 ---- */ const [siteCodeOpen, setSiteCodeOpen] = useState(false); const [siteCodeRecord, setSiteCodeRecord] = useState(null); const [siteCodeSites, setSiteCodeSites] = useState([]); const [siteCodeSitesLoading, setSiteCodeSitesLoading] = useState(false); const [editingSiteId, setEditingSiteId] = useState(null); const [editingCode, setEditingCode] = useState(""); const [editingSaving, setEditingSaving] = useState(false); const [siteCodeHistory, setSiteCodeHistory] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); const [selectedSiteId, setSelectedSiteId] = useState(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 = {}; 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 codes(Step 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 = [ { 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" ? ( 店铺管理员 ) : ( 租户管理员 ), }, { title: "管辖门店", dataIndex: "managedSiteIds", width: 200, render: (ids: number[]) => ids && ids.length > 0 ? ids.map((id) => ( {id} )) : "-", }, { title: "状态", dataIndex: "isActive", width: 80, render: (v: boolean) => v ? ( 启用 ) : ( 禁用 ), }, { 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) => ( handleDelete(record.id)} okText="确定" cancelText="取消" > ), }, ]; /* ---- 渲染 ---- */ return (
<TeamOutlined style={{ marginRight: 8 }} /> 租户管理员 `共 ${t} 条`, onChange: (p, ps) => { setPage(p); setPageSize(ps); }, }} size="middle" /> {/* ---- 创建 Modal(2 步流程) ---- */} ) : ( ) } destroyOnClose > {/* Step 1: 基本信息 */}
v || `店铺 ${r.siteId}`, }, { title: "当前简写ID", dataIndex: "siteCode", width: 120, render: (v: string | null) => v || "-", }, { title: "设置简写ID", key: "newCode", width: 180, render: (_: unknown, record: SiteItem) => ( setSiteCodes((prev) => ({ ...prev, [record.siteId]: e.target.value.toUpperCase(), })) } /> ), }, ]} /> )} {/* ---- 编辑 Modal ---- */} setEditOpen(false)} confirmLoading={submitLoading} destroyOnClose > )} {/* 店铺表格 */} 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 ? {v} : 未设置, }, { title: "操作", key: "action", width: devMode ? 300 : 240, render: (_: unknown, record: SiteItem) => { if (editingSiteId === record.siteId) { return ( setEditingCode(e.target.value.toUpperCase()) } onPressEnter={() => saveEditCode(record.siteId)} /> ); } return ( {devMode && ( handleDeleteSite(record.id)} okText="确定" cancelText="取消" > )} ); }, }, ]} /> {/* 变更历史区域 */} {selectedSiteId && (
简写ID 变更历史(店铺 {selectedSiteId}) 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 ? ( 当前 ) : ( 已退役 ), }, { 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() : "-", }, ]} />
)}
); }; export default TenantAdmins;