Ver código fonte

增加逐月需求所属月份

xueyiming 1 dia atrás
pai
commit
59e328dc64

+ 7 - 1
app/api/routes.py

@@ -42,6 +42,12 @@ ELEMENT_DEMAND_EXPORT_COLUMNS: list[tuple[str, str]] = [
     ("视频列表", "video_list"),
 ]
 
+MONTHLY_ELEMENT_DEMAND_EXPORT_COLUMNS: list[tuple[str, str]] = [
+    *ELEMENT_DEMAND_EXPORT_COLUMNS,
+    ("月份列表", "month_list"),
+    ("频次", "frequency"),
+]
+
 
 def _export_timestamp() -> str:
     return datetime.now(SHANGHAI_TZ).strftime("%Y%m%d_%H%M%S")
@@ -344,7 +350,7 @@ async def export_element_demands_monthly(
     filename = f"逐月特征点_{_export_timestamp()}.xlsx"
     return _excel_streaming_response(
         items,
-        ELEMENT_DEMAND_EXPORT_COLUMNS,
+        MONTHLY_ELEMENT_DEMAND_EXPORT_COLUMNS,
         sheet_name="特征点明细",
         filename=filename,
     )

+ 13 - 0
app/services/element_search_service.py

@@ -378,6 +378,13 @@ element_freq AS (
     FROM element_monthly_metrics
     GROUP BY 原始元素
 )
+,element_month_list AS (
+    SELECT
+        原始元素,
+        TO_JSON(SORT_ARRAY(COLLECT_SET(ym))) AS month_list
+    FROM element_monthly_metrics
+    GROUP BY 原始元素
+)
 SELECT
     '逐月' AS strategy,
     md5(CONCAT('逐月', r.原始元素, '{bizdate}')) AS demand_id,
@@ -385,12 +392,16 @@ SELECT
     r.avg_rov AS weight,
     COALESCE(v.vid_count, 0) AS video_count,
     v.vid_list AS video_list,
+    ml.month_list AS month_list, 
+    COALESCE(f.频次, 0) AS frequency,
     '{{}}' AS ext_info
 FROM element_total_rov r
 LEFT JOIN element_vid_stats v
   ON r.原始元素 = v.原始元素
 LEFT JOIN element_freq f
   ON r.原始元素 = f.原始元素
+LEFT JOIN element_month_list ml           -- 新增 JOIN
+  ON r.原始元素 = ml.原始元素
 WHERE r.原始元素 NOT IN (
     '元旦','腊八节','小年','除夕','春节','正月初一','正月初二','正月初三','正月初四','正月初五',
     '情人节','元宵节','龙抬头','妇女节','植树节','劳动节','母亲节','儿童节','端午节','父亲节',
@@ -418,6 +429,8 @@ ORDER BY r.avg_rov DESC
                     "weight": _normalize_scalar(record["weight"]),
                     "video_count": record["video_count"],
                     "video_list": _serialize_video_list(record["video_list"]),
+                    "month_list": _serialize_video_list(record["month_list"]),
+                    "frequency": _normalize_scalar(record["frequency"]),
                     "ext_info": record["ext_info"],
                 }
             )

+ 112 - 12
frontend/src/App.tsx

@@ -56,6 +56,8 @@ type ElementDemandItem = {
   weight: number | null;
   video_count: number | null;
   video_list: string | null;
+  month_list?: string | null;
+  frequency?: number | null;
   ext_info: string | null;
 };
 
@@ -79,6 +81,25 @@ function parseVideoIdsFromList(raw: string | null): string[] {
   return [];
 }
 
+function formatYmDisplay(ym: string): string {
+  if (/^\d{6}$/.test(ym)) {
+    return `${ym.slice(0, 4)}-${ym.slice(4, 6)}`;
+  }
+  return ym;
+}
+
+function formatMonthListPreview(raw: string | null): string {
+  const months = parseVideoIdsFromList(raw);
+  if (months.length > 0) {
+    return `共 ${months.length} 个月,点击查看`;
+  }
+  const trimmed = (raw ?? "").trim();
+  if (!trimmed) {
+    return "-";
+  }
+  return trimmed.length > 36 ? `${trimmed.slice(0, 36)}…` : trimmed;
+}
+
 const API_BASE_URL =
   import.meta.env.VITE_API_BASE_URL ?? "/demand/api/v1";
 
