Selaa lähdekoodia

每日dp转链数据

dodo hold 2 kuukautta sitten
vanhempi
sitoutus
0761fec4b5

+ 47 - 0
src/api/deeplinkReport.ts

@@ -0,0 +1,47 @@
+import axios from 'axios';
+
+export interface DeeplinkDailyReportRecord {
+    row_key: string;
+    row_type: 'daily' | 'channel';
+    report_date: string;
+    channel_name?: string;
+    display_name?: string;
+    total_count: number;
+    success_count: number;
+    fail_count: number;
+    children?: DeeplinkDailyReportRecord[];
+}
+
+export interface DeeplinkDailyReportSummary {
+    total_count: number;
+    success_count: number;
+    fail_count: number;
+}
+
+export interface DeeplinkDailyReportChannel {
+    channel_name: string;
+    display_name: string;
+    total_count: number;
+    success_count: number;
+    fail_count: number;
+}
+
+export interface DeeplinkDailyReportParams {
+    query_date: string[];
+    channel_name?: string;
+}
+
+export interface DeeplinkDailyReportRes {
+    list: DeeplinkDailyReportRecord[];
+    count: number;
+    summary: DeeplinkDailyReportSummary;
+    channels: DeeplinkDailyReportChannel[];
+    maxRangeDays: number;
+}
+
+export function queryDeeplinkDailyReport(data: DeeplinkDailyReportParams) {
+    return axios.post<DeeplinkDailyReportRes>(
+        '/api/DeeplinkReport/daily',
+        data
+    );
+}

+ 3 - 4
src/locale/en-US/taoke.ts

@@ -5,6 +5,7 @@ export default {
     'menu.settings.aliyun': 'Aliyun Resources',
     'menu.settings.apiAccount': 'API Account',
     'menu.settings.proxyNodes': 'Proxy Nodes',
+    'menu.deeplinkReport': 'DP Request Report',
 
     'menu.apiAccount': 'API Account',
     'menu.apiAccount.update': 'Update Account',
@@ -17,7 +18,6 @@ export default {
     'menu.jd.updateCookies': 'Update JD Cookies',
     'menu.jd.list': 'Union Account',
 
-
     'menu.pdd': 'Pdd Union',
     'menu.pdd.updateCookies': 'Update Pdd Cookies',
     'menu.pdd.list': 'Union Account',
@@ -44,10 +44,9 @@ export default {
 
     'menu.jd.bill_dailys': '效果统计-推广效果',
     'menu.jd.estimate_comm': '结算中心-收入概览',
-    
+
     'menu.pdd.estimate_revenue': '效果数据-推广效果',
-    
-    
+
     'menu.list.searchTable': 'Search Table',
     'searchTable.form.number': 'Set Number',
     'searchTable.form.number.placeholder': 'Please enter Set Number',

+ 99 - 103
src/locale/zh-CN/taoke.ts

@@ -1,105 +1,101 @@
 export default {
-  'menu.settings': '系统设置',
-  'menu.settings.config': '全局配置',
-  'menu.settings.xhsZlongReport': '多渠道转发报表',
-  'menu.settings.aliyun': '阿里云资源管理',
-  'menu.settings.apiAccount': '接口授权管理',
-  'menu.settings.proxyNodes': '代理节点列表',
-
-  'menu.apiAccount': '接口授权管理',
-  'menu.apiAccount.update': '更新账号',
-  'menu.apiAccount.list': '授权账号',
-
-  'menu.overrideRules': '文本覆盖规则',
-  'menu.overrideRules.list': '规则列表',
-
-
-  'menu.pushReport': '数据回传',
-  'menu.pushReport.list': '数据回传列表',
-
-  'menu.tracks': '监测链接',
-  'menu.tracksReport': '链接报表',
-
-
-  'menu.jd': '京东联盟',
-  'menu.jd.updateCookies': '更新 JD Cookies',
-  'menu.jd.list': '联盟账号管理',
-
-
-
-  'menu.pdd': '多多联盟',
-  'menu.pdd.updateCookies': '更新 PDD Cookies',
-  'menu.pdd.list': '联盟账号管理',
-
-
-  'menu.taobao': '口令',
-  'menu.taobaoCoupon': '优惠券',
-  'menu.taobao.updateCookies': '更新Cookies',
-  'menu.taobao.list': '联盟账号管理',
-  'menu.taobao.overrideRules': '文本覆盖规则',
-
-  'taobao.cookies.label': 'Cookies',
-  'taobao.cookies.placeholder': '粘贴整段Cookies',
-
-  'form.saveSuccess': '保存成功',
-  'form.save': '保存',
-  'form.cancel': '取消',
-  'form.reset': '重置',
-  'form.button.withdraw': '撤回',
-
-  'menu.taobao.income_report': '收入报表',
-  'menu.taobao.report_daily': '收入列表',
-  'menu.taobao.bill_dailys': '联盟预计收益',
-  'menu.taobao.orders': '联盟推广订单',
-
-  'menu.jd.bill_dailys': '效果统计-推广效果',
-  'menu.jd.estimate_comm': '结算中心-收入概览',
-
-  'menu.pdd.estimate_revenue': '效果数据-推广效果',
-
-  'menu.list.searchTable': '查询表格',
-  'searchTable.form.number': '集合编号',
-  'searchTable.form.number.placeholder': '请输入集合编号',
-  'searchTable.form.name': '集合名称',
-  'searchTable.form.name.placeholder': '请输入集合名称',
-  'searchTable.form.contentType': '内容体裁',
-  'searchTable.form.contentType.img': '图文',
-  'searchTable.form.contentType.horizontalVideo': '横版短视频',
-  'searchTable.form.contentType.verticalVideo': '竖版小视频',
-  'searchTable.form.filterType': '筛选方式',
-  'searchTable.form.filterType.artificial': '人工筛选',
-  'searchTable.form.filterType.rules': '规则筛选',
-  'searchTable.form.lastTime': '最后更新',
-  'searchTable.form.order_date': '订单日期',
-  'searchTable.form.report_date': '报表日期',
-  'searchTable.form.status': '状态',
-  'searchTable.form.status.online': '已上线',
-  'searchTable.form.status.offline': '已下线',
-  'searchTable.form.search': '查询',
-  'searchTable.form.reset': '重置',
-  'searchTable.form.selectDefault': '全部',
-  'searchTable.operation.create': '新建',
-  'searchTable.operation.import': '批量导入',
-  'searchTable.operation.download': '下载',
-  // columns
-  'searchTable.columns.index': '#',
-  'searchTable.columns.number': '集合编号',
-  'searchTable.columns.name': '集合名称',
-  'searchTable.columns.contentType': '内容体裁',
-  'searchTable.columns.filterType': '筛选方式',
-  'searchTable.columns.count': '内容量',
-  'searchTable.columns.lastTime': '最后更新',
-  'searchTable.columns.status': '状态',
-  'searchTable.columns.operations': '操作',
-  'searchTable.columns.operations.view': '查看',
-
-  // size
-  'searchTable.size.mini': '迷你',
-  'searchTable.size.small': '偏小',
-  'searchTable.size.medium': '中等',
-  'searchTable.size.large': '偏大',
-  // actions
-  'searchTable.actions.refresh': '刷新',
-  'searchTable.actions.density': '密度',
-  'searchTable.actions.columnSetting': '列设置',
+    'menu.settings': '系统设置',
+    'menu.settings.config': '全局配置',
+    'menu.settings.xhsZlongReport': '多渠道转发报表',
+    'menu.settings.aliyun': '阿里云资源管理',
+    'menu.settings.apiAccount': '接口授权管理',
+    'menu.settings.proxyNodes': '代理节点列表',
+
+    'menu.apiAccount': '接口授权管理',
+    'menu.apiAccount.update': '更新账号',
+    'menu.apiAccount.list': '授权账号',
+
+    'menu.overrideRules': '文本覆盖规则',
+    'menu.overrideRules.list': '规则列表',
+
+    'menu.pushReport': '数据回传',
+    'menu.pushReport.list': '数据回传列表',
+
+    'menu.tracks': '监测链接',
+    'menu.tracksReport': '链接报表',
+    'menu.deeplinkReport': 'DP请求报表',
+
+    'menu.jd': '京东联盟',
+    'menu.jd.updateCookies': '更新 JD Cookies',
+    'menu.jd.list': '联盟账号管理',
+
+    'menu.pdd': '多多联盟',
+    'menu.pdd.updateCookies': '更新 PDD Cookies',
+    'menu.pdd.list': '联盟账号管理',
+
+    'menu.taobao': '口令',
+    'menu.taobaoCoupon': '优惠券',
+    'menu.taobao.updateCookies': '更新Cookies',
+    'menu.taobao.list': '联盟账号管理',
+    'menu.taobao.overrideRules': '文本覆盖规则',
+
+    'taobao.cookies.label': 'Cookies',
+    'taobao.cookies.placeholder': '粘贴整段Cookies',
+
+    'form.saveSuccess': '保存成功',
+    'form.save': '保存',
+    'form.cancel': '取消',
+    'form.reset': '重置',
+    'form.button.withdraw': '撤回',
+
+    'menu.taobao.income_report': '收入报表',
+    'menu.taobao.report_daily': '收入列表',
+    'menu.taobao.bill_dailys': '联盟预计收益',
+    'menu.taobao.orders': '联盟推广订单',
+
+    'menu.jd.bill_dailys': '效果统计-推广效果',
+    'menu.jd.estimate_comm': '结算中心-收入概览',
+
+    'menu.pdd.estimate_revenue': '效果数据-推广效果',
+
+    'menu.list.searchTable': '查询表格',
+    'searchTable.form.number': '集合编号',
+    'searchTable.form.number.placeholder': '请输入集合编号',
+    'searchTable.form.name': '集合名称',
+    'searchTable.form.name.placeholder': '请输入集合名称',
+    'searchTable.form.contentType': '内容体裁',
+    'searchTable.form.contentType.img': '图文',
+    'searchTable.form.contentType.horizontalVideo': '横版短视频',
+    'searchTable.form.contentType.verticalVideo': '竖版小视频',
+    'searchTable.form.filterType': '筛选方式',
+    'searchTable.form.filterType.artificial': '人工筛选',
+    'searchTable.form.filterType.rules': '规则筛选',
+    'searchTable.form.lastTime': '最后更新',
+    'searchTable.form.order_date': '订单日期',
+    'searchTable.form.report_date': '报表日期',
+    'searchTable.form.status': '状态',
+    'searchTable.form.status.online': '已上线',
+    'searchTable.form.status.offline': '已下线',
+    'searchTable.form.search': '查询',
+    'searchTable.form.reset': '重置',
+    'searchTable.form.selectDefault': '全部',
+    'searchTable.operation.create': '新建',
+    'searchTable.operation.import': '批量导入',
+    'searchTable.operation.download': '下载',
+    // columns
+    'searchTable.columns.index': '#',
+    'searchTable.columns.number': '集合编号',
+    'searchTable.columns.name': '集合名称',
+    'searchTable.columns.contentType': '内容体裁',
+    'searchTable.columns.filterType': '筛选方式',
+    'searchTable.columns.count': '内容量',
+    'searchTable.columns.lastTime': '最后更新',
+    'searchTable.columns.status': '状态',
+    'searchTable.columns.operations': '操作',
+    'searchTable.columns.operations.view': '查看',
+
+    // size
+    'searchTable.size.mini': '迷你',
+    'searchTable.size.small': '偏小',
+    'searchTable.size.medium': '中等',
+    'searchTable.size.large': '偏大',
+    // actions
+    'searchTable.actions.refresh': '刷新',
+    'searchTable.actions.density': '密度',
+    'searchTable.actions.columnSetting': '列设置',
 };

+ 99 - 89
src/router/routes/modules/settings.ts

@@ -2,97 +2,107 @@ import { DEFAULT_LAYOUT } from '../base';
 import { AppRouteRecordRaw } from '../types';
 
 const SETTINGS: AppRouteRecordRaw = {
-  path: '/settings',
-  name: 'settings',
-  component: DEFAULT_LAYOUT,
-  meta: {
-    locale: 'menu.settings',
-    requiresAuth: true,
-    icon: 'icon-settings',
-    order: 99,
-  },
-  children: [
-    {
-      path: 'config',
-      name: 'config',
-      component: () => import('@/views/settings/config.vue'),
-      meta: {
-        locale: 'menu.settings.config',
+    path: '/settings',
+    name: 'settings',
+    component: DEFAULT_LAYOUT,
+    meta: {
+        locale: 'menu.settings',
         requiresAuth: true,
-        roles: ['*'],
-      },
+        icon: 'icon-settings',
+        order: 99,
     },
-    {
-      path: 'xhs_zlong_report',
-      name: 'xhsZlongReport',
-      component: () => import('@/views/settings/xhsZlongReport.vue'),
-      meta: {
-        locale: 'menu.settings.xhsZlongReport',
-        requiresAuth: true,
-        roles: ['*'],
-      },
-    },
-    {
-      path: 'aliyun',
-      name: 'aliyun',
-      component: () => import('@/views/settings/aliyun.vue'),
-      meta: {
-        locale: 'menu.settings.aliyun',
-        requiresAuth: true,
-        roles: ['*'],
-      },
-    },
-    {
-      path: 'push_report',
-      name: 'pushReport',
-      component: () => import('@/views/site/pushReport.vue'),
-      meta: {
-        locale: 'menu.pushReport',
-        requiresAuth: true,
-        roles: ['*'],
-      },
-    },
-    {
-      path: 'apiAccount',
-      name: 'apiAccount',
-      component: () => import('@/views/settings/apiAccount.vue'),
-      meta: {
-        locale: 'menu.settings.apiAccount',
-        requiresAuth: true,
-        roles: ['*'],
-      },
-    },
-    {
-      path: 'proxy_nodes',
-      name: 'proxyNodes',
-      component: () => import('@/views/settings/proxyNodes.vue'),
-      meta: {
-        locale: 'menu.settings.proxyNodes',
-        requiresAuth: true,
-        roles: ['*'],
-      },
-    },
-    {
-      path: 'tracks',
-      name: 'tracks',
-      component: () => import('@/views/site/tracks.vue'),
-      meta: {
-        locale: 'menu.tracks',
-        requiresAuth: true,
-        roles: ['*'],
-      },
-    },
-    {
-      path: 'tracks_report',
-      name: 'tracksReport',
-      component: () => import('@/views/site/tracksReport.vue'),
-      meta: {
-        locale: 'menu.tracksReport',
-        requiresAuth: true,
-        roles: ['*'],
-      },
-    },
-  ],
+    children: [
+        {
+            path: 'config',
+            name: 'config',
+            component: () => import('@/views/settings/config.vue'),
+            meta: {
+                locale: 'menu.settings.config',
+                requiresAuth: true,
+                roles: ['*'],
+            },
+        },
+        {
+            path: 'xhs_zlong_report',
+            name: 'xhsZlongReport',
+            component: () => import('@/views/settings/xhsZlongReport.vue'),
+            meta: {
+                locale: 'menu.settings.xhsZlongReport',
+                requiresAuth: true,
+                roles: ['*'],
+            },
+        },
+        {
+            path: 'aliyun',
+            name: 'aliyun',
+            component: () => import('@/views/settings/aliyun.vue'),
+            meta: {
+                locale: 'menu.settings.aliyun',
+                requiresAuth: true,
+                roles: ['*'],
+            },
+        },
+        {
+            path: 'push_report',
+            name: 'pushReport',
+            component: () => import('@/views/site/pushReport.vue'),
+            meta: {
+                locale: 'menu.pushReport',
+                requiresAuth: true,
+                roles: ['*'],
+            },
+        },
+        {
+            path: 'apiAccount',
+            name: 'apiAccount',
+            component: () => import('@/views/settings/apiAccount.vue'),
+            meta: {
+                locale: 'menu.settings.apiAccount',
+                requiresAuth: true,
+                roles: ['*'],
+            },
+        },
+        {
+            path: 'proxy_nodes',
+            name: 'proxyNodes',
+            component: () => import('@/views/settings/proxyNodes.vue'),
+            meta: {
+                locale: 'menu.settings.proxyNodes',
+                requiresAuth: true,
+                roles: ['*'],
+            },
+        },
+        {
+            path: 'tracks',
+            name: 'tracks',
+            component: () => import('@/views/site/tracks.vue'),
+            meta: {
+                locale: 'menu.tracks',
+                requiresAuth: true,
+                roles: ['*'],
+            },
+        },
+        {
+            path: 'tracks_report',
+            name: 'tracksReport',
+            component: () => import('@/views/site/tracksReport.vue'),
+            meta: {
+                locale: 'menu.tracksReport',
+                requiresAuth: true,
+                roles: ['*'],
+            },
+        },
+        {
+            path: 'deeplink_report',
+            name: 'deeplinkReport',
+            component: () => import('@/views/site/deeplinkReport.vue'),
+            meta: {
+                locale: 'menu.deeplinkReport',
+                requiresAuth: true,
+                roles: ['*'],
+            },
+        },
+    ],
 };
 
 export default SETTINGS;

+ 48 - 11
src/views/pdd/list.vue

@@ -426,20 +426,21 @@
                                 dayjs(record.last_time).format('MM-DD HH:mm')
                             }}</div
                         >
-                        <div
-                            v-if="
-                                dayjs().diff(
-                                    dayjs(record.login_time),
-                                    'hours'
-                                ) < 48
-                            "
-                        >
-                            登录:{{
-                                dayjs(record.login_time).format('MM-DD HH:mm')
-                            }}</div
+                        <div v-if="formatDateTime(record.login_time) !== '-'">
+                            登录:{{ formatDateTime(record.login_time) }}</div
                         >
                     </div>
                 </template>
+                <template #login_expire_countdown="{ record }">
+                    <span
+                        :class="{
+                            'expire-warning':
+                                getLoginExpireHours(record.login_time) <= 24,
+                        }"
+                    >
+                        {{ getLoginExpireText(record.login_time) }}
+                    </span>
+                </template>
                 <template #action="{ record }">
                     <a-button
                         type="text"
@@ -725,6 +726,12 @@
             slotName: 'time',
             width: 170,
         },
+        {
+            title: '失效倒计时',
+            dataIndex: 'login_expire_countdown',
+            slotName: 'login_expire_countdown',
+            width: 120,
+        },
         {
             title: '操作',
             slotName: 'action',
@@ -751,6 +758,31 @@
         return record.is_hide ? 'special-table-row' : '';
     };
 
+    const formatDateTime = (time?: Date | string | null) => {
+        if (!time || !dayjs(time).isValid()) {
+            return '-';
+        }
+
+        return dayjs(time).format('MM-DD HH:mm');
+    };
+
+    const getLoginExpireHours = (loginTime?: Date | string | null) => {
+        if (!loginTime || !dayjs(loginTime).isValid()) {
+            return Number.POSITIVE_INFINITY;
+        }
+
+        return 72 - dayjs().diff(dayjs(loginTime), 'hours');
+    };
+
+    const getLoginExpireText = (loginTime?: Date | string | null) => {
+        const hours = getLoginExpireHours(loginTime);
+        if (!Number.isFinite(hours)) {
+            return '-';
+        }
+
+        return `${hours} 小时失效`;
+    };
+
     if (route.query.show_hide === '1') {
         formModel.value.show_hide = true;
     }
@@ -1174,6 +1206,11 @@
         }
     }
 
+    .expire-warning {
+        color: rgb(var(--red-6));
+        font-weight: 500;
+    }
+
     :deep(.special-table-row td) {
         background-color: var(--color-warning-light-2) !important;
     }

+ 321 - 0
src/views/site/deeplinkReport.vue

@@ -0,0 +1,321 @@
+<template>
+    <div class="container">
+        <Breadcrumb :items="['menu.settings', 'menu.deeplinkReport']" />
+        <a-card class="general-card" :bordered="false">
+            <div class="page-header">
+                <div>
+                    <div class="page-title">DP 请求报表</div>
+                    <div class="page-subtitle">
+                        Redis · {{ dateRangeText }} ·
+                        {{ formatNumber(summary.total_count) }} 次转链 ·
+                        {{ channels.length }} 个渠道
+                    </div>
+                </div>
+                <a-tag color="green">Redis</a-tag>
+            </div>
+
+            <div class="filter-bar">
+                <a-range-picker
+                    v-model="dateRange"
+                    class="filter-date"
+                    @change="search"
+                />
+                <a-input
+                    v-model="formModel.channel_name"
+                    class="filter-control"
+                    placeholder="渠道名"
+                    allow-clear
+                    @press-enter="search"
+                />
+                <div class="filter-actions">
+                    <a-button @click="resetFilters">重置</a-button>
+                    <a-button type="primary" :loading="loading" @click="search">
+                        <template #icon><icon-search /></template>
+                        查询
+                    </a-button>
+                </div>
+            </div>
+
+            <a-table
+                row-key="row_key"
+                :loading="loading"
+                :pagination="pagination"
+                :columns="columns"
+                :data="renderData"
+                :bordered="{ headerCell: true }"
+                :scroll="{ x: 520 }"
+                :expanded-keys="expandedKeys"
+                hide-expand-button-on-empty
+                size="small"
+                @expanded-change="onExpandedChange"
+                @page-change="onPageChange"
+                @page-size-change="onPageSizeChange"
+            >
+                <template #report_date="{ record }">
+                    <strong v-if="record.row_type === 'daily'">
+                        {{ record.report_date }}
+                    </strong>
+                    <span v-else class="channel-name">
+                        <span>{{
+                            record.display_name || record.report_date
+                        }}</span>
+                        <span
+                            v-if="
+                                record.channel_name &&
+                                record.channel_name !==
+                                    (record.display_name || record.report_date)
+                            "
+                            class="channel-code"
+                        >
+                            {{ record.channel_name }}
+                        </span>
+                    </span>
+                </template>
+                <template #total_count="{ record }">
+                    <span
+                        class="number-cell"
+                        :class="{ highlight: record.row_type === 'daily' }"
+                    >
+                        {{ formatNumber(record.total_count) }}
+                    </span>
+                </template>
+                <template #success_count="{ record }">
+                    <span class="number-cell success">
+                        {{ formatNumber(record.success_count) }}
+                    </span>
+                </template>
+                <template #fail_count="{ record }">
+                    <span class="number-cell danger">
+                        {{ formatNumber(record.fail_count) }}
+                    </span>
+                </template>
+            </a-table>
+        </a-card>
+    </div>
+</template>
+
+<script lang="ts" setup>
+    import { computed, onMounted, reactive, ref } from 'vue';
+    import dayjs from 'dayjs';
+    import useLoading from '@/hooks/loading';
+    import { Pagination } from '@/types/global';
+    import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
+    import {
+        DeeplinkDailyReportChannel,
+        DeeplinkDailyReportParams,
+        DeeplinkDailyReportRecord,
+        DeeplinkDailyReportSummary,
+        queryDeeplinkDailyReport,
+    } from '@/api/deeplinkReport';
+
+    const defaultDateRange = () => [
+        dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
+        dayjs().format('YYYY-MM-DD'),
+    ];
+
+    const emptySummary = (): DeeplinkDailyReportSummary => ({
+        total_count: 0,
+        success_count: 0,
+        fail_count: 0,
+    });
+
+    const { loading, setLoading } = useLoading(false);
+    const dateRange = ref<string[]>(defaultDateRange());
+    const renderData = ref<DeeplinkDailyReportRecord[]>([]);
+    const summary = ref<DeeplinkDailyReportSummary>(emptySummary());
+    const channels = ref<DeeplinkDailyReportChannel[]>([]);
+    const expandedKeys = ref<string[]>([]);
+
+    const formModel = reactive({
+        channel_name: '',
+    });
+
+    const pagination = reactive<Pagination>({
+        current: 1,
+        pageSize: 20,
+        total: 0,
+        showPageSize: true,
+        showTotal: true,
+        pageSizeOptions: [10, 20, 50, 100],
+    });
+
+    const formatNumber = (value?: number | null) =>
+        Number(value || 0).toLocaleString();
+
+    const dateRangeText = computed(() => {
+        if (dateRange.value.length !== 2) return '-';
+        return `${dateRange.value[0]} 至 ${dateRange.value[1]}`;
+    });
+
+    const columns = computed<TableColumnData[]>(() => [
+        {
+            title: '日期 / 渠道',
+            dataIndex: 'report_date',
+            slotName: 'report_date',
+            width: 180,
+        },
+        {
+            title: '转链数',
+            dataIndex: 'total_count',
+            slotName: 'total_count',
+            align: 'right',
+            width: 130,
+        },
+        {
+            title: '成功',
+            dataIndex: 'success_count',
+            slotName: 'success_count',
+            align: 'right',
+            width: 110,
+        },
+        {
+            title: '失败',
+            dataIndex: 'fail_count',
+            slotName: 'fail_count',
+            align: 'right',
+            width: 110,
+        },
+    ]);
+
+    const buildParams = (): DeeplinkDailyReportParams => ({
+        query_date: dateRange.value,
+        channel_name: formModel.channel_name,
+    });
+
+    const fetchData = async () => {
+        setLoading(true);
+        try {
+            const res: any = await queryDeeplinkDailyReport(buildParams());
+            const data = res.data || {};
+            renderData.value = data.list || [];
+            summary.value = data.summary || emptySummary();
+            channels.value = data.channels || [];
+            expandedKeys.value = renderData.value[0]?.row_key
+                ? [renderData.value[0].row_key]
+                : [];
+            pagination.current = 1;
+            pagination.total = data.count || renderData.value.length;
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    const search = () => {
+        fetchData();
+    };
+
+    const resetFilters = () => {
+        dateRange.value = defaultDateRange();
+        formModel.channel_name = '';
+        fetchData();
+    };
+
+    const onExpandedChange = (keys: Array<string | number>) => {
+        expandedKeys.value = keys.map((item) => String(item));
+    };
+
+    const onPageChange = (current: number) => {
+        pagination.current = current;
+    };
+
+    const onPageSizeChange = (pageSize: number) => {
+        pagination.pageSize = pageSize;
+        pagination.current = 1;
+    };
+
+    onMounted(() => {
+        fetchData();
+    });
+</script>
+
+<style scoped lang="less">
+    .container {
+        padding: 0 20px 20px;
+    }
+
+    .page-header {
+        display: flex;
+        align-items: flex-start;
+        justify-content: space-between;
+        gap: 16px;
+        margin-bottom: 16px;
+    }
+
+    .page-title {
+        color: var(--color-text-1);
+        font-size: 20px;
+        font-weight: 600;
+        line-height: 28px;
+    }
+
+    .page-subtitle {
+        margin-top: 4px;
+        color: var(--color-text-3);
+        font-size: 13px;
+    }
+
+    .filter-bar {
+        display: flex;
+        flex-wrap: nowrap;
+        gap: 12px;
+        align-items: center;
+        overflow-x: auto;
+        padding-bottom: 16px;
+        margin-bottom: 16px;
+        border-bottom: 1px solid var(--color-border-2);
+    }
+
+    .filter-date {
+        flex: 0 0 300px;
+        width: 300px;
+    }
+
+    .filter-control {
+        flex: 0 0 150px;
+        width: 150px;
+    }
+
+    .filter-actions {
+        display: flex;
+        flex: 0 0 auto;
+        gap: 8px;
+        margin-left: auto;
+    }
+
+    .number-cell {
+        font-variant-numeric: tabular-nums;
+    }
+
+    .highlight {
+        padding: 2px 8px;
+        color: rgb(var(--green-7));
+        background: rgb(var(--green-1));
+        border-radius: 2px;
+    }
+
+    .channel-name {
+        display: inline-flex;
+        gap: 8px;
+        align-items: center;
+        color: var(--color-text-2);
+    }
+
+    .channel-code {
+        color: var(--color-text-4);
+        font-size: 12px;
+    }
+
+    .success {
+        color: rgb(var(--green-6));
+    }
+
+    .danger {
+        color: rgb(var(--red-6));
+    }
+
+    @media (max-width: 1080px) {
+        .filter-actions {
+            margin-left: auto;
+        }
+    }
+</style>