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.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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue