2026-06-11 09:22:57 +00:00
|
|
|
|
import { ReloadOutlined, SearchOutlined } from "@ant-design/icons";
|
|
|
|
|
|
import { getCurrentUser } from "@/api";
|
|
|
|
|
|
import AppPagination from "@/components/shared/AppPagination";
|
2026-06-29 02:17:30 +00:00
|
|
|
|
import DataListPanel from "@/components/shared/DataListPanel";
|
2026-06-11 09:22:57 +00:00
|
|
|
|
import ListTable from "@/components/shared/ListTable/ListTable";
|
|
|
|
|
|
import PageContainer from "@/components/shared/PageContainer";
|
2026-06-29 02:17:30 +00:00
|
|
|
|
import SectionCard from "@/components/shared/SectionCard";
|
2026-06-15 08:15:36 +00:00
|
|
|
|
import {usePermission} from "@/hooks/usePermission";
|
2026-06-11 09:22:57 +00:00
|
|
|
|
import {
|
|
|
|
|
|
getCurrentTenantMeetingPointsSetting,
|
|
|
|
|
|
pageTenantMeetingPointsSettings,
|
|
|
|
|
|
type TenantMeetingPointsSettingVO,
|
|
|
|
|
|
updateTenantMeetingPointsBalanceCheck,
|
|
|
|
|
|
} from "@/api/business/meetingPoints";
|
|
|
|
|
|
import type { UserProfile } from "@/types";
|
|
|
|
|
|
import { Button, Card, Input, message, Modal, Select, Space, Statistic, Tag, Typography } from "antd";
|
|
|
|
|
|
import { useEffect, useState } from "react";
|
2026-06-29 02:17:30 +00:00
|
|
|
|
import "./TenantMeetingPointsSettings.css";
|
2026-06-11 09:22:57 +00:00
|
|
|
|
|
|
|
|
|
|
const { Text } = Typography;
|
2026-06-15 08:15:36 +00:00
|
|
|
|
const BALANCE_CHECK_UPDATE_PERMISSION = "biz:tenant-meeting-points:balance-check:update";
|
2026-06-11 09:22:57 +00:00
|
|
|
|
|
|
|
|
|
|
function formatDateTime(value?: string) {
|
|
|
|
|
|
return value ? value.replace("T", " ").substring(0, 19) : "-";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderStatusTag(enabled?: boolean) {
|
|
|
|
|
|
return enabled === false
|
|
|
|
|
|
? <Tag color="volcano">无限余额模式</Tag>
|
|
|
|
|
|
: <Tag color="green">校验余额模式</Tag>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function TenantMeetingPointsSettings() {
|
2026-06-15 08:15:36 +00:00
|
|
|
|
const {can} = usePermission();
|
2026-06-11 09:22:57 +00:00
|
|
|
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [switchingTenantId, setSwitchingTenantId] = useState<number | null>(null);
|
|
|
|
|
|
const [records, setRecords] = useState<TenantMeetingPointsSettingVO[]>([]);
|
|
|
|
|
|
const [total, setTotal] = useState(0);
|
|
|
|
|
|
const [currentTenantSetting, setCurrentTenantSetting] = useState<TenantMeetingPointsSettingVO | null>(null);
|
|
|
|
|
|
const [params, setParams] = useState({
|
|
|
|
|
|
current: 1,
|
2026-06-29 02:17:30 +00:00
|
|
|
|
size: 10,
|
2026-06-11 09:22:57 +00:00
|
|
|
|
tenantName: "",
|
|
|
|
|
|
tenantCode: "",
|
|
|
|
|
|
balanceCheckEnabled: "",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const isPlatformAdmin = Boolean(profile?.isPlatformAdmin);
|
|
|
|
|
|
const isTenantAdmin = Boolean(profile?.isTenantAdmin);
|
2026-06-15 08:15:36 +00:00
|
|
|
|
const canUpdateBalanceCheck = isPlatformAdmin || (isTenantAdmin && can(BALANCE_CHECK_UPDATE_PERMISSION));
|
2026-06-11 09:22:57 +00:00
|
|
|
|
|
|
|
|
|
|
const loadPlatformPage = async (nextParams = params) => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await pageTenantMeetingPointsSettings({
|
|
|
|
|
|
current: nextParams.current,
|
|
|
|
|
|
size: nextParams.size,
|
|
|
|
|
|
tenantName: nextParams.tenantName || undefined,
|
|
|
|
|
|
tenantCode: nextParams.tenantCode || undefined,
|
|
|
|
|
|
balanceCheckEnabled: nextParams.balanceCheckEnabled === "" ? undefined : nextParams.balanceCheckEnabled === "true",
|
|
|
|
|
|
});
|
|
|
|
|
|
setRecords(result.records || []);
|
|
|
|
|
|
setTotal(result.total || 0);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const loadCurrentTenant = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await getCurrentTenantMeetingPointsSetting();
|
|
|
|
|
|
setCurrentTenantSetting(data);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const loadData = async () => {
|
|
|
|
|
|
const nextProfile = await getCurrentUser();
|
|
|
|
|
|
setProfile(nextProfile);
|
|
|
|
|
|
if (nextProfile.isPlatformAdmin) {
|
|
|
|
|
|
await loadPlatformPage();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (nextProfile.isTenantAdmin) {
|
|
|
|
|
|
await loadCurrentTenant();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setCurrentTenantSetting(null);
|
|
|
|
|
|
setRecords([]);
|
|
|
|
|
|
setTotal(0);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
void loadData();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const handleRefresh = async () => {
|
|
|
|
|
|
if (isPlatformAdmin) {
|
|
|
|
|
|
await loadPlatformPage();
|
|
|
|
|
|
} else if (isTenantAdmin) {
|
|
|
|
|
|
await loadCurrentTenant();
|
|
|
|
|
|
}
|
|
|
|
|
|
message.success("已刷新租户积分校验配置");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const confirmSwitch = (record: TenantMeetingPointsSettingVO, nextEnabled: boolean) => {
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: nextEnabled ? "开启余额校验" : "关闭余额校验",
|
|
|
|
|
|
content: nextEnabled
|
|
|
|
|
|
? "开启后将按当前账面余额重新执行后续会议提交校验。"
|
|
|
|
|
|
: "关闭后将进入无限余额模式,只记录消耗和流水,不扣减账面余额。",
|
|
|
|
|
|
okText: "确认",
|
|
|
|
|
|
cancelText: "取消",
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
setSwitchingTenantId(record.tenantId);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await updateTenantMeetingPointsBalanceCheck(record.tenantId, { balanceCheckEnabled: nextEnabled });
|
|
|
|
|
|
message.success("余额校验开关已更新");
|
|
|
|
|
|
if (isPlatformAdmin) {
|
|
|
|
|
|
await loadPlatformPage();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await loadCurrentTenant();
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSwitchingTenantId(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const columns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "租户名称",
|
|
|
|
|
|
dataIndex: "tenantName",
|
|
|
|
|
|
key: "tenantName",
|
|
|
|
|
|
width: 180,
|
|
|
|
|
|
render: (value: string) => <Text strong>{value || "-"}</Text>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "租户编码",
|
|
|
|
|
|
dataIndex: "tenantCode",
|
|
|
|
|
|
key: "tenantCode",
|
|
|
|
|
|
width: 160,
|
|
|
|
|
|
render: (value: string) => <Text>{value || "-"}</Text>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "余额校验状态",
|
|
|
|
|
|
dataIndex: "balanceCheckEnabled",
|
|
|
|
|
|
key: "balanceCheckEnabled",
|
|
|
|
|
|
width: 160,
|
|
|
|
|
|
render: (value: boolean) => renderStatusTag(value),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "公共账户余额",
|
|
|
|
|
|
dataIndex: "publicBalance",
|
|
|
|
|
|
key: "publicBalance",
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
render: (value: number) => value ?? 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "公共账户累计消耗",
|
|
|
|
|
|
dataIndex: "publicTotalPointsUsed",
|
|
|
|
|
|
key: "publicTotalPointsUsed",
|
|
|
|
|
|
width: 160,
|
|
|
|
|
|
render: (value: number) => value ?? 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "最近切换时间",
|
|
|
|
|
|
dataIndex: "lastSwitchAt",
|
|
|
|
|
|
key: "lastSwitchAt",
|
|
|
|
|
|
width: 180,
|
|
|
|
|
|
render: (value: string) => formatDateTime(value),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "最近切换人",
|
|
|
|
|
|
dataIndex: "lastSwitchByName",
|
|
|
|
|
|
key: "lastSwitchByName",
|
|
|
|
|
|
width: 160,
|
|
|
|
|
|
render: (value: string) => value || "-",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "操作",
|
|
|
|
|
|
key: "action",
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
fixed: "right" as const,
|
2026-06-15 08:15:36 +00:00
|
|
|
|
render: (_: unknown, record: TenantMeetingPointsSettingVO) => {
|
|
|
|
|
|
if (!canUpdateBalanceCheck) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
loading={switchingTenantId === record.tenantId}
|
|
|
|
|
|
onClick={() => confirmSwitch(record, !record.balanceCheckEnabled)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{record.balanceCheckEnabled ? "切换为无限余额" : "开启余额校验"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
2026-06-11 09:22:57 +00:00
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const renderTenantAdminCard = () => {
|
|
|
|
|
|
if (!currentTenantSetting) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return (
|
2026-06-29 02:17:30 +00:00
|
|
|
|
<SectionCard
|
|
|
|
|
|
title="当前租户"
|
|
|
|
|
|
description="租户管理员可查看并切换当前租户的积分余额校验模式。"
|
|
|
|
|
|
layout="auto"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Space direction="vertical" size="large" className="tenant-meeting-points__tenant-card">
|
|
|
|
|
|
<div className="tenant-meeting-points__tenant-heading">
|
2026-06-11 09:22:57 +00:00
|
|
|
|
<Space direction="vertical" size={4}>
|
|
|
|
|
|
<Text strong style={{ fontSize: 18 }}>{currentTenantSetting.tenantName || "当前租户"}</Text>
|
|
|
|
|
|
<Text type="secondary">租户编码:{currentTenantSetting.tenantCode || "-"}</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
{renderStatusTag(currentTenantSetting.balanceCheckEnabled)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-29 02:17:30 +00:00
|
|
|
|
<Card
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
className={
|
|
|
|
|
|
currentTenantSetting.balanceCheckEnabled
|
|
|
|
|
|
? "tenant-meeting-points__mode-card tenant-meeting-points__mode-card--enabled"
|
|
|
|
|
|
: "tenant-meeting-points__mode-card tenant-meeting-points__mode-card--unlimited"
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2026-06-11 09:22:57 +00:00
|
|
|
|
<Space direction="vertical" size={4}>
|
|
|
|
|
|
<Text strong>
|
|
|
|
|
|
{currentTenantSetting.balanceCheckEnabled ? "当前为校验余额模式" : "当前为无限余额模式"}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Text type="secondary">
|
|
|
|
|
|
{currentTenantSetting.balanceCheckEnabled
|
|
|
|
|
|
? "后续会议提交将按当前账面余额执行拦截与扣减。"
|
|
|
|
|
|
: "后续会议只记录消耗与流水,不扣减账面余额,积分分配也会被禁用。"}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
2026-06-29 02:17:30 +00:00
|
|
|
|
<Space size={40} wrap className="tenant-meeting-points__stats">
|
2026-06-11 09:22:57 +00:00
|
|
|
|
<Statistic title="当前可用额度" value={currentTenantSetting.balanceCheckEnabled ? currentTenantSetting.publicBalance ?? 0 : "无限"} />
|
|
|
|
|
|
<Statistic title="公共账户余额" value={currentTenantSetting.publicBalance ?? 0} />
|
|
|
|
|
|
<Statistic title="公共账户累计消耗" value={currentTenantSetting.publicTotalPointsUsed ?? 0} />
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
|
|
|
|
|
|
<Space direction="vertical" size={4}>
|
|
|
|
|
|
<Text type="secondary">最近切换时间:{formatDateTime(currentTenantSetting.lastSwitchAt)}</Text>
|
|
|
|
|
|
<Text type="secondary">最近切换人:{currentTenantSetting.lastSwitchByName || "-"}</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
|
|
|
|
|
|
<Space>
|
2026-06-15 08:15:36 +00:00
|
|
|
|
{canUpdateBalanceCheck ? (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
loading={switchingTenantId === currentTenantSetting.tenantId}
|
|
|
|
|
|
onClick={() => confirmSwitch(currentTenantSetting, !currentTenantSetting.balanceCheckEnabled)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{currentTenantSetting.balanceCheckEnabled ? "切换为无限余额" : "开启余额校验"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
) : null}
|
2026-06-11 09:22:57 +00:00
|
|
|
|
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
|
|
|
|
|
|
刷新
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Space>
|
2026-06-29 02:17:30 +00:00
|
|
|
|
</SectionCard>
|
2026-06-11 09:22:57 +00:00
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<PageContainer
|
2026-06-29 02:17:30 +00:00
|
|
|
|
title={null}
|
|
|
|
|
|
className="tenant-meeting-points"
|
2026-06-11 09:22:57 +00:00
|
|
|
|
>
|
|
|
|
|
|
{isPlatformAdmin ? (
|
2026-06-29 02:17:30 +00:00
|
|
|
|
<SectionCard
|
|
|
|
|
|
title="租户积分校验"
|
|
|
|
|
|
description="按租户控制会议积分是否校验余额,关闭后按无限余额模式记录消耗。"
|
|
|
|
|
|
>
|
|
|
|
|
|
<DataListPanel
|
|
|
|
|
|
leftActions={
|
|
|
|
|
|
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
|
|
|
|
|
|
刷新
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
}
|
|
|
|
|
|
rightActions={
|
|
|
|
|
|
<Space wrap>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="按租户名称搜索"
|
|
|
|
|
|
value={params.tenantName}
|
|
|
|
|
|
onChange={(event) => setParams((prev) => ({ ...prev, tenantName: event.target.value }))}
|
|
|
|
|
|
style={{ width: 220 }}
|
|
|
|
|
|
prefix={<SearchOutlined className="text-gray-400" />}
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="按租户编码搜索"
|
|
|
|
|
|
value={params.tenantCode}
|
|
|
|
|
|
onChange={(event) => setParams((prev) => ({ ...prev, tenantCode: event.target.value }))}
|
|
|
|
|
|
style={{ width: 180 }}
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
style={{ width: 180 }}
|
|
|
|
|
|
value={params.balanceCheckEnabled}
|
|
|
|
|
|
onChange={(value) => setParams((prev) => ({ ...prev, balanceCheckEnabled: value }))}
|
|
|
|
|
|
options={[
|
|
|
|
|
|
{ label: "全部状态", value: "" },
|
|
|
|
|
|
{ label: "校验余额模式", value: "true" },
|
|
|
|
|
|
{ label: "无限余额模式", value: "false" },
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<SearchOutlined />}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const nextParams = { ...params, current: 1 };
|
|
|
|
|
|
setParams(nextParams);
|
|
|
|
|
|
void loadPlatformPage(nextParams);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
查询
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const nextParams = { current: 1, size: 10, tenantName: "", tenantCode: "", balanceCheckEnabled: "" };
|
|
|
|
|
|
setParams(nextParams);
|
|
|
|
|
|
void loadPlatformPage(nextParams);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
重置
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
footer={
|
|
|
|
|
|
<AppPagination
|
|
|
|
|
|
current={params.current}
|
|
|
|
|
|
pageSize={params.size}
|
|
|
|
|
|
total={total}
|
|
|
|
|
|
onChange={(page, pageSize) => {
|
|
|
|
|
|
const nextParams = { ...params, current: page, size: pageSize };
|
|
|
|
|
|
setParams(nextParams);
|
|
|
|
|
|
void loadPlatformPage(nextParams);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2026-06-11 09:22:57 +00:00
|
|
|
|
<ListTable<TenantMeetingPointsSettingVO>
|
|
|
|
|
|
rowKey="tenantId"
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
dataSource={records}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
totalCount={total}
|
2026-06-29 02:17:30 +00:00
|
|
|
|
scroll={{x: 1200, y: "100%"}}
|
2026-06-11 09:22:57 +00:00
|
|
|
|
pagination={false}
|
|
|
|
|
|
/>
|
2026-06-29 02:17:30 +00:00
|
|
|
|
</DataListPanel>
|
|
|
|
|
|
</SectionCard>
|
2026-06-11 09:22:57 +00:00
|
|
|
|
) : renderTenantAdminCard()}
|
|
|
|
|
|
</PageContainer>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|