refactor: 更新异常处理和前端页面布局

- 在 `AndroidAuthServiceImpl` 中将 `RuntimeException` 替换为 `BusinessException`,并使用 `ErrorCodeEnum.UNAUTHORIZED`
- 优化前端 `MeetingPointsManagement` 页面布局,移除不必要的组件和样式
- 添加个人账户分页功能和新的统计卡片样式
- 调整积分管理页面的表格和标签样式,提升用户体验
dev_na
chenhao 2026-06-11 14:38:07 +08:00
parent 2e05a25e63
commit e330edc965
2 changed files with 330 additions and 310 deletions

View File

@ -9,6 +9,7 @@ import com.imeeting.service.android.AndroidAuthService;
import com.imeeting.service.android.AndroidDeviceBindingService; import com.imeeting.service.android.AndroidDeviceBindingService;
import com.imeeting.service.biz.LicenseService; import com.imeeting.service.biz.LicenseService;
import com.unisbase.common.exception.BusinessException; import com.unisbase.common.exception.BusinessException;
import com.unisbase.common.exception.ErrorCodeEnum;
import com.unisbase.dto.InternalAuthCheckResponse; import com.unisbase.dto.InternalAuthCheckResponse;
import com.unisbase.security.LoginUser; import com.unisbase.security.LoginUser;
import com.unisbase.service.TokenValidationService; import com.unisbase.service.TokenValidationService;
@ -205,14 +206,14 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
private InternalAuthCheckResponse validateToken(String token) { private InternalAuthCheckResponse validateToken(String token) {
String resolvedToken = normalizeToken(token); String resolvedToken = normalizeToken(token);
if (!StringUtils.hasText(resolvedToken)) { if (!StringUtils.hasText(resolvedToken)) {
throw new RuntimeException("Missing Android access token"); throw new BusinessException(ErrorCodeEnum.UNAUTHORIZED.getCode(),"Missing Android access token");
} }
InternalAuthCheckResponse authResult = tokenValidationService.validateAccessToken(resolvedToken); InternalAuthCheckResponse authResult = tokenValidationService.validateAccessToken(resolvedToken);
if (authResult == null || !authResult.isValid()) { if (authResult == null || !authResult.isValid()) {
throw new RuntimeException(authResult == null || !StringUtils.hasText(authResult.getMessage()) ? "Android access token is invalid" : authResult.getMessage()); throw new BusinessException(ErrorCodeEnum.UNAUTHORIZED.getCode(),authResult == null || !StringUtils.hasText(authResult.getMessage()) ? "Android access token is invalid" : authResult.getMessage());
} }
if (authResult.getUserId() == null || authResult.getTenantId() == null) { if (authResult.getUserId() == null || authResult.getTenantId() == null) {
throw new RuntimeException("Android access token is missing user or tenant context"); throw new BusinessException(ErrorCodeEnum.UNAUTHORIZED.getCode(),"Android access token is missing user or tenant context");
} }
return authResult; return authResult;
} }

View File

