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>
This commit is contained in:
351
apps/tenant-admin/src/pages/SiteAdmins/index.tsx
Normal file
351
apps/tenant-admin/src/pages/SiteAdmins/index.tsx
Normal file
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* 店铺管理员管理页面。
|
||||
*
|
||||
* 功能:列表(分页+搜索)、创建、编辑、重置密码、删除。
|
||||
* 仅 admin_type='tenant_admin' 可访问(后端 + 前端双重守卫)。
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Table, Button, Input, Space, Modal, Form, Select, Tag, Switch,
|
||||
message, Popconfirm, Card,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined, SearchOutlined, ReloadOutlined, LockOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { tenantApi } from "../../services/api";
|
||||
import { useAuth } from "../../hooks/useAuth";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 类型 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
interface SiteAdminItem {
|
||||
id: number;
|
||||
username: string;
|
||||
displayName: string | null;
|
||||
managedSiteIds: number[];
|
||||
isActive: boolean;
|
||||
createdAt: string | null;
|
||||
lastLoginAt: string | null;
|
||||
}
|
||||
|
||||
interface SiteOption {
|
||||
siteId: number;
|
||||
siteName: string;
|
||||
siteCode: string;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 主组件 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const SiteAdmins: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
/* ---- 状态 ---- */
|
||||
const [items, setItems] = useState<SiteAdminItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(20);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sites, setSites] = useState<SiteOption[]>([]);
|
||||
|
||||
// Modal 状态
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [resetPwdOpen, setResetPwdOpen] = useState(false);
|
||||
const [currentAdmin, setCurrentAdmin] = useState<SiteAdminItem | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const [createForm] = Form.useForm();
|
||||
const [editForm] = Form.useForm();
|
||||
const [resetPwdForm] = Form.useForm();
|
||||
|
||||
/* ---- 加载管辖店铺列表 ---- */
|
||||
const loadSites = useCallback(async () => {
|
||||
try {
|
||||
const { data } = await tenantApi.get<SiteOption[]>("/my-sites");
|
||||
setSites(Array.isArray(data) ? data : []);
|
||||
} catch {
|
||||
/* 静默 */
|
||||
}
|
||||
}, []);
|
||||
|
||||
/* ---- 加载列表 ---- */
|
||||
const loadList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await tenantApi.get("/site-admins", {
|
||||
params: { page, page_size: pageSize, keyword: keyword || undefined },
|
||||
});
|
||||
setItems(data.items ?? []);
|
||||
setTotal(data.total ?? 0);
|
||||
} catch {
|
||||
message.error("加载店铺管理员列表失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, pageSize, keyword]);
|
||||
|
||||
useEffect(() => { loadSites(); }, [loadSites]);
|
||||
useEffect(() => { loadList(); }, [loadList]);
|
||||
|
||||
/* ---- 创建 ---- */
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
const values = await createForm.validateFields();
|
||||
setSubmitting(true);
|
||||
await tenantApi.post("/site-admins", {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
displayName: values.displayName || null,
|
||||
managedSiteIds: values.managedSiteIds,
|
||||
});
|
||||
message.success("创建成功");
|
||||
setCreateOpen(false);
|
||||
createForm.resetFields();
|
||||
loadList();
|
||||
} catch (err: any) {
|
||||
if (err?.response?.data?.detail) {
|
||||
message.error(err.response.data.detail);
|
||||
}
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
/* ---- 编辑 ---- */
|
||||
const openEdit = (record: SiteAdminItem) => {
|
||||
setCurrentAdmin(record);
|
||||
editForm.setFieldsValue({
|
||||
displayName: record.displayName,
|
||||
managedSiteIds: record.managedSiteIds,
|
||||
isActive: record.isActive,
|
||||
});
|
||||
setEditOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = async () => {
|
||||
if (!currentAdmin) return;
|
||||
try {
|
||||
const values = await editForm.validateFields();
|
||||
setSubmitting(true);
|
||||
await tenantApi.patch(`/site-admins/${currentAdmin.id}`, {
|
||||
displayName: values.displayName || null,
|
||||
managedSiteIds: values.managedSiteIds,
|
||||
isActive: values.isActive,
|
||||
});
|
||||
message.success("修改成功");
|
||||
setEditOpen(false);
|
||||
loadList();
|
||||
} catch (err: any) {
|
||||
if (err?.response?.data?.detail) {
|
||||
message.error(err.response.data.detail);
|
||||
}
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
/* ---- 重置密码 ---- */
|
||||
const openResetPwd = (record: SiteAdminItem) => {
|
||||
setCurrentAdmin(record);
|
||||
resetPwdForm.resetFields();
|
||||
setResetPwdOpen(true);
|
||||
};
|
||||
|
||||
const handleResetPwd = async () => {
|
||||
if (!currentAdmin) return;
|
||||
try {
|
||||
const values = await resetPwdForm.validateFields();
|
||||
setSubmitting(true);
|
||||
await tenantApi.post(`/site-admins/${currentAdmin.id}/reset-password`, {
|
||||
newPassword: values.newPassword,
|
||||
});
|
||||
message.success("密码已重置");
|
||||
setResetPwdOpen(false);
|
||||
} catch (err: any) {
|
||||
if (err?.response?.data?.detail) {
|
||||
message.error(err.response.data.detail);
|
||||
}
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
/* ---- 删除 ---- */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await tenantApi.delete(`/site-admins/${id}`);
|
||||
message.success("已删除");
|
||||
loadList();
|
||||
} catch {
|
||||
message.error("删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
/* ---- 店铺名称映射 ---- */
|
||||
const siteMap = new Map(sites.map((s) => [s.siteId, s]));
|
||||
|
||||
/* ---- 表格列 ---- */
|
||||
const columns: ColumnsType<SiteAdminItem> = [
|
||||
{ title: "用户名", dataIndex: "username", width: 160 },
|
||||
{ title: "显示名称", dataIndex: "displayName", width: 120 },
|
||||
{
|
||||
title: "管辖门店", dataIndex: "managedSiteIds", width: 200,
|
||||
render: (ids: number[]) =>
|
||||
ids.map((id) => {
|
||||
const s = siteMap.get(id);
|
||||
return <Tag key={id}>{s ? `${s.siteCode} ${s.siteName}` : `#${id}`}</Tag>;
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: "状态", dataIndex: "isActive", width: 80,
|
||||
render: (v: boolean) => v ? <Tag color="green">启用</Tag> : <Tag color="red">禁用</Tag>,
|
||||
},
|
||||
{ title: "创建时间", dataIndex: "createdAt", width: 170 },
|
||||
{ title: "最后登录", dataIndex: "lastLoginAt", width: 170 },
|
||||
{
|
||||
title: "操作", width: 220, fixed: "right",
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Button size="small" onClick={() => openEdit(record)}>编辑</Button>
|
||||
<Button size="small" icon={<LockOutlined />} onClick={() => openResetPwd(record)}>重置密码</Button>
|
||||
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button size="small" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
/* ---- 权限守卫 ---- */
|
||||
if (user?.adminType !== "tenant_admin") {
|
||||
return <Card><p>仅租户管理员可管理店铺管理员</p></Card>;
|
||||
}
|
||||
|
||||
/* ---- 用户名前缀提示 ---- */
|
||||
const siteCodePrefix = sites.length > 0 ? sites[0].siteCode : "";
|
||||
|
||||
return (
|
||||
<Card title="店铺管理员管理">
|
||||
{/* 工具栏 */}
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="搜索用户名/名称"
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
value={keyword}
|
||||
onChange={(e) => { setKeyword(e.target.value); setPage(1); }}
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadList}>刷新</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>
|
||||
新建店铺管理员
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{/* 表格 */}
|
||||
<Table<SiteAdminItem>
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
loading={loading}
|
||||
scroll={{ x: 1100 }}
|
||||
pagination={{
|
||||
current: page, pageSize, total, showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (p) => setPage(p),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 创建 Modal */}
|
||||
<Modal
|
||||
title="新建店铺管理员"
|
||||
open={createOpen}
|
||||
onCancel={() => setCreateOpen(false)}
|
||||
onOk={handleCreate}
|
||||
confirmLoading={submitting}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={createForm} layout="vertical" preserve={false}>
|
||||
<Form.Item
|
||||
name="managedSiteIds" label="管辖门店"
|
||||
rules={[{ required: true, message: "请选择至少一个门店" }]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple" placeholder="选择门店"
|
||||
options={sites.map((s) => ({ value: s.siteId, label: `${s.siteCode} ${s.siteName}` }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="username" label="用户名"
|
||||
rules={[{ required: true, message: "请输入用户名" }]}
|
||||
extra={siteCodePrefix ? `用户名须以 ${siteCodePrefix} 开头` : ""}
|
||||
>
|
||||
<Input maxLength={56} placeholder={siteCodePrefix ? `${siteCodePrefix}xxx` : "用户名"} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password" label="初始密码"
|
||||
rules={[{ required: true, message: "请输入密码" }, { min: 6, message: "至少 6 位" }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item name="displayName" label="显示名称">
|
||||
<Input maxLength={100} placeholder="可选" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑 Modal */}
|
||||
<Modal
|
||||
title={`编辑 — ${currentAdmin?.username ?? ""}`}
|
||||
open={editOpen}
|
||||
onCancel={() => setEditOpen(false)}
|
||||
onOk={handleEdit}
|
||||
confirmLoading={submitting}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={editForm} layout="vertical" preserve={false}>
|
||||
<Form.Item name="displayName" label="显示名称">
|
||||
<Input maxLength={100} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="managedSiteIds" label="管辖门店"
|
||||
rules={[{ required: true, message: "请选择至少一个门店" }]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple" placeholder="选择门店"
|
||||
options={sites.map((s) => ({ value: s.siteId, label: `${s.siteCode} ${s.siteName}` }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="isActive" label="启用状态" valuePropName="checked">
|
||||
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 重置密码 Modal */}
|
||||
<Modal
|
||||
title={`重置密码 — ${currentAdmin?.username ?? ""}`}
|
||||
open={resetPwdOpen}
|
||||
onCancel={() => setResetPwdOpen(false)}
|
||||
onOk={handleResetPwd}
|
||||
confirmLoading={submitting}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={resetPwdForm} layout="vertical" preserve={false}>
|
||||
<Form.Item
|
||||
name="newPassword" label="新密码"
|
||||
rules={[{ required: true, message: "请输入新密码" }, { min: 6, message: "至少 6 位" }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteAdmins;
|
||||
Reference in New Issue
Block a user