333 lines
11 KiB
TypeScript
333 lines
11 KiB
TypeScript
|
|
import { Alert, Button, Card, Col, Descriptions, Input, Modal, Row, Select, Space, Statistic, Tag, Typography, message } from "antd";
|
||
|
|
import { EyeOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
|
||
|
|
import { useEffect, useMemo, useState } from "react";
|
||
|
|
import PageContainer from "@/components/shared/PageContainer";
|
||
|
|
import ListTable from "@/components/shared/ListTable/ListTable";
|
||
|
|
import AppPagination from "@/components/shared/AppPagination";
|
||
|
|
import {
|
||
|
|
getMeetingPointsLedgerDetail,
|
||
|
|
getMeetingPointsLedgerPage,
|
||
|
|
getMeetingPointsOverview,
|
||
|
|
type MeetingPointsLedgerDetailVO,
|
||
|
|
type MeetingPointsLedgerListItemVO,
|
||
|
|
type MeetingPointsOverviewVO,
|
||
|
|
} from "@/api/business/meetingPoints";
|
||
|
|
|
||
|
|
const { Text } = Typography;
|
||
|
|
|
||
|
|
const POINTS_TYPE_OPTIONS = [
|
||
|
|
{ label: "全部类型", value: "" },
|
||
|
|
{ label: "转录", value: "ASR" },
|
||
|
|
{ label: "总结", value: "LLM" },
|
||
|
|
];
|
||
|
|
|
||
|
|
function getAccountModeLabel(mode?: string) {
|
||
|
|
return mode === "PERSONAL" ? "个人账户" : "公共账户";
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPointsTypeLabel(value?: string) {
|
||
|
|
if (value === "ASR") {
|
||
|
|
return "转录";
|
||
|
|
}
|
||
|
|
if (value === "LLM") {
|
||
|
|
return "总结";
|
||
|
|
}
|
||
|
|
if (value === "INIT") {
|
||
|
|
return "初始化";
|
||
|
|
}
|
||
|
|
if (value === "RECHARGE") {
|
||
|
|
return "充值";
|
||
|
|
}
|
||
|
|
return value || "-";
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPointsTypeColor(value?: string) {
|
||
|
|
if (value === "ASR") {
|
||
|
|
return "blue";
|
||
|
|
}
|
||
|
|
if (value === "LLM") {
|
||
|
|
return "purple";
|
||
|
|
}
|
||
|
|
if (value === "RECHARGE") {
|
||
|
|
return "green";
|
||
|
|
}
|
||
|
|
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) : "-";
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function MeetingPointsManagement() {
|
||
|
|
const [overview, setOverview] = useState<MeetingPointsOverviewVO | null>(null);
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
const [detailLoading, setDetailLoading] = useState(false);
|
||
|
|
const [records, setRecords] = useState<MeetingPointsLedgerListItemVO[]>([]);
|
||
|
|
const [total, setTotal] = useState(0);
|
||
|
|
const [detailOpen, setDetailOpen] = useState(false);
|
||
|
|
const [detail, setDetail] = useState<MeetingPointsLedgerDetailVO | null>(null);
|
||
|
|
const [params, setParams] = useState({
|
||
|
|
current: 1,
|
||
|
|
size: 20,
|
||
|
|
username: "",
|
||
|
|
pointsType: "",
|
||
|
|
});
|
||
|
|
|
||
|
|
const loadOverview = async () => {
|
||
|
|
const data = await getMeetingPointsOverview();
|
||
|
|
setOverview(data);
|
||
|
|
};
|
||
|
|
|
||
|
|
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 loadOverview();
|
||
|
|
void loadPage();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleSearch = () => {
|
||
|
|
const nextParams = { ...params, current: 1 };
|
||
|
|
setParams(nextParams);
|
||
|
|
void loadPage(nextParams);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleReset = () => {
|
||
|
|
const nextParams = {
|
||
|
|
current: 1,
|
||
|
|
size: 20,
|
||
|
|
username: "",
|
||
|
|
pointsType: "",
|
||
|
|
};
|
||
|
|
setParams(nextParams);
|
||
|
|
void loadPage(nextParams);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleRefresh = async () => {
|
||
|
|
await Promise.all([loadOverview(), loadPage()]);
|
||
|
|
message.success("已刷新积分数据");
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleOpenDetail = async (ledgerId: number) => {
|
||
|
|
setDetailLoading(true);
|
||
|
|
setDetailOpen(true);
|
||
|
|
try {
|
||
|
|
const data = await getMeetingPointsLedgerDetail(ledgerId);
|
||
|
|
setDetail(data);
|
||
|
|
} finally {
|
||
|
|
setDetailLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const columns = useMemo(
|
||
|
|
() => [
|
||
|
|
{
|
||
|
|
title: "用户名",
|
||
|
|
dataIndex: "ownerUserName",
|
||
|
|
key: "ownerUserName",
|
||
|
|
width: 140,
|
||
|
|
render: (value: string) => <Text strong>{value || "-"}</Text>,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
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>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
],
|
||
|
|
[],
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<PageContainer
|
||
|
|
title="积分管理"
|
||
|
|
subtitle="当前页面展示余额与消耗流水"
|
||
|
|
headerExtra={
|
||
|
|
<Space>
|
||
|
|
{/*<Tag color="processing">当前结算模式:{getAccountModeLabel(overview?.accountMode)}</Tag>*/}
|
||
|
|
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
|
||
|
|
刷新
|
||
|
|
</Button>
|
||
|
|
</Space>
|
||
|
|
}
|
||
|
|
toolbar={
|
||
|
|
<Space wrap size="middle">
|
||
|
|
<Input
|
||
|
|
placeholder="按用户名搜索"
|
||
|
|
value={params.username}
|
||
|
|
onChange={(event) => setParams((prev) => ({ ...prev, username: event.target.value }))}
|
||
|
|
style={{ width: 220 }}
|
||
|
|
prefix={<SearchOutlined className="text-gray-400" />}
|
||
|
|
allowClear
|
||
|
|
/>
|
||
|
|
<Select
|
||
|
|
style={{ width: 140 }}
|
||
|
|
value={params.pointsType}
|
||
|
|
onChange={(value) => setParams((prev) => ({ ...prev, pointsType: value }))}
|
||
|
|
options={POINTS_TYPE_OPTIONS}
|
||
|
|
/>
|
||
|
|
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
|
||
|
|
查询
|
||
|
|
</Button>
|
||
|
|
<Button onClick={handleReset}>重置</Button>
|
||
|
|
</Space>
|
||
|
|
}
|
||
|
|
>
|
||
|
|
|
||
|
|
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||
|
|
{/*<Col xs={24} md={6}>*/}
|
||
|
|
{/* <Card>*/}
|
||
|
|
{/* <Statistic title="当前结算账户" value={getAccountModeLabel(overview?.accountMode)} />*/}
|
||
|
|
{/* </Card>*/}
|
||
|
|
{/*</Col>*/}
|
||
|
|
<Col xs={24} md={6}>
|
||
|
|
<Card>
|
||
|
|
<Statistic title="剩余总积分" value={overview?.publicBalance ?? 0} />
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
<Col xs={24} md={6}>
|
||
|
|
<Card>
|
||
|
|
<Statistic title="累计消耗总积分" value={overview?.publicTotalPointsUsed ?? 0} />
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
<Col xs={24} md={6}>
|
||
|
|
<Card>
|
||
|
|
<Statistic title="累计消耗次数" value={overview?.totalChargeCount ?? 0} />
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
|
||
|
|
<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 any}
|
||
|
|
dataSource={records}
|
||
|
|
loading={loading}
|
||
|
|
totalCount={total}
|
||
|
|
scroll={{ y: "calc(100vh - 430px)", x: 1100 }}
|
||
|
|
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>
|
||
|
|
|
||
|
|
<Modal
|
||
|
|
title="消耗详情"
|
||
|
|
open={detailOpen}
|
||
|
|
onCancel={() => {
|
||
|
|
setDetailOpen(false);
|
||
|
|
setDetail(null);
|
||
|
|
}}
|
||
|
|
footer={[
|
||
|
|
<Button
|
||
|
|
key="close"
|
||
|
|
onClick={() => {
|
||
|
|
setDetailOpen(false);
|
||
|
|
setDetail(null);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
关闭
|
||
|
|
</Button>,
|
||
|
|
]}
|
||
|
|
width={720}
|
||
|
|
confirmLoading={detailLoading}
|
||
|
|
>
|
||
|
|
{detail && (
|
||
|
|
<Descriptions bordered column={1} size="small">
|
||
|
|
<Descriptions.Item label="用户名">{detail.ownerUserName || "-"}</Descriptions.Item>
|
||
|
|
{/*<Descriptions.Item label="扣费账户">{getAccountModeLabel(detail.chargeAccountType)}</Descriptions.Item>*/}
|
||
|
|
<Descriptions.Item label="消耗类型">{getPointsTypeLabel(detail.pointsType)}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="消耗积分">{detail.consumedPoints ?? 0}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="消耗时间">{formatDateTime(detail.createdAt)}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="会议标题">{detail.meetingTitle || "-"}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="触发类型">{getChargeTriggerLabel(detail.chargeTriggerType)}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="录音时长(秒)">{detail.audioDurationSeconds ?? "-"}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="计费分钟数">{detail.chargedMinutes ?? "-"}</Descriptions.Item>
|
||
|
|
{/*<Descriptions.Item label="计费单位数">{detail.billingUnits ?? "-"}</Descriptions.Item>*/}
|
||
|
|
{/*<Descriptions.Item label="单位分钟数">{detail.unitMinutesSnapshot ?? "-"}</Descriptions.Item>*/}
|
||
|
|
{/*<Descriptions.Item label="单位价格">{detail.costPerUnitSnapshot ?? "-"}</Descriptions.Item>*/}
|
||
|
|
{/*<Descriptions.Item label="应计总积分">{detail.totalPoints ?? 0}</Descriptions.Item>*/}
|
||
|
|
{/*<Descriptions.Item label="已扣总积分">{detail.chargedTotalPoints ?? 0}</Descriptions.Item>*/}
|
||
|
|
{/*<Descriptions.Item label="应计 ASR 积分">{detail.asrPoints ?? 0}</Descriptions.Item>*/}
|
||
|
|
{/*<Descriptions.Item label="已扣 ASR 积分">{detail.chargedAsrPoints ?? 0}</Descriptions.Item>*/}
|
||
|
|
{/*<Descriptions.Item label="应计 LLM 积分">{detail.llmPoints ?? 0}</Descriptions.Item>*/}
|
||
|
|
{/*<Descriptions.Item label="已扣 LLM 积分">{detail.chargedLlmPoints ?? 0}</Descriptions.Item>*/}
|
||
|
|
<Descriptions.Item label="扣费前余额">{detail.balanceBefore ?? "-"}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="扣费后余额">{detail.balanceAfter ?? "-"}</Descriptions.Item>
|
||
|
|
{/*<Descriptions.Item label="记录状态">{detail.summaryStatus || "-"}</Descriptions.Item>*/}
|
||
|
|
{/*<Descriptions.Item label="失败原因">{detail.failureReason || "-"}</Descriptions.Item>*/}
|
||
|
|
</Descriptions>
|
||
|
|
)}
|
||
|
|
</Modal>
|
||
|
|
</PageContainer>
|
||
|
|
);
|
||
|
|
}
|