Ver Fonte

bashboard改造

guoziyun há 1 mês atrás
pai
commit
3a78ee7159

+ 26 - 0
oms/dist/src/controllers/dashboardController.js

@@ -69,6 +69,32 @@ class DashboardController {
             });
         }
     }
+    /**
+     * 获取最近 N 天上新作品分日数据(用于 tab 展示)
+     * GET /api/dashboard/new-artwork-tabs?days=7&topPerDay=20
+     */
+    async getNewArtworkTabs(req, res) {
+        try {
+            const days = Math.min(Math.max(parseInt(req.query.days) || 7, 1), 30);
+            const topPerDay = Math.min(Math.max(parseInt(req.query.topPerDay) || 20, 1), 100);
+            console.log(`[DashboardController] getNewArtworkTabs start, days=${days}, topPerDay=${topPerDay}`);
+            const startedAt = Date.now();
+            const data = await dashboardService_1.default.getNewArtworkTabs(days, topPerDay);
+            console.log(`[DashboardController] getNewArtworkTabs end, durationMs=${Date.now() - startedAt}`);
+            res.status(200).json({
+                success: true,
+                data,
+            });
+        }
+        catch (error) {
+            console.error("[DashboardController] Error in getNewArtworkTabs:", error);
+            res.status(500).json({
+                success: false,
+                message: "Failed to fetch new artwork tabs",
+                error: error.message,
+            });
+        }
+    }
 }
 const dashboardController = new DashboardController();
 exports.default = dashboardController;

+ 1 - 0
oms/dist/src/routes/apiRoutes.js