@@ -652,6 +673,10 @@ function ElementDemandQueryPanel({
   const [videoModalTitleName, setVideoModalTitleName] = useState("");
   const [videoModalIds, setVideoModalIds] = useState<string[]>([]);
   const [videoModalRaw, setVideoModalRaw] = useState("");
+  const [monthModalOpen, setMonthModalOpen] = useState(false);
+  const [monthModalTitleName, setMonthModalTitleName] = useState("");
+  const [monthModalMonths, setMonthModalMonths] = useState<string[]>([]);
+  const [monthModalRaw, setMonthModalRaw] = useState("");
   const [decodeLoadingVid, setDecodeLoadingVid] = useState<string | null>(null);
 
   const openVideoCmsDetail = useCallback((vid: string) => {
@@ -824,7 +849,8 @@ function ElementDemandQueryPanel({
   }, [items, page, pageSize]);
 
   const columns: ColumnsType<ElementDemandItem> = useMemo(
-    () => [
+    () => {
+      const baseColumns: ColumnsType<ElementDemandItem> = [
       {
         title: "策略",
         dataIndex: "strategy",
@@ -843,13 +869,8 @@ function ElementDemandQueryPanel({
         width: 110,
         render: (v) => (v === null || v === undefined ? "-" : String(v)),
       },
-      {
-        title: "视频数",
-        dataIndex: "video_count",
-        width: 100,
-        render: (v) => v ?? "-",
-      },
-      {
+      ];
+      const videoListColumn: ColumnsType<ElementDemandItem>[number] = {
         title: "视频列表",
         dataIndex: "video_list",
         width: 220,
@@ -879,9 +900,53 @@ function ElementDemandQueryPanel({
             </Typography.Link>
           );
         },
-      },
-    ],
-    []
+      };
+      const videoCountColumn: ColumnsType<ElementDemandItem>[number] = {
+        title: "视频数",
+        dataIndex: "video_count",
+        width: 100,
+        render: (v) => v ?? "-",
+      };
+      if (mode === "monthly") {
+        baseColumns.push({
+          title: "频次",
+          dataIndex: "frequency",
+          width: 90,
+          render: (v) => (v === null || v === undefined ? "-" : String(v)),
+        });
+        baseColumns.push(videoCountColumn);
+        baseColumns.push({
+          title: "月份列表",
+          dataIndex: "month_list",
+          width: 180,
+          render: (_, record) => {
+            const raw = record.month_list ?? "";
+            const trimmed = raw.trim();
+            if (!trimmed) {
+              return "-";
+            }
+            const months = parseVideoIdsFromList(raw);
+            return (
+              <Typography.Link
+                onClick={() => {
+                  setMonthModalTitleName(record.demand_name ?? "");
+                  setMonthModalMonths(months);
+                  setMonthModalRaw(raw);
+                  setMonthModalOpen(true);
+                }}
+              >
+                {formatMonthListPreview(raw)}
+              </Typography.Link>
+            );
+          },
+        });
+      } else {
+        baseColumns.push(videoCountColumn);
+      }
+      baseColumns.push(videoListColumn);
+      return baseColumns;
+    },
+    [mode]
   );
 
   return (
@@ -1003,7 +1068,7 @@ function ElementDemandQueryPanel({
               columns={columns}
               dataSource={pagedItems}
               pagination={false}
-              scroll={{ x: 880 }}
+              scroll={{ x: mode === "monthly" ? 1150 : 880 }}
               rowClassName={(_, index) => (index % 2 === 0 ? "row-even" : "row-odd")}
             />
           </div>
@@ -1083,6 +1148,41 @@ function ElementDemandQueryPanel({
             </Typography.Paragraph>
           )}
         </Modal>
+        <Modal
+          title={`月份列表${monthModalTitleName ? ` — ${monthModalTitleName}` : ""}`}
+          open={monthModalOpen}
+          onCancel={() => setMonthModalOpen(false)}
+          footer={
+            <Button type="primary" onClick={() => setMonthModalOpen(false)}>
+              关闭
+            </Button>
+          }
+          width={560}
+          destroyOnHidden
+        >
+          {monthModalMonths.length > 0 ? (
+            <List
+              size="small"
+              bordered
+              dataSource={monthModalMonths}
+              style={{ maxHeight: 480, overflow: "auto" }}
+              renderItem={(ym, idx) => (
+                <List.Item style={{ paddingBlock: 10, paddingInline: 16 }}>
+                  <Typography.Text copyable={{ text: ym }}>
+                    {idx + 1}. {formatYmDisplay(ym)}
+                  </Typography.Text>
+                </List.Item>
+              )}
+            />
+          ) : (
+            <Typography.Paragraph
+              copyable={{ text: monthModalRaw }}
+              style={{ marginBottom: 0, whiteSpace: "pre-wrap", wordBreak: "break-all" }}
+            >
+              {monthModalRaw || "(空)"}
+            </Typography.Paragraph>
+          )}
+        </Modal>
         <div className="panel-footer">
           <Pagination
             current={page}

+ 9 - 0
frontend/src/vite-env.d.ts

@@ -0,0 +1,9 @@
+/// <reference types="vite/client" />
+
+interface ImportMetaEnv {
+  readonly VITE_API_BASE_URL?: string;
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv;
+}