|
|
@@ -1,8 +1,10 @@
|
|
|
"use client";
|
|
|
|
|
|
import { DualAxes } from "@ant-design/charts";
|
|
|
+import { DownloadOutlined } from "@ant-design/icons";
|
|
|
import {
|
|
|
App,
|
|
|
+ Button,
|
|
|
Card,
|
|
|
Col,
|
|
|
DatePicker,
|
|
|
@@ -17,6 +19,9 @@ import type { ColumnsType } from "antd/es/table";
|
|
|
import type { Dayjs } from "dayjs";
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
import {
|
|
|
+ exportOrderDataStatExcel,
|
|
|
+ exportPlaymateDataStatExcel,
|
|
|
+ exportUserDataStatExcel,
|
|
|
fetchOrderDataStatPage,
|
|
|
fetchPlaymateDataStatPage,
|
|
|
fetchUserDataStatPage,
|
|
|
@@ -221,6 +226,75 @@ const StatsDashboardPage: React.FC = () => {
|
|
|
void loadData(getDefaultRange(reportType));
|
|
|
}, [reportType]);
|
|
|
|
|
|
+ // ─── 导出 ────────────────────────────────────────────────────
|
|
|
+ const [exportUserLoading, setExportUserLoading] = useState(false);
|
|
|
+ const [exportOrderLoading, setExportOrderLoading] = useState(false);
|
|
|
+ const [exportPlaymateLoading, setExportPlaymateLoading] = useState(false);
|
|
|
+
|
|
|
+ const triggerDownload = (blob: Blob, filename: string | undefined, fallback: string) => {
|
|
|
+ if (!blob || blob.size === 0) {
|
|
|
+ message.error("导出失败:文件为空");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const now = new Date();
|
|
|
+ const pad = (n: number) => String(n).padStart(2, "0");
|
|
|
+ const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
|
+ const url = window.URL.createObjectURL(blob);
|
|
|
+ const a = document.createElement("a");
|
|
|
+ a.href = url;
|
|
|
+ a.download = filename || `${fallback}_${ts}.xlsx`;
|
|
|
+ document.body.appendChild(a);
|
|
|
+ a.click();
|
|
|
+ a.remove();
|
|
|
+ window.URL.revokeObjectURL(url);
|
|
|
+ message.success("导出成功");
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleExportUser = async () => {
|
|
|
+ setExportUserLoading(true);
|
|
|
+ try {
|
|
|
+ const { blob, filename } = await exportUserDataStatExcel(
|
|
|
+ buildQuery(effectiveRange),
|
|
|
+ );
|
|
|
+ triggerDownload(blob, filename, "user_stat");
|
|
|
+ } catch (err) {
|
|
|
+ console.error("Failed to export user stat excel:", err);
|
|
|
+ message.error("用户数据导出失败");
|
|
|
+ } finally {
|
|
|
+ setExportUserLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleExportOrder = async () => {
|
|
|
+ setExportOrderLoading(true);
|
|
|
+ try {
|
|
|
+ const { blob, filename } = await exportOrderDataStatExcel(
|
|
|
+ buildQuery(effectiveRange),
|
|
|
+ );
|
|
|
+ triggerDownload(blob, filename, "order_stat");
|
|
|
+ } catch (err) {
|
|
|
+ console.error("Failed to export order stat excel:", err);
|
|
|
+ message.error("订单数据导出失败");
|
|
|
+ } finally {
|
|
|
+ setExportOrderLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleExportPlaymate = async () => {
|
|
|
+ setExportPlaymateLoading(true);
|
|
|
+ try {
|
|
|
+ const { blob, filename } = await exportPlaymateDataStatExcel(
|
|
|
+ buildQuery(effectiveRange),
|
|
|
+ );
|
|
|
+ triggerDownload(blob, filename, "playmate_stat");
|
|
|
+ } catch (err) {
|
|
|
+ console.error("Failed to export playmate stat excel:", err);
|
|
|
+ message.error("陪玩师数据导出失败");
|
|
|
+ } finally {
|
|
|
+ setExportPlaymateLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
const handleRangeChange = (values: [Dayjs | null, Dayjs | null] | null) => {
|
|
|
if (!values || !values[0] || !values[1]) {
|
|
|
// User cleared the picker → fall back to default range
|
|
|
@@ -594,7 +668,19 @@ const StatsDashboardPage: React.FC = () => {
|
|
|
</Row>
|
|
|
|
|
|
{/* ── 用户数据表格 ── */}
|
|
|
- <Card title="用户数据">
|
|
|
+ <Card
|
|
|
+ title="用户数据"
|
|
|
+ extra={
|
|
|
+ <Button
|
|
|
+ icon={<DownloadOutlined />}
|
|
|
+ size="small"
|
|
|
+ loading={exportUserLoading}
|
|
|
+ onClick={() => void handleExportUser()}
|
|
|
+ >
|
|
|
+ 导出
|
|
|
+ </Button>
|
|
|
+ }
|
|
|
+ >
|
|
|
<Table<UserDataStatAdminDTO>
|
|
|
rowKey="id"
|
|
|
columns={userColumns}
|
|
|
@@ -607,7 +693,19 @@ const StatsDashboardPage: React.FC = () => {
|
|
|
</Card>
|
|
|
|
|
|
{/* ── 订单数据表格 ── */}
|
|
|
- <Card title="订单数据">
|
|
|
+ <Card
|
|
|
+ title="订单数据"
|
|
|
+ extra={
|
|
|
+ <Button
|
|
|
+ icon={<DownloadOutlined />}
|
|
|
+ size="small"
|
|
|
+ loading={exportOrderLoading}
|
|
|
+ onClick={() => void handleExportOrder()}
|
|
|
+ >
|
|
|
+ 导出
|
|
|
+ </Button>
|
|
|
+ }
|
|
|
+ >
|
|
|
<Table<OrderDataStatAdminDTO>
|
|
|
rowKey="id"
|
|
|
columns={orderColumns}
|
|
|
@@ -620,7 +718,19 @@ const StatsDashboardPage: React.FC = () => {
|
|
|
</Card>
|
|
|
|
|
|
{/* ── 陪玩师数据表格 ── */}
|
|
|
- <Card title="陪玩师数据">
|
|
|
+ <Card
|
|
|
+ title="陪玩师数据"
|
|
|
+ extra={
|
|
|
+ <Button
|
|
|
+ icon={<DownloadOutlined />}
|
|
|
+ size="small"
|
|
|
+ loading={exportPlaymateLoading}
|
|
|
+ onClick={() => void handleExportPlaymate()}
|
|
|
+ >
|
|
|
+ 导出
|
|
|
+ </Button>
|
|
|
+ }
|
|
|
+ >
|
|
|
<Table<PlaymateDataStatAdminDTO>
|
|
|
rowKey="id"
|
|
|
columns={playmateColumns}
|