Răsfoiți Sursa

Remove StatisticsPage component and update menu configuration for statistics dashboard

- Deleted the StatisticsPage component to streamline the dashboard.
- Updated menu configuration to include new child items for "大盘数据" (Dashboard Data) and "总体统计" (Overall Statistics) under the statistics section, enhancing navigation and access to statistical insights.
- Added new types related to statistics in the API definitions for better type management.
0es 3 săptămâni în urmă
părinte
comite
8f57bf418b

+ 640 - 0
src/app/(dashboard)/statistics/dashboard/page.tsx

@@ -0,0 +1,640 @@
+"use client";
+
+import { DualAxes } from "@ant-design/charts";
+import {
+  App,
+  Card,
+  Col,
+  DatePicker,
+  Row,
+  Segmented,
+  Space,
+  Spin,
+  Table,
+  Typography,
+} from "antd";
+import type { ColumnsType } from "antd/es/table";
+import type { Dayjs } from "dayjs";
+import { useCallback, useEffect, useState } from "react";
+import {
+  fetchOrderDataStatPage,
+  fetchPlaymateDataStatPage,
+  fetchUserDataStatPage,
+} from "@/services/statisticsDashboard";
+import type {
+  OrderDataStatAdminDTO,
+  PlaymateDataStatAdminDTO,
+  StatAdminQuery,
+  UserDataStatAdminDTO,
+} from "@/types/api/statisticsDashboard";
+import dayjs from "@/utils/dayjs";
+
+const { RangePicker } = DatePicker;
+const { Title, Text } = Typography;
+
+const REPORT_TYPE_OPTIONS = [
+  { label: "日报", value: 0 },
+  { label: "周报", value: 1 },
+  { label: "月报", value: 2 },
+];
+
+const PAGE_SIZE = 50;
+
+/** Returns the default [start, end] range for a given report type. */
+function getDefaultRange(type: number): [Dayjs, Dayjs] {
+  const today = dayjs().startOf("day");
+  switch (type) {
+    case 0: // 日报:过去 10 天
+      return [today.subtract(9, "day"), today];
+    case 2: // 月报:过去 6 个月
+      return [today.subtract(5, "month").startOf("month"), today];
+    default: // 周报:过去 8 周
+      return [today.subtract(7, "week").startOf("isoWeek"), today];
+  }
+}
+
+/**
+ * Generates all period IDs covered by [start, end] for the given report type.
+ * - type 0 (daily):   "YYYY-MM-DD"
+ * - type 1 (weekly):  "YYYY-Www"  (ISO week)
+ * - type 2 (monthly): "YYYY-MM"
+ */
+function generatePeriodIds(start: Dayjs, end: Dayjs, type: number): string[] {
+  const ids: string[] = [];
+  if (type === 0) {
+    let cur = start.startOf("day");
+    while (!cur.isAfter(end)) {
+      ids.push(cur.format("YYYY-MM-DD"));
+      cur = cur.add(1, "day");
+    }
+  } else if (type === 1) {
+    let cur = start.startOf("isoWeek");
+    while (!cur.isAfter(end)) {
+      const week = cur.isoWeek();
+      const year = cur.isoWeekYear();
+      ids.push(`${year}-W${String(week).padStart(2, "0")}`);
+      cur = cur.add(1, "week");
+    }
+  } else {
+    let cur = start.startOf("month");
+    while (!cur.isAfter(end)) {
+      ids.push(cur.format("YYYY-MM"));
+      cur = cur.add(1, "month");
+    }
+  }
+  return ids;
+}
+
+function fillUserData(
+  items: UserDataStatAdminDTO[],
+  ids: string[],
+): UserDataStatAdminDTO[] {
+  const map = new Map(items.map((x) => [x.id, x]));
+  return ids.map(
+    (id) =>
+      map.get(id) ?? {
+        id,
+        totalUserNum: 0,
+        newRegisterUserNum: 0,
+        activeUserNum: 0,
+        activeUserRetentionRate1Day: 0,
+        activeUserRetentionRate3Day: 0,
+        activeUserRetentionRate7Day: 0,
+        newRegisterUserRetentionRate1Day: 0,
+        newRegisterUserRetentionRate3Day: 0,
+        newRegisterUserRetentionRate7Day: 0,
+      },
+  );
+}
+
+function fillOrderData(
+  items: OrderDataStatAdminDTO[],
+  ids: string[],
+): OrderDataStatAdminDTO[] {
+  const map = new Map(items.map((x) => [x.id, x]));
+  return ids.map(
+    (id) =>
+      map.get(id) ?? {
+        id,
+        totalAmount: 0,
+        orderCount: 0,
+        statusOkCount: 0,
+        statusRefundCount: 0,
+        uniqueSellersCount: 0,
+        totalBuyers: 0,
+        completedBuyers: 0,
+        repeatCompletedBuyers: 0,
+        avgAmountPerBuyer: 0,
+        repeatRate: 0,
+        successRate: 0,
+        refundRate: 0,
+      },
+  );
+}
+
+function fillPlaymateData(
+  items: PlaymateDataStatAdminDTO[],
+  ids: string[],
+): PlaymateDataStatAdminDTO[] {
+  const map = new Map(items.map((x) => [x.id, x]));
+  return ids.map(
+    (id) =>
+      map.get(id) ?? {
+        id,
+        totalPlaymateNum: 0,
+        newPlaymateNum: 0,
+        activePlaymateNum: 0,
+        acceptOrderPlaymateNum: 0,
+        totalOrderAmount: 0,
+        acceptOrderRate: 0,
+        avgAmountAcceptOrder: 0,
+      },
+  );
+}
+
+function fmtPct(val: number | undefined | null): string {
+  if (val == null) return "-";
+  return `${(val * 100).toFixed(2)}%`;
+}
+
+function fmtAmt(val: number | undefined | null): string {
+  if (val == null) return "-";
+  return `¥${val.toFixed(2)}`;
+}
+
+const StatsDashboardPage: React.FC = () => {
+  const { message } = App.useApp();
+
+  const [reportType, setReportType] = useState<number>(1);
+  // null = use default range derived from reportType
+  const [dateRange, setDateRange] = useState<[Dayjs, Dayjs] | null>(null);
+  const [loading, setLoading] = useState(false);
+
+  /** The range actually in effect (user-selected or type-default). */
+  const effectiveRange = dateRange ?? getDefaultRange(reportType);
+
+  const [userData, setUserData] = useState<UserDataStatAdminDTO[]>([]);
+  const [orderData, setOrderData] = useState<OrderDataStatAdminDTO[]>([]);
+  const [playmateData, setPlaymateData] = useState<PlaymateDataStatAdminDTO[]>(
+    [],
+  );
+
+  const buildQuery = useCallback(
+    (range: [Dayjs, Dayjs]): StatAdminQuery => ({
+      pageIndex: 1,
+      pageSize: PAGE_SIZE,
+      type: reportType,
+      startDay: range[0].format("YYYY-MM-DD"),
+      endDay: range[1].format("YYYY-MM-DD"),
+    }),
+    [reportType],
+  );
+
+  const loadData = useCallback(
+    async (range: [Dayjs, Dayjs]) => {
+      setLoading(true);
+      try {
+        const query = buildQuery(range);
+        const ids = generatePeriodIds(range[0], range[1], reportType);
+        const [userRes, orderRes, playmateRes] = await Promise.all([
+          fetchUserDataStatPage(query),
+          fetchOrderDataStatPage(query),
+          fetchPlaymateDataStatPage(query),
+        ]);
+        setUserData(fillUserData(userRes?.items ?? [], ids));
+        setOrderData(fillOrderData(orderRes?.items ?? [], ids));
+        setPlaymateData(fillPlaymateData(playmateRes?.items ?? [], ids));
+      } catch (err) {
+        console.error("Failed to load dashboard data:", err);
+        message.error("加载大盘数据失败");
+      } finally {
+        setLoading(false);
+      }
+    },
+    [buildQuery, message, reportType],
+  );
+
+  // When reportType changes: clear user-selected range and reload with new default.
+  // biome-ignore lint/correctness/useExhaustiveDependencies: loadData changes with reportType; resetting dateRange here is intentional
+  useEffect(() => {
+    setDateRange(null);
+    void loadData(getDefaultRange(reportType));
+  }, [reportType]);
+
+  const handleRangeChange = (values: [Dayjs | null, Dayjs | null] | null) => {
+    if (!values || !values[0] || !values[1]) {
+      // User cleared the picker → fall back to default range
+      const defaultRange = getDefaultRange(reportType);
+      setDateRange(null);
+      void loadData(defaultRange);
+      return;
+    }
+    const range: [Dayjs, Dayjs] = [values[0], values[1]];
+    setDateRange(range);
+    void loadData(range);
+  };
+
+  // ─── 用户数据表格列 ───────────────────────────────────────────
+  const userColumns: ColumnsType<UserDataStatAdminDTO> = [
+    { title: "周期", dataIndex: "id", key: "id", fixed: "left", width: 130 },
+    {
+      title: "总用户数",
+      dataIndex: "totalUserNum",
+      key: "totalUserNum",
+      width: 100,
+    },
+    {
+      title: "新注册",
+      dataIndex: "newRegisterUserNum",
+      key: "newRegisterUserNum",
+      width: 90,
+    },
+    {
+      title: "活跃数",
+      dataIndex: "activeUserNum",
+      key: "activeUserNum",
+      width: 90,
+    },
+    {
+      title: "活跃留存",
+      children: [
+        {
+          title: "次日",
+          dataIndex: "activeUserRetentionRate1Day",
+          key: "activeR1",
+          width: 80,
+          render: fmtPct,
+        },
+        {
+          title: "3日",
+          dataIndex: "activeUserRetentionRate3Day",
+          key: "activeR3",
+          width: 80,
+          render: fmtPct,
+        },
+        {
+          title: "7日",
+          dataIndex: "activeUserRetentionRate7Day",
+          key: "activeR7",
+          width: 80,
+          render: fmtPct,
+        },
+      ],
+    },
+    {
+      title: "新注册活跃留存",
+      children: [
+        {
+          title: "次日",
+          dataIndex: "newRegisterUserRetentionRate1Day",
+          key: "newR1",
+          width: 80,
+          render: fmtPct,
+        },
+        {
+          title: "3日",
+          dataIndex: "newRegisterUserRetentionRate3Day",
+          key: "newR3",
+          width: 80,
+          render: fmtPct,
+        },
+        {
+          title: "7日",
+          dataIndex: "newRegisterUserRetentionRate7Day",
+          key: "newR7",
+          width: 80,
+          render: fmtPct,
+        },
+      ],
+    },
+  ];
+
+  // ─── 订单数据表格列 ───────────────────────────────────────────
+  const orderColumns: ColumnsType<OrderDataStatAdminDTO> = [
+    { title: "周期", dataIndex: "id", key: "id", fixed: "left", width: 130 },
+    {
+      title: "订单金额",
+      dataIndex: "totalAmount",
+      key: "totalAmount",
+      width: 110,
+      render: fmtAmt,
+    },
+    { title: "订单量", dataIndex: "orderCount", key: "orderCount", width: 90 },
+    {
+      title: "下单用户数",
+      dataIndex: "totalBuyers",
+      key: "totalBuyers",
+      width: 100,
+    },
+    {
+      title: "接单陪玩师",
+      dataIndex: "uniqueSellersCount",
+      key: "uniqueSellersCount",
+      width: 100,
+    },
+    {
+      title: "人均下单金额",
+      dataIndex: "avgAmountPerBuyer",
+      key: "avgAmountPerBuyer",
+      width: 120,
+      render: fmtAmt,
+    },
+    {
+      title: "复购率",
+      dataIndex: "repeatRate",
+      key: "repeatRate",
+      width: 90,
+      render: fmtPct,
+    },
+    {
+      title: "支付成功率",
+      dataIndex: "successRate",
+      key: "successRate",
+      width: 100,
+      render: fmtPct,
+    },
+    {
+      title: "退款率",
+      dataIndex: "refundRate",
+      key: "refundRate",
+      width: 90,
+      render: fmtPct,
+    },
+  ];
+
+  // ─── 陪玩师数据表格列 ─────────────────────────────────────────
+  const playmateColumns: ColumnsType<PlaymateDataStatAdminDTO> = [
+    { title: "周期", dataIndex: "id", key: "id", fixed: "left", width: 130 },
+    {
+      title: "陪玩师总数",
+      dataIndex: "totalPlaymateNum",
+      key: "totalPlaymateNum",
+      width: 100,
+    },
+    {
+      title: "新增陪玩师",
+      dataIndex: "newPlaymateNum",
+      key: "newPlaymateNum",
+      width: 90,
+    },
+    {
+      title: "活跃陪玩师数",
+      dataIndex: "activePlaymateNum",
+      key: "activePlaymateNum",
+      width: 90,
+    },
+    {
+      title: "接单陪玩师数",
+      dataIndex: "acceptOrderPlaymateNum",
+      key: "acceptOrderPlaymateNum",
+      width: 90,
+    },
+    {
+      title: "接单率",
+      dataIndex: "acceptOrderRate",
+      key: "acceptOrderRate",
+      width: 90,
+      render: fmtPct,
+    },
+    {
+      title: "人均接单金额",
+      dataIndex: "avgAmountAcceptOrder",
+      key: "avgAmountAcceptOrder",
+      width: 120,
+      render: fmtAmt,
+    },
+  ];
+
+  // ─── 图表 1:下单金额情况(柱形 + 折线双轴)──────────────────
+  const amountChartData = orderData.map((item) => ({
+    id: item.id,
+    totalAmount: item.totalAmount,
+    avgAmountPerBuyer: item.avgAmountPerBuyer,
+  }));
+
+  const amountChartConfig = {
+    data: amountChartData,
+    xField: "id",
+    legend: true,
+    children: [
+      {
+        type: "interval",
+        yField: "totalAmount",
+        axis: {
+          y: {
+            title: "订单金额 (¥)",
+            labelFormatter: (v: number) => `¥${v}`,
+            position: "left",
+          },
+        },
+        label: {
+          text: (d: { totalAmount: number }) => `¥${d.totalAmount.toFixed(0)}`,
+          position: "top",
+          style: { fontSize: 10 },
+        },
+        style: { fill: "#4096ff" },
+        tooltip: {
+          items: [
+            (d: { totalAmount: number }) => ({
+              name: "订单金额",
+              value: `¥${d.totalAmount.toFixed(2)}`,
+            }),
+          ],
+        },
+      },
+      {
+        type: "line",
+        yField: "avgAmountPerBuyer",
+        axis: {
+          y: {
+            title: "人均下单金额 (¥)",
+            labelFormatter: (v: number) => `¥${v}`,
+            position: "right",
+          },
+        },
+        label: {
+          text: (d: { avgAmountPerBuyer: number }) =>
+            `¥${d.avgAmountPerBuyer.toFixed(0)}`,
+          position: "top",
+          style: { fontSize: 10 },
+        },
+        style: { stroke: "#ff7a00", lineWidth: 2 },
+        point: { fill: "#ff7a00", r: 4 },
+        tooltip: {
+          items: [
+            (d: { avgAmountPerBuyer: number }) => ({
+              name: "人均下单金额",
+              value: `¥${d.avgAmountPerBuyer.toFixed(2)}`,
+            }),
+          ],
+        },
+      },
+    ],
+  };
+
+  // ─── 图表 2:下单用户情况(柱形 + 折线双轴)──────────────────
+  const buyerChartData = orderData.map((item) => ({
+    id: item.id,
+    totalBuyers: item.totalBuyers,
+    repeatRate: item.repeatRate,
+  }));
+
+  const buyerChartConfig = {
+    data: buyerChartData,
+    xField: "id",
+    legend: true,
+    children: [
+      {
+        type: "interval",
+        yField: "totalBuyers",
+        axis: {
+          y: {
+            title: "下单用户数",
+            position: "left",
+          },
+        },
+        label: {
+          text: (d: { totalBuyers: number }) => `${d.totalBuyers}`,
+          position: "top",
+          style: { fontSize: 10 },
+        },
+        style: { fill: "#36cfc9" },
+        tooltip: {
+          items: [
+            (d: { totalBuyers: number }) => ({
+              name: "下单用户数",
+              value: `${d.totalBuyers}`,
+            }),
+          ],
+        },
+      },
+      {
+        type: "line",
+        yField: "repeatRate",
+        axis: {
+          y: {
+            title: "复购率",
+            labelFormatter: (v: number) => `${(v * 100).toFixed(1)}%`,
+            position: "right",
+          },
+        },
+        label: {
+          text: (d: { repeatRate: number }) =>
+            `${(d.repeatRate * 100).toFixed(1)}%`,
+          position: "top",
+          style: { fontSize: 10 },
+        },
+        style: { stroke: "#f759ab", lineWidth: 2 },
+        point: { fill: "#f759ab", r: 4 },
+        tooltip: {
+          items: [
+            (d: { repeatRate: number }) => ({
+              name: "复购率",
+              value: `${(d.repeatRate * 100).toFixed(2)}%`,
+            }),
+          ],
+        },
+      },
+    ],
+  };
+
+  return (
+    <div className="flex flex-col gap-6 p-6">
+      {/* 页头 */}
+      <div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
+        <div>
+          <Title level={3} style={{ marginBottom: 0 }}>
+            大盘数据
+          </Title>
+          <Text type="secondary">
+            用户 / 订单 / 陪玩师周期统计数据,支持日报、周报、月报切换
+          </Text>
+        </div>
+        <Space wrap>
+          <Segmented
+            options={REPORT_TYPE_OPTIONS}
+            value={reportType}
+            onChange={(v) => setReportType(v as number)}
+          />
+          <RangePicker
+            allowClear
+            value={effectiveRange}
+            onChange={handleRangeChange as never}
+            format="YYYY-MM-DD"
+          />
+        </Space>
+      </div>
+
+      <Spin spinning={loading}>
+        <Space direction="vertical" size="large" style={{ width: "100%" }}>
+          {/* ── 图表区 ── */}
+          <Row gutter={[16, 16]}>
+            <Col xs={24} xl={12}>
+              <Card title="下单金额情况">
+                {orderData.length > 0 ? (
+                  <DualAxes {...amountChartConfig} height={300} />
+                ) : (
+                  <div className="flex h-[300px] items-center justify-center">
+                    <Text type="secondary">暂无数据</Text>
+                  </div>
+                )}
+              </Card>
+            </Col>
+            <Col xs={24} xl={12}>
+              <Card title="下单用户情况">
+                {orderData.length > 0 ? (
+                  <DualAxes {...buyerChartConfig} height={300} />
+                ) : (
+                  <div className="flex h-[300px] items-center justify-center">
+                    <Text type="secondary">暂无数据</Text>
+                  </div>
+                )}
+              </Card>
+            </Col>
+          </Row>
+
+          {/* ── 用户数据表格 ── */}
+          <Card title="用户数据">
+            <Table<UserDataStatAdminDTO>
+              rowKey="id"
+              columns={userColumns}
+              dataSource={userData}
+              pagination={false}
+              size="small"
+              scroll={{ x: "max-content" }}
+              bordered
+            />
+          </Card>
+
+          {/* ── 订单数据表格 ── */}
+          <Card title="订单数据">
+            <Table<OrderDataStatAdminDTO>
+              rowKey="id"
+              columns={orderColumns}
+              dataSource={orderData}
+              pagination={false}
+              size="small"
+              scroll={{ x: "max-content" }}
+              bordered
+            />
+          </Card>
+
+          {/* ── 陪玩师数据表格 ── */}
+          <Card title="陪玩师数据">
+            <Table<PlaymateDataStatAdminDTO>
+              rowKey="id"
+              columns={playmateColumns}
+              dataSource={playmateData}
+              pagination={false}
+              size="small"
+              scroll={{ x: "max-content" }}
+              bordered
+            />
+          </Card>
+        </Space>
+      </Spin>
+    </div>
+  );
+};
+
+export default StatsDashboardPage;

