refactor: 更新异常处理和前端页面布局
- 在 `AndroidAuthServiceImpl` 中将 `RuntimeException` 替换为 `BusinessException`,并使用 `ErrorCodeEnum.UNAUTHORIZED` - 优化前端 `MeetingPointsManagement` 页面布局,移除不必要的组件和样式 - 添加个人账户分页功能和新的统计卡片样式 - 调整积分管理页面的表格和标签样式,提升用户体验dev_na
parent
2e05a25e63
commit
e330edc965
|
|
@ -9,6 +9,7 @@ import com.imeeting.service.android.AndroidAuthService;
|
|||
import com.imeeting.service.android.AndroidDeviceBindingService;
|
||||
import com.imeeting.service.biz.LicenseService;
|
||||
import com.unisbase.common.exception.BusinessException;
|
||||
import com.unisbase.common.exception.ErrorCodeEnum;
|
||||
import com.unisbase.dto.InternalAuthCheckResponse;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import com.unisbase.service.TokenValidationService;
|
||||
|
|
@ -205,14 +206,14 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
|||
private InternalAuthCheckResponse validateToken(String token) {
|
||||
String resolvedToken = normalizeToken(token);
|
||||
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);
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Descriptions,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
|
|
@ -16,6 +15,7 @@ import {
|
|||
Space,
|
||||
Statistic,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
|
|
@ -36,7 +36,7 @@ import {
|
|||
} from "@/api/business/meetingPoints";
|
||||
import type { SysUser } from "@/types";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
const { Text } = Typography;
|
||||
|
||||
const POINTS_TYPE_OPTIONS = [
|
||||
{ label: "全部类型", value: "" },
|
||||
|
|
@ -93,6 +93,7 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) {
|
|||
if (!overview) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isAdmin = Boolean(overview.admin);
|
||||
const isPublicOnly = overview.accountMode === ACCOUNT_MODE_PUBLIC;
|
||||
const isPersonalOnly = overview.accountMode === ACCOUNT_MODE_PERSONAL;
|
||||
|
|
@ -113,14 +114,14 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) {
|
|||
key: "public-balance",
|
||||
title: "公共账户余额",
|
||||
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: "用于统一分配和公共扣费",
|
||||
},
|
||||
{
|
||||
key: "public-used",
|
||||
title: "公共账户累计消耗积分",
|
||||
title: "公共账户累计消耗",
|
||||
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: "公共账户历史累计扣减",
|
||||
},
|
||||
);
|
||||
|
|
@ -132,14 +133,14 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) {
|
|||
key: "personal-balance",
|
||||
title: isAdmin ? "个人账户余额汇总" : "个人账户余额",
|
||||
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 ? "管理员视角下的个人账户汇总" : "当前账号可用积分",
|
||||
},
|
||||
{
|
||||
key: "personal-used",
|
||||
title: isAdmin ? "个人账户累计消耗积分汇总" : "个人账户累计消耗积分",
|
||||
title: isAdmin ? "个人账户累计消耗汇总" : "个人账户累计消耗",
|
||||
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 ? "管理员视角下的个人账户消耗汇总" : "当前账号历史累计扣减",
|
||||
},
|
||||
);
|
||||
|
|
@ -149,7 +150,7 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) {
|
|||
key: "charge-count",
|
||||
title: "累计消耗次数",
|
||||
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: "已发生扣费的总结记录数",
|
||||
});
|
||||
|
||||
|
|
@ -168,6 +169,11 @@ export default function MeetingPointsManagement() {
|
|||
const [transferOpen, setTransferOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<MeetingPointsLedgerDetailVO | null>(null);
|
||||
const [users, setUsers] = useState<SysUser[]>([]);
|
||||
const [contentTab, setContentTab] = useState("ledger");
|
||||
const [personalAccountPagination, setPersonalAccountPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [params, setParams] = useState({
|
||||
current: 1,
|
||||
size: 20,
|
||||
|
|
@ -180,8 +186,46 @@ export default function MeetingPointsManagement() {
|
|||
const isPublicOnly = overview?.accountMode === ACCOUNT_MODE_PUBLIC;
|
||||
const isPersonalOnly = overview?.accountMode === ACCOUNT_MODE_PERSONAL;
|
||||
const showTransferButton = isAdmin && !isPublicOnly;
|
||||
const showPersonalGallery = isAdmin && !isPublicOnly;
|
||||
const summaryCards = buildSummaryCards(overview);
|
||||
const showPersonalAccountSection = Boolean(overview) && !isPublicOnly;
|
||||
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 data = await getMeetingPointsOverview();
|
||||
|
|
@ -267,118 +311,205 @@ export default function MeetingPointsManagement() {
|
|||
}
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: "用户",
|
||||
dataIndex: "ownerUserName",
|
||||
key: "ownerUserName",
|
||||
width: 140,
|
||||
render: (value: string) => <Text strong>{value || "-"}</Text>,
|
||||
},
|
||||
{
|
||||
title: "扣费账户",
|
||||
dataIndex: "chargeAccountType",
|
||||
key: "chargeAccountType",
|
||||
width: 120,
|
||||
render: (value: string) => <Tag>{getAccountTypeLabel(value)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "消耗类型",
|
||||
dataIndex: "pointsType",
|
||||
key: "pointsType",
|
||||
width: 100,
|
||||
render: (value: string) => <Tag color={getPointsTypeColor(value)}>{getPointsTypeLabel(value)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "消耗积分",
|
||||
dataIndex: "consumedPoints",
|
||||
key: "consumedPoints",
|
||||
width: 110,
|
||||
render: (value: number) => <Text>{value ?? 0}</Text>,
|
||||
},
|
||||
{
|
||||
title: "会议标题",
|
||||
dataIndex: "meetingTitle",
|
||||
key: "meetingTitle",
|
||||
ellipsis: true,
|
||||
render: (value: string) => <Text>{value || "-"}</Text>,
|
||||
},
|
||||
{
|
||||
title: "触发类型",
|
||||
dataIndex: "chargeTriggerType",
|
||||
key: "chargeTriggerType",
|
||||
width: 130,
|
||||
render: (value: string) => <Tag>{getChargeTriggerLabel(value)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "消耗时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
render: (value: string) => <Text>{formatDateTime(value)}</Text>,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 88,
|
||||
fixed: "right" as const,
|
||||
render: (_: unknown, record: MeetingPointsLedgerListItemVO) => (
|
||||
<Button type="text" size="small" icon={<EyeOutlined />} onClick={() => void handleOpenDetail(record.id)}>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
const ledgerColumns = [
|
||||
{
|
||||
title: "用户",
|
||||
dataIndex: "ownerUserName",
|
||||
key: "ownerUserName",
|
||||
width: 140,
|
||||
render: (value: string) => <Text strong>{value || "-"}</Text>,
|
||||
},
|
||||
{
|
||||
title: "扣费账户",
|
||||
dataIndex: "chargeAccountType",
|
||||
key: "chargeAccountType",
|
||||
width: 120,
|
||||
render: (value: string) => <Tag>{getAccountTypeLabel(value)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "消耗类型",
|
||||
dataIndex: "pointsType",
|
||||
key: "pointsType",
|
||||
width: 100,
|
||||
render: (value: string) => <Tag color={getPointsTypeColor(value)}>{getPointsTypeLabel(value)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "消耗积分",
|
||||
dataIndex: "consumedPoints",
|
||||
key: "consumedPoints",
|
||||
width: 110,
|
||||
render: (value: number) => <Text>{value ?? 0}</Text>,
|
||||
},
|
||||
{
|
||||
title: "会议标题",
|
||||
dataIndex: "meetingTitle",
|
||||
key: "meetingTitle",
|
||||
ellipsis: true,
|
||||
render: (value: string) => <Text>{value || "-"}</Text>,
|
||||
},
|
||||
{
|
||||
title: "触发类型",
|
||||
dataIndex: "chargeTriggerType",
|
||||
key: "chargeTriggerType",
|
||||
width: 130,
|
||||
render: (value: string) => <Tag>{getChargeTriggerLabel(value)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "消耗时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
render: (value: string) => <Text>{formatDateTime(value)}</Text>,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 88,
|
||||
fixed: "right" as const,
|
||||
render: (_: unknown, record: MeetingPointsLedgerListItemVO) => (
|
||||
<Button type="text" size="small" icon={<EyeOutlined />} onClick={() => void handleOpenDetail(record.id)}>
|
||||
详情
|
||||
</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(
|
||||
() => [
|
||||
{
|
||||
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 personalAccountTableContent = (
|
||||
<div style={{ display: "flex", flexDirection: "column",height:'400px' }}>
|
||||
<div className="app-page__table-wrap" style={{ overflow: "auto", padding: "0 24px" }}>
|
||||
<ListTable<MeetingPointsPersonalAccountVO>
|
||||
rowKey="userId"
|
||||
columns={personalAccountColumns}
|
||||
dataSource={pagedPersonalAccounts}
|
||||
totalCount={personalAccountRows.length}
|
||||
scroll={{ x: 1200,y: 400 }}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ padding: "16px 24px" }}>
|
||||
<AppPagination
|
||||
current={personalAccountPagination.current}
|
||||
pageSize={personalAccountPagination.pageSize}
|
||||
total={personalAccountRows.length}
|
||||
onChange={(page, pageSize) => {
|
||||
setPersonalAccountPagination({ current: page, pageSize });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="积分管理"
|
||||
subtitle="根据当前扣费模式查看公共账户、个人账户和会议积分消耗轨迹"
|
||||
style={{ height: "auto" }}
|
||||
headerExtra={
|
||||
<Space>
|
||||
<Tag color="processing">当前模式:{getAccountModeLabel(overview?.accountMode)}</Tag>
|
||||
<Tag color="blue">优先级:{getChargePriorityLabel(overview?.chargePriority)}</Tag>
|
||||
{showTransferButton ? (
|
||||
<Button icon={<PlusOutlined />} onClick={() => void handleOpenTransfer()}>
|
||||
分配积分
|
||||
|
|
@ -412,204 +543,92 @@ export default function MeetingPointsManagement() {
|
|||
</Space>
|
||||
}
|
||||
>
|
||||
<Card
|
||||
bordered={false}
|
||||
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}
|
||||
<div style={{ marginBottom: 20, padding: "0 4px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
gap: 16,
|
||||
flexWrap: "wrap",
|
||||
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%" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: 16, flexWrap: "wrap" }}>
|
||||
<div>
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
个人账户余额画廊
|
||||
</Title>
|
||||
<Text type="secondary">
|
||||
仅管理员可见,按当前余额从高到低展示全部个人账户。
|
||||
</Text>
|
||||
</div>
|
||||
<Tag color="purple">{overview?.personalAccounts?.length ?? 0} 个账户</Tag>
|
||||
</div>
|
||||
|
||||
{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="当前没有可展示的个人账户数据" />
|
||||
)}
|
||||
<Text strong style={{ fontSize: 16 }}>
|
||||
账户概览
|
||||
</Text>
|
||||
<Space wrap>
|
||||
<Tag color="processing" bordered={false}>
|
||||
模式:{getAccountModeLabel(overview?.accountMode)}
|
||||
</Tag>
|
||||
<Tag color="blue" bordered={false}>
|
||||
优先级:{getChargePriorityLabel(overview?.chargePriority)}
|
||||
</Tag>
|
||||
<Tag color={isAdmin ? "gold" : "default"} bordered={false}>
|
||||
{isAdmin ? "管理员视角" : "当前用户视角"}
|
||||
</Tag>
|
||||
</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>
|
||||
<AppPagination
|
||||
current={params.current}
|
||||
pageSize={params.size}
|
||||
total={total}
|
||||
onChange={(page, pageSize) => {
|
||||
const nextParams = { ...params, current: page, size: pageSize };
|
||||
setParams(nextParams);
|
||||
void loadPage(nextParams);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Row gutter={[40, 16]}>
|
||||
{summaryCards.map((item) => (
|
||||
<Col key={item.key}>
|
||||
<Statistic
|
||||
title={<span style={{ color: "rgba(0,0,0,0.45)", fontSize: 12 }}>{item.title}</span>}
|
||||
value={item.value}
|
||||
valueStyle={{ fontSize: 24, fontWeight: 700 }}
|
||||
/>
|
||||
<div style={{ fontSize: 11, color: "rgba(0,0,0,0.45)", marginTop: 2 }}>{item.note}</div>
|
||||
</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
|
||||
title="积分流水详情"
|
||||
|
|
@ -655,7 +674,7 @@ export default function MeetingPointsManagement() {
|
|||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
columns={chargeItemColumns as never}
|
||||
columns={chargeItemColumns}
|
||||
dataSource={detail.chargeItems || []}
|
||||
/>
|
||||
</Card>
|
||||
|
|
|
|||
Loading…
Reference in New Issue