@@ -38,6 +38,7 @@ router.get("/message/daily/trends/by-image/:image", messageRecordController_1.de
 // 新增:Dashboard 路由
 router.get("/dashboard/kpi", dashboardController_1.default.getKpi);
 router.get("/dashboard/artwork-stats", dashboardController_1.default.getArtworkStats);
+router.get("/dashboard/new-artwork-tabs", dashboardController_1.default.getNewArtworkTabs);
 // 应用认证中间件,保护所有下面的路由
 router.use(authMiddleware_1.authMiddleware);
 // User routes

+ 138 - 29
oms/dist/src/services/dashboardService.js

@@ -5,10 +5,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
 Object.defineProperty(exports, "__esModule", { value: true });
 const clients_1 = require("./clients");
 const dayjs_1 = __importDefault(require("dayjs"));
+const artModel_1 = __importDefault(require("../models/artModel"));
 class DashboardService {
     constructor() {
         this.eventsTable = "events";
     }
+    getDayLabelByIndex(index) {
+        if (index === 0)
+            return "今日";
+        if (index === 1)
+            return "昨天";
+        if (index === 2)
+            return "前天";
+        return `${index}天前`;
+    }
     /**
      * 获取 KPI 数据:日活用户数、日活曲线、广告收益、收益曲线
      * @param dateRange 显示过去 N 天的数据,默认 7 天
@@ -19,7 +29,7 @@ class DashboardService {
             const startDate = (0, dayjs_1.default)().subtract(dateRange - 1, "day").format("YYYY-MM-DD");
             const endDate = (0, dayjs_1.default)().format("YYYY-MM-DD");
             const today = (0, dayjs_1.default)().format("YYYY-MM-DD");
-            // 查询日活用户数和收益的聚合数据
+            // 查询日活用户数聚合数据
             const dauQuery = `
         SELECT 
           toDate(time) as date,
@@ -30,17 +40,6 @@ class DashboardService {
           AND project = 1
         GROUP BY date
         ORDER BY date
-      `;
-            const revenueQuery = `
-        SELECT 
-          toDate(time) as date,
-          sumIf(revenue, event = 'revenue' AND revenue > 0) as total_revenue
-        FROM ${this.eventsTable}
-        WHERE toDate(time) >= '${startDate}' 
-          AND toDate(time) <= '${endDate}'
-          AND project = 1
-        GROUP BY date
-        ORDER BY date
       `;
             // 获取今日具体数据
             const todayDauQuery = `
@@ -48,20 +47,12 @@ class DashboardService {
         FROM ${this.eventsTable}
         WHERE toDate(time) = '${today}' AND project = 1
       `;
-            const todayRevenueQuery = `
-        SELECT sumIf(revenue, event = 'revenue' AND revenue > 0) as total_revenue
-        FROM ${this.eventsTable}
-        WHERE toDate(time) = '${today}' AND project = 1
-      `;
-            // 执行所有查询
-            const [dauTrend, revenueTrend, todayDauResult, todayRevenueResult] = await Promise.all([
+            // 执行查询
+            const [dauTrend, todayDauResult] = await Promise.all([
                 clients_1.clickhouseService.queryEvents(dauQuery),
-                clients_1.clickhouseService.queryEvents(revenueQuery),
                 clients_1.clickhouseService.queryEvents(todayDauQuery),
-                clients_1.clickhouseService.queryEvents(todayRevenueQuery),
             ]);
             const todayDau = todayDauResult?.[0]?.dau ?? 0;
-            const todayRevenue = todayRevenueResult?.[0]?.total_revenue ?? 0;
             return {
                 dau: {
                     today: todayDau,
@@ -70,13 +61,6 @@ class DashboardService {
                         dau: item.dau,
                     })),
                 },
-                revenue: {
-                    today: parseFloat(todayRevenue.toFixed(2)),
-                    trend: revenueTrend.map((item) => ({
-                        date: item.date,
-                        revenue: parseFloat(item.total_revenue.toFixed(2)),
-                    })),
-                },
             };
         }
         catch (error) {
@@ -138,5 +122,130 @@ class DashboardService {
             throw error;
         }
     }
+    /**
+     * 获取最近 N 天上新作品分日统计(点击率/完成率)
+     * @param days 天数,默认 7
+     * @param topPerDay 每日最多返回作品数,默认 20
+     */
+    async getNewArtworkTabs(days = 7, topPerDay = 20) {
+        try {
+            const latestPublished = await artModel_1.default.findOne({
+                publishTime: { $exists: true, $ne: null },
+                drop: { $ne: true },
+            })
+                .sort({ publishTime: -1 })
+                .select({ publishTime: 1 })
+                .lean();
+            if (!latestPublished?.publishTime) {
+                return { tabs: [] };
+            }
+            const safeDays = Math.max(1, Math.min(30, Math.floor(days || 7)));
+            const latestDayStart = (0, dayjs_1.default)(latestPublished.publishTime).startOf("day");
+            const rangeStart = latestDayStart.subtract(safeDays - 1, "day");
+            const rangeEnd = latestDayStart.add(1, "day");
+            const arts = await artModel_1.default.find({
+                publishTime: {
+                    $gte: rangeStart.toDate(),
+                    $lt: rangeEnd.toDate(),
+                },
+                drop: { $ne: true },
+            })
+                .select({ _id: 1, name: 1, pageId: 1, publishTime: 1 })
+                .lean();
+            const dayKeys = Array.from({ length: safeDays }, (_, index) => latestDayStart.subtract(index, "day").format("YYYY-MM-DD"));
+            const artsByDay = new Map();
+            for (const dayKey of dayKeys) {
+                artsByDay.set(dayKey, []);
+            }
+            const allResIds = [];
+            for (const art of arts) {
+                if (!art.publishTime)
+                    continue;
+                const dayKey = (0, dayjs_1.default)(art.publishTime).format("YYYY-MM-DD");
+                if (!artsByDay.has(dayKey))
+                    continue;
+                const resId = String(art._id);
+                allResIds.push(resId);
+                artsByDay.get(dayKey)?.push({
+                    resId,
+                    name: art.name || "未命名作品",
+                    pageId: String(art.pageId || ""),
+                });
+            }
+            const uniqueResIds = Array.from(new Set(allResIds));
+            const oldestDay = dayKeys[dayKeys.length - 1];
+            const newestDay = dayKeys[0];
+            const dauQuery = `
+        SELECT
+          toDate(time) AS date,
+          countDistinct(uid) AS dau
+        FROM ${this.eventsTable}
+        WHERE project = 1
+          AND toDate(time) >= '${oldestDay}'
+          AND toDate(time) <= '${newestDay}'
+        GROUP BY date
+        ORDER BY date
+      `;
+            const dauRows = await clients_1.clickhouseService.queryEvents(dauQuery);
+            const dauByDay = new Map(dauRows.map((row) => [String(row.date), row.dau]));
+            const statsByDayAndRes = new Map();
+            if (uniqueResIds.length > 0) {
+                const inClause = uniqueResIds.map((id) => `'${id}'`).join(",");
+                const artworkStatsQuery = `
+          SELECT
+            toDate(time) AS date,
+            res AS resId,
+            countIf(event = 'color_start') AS clickCount,
+            countIf(event = 'color_done') AS completionCount
+          FROM ${this.eventsTable}
+          WHERE project = 1
+            AND toDate(time) >= '${oldestDay}'
+            AND toDate(time) <= '${newestDay}'
+            AND event IN ('color_start', 'color_done')
+            AND res IN (${inClause})
+          GROUP BY date, res
+        `;
+                const statRows = await clients_1.clickhouseService.queryEvents(artworkStatsQuery);
+                for (const row of statRows) {
+                    statsByDayAndRes.set(`${row.date}|${row.resId}`, {
+                        clickCount: row.clickCount,
+                        completionCount: row.completionCount,
+                    });
+                }
+            }
+            const tabs = dayKeys.map((dayKey, index) => {
+                const dau = dauByDay.get(dayKey) ?? 0;
+                const dayArts = (artsByDay.get(dayKey) || []).map((art) => {
+                    const stat = statsByDayAndRes.get(`${dayKey}|${art.resId}`) || {
+                        clickCount: 0,
+                        completionCount: 0,
+                    };
+                    const clickRate = dau > 0 ? stat.clickCount / dau : 0;
+                    const completionRate = stat.clickCount > 0 ? stat.completionCount / stat.clickCount : 0;
+                    return {
+                        resId: art.resId,
+                        name: art.name,
+                        thumbnail: art.pageId ? `/thumbs/v2/page/320/${art.pageId}.png` : "",
+                        clickCount: stat.clickCount,
+                        completionCount: stat.completionCount,
+                        clickRate: parseFloat(clickRate.toFixed(4)),
+                        completionRate: parseFloat(completionRate.toFixed(4)),
+                    };
+                });
+                dayArts.sort((a, b) => b.clickCount - a.clickCount);
+                return {
+                    label: this.getDayLabelByIndex(index),
+                    date: dayKey,
+                    dau,
+                    artworks: dayArts.slice(0, Math.max(1, Math.min(100, topPerDay))),
+                };
+            });
+            return { tabs };
+        }
+        catch (error) {
+            console.error("[DashboardService] Error fetching new artwork tabs:", error);
+            throw error;
+        }
+    }
 }
 exports.default = new DashboardService();

+ 1 - 1
oms/public/app/index.html

@@ -9,5 +9,5 @@
   <style>body,html{width:100%;height:100%}*,:after,:before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}body{margin:0;color:#000000d9;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-variant:tabular-nums;line-height:1.5715;background-color:#fff;font-feature-settings:"tnum"}html{--antd-wave-shadow-color:#1890ff;--scroll-bar:0}</style><link rel="stylesheet" href="styles-LXBSU6DF.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-LXBSU6DF.css"></noscript></head>
   <body>
     <app-root></app-root>
-  <script src="polyfills-B6TNHZQ6.js" type="module"></script><script src="main-NTRDYMBN.js" type="module"></script></body>
+  <script src="polyfills-B6TNHZQ6.js" type="module"></script><script src="main-52DEBLCW.js" type="module"></script></body>
 </html>

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
oms/public/app/main-52DEBLCW.js


+ 30 - 0
oms/src/controllers/dashboardController.ts

@@ -77,6 +77,36 @@ class DashboardController {
       });
     }
   }
+
+  /**
+   * 获取最近 N 天上新作品分日数据(用于 tab 展示)
+   * GET /api/dashboard/new-artwork-tabs?days=7&topPerDay=20
+   */
+  public async getNewArtworkTabs(req: Request, res: Response): Promise<void> {
+    try {
+      const days = Math.min(Math.max(parseInt(req.query.days as string) || 7, 1), 30);
+      const topPerDay = Math.min(Math.max(parseInt(req.query.topPerDay as string) || 20, 1), 100);
+
+      console.log(`[DashboardController] getNewArtworkTabs start, days=${days}, topPerDay=${topPerDay}`);
+      const startedAt = Date.now();
+
+      const data = await dashboardService.getNewArtworkTabs(days, topPerDay);
+
+      console.log(`[DashboardController] getNewArtworkTabs end, durationMs=${Date.now() - startedAt}`);
+
+      res.status(200).json({
+        success: true,
+        data,
+      });
+    } catch (error: any) {
+      console.error("[DashboardController] Error in getNewArtworkTabs:", error);
+      res.status(500).json({
+        success: false,
+        message: "Failed to fetch new artwork tabs",
+        error: error.message,
+      });
+    }
+  }
 }
 
 const dashboardController = new DashboardController();