@ -1,11 +1,10 @@
import { EyeOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons"; import { EyeOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
import { listUsers } from "@/api"; import { listUsers } from "@/api";
import { import {
Button, Button,
Card, Card,
Col, Col,
Descriptions, Descriptions,
Empty,
Form, Form,
Input, Input,
InputNumber, InputNumber,
@ -16,6 +15,7 @@ import {
Space, Space,
Statistic, Statistic,
Table, Table,
Tabs,
Tag, Tag,
Typography, Typography,
} from "antd"; } from "antd";
@ -36,7 +36,7 @@ import {
} from "@/api/business/meetingPoints"; } from "@/api/business/meetingPoints";
import type { SysUser } from "@/types"; import type { SysUser } from "@/types";
const { Text, Title } = Typography; const { Text } = Typography;
const POINTS_TYPE_OPTIONS = [ const POINTS_TYPE_OPTIONS = [
{ label: "全部类型", value: "" }, { label: "全部类型", value: "" },
@ -93,6 +93,7 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) {
if (!overview) { if (!overview) {
return []; return [];
} }
const isAdmin = Boolean(overview.admin); const isAdmin = Boolean(overview.admin);
const isPublicOnly = overview.accountMode === ACCOUNT_MODE_PUBLIC; const isPublicOnly = overview.accountMode === ACCOUNT_MODE_PUBLIC;
const isPersonalOnly = overview.accountMode === ACCOUNT_MODE_PERSONAL; const isPersonalOnly = overview.accountMode === ACCOUNT_MODE_PERSONAL;
@ -113,14 +114,14 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) {
key: "public-balance", key: "public-balance",
title: "公共账户余额", title: "公共账户余额",
value: overview.publicBalance ?? 0, value: overview.publicBalance ?? 0,
accent: "linear-gradient(135deg, rgba(18, 87, 241, 0.14), rgba(18, 87, 241, 0.02))", accent: "rgba(24, 144, 255, 0.08)",
note: "用于统一分配和公共扣费", note: "用于统一分配和公共扣费",
}, },
{ {
key: "public-used", key: "public-used",
title: "公共账户累计消耗积分", title: "公共账户累计消耗",
value: overview.publicTotalPointsUsed ?? 0, value: overview.publicTotalPointsUsed ?? 0,
accent: "linear-gradient(135deg, rgba(11, 132, 98, 0.14), rgba(11, 132, 98, 0.02))", accent: "rgba(82, 196, 26, 0.08)",
note: "公共账户历史累计扣减", note: "公共账户历史累计扣减",
}, },
); );
@ -132,14 +133,14 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) {
key: "personal-balance", key: "personal-balance",
title: isAdmin ? "个人账户余额汇总" : "个人账户余额", title: isAdmin ? "个人账户余额汇总" : "个人账户余额",
value: overview.personalBalance ?? 0, value: overview.personalBalance ?? 0,
accent: "linear-gradient(135deg, rgba(148, 77, 255, 0.14), rgba(148, 77, 255, 0.02))", accent: "rgba(114, 46, 209, 0.08)",
note: isAdmin ? "管理员视角下的个人账户汇总" : "当前账号可用积分", note: isAdmin ? "管理员视角下的个人账户汇总" : "当前账号可用积分",
}, },
{ {
key: "personal-used", key: "personal-used",
title: isAdmin ? "个人账户累计消耗积分汇总" : "个人账户累计消耗积分", title: isAdmin ? "个人账户累计消耗汇总" : "个人账户累计消耗",
value: overview.personalTotalPointsUsed ?? 0, value: overview.personalTotalPointsUsed ?? 0,
accent: "linear-gradient(135deg, rgba(237, 108, 2, 0.14), rgba(237, 108, 2, 0.02))", accent: "rgba(250, 140, 22, 0.08)",
note: isAdmin ? "管理员视角下的个人账户消耗汇总" : "当前账号历史累计扣减", note: isAdmin ? "管理员视角下的个人账户消耗汇总" : "当前账号历史累计扣减",
}, },
); );
@ -149,7 +150,7 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) {
key: "charge-count", key: "charge-count",
title: "累计消耗次数", title: "累计消耗次数",
value: overview.totalChargeCount ?? 0, value: overview.totalChargeCount ?? 0,
accent: "linear-gradient(135deg, rgba(48, 48, 48, 0.12), rgba(48, 48, 48, 0.02))", accent: "rgba(38, 38, 38, 0.06)",
note: "已发生扣费的总结记录数", note: "已发生扣费的总结记录数",
}); });
@ -168,6 +169,11 @@ export default function MeetingPointsManagement() {
const [transferOpen, setTransferOpen] = useState(false); const [transferOpen, setTransferOpen] = useState(false);
const [detail, setDetail] = useState<MeetingPointsLedgerDetailVO | null>(null); const [detail, setDetail] = useState<MeetingPointsLedgerDetailVO | null>(null);
const [users, setUsers] = useState<SysUser[]>([]); const [users, setUsers] = useState<SysUser[]>([]);
const [contentTab, setContentTab] = useState("ledger");
const [personalAccountPagination, setPersonalAccountPagination] = useState({
current: 1,
pageSize: 10,
});
const [params, setParams] = useState({ const [params, setParams] = useState({
current: 1, current: 1,
size: 20, size: 20,
@ -180,8 +186,46 @@ export default function MeetingPointsManagement() {
const isPublicOnly = overview?.accountMode === ACCOUNT_MODE_PUBLIC; const isPublicOnly = overview?.accountMode === ACCOUNT_MODE_PUBLIC;
const isPersonalOnly = overview?.accountMode === ACCOUNT_MODE_PERSONAL; const isPersonalOnly = overview?.accountMode === ACCOUNT_MODE_PERSONAL;
const showTransferButton = isAdmin && !isPublicOnly; const showTransferButton = isAdmin && !isPublicOnly;
const showPersonalGallery = isAdmin && !isPublicOnly; const showPersonalAccountSection = Boolean(overview) && !isPublicOnly;
const summaryCards = buildSummaryCards(overview); const summaryCards = useMemo(() => buildSummaryCards(overview), [overview]);
const personalAccountRows = useMemo<MeetingPointsPersonalAccountVO[]>(() => {
if (!overview || isPublicOnly) {
return [];
}
if (isAdmin) {
return overview.personalAccounts || [];
}
return [
{
userId: -1,
displayName: "当前账号",
currentBalance: overview.personalBalance ?? 0,
totalPointsUsed: overview.personalTotalPointsUsed ?? 0,
},
];
}, [overview, isAdmin, isPublicOnly]);
const pagedPersonalAccounts = useMemo(() => {
const start = (personalAccountPagination.current - 1) * personalAccountPagination.pageSize;
const end = start + personalAccountPagination.pageSize;
return personalAccountRows.slice(start, end);
}, [personalAccountPagination, personalAccountRows]);
useEffect(() => {
setPersonalAccountPagination((prev) => {
const maxPage = Math.max(1, Math.ceil(personalAccountRows.length / prev.pageSize));
return prev.current > maxPage ? { ...prev, current: maxPage } : prev;
});
}, [personalAccountRows.length]);
useEffect(() => {
if (!isPersonalOnly && contentTab !== "ledger") {
setContentTab("ledger");
}
}, [contentTab, isPersonalOnly]);
const loadOverview = async () => { const loadOverview = async () => {
const data = await getMeetingPointsOverview(); const data = await getMeetingPointsOverview();
@ -267,118 +311,205 @@ export default function MeetingPointsManagement() {
} }
}; };
const columns = useMemo( const ledgerColumns = [
() => [ {
{ title: "用户",
title: "用户", dataIndex: "ownerUserName",
dataIndex: "ownerUserName", key: "ownerUserName",
key: "ownerUserName", width: 140,
width: 140, render: (value: string) => <Text strong>{value || "-"}</Text>,
render: (value: string) => <Text strong>{value || "-"}</Text>, },
}, {
{ title: "扣费账户",
title: "扣费账户", dataIndex: "chargeAccountType",
dataIndex: "chargeAccountType", key: "chargeAccountType",
key: "chargeAccountType", width: 120,
width: 120, render: (value: string) => <Tag>{getAccountTypeLabel(value)}</Tag>,
render: (value: string) => <Tag>{getAccountTypeLabel(value)}</Tag>, },
}, {
{ title: "消耗类型",
title: "消耗类型", dataIndex: "pointsType",
dataIndex: "pointsType", key: "pointsType",
key: "pointsType", width: 100,
width: 100, render: (value: string) => <Tag color={getPointsTypeColor(value)}>{getPointsTypeLabel(value)}</Tag>,
render: (value: string) => <Tag color={getPointsTypeColor(value)}>{getPointsTypeLabel(value)}</Tag>, },
}, {
{ title: "消耗积分",
title: "消耗积分", dataIndex: "consumedPoints",
dataIndex: "consumedPoints", key: "consumedPoints",
key: "consumedPoints", width: 110,
width: 110, render: (value: number) => <Text>{value ?? 0}</Text>,
render: (value: number) => <Text>{value ?? 0}</Text>, },
}, {
{ title: "会议标题",
title: "会议标题", dataIndex: "meetingTitle",
dataIndex: "meetingTitle", key: "meetingTitle",
key: "meetingTitle", ellipsis: true,
ellipsis: true, render: (value: string) => <Text>{value || "-"}</Text>,
render: (value: string) => <Text>{value || "-"}</Text>, },
}, {
{ title: "触发类型",
title: "触发类型", dataIndex: "chargeTriggerType",
dataIndex: "chargeTriggerType", key: "chargeTriggerType",
key: "chargeTriggerType", width: 130,
width: 130, render: (value: string) => <Tag>{getChargeTriggerLabel(value)}</Tag>,
render: (value: string) => <Tag>{getChargeTriggerLabel(value)}</Tag>, },
}, {
{ title: "消耗时间",
title: "消耗时间", dataIndex: "createdAt",
dataIndex: "createdAt", key: "createdAt",
key: "createdAt", width: 180,
width: 180, render: (value: string) => <Text>{formatDateTime(value)}</Text>,
render: (value: string) => <Text>{formatDateTime(value)}</Text>, },
}, {
{ title: "操作",
title: "操作", key: "action",
key: "action", width: 88,
width: 88, fixed: "right" as const,
fixed: "right" as const, render: (_: unknown, record: MeetingPointsLedgerListItemVO) => (
render: (_: unknown, record: MeetingPointsLedgerListItemVO) => ( <Button type="text" size="small" icon={<EyeOutlined />} onClick={() => void handleOpenDetail(record.id)}>
<Button type="text" size="small" icon={<EyeOutlined />} onClick={() => void handleOpenDetail(record.id)}>
</Button>
</Button> ),
), },
}, ];
],
[], const personalAccountColumns = [
{
title: "序号",
key: "index",
width: 80,
render: (_: unknown, __: MeetingPointsPersonalAccountVO, index: number) =>
(personalAccountPagination.current - 1) * personalAccountPagination.pageSize + index + 1,
},
{
title: "账户",
key: "displayName",
width: 260,
render: (_: unknown, record: MeetingPointsPersonalAccountVO) => (
<Space direction="vertical" size={2}>
<Text strong>{record.displayName || record.username || `用户 #${record.userId}`}</Text>
<Text type="secondary">{record.username ? `@${record.username}` : `ID ${record.userId}`}</Text>
</Space>
),
},
{
title: "当前余额",
dataIndex: "currentBalance",
key: "currentBalance",
width: 140,
render: (value: number) => <Text strong>{value ?? 0}</Text>,
},
{
title: "累计消耗",
dataIndex: "totalPointsUsed",
key: "totalPointsUsed",
width: 140,
render: (value: number) => <Text>{value ?? 0}</Text>,
},
{
title: "账户类型",
key: "accountType",
width: 120,
render: () => <Tag color="purple"></Tag>,
},
];
const chargeItemColumns = [
{
title: "阶段",
dataIndex: "chargeStage",
key: "chargeStage",
render: (value: string) => <Tag color={getPointsTypeColor(value)}>{getPointsTypeLabel(value)}</Tag>,
},
{
title: "扣费账户",
dataIndex: "accountType",
key: "accountType",
render: (value: string) => getAccountTypeLabel(value),
},
{
title: "账户用户ID",
dataIndex: "accountUserId",
key: "accountUserId",
},
{
title: "扣费积分",
dataIndex: "chargedPoints",
key: "chargedPoints",
},
{
title: "扣费前余额",
dataIndex: "balanceBefore",
key: "balanceBefore",
},
{
title: "扣费后余额",
dataIndex: "balanceAfter",
key: "balanceAfter",
},
];
const ledgerTableContent = (
<div style={{ display: "flex", flexDirection: "column" , height:"400px"}}>
<div className="app-page__table-wrap" style={{ overflow: "auto", padding: "0 24px" }}>
<ListTable<MeetingPointsLedgerListItemVO>
rowKey="id"
columns={ledgerColumns}
dataSource={records}
loading={loading}
totalCount={total}
scroll={{ x: 1200,y: 400 }}
pagination={false}
/>
</div>
<div style={{ padding: "16px 24px" }}>
<AppPagination
current={params.current}
pageSize={params.size}
total={total}
onChange={(page, pageSize) => {
const nextParams = { ...params, current: page, size: pageSize };
setParams(nextParams);
void loadPage(nextParams);
}}
/>
</div>
</div>
); );
const chargeItemColumns = useMemo( const personalAccountTableContent = (
() => [ <div style={{ display: "flex", flexDirection: "column",height:'400px' }}>
{ <div className="app-page__table-wrap" style={{ overflow: "auto", padding: "0 24px" }}>
title: "阶段", <ListTable<MeetingPointsPersonalAccountVO>
dataIndex: "chargeStage", rowKey="userId"
key: "chargeStage", columns={personalAccountColumns}
render: (value: string) => <Tag color={getPointsTypeColor(value)}>{getPointsTypeLabel(value)}</Tag>, dataSource={pagedPersonalAccounts}
}, totalCount={personalAccountRows.length}
{ scroll={{ x: 1200,y: 400 }}
title: "扣费账户", pagination={false}
dataIndex: "accountType", />
key: "accountType", </div>
render: (value: string) => getAccountTypeLabel(value), <div style={{ padding: "16px 24px" }}>
}, <AppPagination
{ current={personalAccountPagination.current}
title: "账户用户ID", pageSize={personalAccountPagination.pageSize}
dataIndex: "accountUserId", total={personalAccountRows.length}
key: "accountUserId", onChange={(page, pageSize) => {
}, setPersonalAccountPagination({ current: page, pageSize });
{ }}
title: "扣费积分", />
dataIndex: "chargedPoints", </div>
key: "chargedPoints", </div>
},
{
title: "扣费前余额",
dataIndex: "balanceBefore",
key: "balanceBefore",
},
{
title: "扣费后余额",
dataIndex: "balanceAfter",
key: "balanceAfter",
},
],
[],
); );
return ( return (
<PageContainer <PageContainer
title="积分管理" title="积分管理"
subtitle="根据当前扣费模式查看公共账户、个人账户和会议积分消耗轨迹" subtitle="根据当前扣费模式查看公共账户、个人账户和会议积分消耗轨迹"
style={{ height: "auto" }}
headerExtra={ headerExtra={
<Space> <Space>
<Tag color="processing">{getAccountModeLabel(overview?.accountMode)}</Tag>
<Tag color="blue">{getChargePriorityLabel(overview?.chargePriority)}</Tag>
{showTransferButton ? ( {showTransferButton ? (
<Button icon={<PlusOutlined />} onClick={() => void handleOpenTransfer()}> <Button icon={<PlusOutlined />} onClick={() => void handleOpenTransfer()}>
@ -412,204 +543,92 @@ export default function MeetingPointsManagement() {
</Space> </Space>
} }
> >
<Card <div style={{ marginBottom: 20, padding: "0 4px" }}>
bordered={false} <div
style={{
marginBottom: 16,
borderRadius: 24,
background:
"linear-gradient(180deg, rgba(248,250,252,0.96) 0%, rgba(255,255,255,0.98) 100%)",
boxShadow: "0 18px 40px rgba(15, 23, 42, 0.06)",
}}
>
<Space direction="vertical" size={20} style={{ width: "100%" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: 16, flexWrap: "wrap" }}>
<div>
<Text type="secondary" style={{ letterSpacing: 1.4 }}>
POINTS OPERATIONS BOARD
</Text>
<Title level={4} style={{ margin: "8px 0 0" }}>
{isPublicOnly
? "当前为公共账户结算视图"
: isPersonalOnly
? "当前为个人账户结算视图"
: "当前为公共与个人混合结算视图"}
</Title>
</div>
<div style={{ textAlign: "right" }}>
<Text type="secondary"></Text>
<div style={{ marginTop: 6 }}>
<Tag color={isAdmin ? "gold" : "default"}>{isAdmin ? "管理员视角" : "当前用户视角"}</Tag>
</div>
</div>
</div>
<Row gutter={[16, 16]}>
{summaryCards.map((item) => (
<Col xs={24} md={12} xl={24 / Math.min(summaryCards.length, 5)} key={item.key}>
<Card
size="small"
style={{
borderRadius: 20,
border: "1px solid rgba(15, 23, 42, 0.06)",
background: item.accent,
minHeight: 142,
}}
styles={{ body: { padding: 18 } }}
>
<Space direction="vertical" size={10} style={{ width: "100%" }}>
<Text type="secondary">{item.title}</Text>
<Statistic value={item.value} valueStyle={{ fontSize: 30, fontWeight: 700, color: "#111827" }} />
<Text type="secondary" style={{ fontSize: 12 }}>
{item.note}
</Text>
</Space>
</Card>
</Col>
))}
</Row>
</Space>
</Card>
{showPersonalGallery ? (
<Card
bordered={false}
style={{ style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: 16,
flexWrap: "wrap",
marginBottom: 16, marginBottom: 16,
borderRadius: 24,
background: "linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(245,247,250,0.96) 100%)",
boxShadow: "0 16px 34px rgba(15, 23, 42, 0.05)",
}} }}
> >
<Space direction="vertical" size={18} style={{ width: "100%" }}> <Text strong style={{ fontSize: 16 }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: 16, flexWrap: "wrap" }}>
<div> </Text>
<Title level={5} style={{ margin: 0 }}> <Space wrap>
<Tag color="processing" bordered={false}>
</Title> {getAccountModeLabel(overview?.accountMode)}
<Text type="secondary"> </Tag>
<Tag color="blue" bordered={false}>
</Text> {getChargePriorityLabel(overview?.chargePriority)}
</div> </Tag>
<Tag color="purple">{overview?.personalAccounts?.length ?? 0} </Tag> <Tag color={isAdmin ? "gold" : "default"} bordered={false}>
</div> {isAdmin ? "管理员视角" : "当前用户视角"}
</Tag>
{overview?.personalAccounts?.length ? (
<Row gutter={[16, 16]}>
{overview.personalAccounts.map((account: MeetingPointsPersonalAccountVO, index) => (
<Col xs={24} sm={12} xl={8} xxl={6} key={account.userId}>
<Card
size="small"
style={{
borderRadius: 20,
border: "1px solid rgba(124, 58, 237, 0.08)",
background:
index < 3
? "linear-gradient(145deg, rgba(255,255,255,1) 0%, rgba(245,241,255,0.98) 100%)"
: "linear-gradient(145deg, rgba(255,255,255,1) 0%, rgba(249,250,251,0.98) 100%)",
minHeight: 158,
}}
styles={{ body: { padding: 18 } }}
>
<Space direction="vertical" size={14} style={{ width: "100%" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 12 }}>
<Space size={12} align="start">
<div
style={{
width: 38,
height: 38,
borderRadius: 14,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "rgba(124, 58, 237, 0.10)",
color: "#7c3aed",
flexShrink: 0,
}}
>
<UserOutlined />
</div>
<div>
<Text strong style={{ display: "block" }}>
{account.displayName || account.username || `用户 #${account.userId}`}
</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
{account.username ? `@${account.username}` : `ID ${account.userId}`}
</Text>
</div>
</Space>
{index < 3 ? <Tag color="gold">TOP {index + 1}</Tag> : null}
</div>
<div>
<Text type="secondary"></Text>
<div style={{ fontSize: 30, fontWeight: 700, lineHeight: 1.2, color: "#111827" }}>
{account.currentBalance ?? 0}
</div>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
gap: 10,
padding: 12,
borderRadius: 16,
background: "rgba(15, 23, 42, 0.03)",
}}
>
<div>
<Text type="secondary" style={{ fontSize: 12 }}>
</Text>
<div style={{ fontWeight: 600 }}>{account.totalPointsUsed ?? 0}</div>
</div>
<div>
<Text type="secondary" style={{ fontSize: 12 }}>
</Text>
<div style={{ fontWeight: 600 }}></div>
</div>
</div>
</Space>
</Card>
</Col>
))}
</Row>
) : (
<Empty description="当前没有可展示的个人账户数据" />
)}
</Space> </Space>
</Card>
) : null}
<Card
className="app-page__content-card"
style={{ flex: 1, minHeight: 0 }}
styles={{ body: { padding: 0, display: "flex", flexDirection: "column", minHeight: 0 } }}
>
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
<ListTable
rowKey="id"
columns={columns as never}
dataSource={records}
loading={loading}
totalCount={total}
scroll={{ y: "calc(100vh - 510px)", x: 1200 }}
pagination={false}
/>
</div> </div>
<AppPagination
current={params.current} <Row gutter={[40, 16]}>
pageSize={params.size} {summaryCards.map((item) => (
total={total} <Col key={item.key}>
onChange={(page, pageSize) => { <Statistic
const nextParams = { ...params, current: page, size: pageSize }; title={<span style={{ color: "rgba(0,0,0,0.45)", fontSize: 12 }}>{item.title}</span>}
setParams(nextParams); value={item.value}
void loadPage(nextParams); valueStyle={{ fontSize: 24, fontWeight: 700 }}
}} />
/> <div style={{ fontSize: 11, color: "rgba(0,0,0,0.45)", marginTop: 2 }}>{item.note}</div>
</Card> </Col>
))}
</Row>
</div>
{isPersonalOnly ? (
<Card className="app-page__content-card" styles={{ body: { padding: 0 } }}>
<Tabs
activeKey={contentTab}
onChange={setContentTab}
items={[
{
key: "ledger",
label: "积分明细",
children: ledgerTableContent,
},
{
key: "personal-account",
label: "个人账户",
children: personalAccountTableContent,
},
]}
/>
</Card>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<Card className="app-page__content-card" styles={{ body: { padding: 0 } }}>
{ledgerTableContent}
</Card>
{showPersonalAccountSection ? (
<Card className="app-page__content-card" styles={{ body: { padding: 0 } }}>
<div style={{ padding: "20px 24px 8px" }}>
<Text strong style={{ fontSize: 16 }}>
</Text>
<div style={{ marginTop: 4 }}>
<Text type="secondary">
{isAdmin ? "按当前余额展示全部个人账户。" : "展示当前账号的个人积分账户。"}
</Text>
</div>
</div>
{personalAccountTableContent}
</Card>
) : null}
</div>
)}
<Modal <Modal
title="积分流水详情" title="积分流水详情"
@ -655,7 +674,7 @@ export default function MeetingPointsManagement() {
rowKey="id" rowKey="id"
size="small" size="small"
pagination={false} pagination={false}
columns={chargeItemColumns as never} columns={chargeItemColumns}
dataSource={detail.chargeItems || []} dataSource={detail.chargeItems || []}
/> />
</Card> </Card>