+ 0 - 0
src/app/(dashboard)/statistics/page.tsx → src/app/(dashboard)/statistics/index/page.tsx


+ 16 - 0
src/config/menus.tsx

@@ -375,6 +375,22 @@ export const menuConfig: MenuItem[] = [
     icon: <BarChartOutlined />,
     sortOrder: 11,
     permission: "/statistics",
+    children: [
+      {
+        key: "statistics-dashboard",
+        label: "大盘数据",
+        path: "/statistics/dashboard",
+        icon: <BarChartOutlined />,
+        permission: "/statistics/dashboard",
+      },
+      {
+        key: "statistics-index",
+        label: "总体统计",
+        path: "/statistics/index",
+        icon: <BarChartOutlined />,
+        permission: "/statistics/index",
+      },
+    ],
   },
   {
     key: "system",

+ 34 - 0
src/services/statisticsDashboard.ts

@@ -0,0 +1,34 @@
+import { post } from "@/lib/request";
+import type {
+  PagerOrderDataStatAdminDTO,
+  PagerPlaymateDataStatAdminDTO,
+  PagerUserDataStatAdminDTO,
+  StatAdminQuery,
+} from "@/types/api/statisticsDashboard";
+
+export async function fetchUserDataStatPage(
+  query: StatAdminQuery,
+): Promise<PagerUserDataStatAdminDTO> {
+  return post<PagerUserDataStatAdminDTO>(
+    "/stat/mananger/userDataStatPage",
+    query,
+  );
+}
+
+export async function fetchOrderDataStatPage(
+  query: StatAdminQuery,
+): Promise<PagerOrderDataStatAdminDTO> {
+  return post<PagerOrderDataStatAdminDTO>(
+    "/stat/mananger/orderDataStatPage",
+    query,
+  );
+}
+
+export async function fetchPlaymateDataStatPage(
+  query: StatAdminQuery,
+): Promise<PagerPlaymateDataStatAdminDTO> {
+  return post<PagerPlaymateDataStatAdminDTO>(
+    "/stat/mananger/playmateDataStatPage",
+    query,
+  );
+}

+ 10 - 0
src/types/api/index.ts

@@ -202,6 +202,16 @@ export type {
   UserVoiceApplyAdminQuery,
   UserVoiceApprovalAdminDTO,
 } from "./userVoiceApply";
+// 大盘统计类型
+export type {
+  OrderDataStatAdminDTO,
+  PagerOrderDataStatAdminDTO,
+  PagerPlaymateDataStatAdminDTO,
+  PagerUserDataStatAdminDTO,
+  PlaymateDataStatAdminDTO,
+  StatAdminQuery,
+  UserDataStatAdminDTO,
+} from "./statisticsDashboard";
 // Wallet types
 export type {
   PagerWalletDiamondRecordAdminDTO,

+ 110 - 0
src/types/api/statisticsDashboard.ts

@@ -0,0 +1,110 @@
+// 大盘统计相关类型定义
+
+import type { QuerySort } from "./common";
+
+// 统计查询公共参数
+export interface StatAdminQuery {
+  pageSize?: number;
+  pageIndex?: number;
+  sort?: QuerySort;
+  /** 创建时间起始日期: yyyy-MM-dd */
+  startDay?: string;
+  /** 创建时间结束日期: yyyy-MM-dd */
+  endDay?: string;
+  /** 0:日报,1:周报,2:月报 */
+  type?: number;
+  skip?: number;
+}
+
+// 用户数据统计 DTO
+export interface UserDataStatAdminDTO {
+  /** 日期(日: yyyy-mm-dd;周: yyyy-Www;月: yyyy-mm) */
+  id: string;
+  /** 总用户数量 */
+  totalUserNum: number;
+  /** 当日注册人数 */
+  newRegisterUserNum: number;
+  /** 当日活跃人数 */
+  activeUserNum: number;
+  /** 活跃用户留存率-次日留存率 */
+  activeUserRetentionRate1Day: number;
+  /** 活跃用户留存率-3日留存率 */
+  activeUserRetentionRate3Day: number;
+  /** 活跃用户留存率-七日留存率 */
+  activeUserRetentionRate7Day: number;
+  /** 新注册用户-次日留存率 */
+  newRegisterUserRetentionRate1Day: number;
+  /** 新注册用户-3日留存率 */
+  newRegisterUserRetentionRate3Day: number;
+  /** 新注册用户-七日留存率 */
+  newRegisterUserRetentionRate7Day: number;
+}
+
+export interface PagerUserDataStatAdminDTO {
+  total: number;
+  pageIndex: number;
+  pageSize: number;
+  items: UserDataStatAdminDTO[];
+}
+
+// 订单数据统计 DTO
+export interface OrderDataStatAdminDTO {
+  /** 日期(日: yyyy-mm-dd;周: yyyy-Www;月: yyyy-mm) */
+  id: string;
+  /** 订单总金额 */
+  totalAmount: number;
+  /** 总订单数 */
+  orderCount: number;
+  /** 已完成订单数 */
+  statusOkCount: number;
+  /** 退款/取消订单数量 */
+  statusRefundCount: number;
+  /** 接单陪玩去重数 */
+  uniqueSellersCount: number;
+  /** 下单用户去重数(所有订单) */
+  totalBuyers: number;
+  /** 有完成订单的用户数 */
+  completedBuyers: number;
+  /** 完成订单复购用户数(≥2笔完成订单) */
+  repeatCompletedBuyers: number;
+  /** 人均下单金额 */
+  avgAmountPerBuyer: number;
+  /** 复购率 */
+  repeatRate: number;
+  /** 成单率 */
+  successRate: number;
+  /** 退款率 */
+  refundRate: number;
+}
+
+export interface PagerOrderDataStatAdminDTO {
+  total: number;
+  pageIndex: number;
+  pageSize: number;
+  items: OrderDataStatAdminDTO[];
+}
+
+// 陪玩师数据统计 DTO
+export interface PlaymateDataStatAdminDTO {
+  id: string;
+  /** 总陪玩师累计 */
+  totalPlaymateNum: number;
+  /** 新玩师数量 */
+  newPlaymateNum: number;
+  activePlaymateNum: number;
+  /** 接单陪玩师人数 */
+  acceptOrderPlaymateNum: number;
+  /** 总订单金额 */
+  totalOrderAmount: number;
+  /** 接单率 */
+  acceptOrderRate: number;
+  /** 人均接单金额 */
+  avgAmountAcceptOrder: number;
+}
+
+export interface PagerPlaymateDataStatAdminDTO {
+  total: number;
+  pageIndex: number;
+  pageSize: number;
+  items: PlaymateDataStatAdminDTO[];
+}

+ 3 - 1
src/utils/dayjs.ts

@@ -1,10 +1,12 @@
 import dayjs from "dayjs";
+import isoWeek from "dayjs/plugin/isoWeek";
 import timezone from "dayjs/plugin/timezone";
 import utc from "dayjs/plugin/utc";
 
-// Enable UTC and timezone support
+// Enable UTC, timezone, and ISO week support
 dayjs.extend(utc);
 dayjs.extend(timezone);
+dayjs.extend(isoWeek);
 
 // Set default timezone to Indonesia (GMT+7)
 dayjs.tz.setDefault("Asia/Jakarta");