+ 1 - 0
oms/src/routes/apiRoutes.ts

@@ -39,6 +39,7 @@ router.get("/message/daily/trends/by-image/:image", messageRecordController.getD
 // 新增:Dashboard 路由
 router.get("/dashboard/kpi", dashboardController.getKpi);
 router.get("/dashboard/artwork-stats", dashboardController.getArtworkStats);
+router.get("/dashboard/new-artwork-tabs", dashboardController.getNewArtworkTabs);
 
 // 应用认证中间件,保护所有下面的路由
 router.use(authMiddleware);

+ 180 - 34
oms/src/services/dashboardService.ts

@@ -1,15 +1,12 @@
 import { clickhouseService } from "./clients";
 import dayjs from "dayjs";
+import Art from "../models/artModel";
 
 interface KpiData {
   dau: {
     today: number;
     trend: Array<{ date: string; dau: number }>;
   };
-  revenue: {
-    today: number;
-    trend: Array<{ date: string; revenue: number }>;
-  };
 }
 
 interface ArtworkStat {
@@ -27,9 +24,37 @@ interface ArtworkStatsResponse {
   artworks: ArtworkStat[];
 }
 
+interface NewArtworkTabItem {
+  resId: string;
+  name: string;
+  thumbnail: string;
+  clickCount: number;
+  completionCount: number;
+  clickRate: number;
+  completionRate: number;
+}
+
+interface NewArtworkTab {
+  label: string;
+  date: string;
+  dau: number;
+  artworks: NewArtworkTabItem[];
+}
+
+interface NewArtworkTabsResponse {
+  tabs: NewArtworkTab[];
+}
+
 class DashboardService {
   private readonly eventsTable = "events";
 
+  private getDayLabelByIndex(index: number): string {
+    if (index === 0) return "今日";
+    if (index === 1) return "昨天";
+    if (index === 2) return "前天";
+    return `${index}天前`;
+  }
+
   /**
    * 获取 KPI 数据:日活用户数、日活曲线、广告收益、收益曲线
    * @param dateRange 显示过去 N 天的数据,默认 7 天
@@ -41,7 +66,7 @@ class DashboardService {
       const endDate = dayjs().format("YYYY-MM-DD");
       const today = dayjs().format("YYYY-MM-DD");
 
-      // 查询日活用户数和收益的聚合数据
+      // 查询日活用户数聚合数据
       const dauQuery = `
         SELECT 
           toDate(time) as date,
@@ -54,18 +79,6 @@ class DashboardService {
         ORDER BY date
       `;
 
-      const revenueQuery = `
-        SELECT 
-          toDate(time) as date,
-          sumIf(revenue, event = 'revenue' AND revenue > 0) as total_revenue
-        FROM ${this.eventsTable}
-        WHERE toDate(time) >= '${startDate}' 
-          AND toDate(time) <= '${endDate}'
-          AND project = 1
-        GROUP BY date
-        ORDER BY date
-      `;
-
       // 获取今日具体数据
       const todayDauQuery = `
         SELECT countDistinct(uid) as dau
@@ -73,22 +86,14 @@ class DashboardService {
         WHERE toDate(time) = '${today}' AND project = 1
       `;
 
-      const todayRevenueQuery = `
-        SELECT sumIf(revenue, event = 'revenue' AND revenue > 0) as total_revenue
-        FROM ${this.eventsTable}
-        WHERE toDate(time) = '${today}' AND project = 1
-      `;
 
-      // 执行所有查询
-      const [dauTrend, revenueTrend, todayDauResult, todayRevenueResult] = await Promise.all([
+      // 执行查询
+      const [dauTrend, todayDauResult] = await Promise.all([
         clickhouseService.queryEvents<{ date: string; dau: number }>(dauQuery),
-        clickhouseService.queryEvents<{ date: string; total_revenue: number }>(revenueQuery),
         clickhouseService.queryEvents<{ dau: number }>(todayDauQuery),
-        clickhouseService.queryEvents<{ total_revenue: number }>(todayRevenueQuery),
       ]);
 
       const todayDau = todayDauResult?.[0]?.dau ?? 0;
-      const todayRevenue = todayRevenueResult?.[0]?.total_revenue ?? 0;
 
       return {
         dau: {
@@ -98,13 +103,6 @@ class DashboardService {
             dau: item.dau,
           })),
         },
-        revenue: {
-          today: parseFloat(todayRevenue.toFixed(2)),
-          trend: revenueTrend.map((item) => ({
-            date: item.date,
-            revenue: parseFloat(item.total_revenue.toFixed(2)),
-          })),
-        },
       };
     } catch (error) {
       console.error("[DashboardService] Error fetching KPI data:", error);
@@ -174,6 +172,154 @@ class DashboardService {
       throw error;
     }
   }
+
+  /**
+   * 获取最近 N 天上新作品分日统计(点击率/完成率)
+   * @param days 天数,默认 7
+   * @param topPerDay 每日最多返回作品数,默认 20
+   */
+  public async getNewArtworkTabs(days: number = 7, topPerDay: number = 20): Promise<NewArtworkTabsResponse> {
+    try {
+      const latestPublished = await Art.findOne({
+        publishTime: { $exists: true, $ne: null },
+        drop: { $ne: true },
+      })
+        .sort({ publishTime: -1 })
+        .select({ publishTime: 1 })
+        .lean();
+
+      if (!latestPublished?.publishTime) {
+        return { tabs: [] };
+      }
+
+      const safeDays = Math.max(1, Math.min(30, Math.floor(days || 7)));
+      const latestDayStart = dayjs(latestPublished.publishTime).startOf("day");
+      const rangeStart = latestDayStart.subtract(safeDays - 1, "day");
+      const rangeEnd = latestDayStart.add(1, "day");
+
+      const arts = await Art.find({
+        publishTime: {
+          $gte: rangeStart.toDate(),
+          $lt: rangeEnd.toDate(),
+        },
+        drop: { $ne: true },
+      })
+        .select({ _id: 1, name: 1, pageId: 1, publishTime: 1 })
+        .lean();
+
+      const dayKeys = Array.from({ length: safeDays }, (_, index) =>
+        latestDayStart.subtract(index, "day").format("YYYY-MM-DD")
+      );
+
+      const artsByDay = new Map<string, Array<{ resId: string; name: string; pageId: string }>>();
+      for (const dayKey of dayKeys) {
+        artsByDay.set(dayKey, []);
+      }
+
+      const allResIds: string[] = [];
+      for (const art of arts) {
+        if (!art.publishTime) continue;
+        const dayKey = dayjs(art.publishTime).format("YYYY-MM-DD");
+        if (!artsByDay.has(dayKey)) continue;
+        const resId = String(art._id);
+        allResIds.push(resId);
+        artsByDay.get(dayKey)?.push({
+          resId,
+          name: art.name || "未命名作品",
+          pageId: String(art.pageId || ""),
+        });
+      }
+
+      const uniqueResIds = Array.from(new Set(allResIds));
+      const oldestDay = dayKeys[dayKeys.length - 1];
+      const newestDay = dayKeys[0];
+
+      const dauQuery = `
+        SELECT
+          toDate(time) AS date,
+          countDistinct(uid) AS dau
+        FROM ${this.eventsTable}
+        WHERE project = 1
+          AND toDate(time) >= '${oldestDay}'
+          AND toDate(time) <= '${newestDay}'
+        GROUP BY date
+        ORDER BY date
+      `;
+
+      const dauRows = await clickhouseService.queryEvents<{ date: string; dau: number }>(dauQuery);
+      const dauByDay = new Map<string, number>(dauRows.map((row) => [String(row.date), row.dau]));
+
+      const statsByDayAndRes = new Map<string, { clickCount: number; completionCount: number }>();
+      if (uniqueResIds.length > 0) {
+        const inClause = uniqueResIds.map((id) => `'${id}'`).join(",");
+        const artworkStatsQuery = `
+          SELECT
+            toDate(time) AS date,
+            res AS resId,
+            countIf(event = 'color_start') AS clickCount,
+            countIf(event = 'color_done') AS completionCount
+          FROM ${this.eventsTable}
+          WHERE project = 1
+            AND toDate(time) >= '${oldestDay}'
+            AND toDate(time) <= '${newestDay}'
+            AND event IN ('color_start', 'color_done')
+            AND res IN (${inClause})
+          GROUP BY date, res
+        `;
+
+        const statRows = await clickhouseService.queryEvents<{
+          date: string;
+          resId: string;
+          clickCount: number;
+          completionCount: number;
+        }>(artworkStatsQuery);
+
+        for (const row of statRows) {
+          statsByDayAndRes.set(`${row.date}|${row.resId}`, {
+            clickCount: row.clickCount,
+            completionCount: row.completionCount,
+          });
+        }
+      }
+
+      const tabs: NewArtworkTab[] = dayKeys.map((dayKey, index) => {
+        const dau = dauByDay.get(dayKey) ?? 0;
+        const dayArts = (artsByDay.get(dayKey) || []).map((art) => {
+          const stat = statsByDayAndRes.get(`${dayKey}|${art.resId}`) || {
+            clickCount: 0,
+            completionCount: 0,
+          };
+
+          const clickRate = dau > 0 ? stat.clickCount / dau : 0;
+          const completionRate = stat.clickCount > 0 ? stat.completionCount / stat.clickCount : 0;
+
+          return {
+            resId: art.resId,
+            name: art.name,
+            thumbnail: art.pageId ? `/thumbs/v2/page/320/${art.pageId}.png` : "",
+            clickCount: stat.clickCount,
+            completionCount: stat.completionCount,
+            clickRate: parseFloat(clickRate.toFixed(4)),
+            completionRate: parseFloat(completionRate.toFixed(4)),
+          };
+        });
+
+        dayArts.sort((a, b) => b.clickCount - a.clickCount);
+
+        return {
+          label: this.getDayLabelByIndex(index),
+          date: dayKey,
+          dau,
+          artworks: dayArts.slice(0, Math.max(1, Math.min(100, topPerDay))),
+        };
+      });
+
+      return { tabs };
+    } catch (error) {
+      console.error("[DashboardService] Error fetching new artwork tabs:", error);
+      throw error;
+    }
+  }
 }
 
 export default new DashboardService();

+ 265 - 361
omsapp/src/app/pages/dashboard.component.ts

@@ -1,20 +1,16 @@
 import { Component, OnInit } from '@angular/core';
-import { CommonModule, NgFor, DatePipe } from '@angular/common';
-import { debounceTime } from 'rxjs/operators';
-import { DashboardService } from '../services/dashboard.service';
+import { CommonModule, NgFor, NgIf, DatePipe } from '@angular/common';
+import { DashboardService, NewArtworkTab } from '../services/dashboard.service';
 
-// NG-ZORRO 组件
 import { NzCardModule } from 'ng-zorro-antd/card';
 import { NzIconModule } from 'ng-zorro-antd/icon';
 import { NzGridModule } from 'ng-zorro-antd/grid';
 import { NzStatisticModule } from 'ng-zorro-antd/statistic';
 import { NzSpinModule } from 'ng-zorro-antd/spin';
 import { NzTableModule } from 'ng-zorro-antd/table';
-import { NzDividerModule } from 'ng-zorro-antd/divider';
 import { NzProgressModule } from 'ng-zorro-antd/progress';
 import { NzPageHeaderModule } from 'ng-zorro-antd/page-header';
-import { NzTagModule } from 'ng-zorro-antd/tag';
-import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
+import { NzTabsModule } from 'ng-zorro-antd/tabs';
 import { NzModalService } from 'ng-zorro-antd/modal';
 import { NzMessageService } from 'ng-zorro-antd/message';
 
@@ -24,19 +20,17 @@ import { NzMessageService } from 'ng-zorro-antd/message';
   imports: [
     CommonModule,
     NgFor,
+    NgIf,
     DatePipe,
-    // NG-ZORRO 模块
     NzCardModule,
     NzIconModule,
     NzGridModule,
     NzStatisticModule,
     NzSpinModule,
     NzTableModule,
-    NzDividerModule,
     NzProgressModule,
     NzPageHeaderModule,
-    NzTagModule,
-    NzToolTipModule,
+    NzTabsModule,
   ],
   providers: [NzModalService, NzMessageService],
   template: `
@@ -44,215 +38,122 @@ import { NzMessageService } from 'ng-zorro-antd/message';
       <nz-page-header [nzGhost]="false">
         <nz-page-header-title>数据看板</nz-page-header-title>
         <nz-page-header-extra>
-          <span
-            nz-icon
-            nzType="sync"
-            nzTheme="outline"
-            (click)="refreshData()"
-          ></span>
+          <span nz-icon nzType="sync" nzTheme="outline" (click)="refreshData()"></span>
         </nz-page-header-extra>
         <nz-page-header-content>
-          <p>
-            最后更新时间:{{ lastUpdateTime | date : 'yyyy-MM-dd HH:mm:ss' }}
-          </p>
+          <p>最后更新时间:{{ lastUpdateTime | date : 'yyyy-MM-dd HH:mm:ss' }}</p>
         </nz-page-header-content>
       </nz-page-header>
 
       <nz-spin [nzSpinning]="isLoading" nzTip="数据加载中...">
-        <!-- 核心指标卡片 -->
-        <div class="metrics-container">
-          <nz-row [nzGutter]="16">
-            <!-- 日活用户数 -->
-            <nz-col [nzXs]="24" [nzSm]="12" [nzMd]="8" [nzLg]="6">
-              <nz-card nzHoverable>
-                <nz-statistic
-                  [nzTitle]="'日活用户(DAU)'"
-                  [nzValue]="activeUsersToday"
-                  [nzPrefix]="userIcon"
-                  [nzSuffix]="'人'"
-                  [nzValueStyle]="{ color: '#1890ff' }"
-                ></nz-statistic>
-                <ng-template #userIcon>
-                  <span nz-icon nzType="user" nzTheme="outline"></span>
-                </ng-template>
-                <div class="compare">
-                  <span>实时数据自动更新</span>
-                </div>
-              </nz-card>
-            </nz-col>
-
-            <!-- 日均广告收益 -->
-            <nz-col [nzXs]="24" [nzSm]="12" [nzMd]="8" [nzLg]="6">
-              <nz-card nzHoverable>
-                <nz-statistic
-                  [nzTitle]="'日广告收益'"
-                  [nzValue]="dailyRevenue"
-                  [nzPrefix]="revenueIcon"
-                  [nzSuffix]="'元'"
-                  [nzValueStyle]="{ color: '#faad14' }"
-                ></nz-statistic>
-                <ng-template #revenueIcon>
-                  <span nz-icon nzType="dollar" nzTheme="outline"></span>
-                </ng-template>
-                <div class="compare">
-                  <span>来自广告平台</span>
-                </div>
-              </nz-card>
-            </nz-col>
-
-            <!-- DAU 趋势占位 -->
-            <nz-col [nzXs]="24" [nzSm]="12" [nzMd]="8" [nzLg]="6">
-              <nz-card nzHoverable>
-                <nz-statistic
-                  [nzTitle]="'7日平均DAU'"
-                  [nzValue]="avgDau7d"
-                  [nzPrefix]="trendIcon"
-                  [nzSuffix]="'人'"
-                  [nzValueStyle]="{ color: '#13c2c2' }"
-                ></nz-statistic>
-                <ng-template #trendIcon>
-                  <span nz-icon nzType="line-chart" nzTheme="outline"></span>
-                </ng-template>
-                <div class="compare">
-                  <span>近 7 天平均值</span>
-                </div>
-              </nz-card>
-            </nz-col>
-
-            <!-- 收益趋势占位 -->
-            <nz-col [nzXs]="24" [nzSm]="12" [nzMd]="8" [nzLg]="6">
-              <nz-card nzHoverable>
-                <nz-statistic
-                  [nzTitle]="'7日收益总额'"
-                  [nzValue]="totalRevenue7d"
-                  [nzPrefix]="totalRevenueIcon"
-                  [nzSuffix]="'元'"
-                  [nzValueStyle]="{ color: '#eb2f96' }"
-                ></nz-statistic>
-                <ng-template #totalRevenueIcon>
-                  <span nz-icon nzType="bar-chart" nzTheme="outline"></span>
-                </ng-template>
-                <div class="compare">
-                  <span>近 7 天累计</span>
-                </div>
-              </nz-card>
-            </nz-col>
-          </nz-row>
-        </div>
-
-        <!-- 广告收益部分 -->
-        <div class="section">
-          <h3 class="section-title">今日广告收益</h3>
-          <nz-row [nzGutter]="16">
-            <nz-col [nzXs]="24" [nzSm]="12" [nzMd]="8">
-              <nz-card nzHoverable>
-                <nz-statistic
-                  [nzTitle]="'Banner广告收益'"
-                  [nzValue]="bannerRevenue"
-                  [nzPrefix]="bannerIcon"
-                  [nzSuffix]="'元'"
-                  [nzValueStyle]="{ color: '#fa8c16' }"
-                ></nz-statistic>
-                <ng-template #bannerIcon>
-                  <span nz-icon nzType="picture" nzTheme="outline"></span>
-                </ng-template>
-                <div class="compare">
-                  <span>展示量 {{ bannerImpressions }} 次</span>
-                </div>
-              </nz-card>
-            </nz-col>
-
-            <nz-col [nzXs]="24" [nzSm]="12" [nzMd]="8">
-              <nz-card nzHoverable>
-                <nz-statistic
-                  [nzTitle]="'插屏广告收益'"
-                  [nzValue]="interstitialRevenue"
-                  [nzPrefix]="interstitialIcon"
-                  [nzSuffix]="'元'"
-                  [nzValueStyle]="{ color: '#fa541c' }"
-                ></nz-statistic>
-                <ng-template #interstitialIcon>
-                  <span nz-icon nzType="switcher" nzTheme="outline"></span>
-                </ng-template>
-                <div class="compare">
-                  <span>展示量 {{ interstitialImpressions }} 次</span>
-                </div>
-              </nz-card>
-            </nz-col>
-
-            <nz-col [nzXs]="24" [nzSm]="12" [nzMd]="8">
-              <nz-card nzHoverable>
-                <nz-statistic
-                  [nzTitle]="'激励广告收益'"
-                  [nzValue]="rewardedRevenue"
-                  [nzPrefix]="rewardedIcon"
-                  [nzSuffix]="'元'"
-                  [nzValueStyle]="{ color: '#eb2f96' }"
-                ></nz-statistic>
-                <ng-template #rewardedIcon>
-                  <span nz-icon nzType="gift" nzTheme="outline"></span>
-                </ng-template>
-                <div class="compare">
-                  <span>展示量 {{ rewardedImpressions }} 次</span>
-                </div>
-              </nz-card>
-            </nz-col>
-          </nz-row>
-        </div>
-
-        <!-- 今日上新作品 -->
-        <div class="section">
-          <h3 class="section-title">今日上新作品</h3>
-          <nz-table
-            #worksTable
-            [nzData]="newWorksToday"
-            [nzLoading]="isLoading"
-            [nzFrontPagination]="false"
-            [nzBordered]="true"
-            [nzSize]="'small'"
-          >
-            <thead>
-              <tr>
-                <th>作品</th>
-                <th>名称</th>
-                <th>填色开始数</th>
-                <th>填色完成数</th>
-                <th>完成率</th>
-                <th>操作</th>
-              </tr>
-            </thead>
-            <tbody>
-              <tr *ngFor="let work of worksTable.data">
-                <td>
-                  <div
-                    class="work-thumbnail"
-                    [style.backgroundImage]="'url(' + work.thumbnail + ')'"
-                  ></div>
-                </td>
-                <td>{{ work.name }}</td>
-                <td>{{ work.startedCount }}</td>
-                <td>{{ work.completedCount }}</td>
-                <td>
-                  <nz-progress
-                    [nzPercent]="work.completionRate"
-                    [nzStrokeColor]="
-                      work.completionRate > 70
-                        ? '#52c41a'
-                        : work.completionRate > 30
-                        ? '#faad14'
-                        : '#f5222d'
-                    "
-                    [nzShowInfo]="true"
-                    [nzStrokeWidth]="5"
-                  ></nz-progress>
-                </td>
-                <td>
-                  <a (click)="showWorkDetails(work)">详情</a>
-                </td>
-              </tr>
-            </tbody>
-          </nz-table>
-        </div>
+        <nz-row [nzGutter]="16">
+          <nz-col [nzXs]="24" [nzLg]="10">
+            <nz-card nzTitle="用户活跃" nzHoverable>
+              <nz-statistic
+                [nzTitle]="'当日日活(DAU)'"
+                [nzValue]="activeUsersToday"
+                [nzPrefix]="userIcon"
+                [nzSuffix]="'人'"
+                [nzValueStyle]="{ color: '#1677ff' }"
+              ></nz-statistic>
+              <ng-template #userIcon>
+                <span nz-icon nzType="user" nzTheme="outline"></span>
+              </ng-template>
+
+              <div class="range-buttons">
+                <button
+                  type="button"
+                  class="range-btn"
+                  [class.active]="selectedDauRange === 7"
+                  (click)="changeDauRange(7)"
+                >7天</button>
+                <button
+                  type="button"
+                  class="range-btn"
+                  [class.active]="selectedDauRange === 14"
+                  (click)="changeDauRange(14)"
+                >14天</button>
+                <button
+                  type="button"
+                  class="range-btn"
+                  [class.active]="selectedDauRange === 30"
+                  (click)="changeDauRange(30)"
+                >30天</button>
+              </div>
+
+              <div class="chart-wrapper">
+                <svg viewBox="0 0 600 180" class="line-chart" preserveAspectRatio="none">
+                  <polyline
+                    *ngIf="dauChartPoints"
+                    [attr.points]="dauChartPoints"
+                    fill="none"
+                    stroke="#1677ff"
+                    stroke-width="3"
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                  ></polyline>
+                </svg>
+              </div>
+
+              <div class="chart-meta">
+                <span>{{ dauTrendStartDate }}</span>
+                <span>Max: {{ dauMax }}</span>
+                <span>Min: {{ dauMin }}</span>
+                <span>{{ dauTrendEndDate }}</span>
+              </div>
+            </nz-card>
+          </nz-col>
+
+          <nz-col [nzXs]="24" [nzLg]="14">
+            <nz-card nzTitle="最近7天上新作品表现" nzHoverable>
+              <nz-tabset [nzSelectedIndex]="activeArtworkTabIndex" (nzSelectedIndexChange)="activeArtworkTabIndex = $event">
+                <nz-tab *ngFor="let tab of artworkTabs" [nzTitle]="tab.label">
+                  <div class="tab-subtitle">{{ tab.date }} · 当日DAU {{ tab.dau }}</div>
+
+                  <nz-table
+                    #worksTable
+                    [nzData]="tab.artworks"
+                    [nzFrontPagination]="false"
+                    [nzBordered]="true"
+                    [nzSize]="'small'"
+                  >
+                    <thead>
+                      <tr>
+                        <th>作品</th>
+                        <th>名称</th>
+                        <th>点击率</th>
+                        <th>完成率</th>
+                        <th>点击数</th>
+                        <th>完成数</th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      <tr *ngFor="let work of worksTable.data">
+                        <td>
+                          <div class="work-thumbnail" [style.backgroundImage]="'url(' + work.thumbnail + ')'" ></div>
+                        </td>
+                        <td>{{ work.name }}</td>
+                        <td>{{ (work.clickRate * 100) | number : '1.1-2' }}%</td>
+                        <td>
+                          <nz-progress
+                            [nzPercent]="(work.completionRate * 100)"
+                            [nzShowInfo]="true"
+                            [nzStrokeWidth]="6"
+                            [nzStrokeColor]="work.completionRate >= 0.6 ? '#52c41a' : work.completionRate >= 0.35 ? '#faad14' : '#ff4d4f'"
+                          ></nz-progress>
+                        </td>
+                        <td>{{ work.clickCount }}</td>
+                        <td>{{ work.completionCount }}</td>
+                      </tr>
+                    </tbody>
+                  </nz-table>
+
+                  <div class="empty-tip" *ngIf="tab.artworks.length === 0">当日无上新作品</div>
+                </nz-tab>
+              </nz-tabset>
+            </nz-card>
+          </nz-col>
+        </nz-row>
       </nz-spin>
     </div>
   `,
@@ -260,69 +161,69 @@ import { NzMessageService } from 'ng-zorro-antd/message';
     `
       .dashboard-container {
         padding: 24px;
-        background: #f0f2f5;
+        background: #f5f7fa;
         min-height: 100%;
       }
 
-      .metrics-container {
-        margin-bottom: 24px;
+      .range-buttons {
+        margin: 12px 0 8px;
+        display: flex;
+        gap: 8px;
       }
 
-      .section {
+      .range-btn {
+        border: 1px solid #d9d9d9;
         background: #fff;
-        padding: 16px 24px;
-        border-radius: 8px;
-        margin-bottom: 24px;
-        box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
+        border-radius: 6px;
+        padding: 4px 10px;
+        cursor: pointer;
       }
 
-      .section-title {
-        margin-bottom: 16px;
-        font-size: 18px;
-        color: rgba(0, 0, 0, 0.85);
+      .range-btn.active {
+        border-color: #1677ff;
+        color: #1677ff;
+        background: #e6f4ff;
       }
 
-      nz-card {
-        margin-bottom: 16px;
-        transition: all 0.3s;
+      .chart-wrapper {
+        margin-top: 8px;
+        border: 1px solid #f0f0f0;
+        border-radius: 8px;
+        background: #fcfdff;
       }
 
-      nz-card:hover {
-        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
-        transform: translateY(-4px);
+      .line-chart {
+        width: 100%;
+        height: 180px;
       }
 
-      .compare {
+      .chart-meta {
         margin-top: 8px;
+        display: flex;
+        justify-content: space-between;
+        color: #8c8c8c;
+        font-size: 12px;
+      }
+
+      .tab-subtitle {
+        margin-bottom: 10px;
+        color: #595959;
         font-size: 12px;
-        color: rgba(0, 0, 0, 0.45);
       }
 
       .work-thumbnail {
-        width: 60px;
-        height: 60px;
+        width: 52px;
+        height: 52px;
+        border-radius: 4px;
         background-size: cover;
         background-position: center;
-        border-radius: 4px;
-      }
-
-      .ant-statistic-content {
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-      }
-
-      .ant-statistic-title {
-        font-size: 14px;
-        color: rgba(0, 0, 0, 0.65);
+        border: 1px solid #f0f0f0;
       }
 
-      .ant-statistic-content-value {
-        font-size: 28px;
-      }
-
-      .ant-statistic-content-suffix {
-        font-size: 16px;
+      .empty-tip {
+        margin-top: 10px;
+        color: #8c8c8c;
+        font-size: 12px;
       }
     `,
   ],
@@ -330,32 +231,20 @@ import { NzMessageService } from 'ng-zorro-antd/message';
 export class DashboardComponent implements OnInit {
   isLoading = true;
   lastUpdateTime = new Date();
-  Math = Math; // 暴露 Math 对象给模板使antml
 
-  // 核心指标数据(来自 KPI 接口)
-  activeUsersToday = 0; // 日活用户数
-  dailyRevenue = 0; // 当日广告收益
-  
-  // DAU 趋势数据
-  dauTrendLabels: string[] = [];
+  activeUsersToday = 0;
+  selectedDauRange = 30;
+
   dauTrendData: number[] = [];
-  avgDau7d = 0;
-  
-  // 收益趋势数据
-  revenueTrendLabels: string[] = [];
-  revenueTrendData: number[] = [];
-  totalRevenue7d = 0;
-
-  // 广告收益数据(暂时保留,后续可删除)
-  bannerRevenue = 0;
-  interstitialRevenue = 0;
-  rewardedRevenue = 0;
-  bannerImpressions = 0;
-  interstitialImpressions = 0;
-  rewardedImpressions = 0;
-
-  // 今日上新作品
-  newWorksToday: any[] = [];
+  dauTrendLabels: string[] = [];
+  dauChartPoints = '';
+  dauMin = 0;
+  dauMax = 0;
+  dauTrendStartDate = '';
+  dauTrendEndDate = '';
+
+  artworkTabs: NewArtworkTab[] = [];
+  activeArtworkTabIndex = 0;
 
   constructor(
     private modalService: NzModalService,
@@ -368,102 +257,117 @@ export class DashboardComponent implements OnInit {
   }
 
   refreshData(): void {
-    this.isLoading = true;
     this.loadDashboardData();
   }
 
-  loadDashboardData(): void {
-    this.dashboardService.getKpi(7).subscribe({
+  changeDauRange(days: number): void {
+    if (this.selectedDauRange === days) return;
+    this.selectedDauRange = days;
+    this.loadUserCardData();
+  }
+
+  private buildDauChartPoints(values: number[]): string {
+    if (!values.length) return '';
+
+    const width = 600;
+    const height = 180;
+    const padding = 16;
+    const minVal = Math.min(...values);
+    const maxVal = Math.max(...values);
+    const span = Math.max(1, maxVal - minVal);
+    const stepX = values.length > 1 ? (width - padding * 2) / (values.length - 1) : 0;
+
+    return values
+      .map((value, idx) => {
+        const x = padding + idx * stepX;
+        const y = height - padding - ((value - minVal) / span) * (height - padding * 2);
+        return `${x},${y}`;
+      })
+      .join(' ');
+  }
+
+  private loadUserCardData(): void {
+    this.isLoading = true;
+    this.dashboardService.getKpi(this.selectedDauRange).subscribe({
       next: (response) => {
-        if (response.success) {
-          const { dau, revenue } = response.data;
-          
-          // 更新 KPI 数据
-          this.activeUsersToday = dau.today;
-          this.dailyRevenue = revenue.today;
-          
-          // 更新 DAU 趋势
-          this.dauTrendLabels = dau.trend.map((item) => item.date);
-          this.dauTrendData = dau.trend.map((item) => item.dau);
-          const dauSum = this.dauTrendData.reduce((sum, value) => sum + value, 0);
-          this.avgDau7d = this.dauTrendData.length > 0 ? Math.round(dauSum / this.dauTrendData.length) : 0;
-          
-          // 更新收益趋势
-          this.revenueTrendLabels = revenue.trend.map((item) => item.date);
-          this.revenueTrendData = revenue.trend.map((item) => item.revenue);
-          const revenueSum = this.revenueTrendData.reduce((sum, value) => sum + value, 0);
-          this.totalRevenue7d = Math.round(revenueSum);
-          
-          // 模拟广告收益细分(后续可从其他接口获取)
-          this.bannerRevenue = revenue.today * 0.35;
-          this.interstitialRevenue = revenue.today * 0.40;
-          this.rewardedRevenue = revenue.today * 0.25;
-          this.bannerImpressions = Math.floor(Math.random() * 30000);
-          this.interstitialImpressions = Math.floor(Math.random() * 10000);
-          this.rewardedImpressions = Math.floor(Math.random() * 8000);
-          
-          // 模拟上新作品数据(后续可从接口获取)
-          this.newWorksToday = [
-            {
-              id: 1,
-              name: '作品 #1',
-              thumbnail: 'https://via.placeholder.com/60',
-              startedCount: Math.floor(Math.random() * 2000),
-              completedCount: Math.floor(Math.random() * 1000),
-            },
-            {
-              id: 2,
-              name: '作品 #2',
-              thumbnail: 'https://via.placeholder.com/60',
-              startedCount: Math.floor(Math.random() * 2000),
-              completedCount: Math.floor(Math.random() * 1000),
-            },
-            {
-              id: 3,
-              name: '作品 #3',
-              thumbnail: 'https://via.placeholder.com/60',
-              startedCount: Math.floor(Math.random() * 2000),
-              completedCount: Math.floor(Math.random() * 1000),
-            },
-          ];
-          
-          this.newWorksToday = this.newWorksToday.map((work) => ({
-            ...work,
-            completionRate: work.startedCount > 0 
-              ? Math.round((work.completedCount / work.startedCount) * 100)
-              : 0,
-          }));
-          
-          this.lastUpdateTime = new Date();
-          this.isLoading = false;
-        } else {
-          this.message.error('获取 KPI 数据失败');
+        if (!response.success) {
+          this.message.error('获取日活数据失败');
           this.isLoading = false;
+          return;
         }
+
+        const { dau } = response.data;
+        this.activeUsersToday = dau.today;
+        this.dauTrendLabels = dau.trend.map((item) => item.date);
+        this.dauTrendData = dau.trend.map((item) => item.dau);
+        this.dauMin = this.dauTrendData.length ? Math.min(...this.dauTrendData) : 0;
+        this.dauMax = this.dauTrendData.length ? Math.max(...this.dauTrendData) : 0;
+        this.dauTrendStartDate = this.dauTrendLabels[0] || '';
+        this.dauTrendEndDate = this.dauTrendLabels[this.dauTrendLabels.length - 1] || '';
+        this.dauChartPoints = this.buildDauChartPoints(this.dauTrendData);
+
+        this.lastUpdateTime = new Date();
+        this.isLoading = false;
       },
       error: (error) => {
-        console.error('Failed to load KPI data:', error);
-        this.message.error('加载数据出错,请重试');
+        console.error('Failed to load DAU data:', error);
+        this.message.error('加载日活数据出错');
         this.isLoading = false;
       },
     });
   }
 
-  showWorkDetails(work: any): void {
-    this.modalService.create({
-      nzTitle: '作品详情',
-      nzContent: `
-        <div style="padding: 16px;">
-          <div style="text-align: center; margin-bottom: 16px;">
-            <img src="${work.thumbnail}" style="max-width: 100%; max-height: 300px; border-radius: 4px;">
-          </div>
-          <p><strong>作品名称:</strong> ${work.name}</p>
-          <p><strong>填色开始数:</strong> ${work.startedCount}</p>
-          <p><strong>填色完成数:</strong> ${work.completedCount}</p>
-          <p><strong>完成率:</strong> ${work.completionRate}%</p>
-        </div>
-      `,
-      nzFooter: null,
+  private loadArtworkTabsData(): Promise<void> {
+    return new Promise((resolve) => {
+      this.dashboardService.getNewArtworkTabs(7, 20).subscribe({
+        next: (response) => {
+          if (response.success) {
+            this.artworkTabs = response.data.tabs || [];
+            this.activeArtworkTabIndex = 0;
+          } else {
+            this.message.error('获取上新作品数据失败');
+          }
+          resolve();
+        },
+        error: (error) => {
+          console.error('Failed to load artwork tabs:', error);
+          this.message.error('加载上新作品数据出错');
+          resolve();
+        },
+      });
+    });
+  }
+
+  loadDashboardData(): void {
+    this.isLoading = true;
+    this.dashboardService.getKpi(this.selectedDauRange).subscribe({
+      next: async (kpiResponse) => {
+        if (!kpiResponse.success) {
+          this.message.error('获取日活数据失败');
+          this.isLoading = false;
+          return;
+        }
+
+        const { dau } = kpiResponse.data;
+        this.activeUsersToday = dau.today;
+        this.dauTrendLabels = dau.trend.map((item) => item.date);
+        this.dauTrendData = dau.trend.map((item) => item.dau);
+        this.dauMin = this.dauTrendData.length ? Math.min(...this.dauTrendData) : 0;
+        this.dauMax = this.dauTrendData.length ? Math.max(...this.dauTrendData) : 0;
+        this.dauTrendStartDate = this.dauTrendLabels[0] || '';
+        this.dauTrendEndDate = this.dauTrendLabels[this.dauTrendLabels.length - 1] || '';
+        this.dauChartPoints = this.buildDauChartPoints(this.dauTrendData);
+
+        await this.loadArtworkTabsData();
+
+        this.lastUpdateTime = new Date();
+        this.isLoading = false;
+      },
+      error: (error) => {
+        console.error('Failed to load dashboard data:', error);
+        this.message.error('加载看板数据出错');
+        this.isLoading = false;
+      },
     });
   }
 }

+ 31 - 4
omsapp/src/app/services/dashboard.service.ts

@@ -17,10 +17,6 @@ export interface KpiData {
     today: number;
     trend: DauTrendItem[];
   };
-  revenue: {
-    today: number;
-    trend: RevenueTrendItem[];
-  };
 }
 
 export interface ArtworkStat {
@@ -38,6 +34,27 @@ export interface ArtworkStatsResponse {
   artworks: ArtworkStat[];
 }
 
+export interface NewArtworkTabItem {
+  resId: string;
+  name: string;
+  thumbnail: string;
+  clickCount: number;
+  completionCount: number;
+  clickRate: number;
+  completionRate: number;
+}
+
+export interface NewArtworkTab {
+  label: string;
+  date: string;
+  dau: number;
+  artworks: NewArtworkTabItem[];
+}
+
+export interface NewArtworkTabsResponse {
+  tabs: NewArtworkTab[];
+}
+
 export interface ApiResponse<T> {
   success: boolean;
   data: T;
@@ -78,4 +95,14 @@ export class DashboardService {
       .set('sortBy', sortBy);
     return this.http.get<ApiResponse<ArtworkStatsResponse>>('/api/dashboard/artwork-stats', { params });
   }
+
+  /**
+   * 获取最近 N 天上新作品分日 tab 数据
+   */
+  getNewArtworkTabs(days: number = 7, topPerDay: number = 20): Observable<ApiResponse<NewArtworkTabsResponse>> {
+    const params = new HttpParams()
+      .set('days', days.toString())
+      .set('topPerDay', topPerDay.toString());
+    return this.http.get<ApiResponse<NewArtworkTabsResponse>>('/api/dashboard/new-artwork-tabs', { params });
+  }
 }

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff