import { PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons"; import { listUsers } from "@/api"; import { Button, Form, Input, InputNumber, message, Modal, Select, Space, Tag, Tabs, Typography, } from "antd"; import { useEffect, useMemo, useState } from "react"; import PageContainer from "@/components/shared/PageContainer"; import AppPagination from "@/components/shared/AppPagination"; import DataListPanel from "@/components/shared/DataListPanel"; import ListTable from "@/components/shared/ListTable/ListTable"; import SectionCard from "@/components/shared/SectionCard"; import { getMeetingPointsLedgerPage, getMeetingPointsOverview, transferMeetingPoints, type MeetingPointsLedgerListItemVO, type MeetingPointsOverviewVO, type MeetingPointsPersonalAccountVO, } from "@/api/business/meetingPoints"; import type { SysUser } from "@/types"; import "./MeetingPointsManagement.css"; const { Text } = Typography; const POINTS_TYPE_OPTIONS = [ { label: "全部类型", value: "" }, { label: "转录", value: "ASR" }, { label: "总结", value: "LLM" }, ]; const ACCOUNT_MODE_PUBLIC = "PUBLIC"; const ACCOUNT_MODE_PERSONAL = "PERSONAL"; const ACCOUNT_MODE_BOTH = "BOTH"; function getAccountModeLabel(mode?: string) { if (mode === ACCOUNT_MODE_PERSONAL) return "个人账户"; if (mode === ACCOUNT_MODE_BOTH) return "公共 + 个人"; return "公共账户"; } function getChargePriorityLabel(priority?: string) { return priority === "PUBLIC_FIRST" ? "公共优先" : "个人优先"; } function getAccountTypeLabel(type?: string) { return type === "PERSONAL" ? "个人账户" : "公共账户"; } function getPointsTypeLabel(value?: string) { if (value === "ASR") return "转录"; if (value === "LLM") return "总结"; if (value === "TRANSFER_OUT") return "转出"; if (value === "TRANSFER_IN") return "转入"; if (value === "INIT") return "初始化"; return value || "-"; } function getPointsTypeColor(value?: string) { if (value === "ASR") return "blue"; if (value === "LLM") return "geekblue"; if (value === "TRANSFER_IN") return "green"; if (value === "TRANSFER_OUT") return "orange"; return "default"; } function getChargeTriggerLabel(value?: string) { if (value === "RESUMMARY") return "重新总结"; if (value === "AUTO_SUMMARY") return "自动总结"; return "-"; } function formatDateTime(value?: string) { return value ? value.replace("T", " ").substring(0, 19) : "-"; } type OverviewRow = { id: string; metric: string; value: string | number; note: string; }; export default function MeetingPointsManagement() { const [overview, setOverview] = useState(null); const [loading, setLoading] = useState(false); const [transferLoading, setTransferLoading] = useState(false); const [usersLoading, setUsersLoading] = useState(false); const [records, setRecords] = useState([]); const [total, setTotal] = useState(0); const [transferOpen, setTransferOpen] = useState(false); const [users, setUsers] = useState([]); const [activeTabKey, setActiveTabKey] = useState("ledger"); const [personalAccountPagination, setPersonalAccountPagination] = useState({ current: 1, pageSize: 10, }); const [params, setParams] = useState({ current: 1, size: 10, username: "", pointsType: "", }); const [transferForm] = Form.useForm(); const isAdmin = Boolean(overview?.admin); const isPublicOnly = overview?.accountMode === ACCOUNT_MODE_PUBLIC; const isUnlimitedBalanceMode = overview?.balanceCheckEnabled === false; const showTransferButton = isAdmin && !isPublicOnly && !isUnlimitedBalanceMode; const showPersonalAccountSection = Boolean(overview) && !isPublicOnly; const sectionTabs = useMemo( () => [ { key: "ledger", label: "积分流水" }, { key: "overview", label: "账户概览" }, ...(showPersonalAccountSection ? [{ key: "personal", label: "个人账户" }] : []), ], [showPersonalAccountSection], ); useEffect(() => { if (!sectionTabs.some((tab) => tab.key === activeTabKey)) { setActiveTabKey("ledger"); } }, [activeTabKey, sectionTabs]); const personalAccountRows = useMemo(() => { 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]); const loadOverview = async () => { const data = await getMeetingPointsOverview(); setOverview(data); }; const loadUsers = async () => { setUsersLoading(true); try { const data = await listUsers(); setUsers(data || []); } finally { setUsersLoading(false); } }; const loadPage = async (nextParams = params) => { setLoading(true); try { const result = await getMeetingPointsLedgerPage(nextParams); setRecords(result.records || []); setTotal(result.total || 0); } finally { setLoading(false); } }; useEffect(() => { void Promise.all([loadOverview(), loadPage()]); }, []); const handleSearch = () => { const nextParams = { ...params, current: 1 }; setParams(nextParams); void loadPage(nextParams); }; const handleReset = () => { const nextParams = { current: 1, size: 10, username: "", pointsType: "", }; setParams(nextParams); void loadPage(nextParams); }; const handleRefresh = async () => { await Promise.all([loadOverview(), loadPage()]); message.success("已刷新积分数据"); }; const handleOpenTransfer = async () => { setTransferOpen(true); if (!users.length) { await loadUsers(); } }; const handleTransferSubmit = async () => { const values = await transferForm.validateFields(); setTransferLoading(true); try { await transferMeetingPoints(values); message.success("积分分配成功"); setTransferOpen(false); transferForm.resetFields(); await Promise.all([loadOverview(), loadPage()]); } finally { setTransferLoading(false); } }; const ledgerColumns = [ { title: "用户", dataIndex: "ownerUserName", key: "ownerUserName", width: 140, render: (value: string) => {value || "-"}, }, { title: "扣费账户", dataIndex: "chargeAccountType", key: "chargeAccountType", width: 120, render: (value: string) => {getAccountTypeLabel(value)}, }, { title: "消耗类型", dataIndex: "pointsType", key: "pointsType", width: 100, render: (value: string) => {getPointsTypeLabel(value)}, }, { title: "消耗积分", dataIndex: "consumedPoints", key: "consumedPoints", width: 110, render: (value: number) => {value ?? 0}, }, { title: "会议标题", dataIndex: "meetingTitle", key: "meetingTitle", ellipsis: true, render: (value: string) => {value || "-"}, }, { title: "触发类型", dataIndex: "chargeTriggerType", key: "chargeTriggerType", width: 130, render: (value: string) => {getChargeTriggerLabel(value)}, }, { title: "消耗时间", dataIndex: "createdAt", key: "createdAt", width: 180, render: (value: string) => {formatDateTime(value)}, }, ]; 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) => ( {record.displayName || record.username || `用户 #${record.userId}`} {record.username ? `@${record.username}` : `ID ${record.userId}`} ), }, { title: "当前余额", dataIndex: "currentBalance", key: "currentBalance", width: 140, render: (value: number) => {value ?? 0}, }, { title: "累计消耗", dataIndex: "totalPointsUsed", key: "totalPointsUsed", width: 140, render: (value: number) => {value ?? 0}, }, { title: "账户类型", key: "accountType", width: 120, render: () => 个人账户, }, ]; const overviewRows = useMemo(() => { if (!overview) return []; return [ { id: "accountMode", metric: "账户模式", value: getAccountModeLabel(overview.accountMode), note: "当前租户积分账户组合方式" }, { id: "chargePriority", metric: "扣费优先级", value: getChargePriorityLabel(overview.chargePriority), note: "公共账户与个人账户的扣费顺序" }, { id: "balanceCheckEnabled", metric: "余额校验状态", value: overview.balanceCheckEnabled ? "校验余额模式" : "无限余额模式", note: "控制会议提交时是否执行余额拦截" }, { id: "publicBalance", metric: "公共账户余额", value: overview.publicBalance ?? 0, note: "公共账户当前可用积分" }, { id: "publicTotalPointsUsed", metric: "公共账户累计消耗", value: overview.publicTotalPointsUsed ?? 0, note: "公共账户已消耗积分总量" }, { id: "personalBalance", metric: "个人账户余额", value: overview.personalBalance ?? 0, note: "个人账户当前可用积分" }, { id: "personalTotalPointsUsed", metric: "个人账户累计消耗", value: overview.personalTotalPointsUsed ?? 0, note: "个人账户已消耗积分总量" }, { id: "totalAvailableBalance", metric: "总可用余额", value: overview.totalAvailableBalance ?? 0, note: "当前账户体系可直接使用的积分余额" }, { id: "totalChargeCount", metric: "累计扣费次数", value: overview.totalChargeCount ?? 0, note: "已产生的积分扣费记录数" }, ]; }, [overview]); const overviewColumns = [ { title: "指标", dataIndex: "metric", key: "metric", width: 180, render: (value: string) => {value || "-"}, }, { title: "数值", dataIndex: "value", key: "value", width: 180, render: (value: string | number, record: OverviewRow) => record.id === "balanceCheckEnabled" ? ( {value} ) : ( {value ?? "-"} ), }, { title: "说明", dataIndex: "note", key: "note", ellipsis: true, render: (value: string) => {value || "-"}, }, ]; return ( } > } onClick={() => void handleOpenTransfer()}> 分配积分 ) : null } rightActions={ {activeTabKey === "ledger" ? ( <> setParams((prev) => ({ ...prev, username: event.target.value }))} style={{ width: 220 }} prefix={} allowClear /> user.userId && user.userId > 0) .map((user) => ({ label: `${user.displayName || user.username} (#${user.userId})`, value: user.userId, }))} /> ); } function isLookupTab(tabKey: string) { return tabKey === "ledger" || tabKey === "personal"; }