Jelajahi Sumber

会员管理新增页面

pxb 2 minggu lalu
induk
melakukan
a2a70038fc

+ 21 - 8
src/router/index.js

@@ -1,4 +1,4 @@
-import Vue from 'vue'
+import Vue from 'vue'
 import Router from 'vue-router'
 /* Layout */
 import Layout from '@/layout'
@@ -74,7 +74,7 @@ export const constantRoutes = [
     name: 'user',
     component: Layout,
     redirect: '/user/userList',
-    meta: { title: '会员列表', icon: 'form' },
+    meta: { title: '会员管理', icon: 'form' },
     children: [
       {
         path: 'userList',
@@ -88,14 +88,26 @@ export const constantRoutes = [
         name: 'userLevel',
         meta: { title: '会员等级' }
       },
-        {
-        path: 'userLoginLog',
-        component: () => import('@/views/user/userLoginLog/index'),
-        name: 'userLoginLog',
-        meta: { title: '用户登录日志' }
+      {
+        path: 'userStatistics',
+        component: () => import('@/views/user/userStatistics/index'),
+        name: 'userStatistics',
+        meta: { title: '会员统计' }
+      },
+      {
+        path: 'userDiary',
+        component: () => import('@/views/user/userDiary/index'),
+        name: 'userDiary',
+        meta: { title: '会员登录日志' }
+      },
+      {
+        path: 'userOnlineSummary',
+        component: () => import('@/views/user/userOnlineSummary/index'),
+        name: 'userOnlineSummary',
+        meta: { title: '在线汇总' }
       }
 
-    ]
+      ]
   },
   {
     path: '/chamber',
@@ -577,3 +589,4 @@ export function resetRouter() {
 }
 
 export default router
+

+ 87 - 0
src/views/user/userDiary/index.vue

@@ -0,0 +1,87 @@
+<template>
+  <div class="new-app-container layout-vertical">
+    <YMTableSearch :loading="loading" :search-form="searchForm" @submit="onSubmit" />
+    <YMTable ref="table" :table-key="5" :index="false" :selection="false" :columns="columns"
+      :apis="apis" :custom-style="{ height: 'calc(100vh - 200px)' }">
+    </YMTable>
+  </div>
+</template>
+
+<script>
+import { userLoginLog } from '@/api/data'
+
+export default {
+  name: 'UserDiary',
+  data() {
+    return {
+      loading: false,
+      searchForm: [
+        { label: '会员ID', type: 'input', prop: 'member_id', clearable: true },
+        { label: '会员昵称', type: 'input', prop: 'first_name', clearable: true },
+        { label: '日期范围', type: 'daterange', prop: 'dateRange', valueFormat: 'yyyy-MM-dd',
+          startPlaceholder: '开始日期', endPlaceholder: '结束日期' }
+      ]
+    }
+  },
+  computed: {
+    apis() {
+      return {
+        list: {
+          url: 'user/loginLog',
+          dataParam: 'data.data',
+          resultParam: 'data'
+        }
+      }
+    },
+    columns() {
+      return [
+        { label: '会员ID', prop: 'member_id', width: 100 },
+        { label: '会员昵称', prop: 'first_name', width: 140 },
+        { label: 'IP地址', prop: 'login_ip', width: 150, align: 'center',
+          method: (row) => row.login_ip || '--' },
+        { label: '国家/地区', prop: 'country', width: 120, align: 'center',
+          method: (row) => row.country || '--' },
+        { label: '登入网站/包名称', prop: 'website', minWidth: 200, align: 'center',
+          method: (row) => row.website || '--' },
+        { label: '平台', prop: 'platform', width: 100, align: 'center',
+          method: (row) => row.platform || '--' },
+        { label: '浏览器核心', prop: 'browser', width: 120, align: 'center',
+          method: (row) => row.browser || '--' },
+        { label: '操作系统', prop: 'os', width: 120, align: 'center',
+          method: (row) => row.os || '--' },
+        { label: '状态', prop: 'status', width: 80, align: 'center',
+          method: (row) => row.status ? (Number(row.status) === 1 ? '在线' : '离线') : '--' },
+        { label: '在线时长(分)', prop: 'online_duration', width: 120, align: 'center',
+          method: (row) => row.online_duration != null ? row.online_duration : '--' },
+        { label: '登入时间', prop: 'created_at', width: 175, align: 'center' },
+        { label: '登出时间', prop: 'logout_time', width: 175, align: 'center',
+          method: (row) => row.logout_time ? this.formatTime(row.logout_time) : '--' },
+      ]
+    }
+  },
+  methods: {
+    formatTime(ts) {
+      if (!ts) return '--'
+      const d = new Date(ts * 1000)
+      const pad = n => String(n).padStart(2, '0')
+      return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
+    },
+    onSubmit(params = {}) {
+      const query = { ...params }
+      if (params.dateRange && params.dateRange.length === 2) {
+        query.start_time = params.dateRange[0]
+        query.end_time = params.dateRange[1]
+      }
+      delete query.dateRange
+      this.$refs.table.getList(query)
+    }
+  },
+  mounted() {
+    this.onSubmit()
+  }
+}
+</script>
+
+<style scoped>
+.new-app-container { display: flex; flex-direction: column; height: 100%; padding: 16px; background: #f5f7fa; }
+</style>

+ 0 - 117
src/views/user/userLoginLog/index.vue

@@ -1,117 +0,0 @@
-<template>
-  <div class="new-app-container layout-vertical">
-    <YMTableSearch 
-      :loading="loading" 
-      :search-form="searchForm" 
-      @submit="onSubmit"
-    />
-
-    <YMTable
-      ref="table"
-      :table-key="2"
-      :index="true"
-      :selection="false"
-      :columns="columns"
-      :apis="apis"
-      :custom-style="{ height: 'calc(100vh - 200px)' }"
-    />
-  </div>
-</template>
-
-<script>
-import { userLoginLog } from '@/api/data'
-
-export default {
-  name: 'UserLogList',
-  data() {
-    return {
-      loading: false,
-      // 搜索配置
-      searchForm: [
-        {
-          label: '会员ID',
-          type: 'input',
-          prop: 'member_id',
-          clearable: true
-        },
-        {
-          label: '会员昵称',
-          type: 'input',
-          prop: 'first_name',
-          clearable: true
-        },
-        {
-          label: '日期范围',
-          type: 'daterange',
-          prop: 'dateRange',
-          valueFormat: 'yyyy-MM-dd',
-          startPlaceholder: '开始日期',
-          endPlaceholder: '结束日期'
-        }
-      ],
-      // 表格列配置
-      columns: [
-        {
-          label: '会员ID',
-          prop: 'member_id',
-          width: 120
-        },
-        {
-          label: '会员昵称',
-          prop: 'first_name', 
-          width: 150
-        },
-        
-        {
-          label: '登录IP',
-          prop: 'login_ip',
-          align: 'center'
-        },
-        {
-          label: '登录时间',
-          prop: 'created_at',
-          align: 'center',
-          sortable: 'custom'
-        }
-      ],
-      // 接口配置
-      apis: {
-        list: {
-          // 注意:此处建议封装一个标准的 list 接口适配器
-          // 如果 userLoginLog 返回结构与 YMTable 要求一致,可直接写 URL
-          url: 'user/loginLog', 
-          dataParam: 'data.data',
-          resultParam: 'data'
-        }
-      }
-    }
-  },
-  methods: {
-    // 搜索提交
-    onSubmit(params = {}) {
-      const query = { ...params }
-      
-      // 处理日期范围格式,适配接口需要的 start_time 和 end_time
-      if (params.dateRange && params.dateRange.length === 2) {
-        query.start_time = params.dateRange[0]
-        query.end_time = params.dateRange[1]
-      }
-      delete query.dateRange
-      
-      this.$refs.table.getList(query)
-    }
-  },
-  mounted() {
-    // 如果需要进入页面自动查询,请取消注释
-    // this.onSubmit()
-  }
-}
-</script>
-
-<style scoped>
-/* 保持与原页面一致的布局样式 */
-.layout-vertical {
-  display: flex;
-  flex-direction: column;
-}
-</style>

+ 212 - 0
src/views/user/userOnlineSummary/index.vue

@@ -0,0 +1,212 @@
+<template>
+  <div class="new-app-container layout-vertical summary-page">
+    <div class="content-panel">
+      <el-tabs v-model="activeTab" class="summary-tabs" @tab-click="handleTabClick">
+        <el-tab-pane label="在线用户" name="online" />
+        <el-tab-pane label="新用户汇总" name="newUser" />
+      </el-tabs>
+
+      <!-- 在线用户 -->
+      <template v-if="activeTab === 'online'">
+        <YMTableSearch :loading="loading" :search-form="onlineSearchForm" @submit="onSubmit" />
+        <div class="table-section">
+          <el-table v-loading="loading" :data="onlineFilteredData" border stripe
+            height="calc(100vh - 340px)" style="width: 100%; border-radius: 8px;"
+            empty-text="暂无在线用户数据">
+            <el-table-column prop="member_id" label="会员ID" width="100" align="center" />
+            <el-table-column prop="first_name" label="会员昵称" width="130" align="center" />
+            <el-table-column prop="balance" label="账户余额" width="120" align="center" />
+            <el-table-column prop="ip" label="IP地址" width="150" align="center" />
+            <el-table-column prop="location" label="位置" width="130" align="center" />
+            <el-table-column prop="website" label="登录网站" min-width="180" align="center" />
+            <el-table-column prop="platform" label="平台" width="100" align="center" />
+            <el-table-column prop="browser" label="浏览器" width="120" align="center" />
+            <el-table-column prop="os" label="操作系统" width="120" align="center" />
+            <el-table-column prop="login_time" label="登入时间" width="175" align="center" :formatter="formatTime" />
+          </el-table>
+        </div>
+      </template>
+
+      <!-- 新用户汇总 -->
+      <template v-if="activeTab === 'newUser'">
+        <YMTableSearch :loading="loading" :search-form="newUserSearchForm" @submit="onSubmit" />
+        <div class="info-lines">
+          <div class="info-line">1. 活跃会员:新注册会员下注&gt;=¥50</div>
+          <div class="info-line">2. 有效会员:下注金额&gt;=¥50的会员数</div>
+        </div>
+        <div class="table-section">
+          <el-table v-loading="loading" :data="newUserFilteredData" border stripe
+            height="calc(100vh - 440px)" style="width: 100%; border-radius: 8px;"
+            empty-text="暂无新用户汇总数据">
+            <el-table-column prop="date" label="时间" width="150" align="center" />
+            <el-table-column prop="h5_reg" label="H5注册" width="100" align="center" />
+            <el-table-column prop="app_reg" label="APP注册" width="100" align="center" />
+            <el-table-column prop="h5_first_deposit" label="H5首存次数" width="110" align="center" />
+            <el-table-column prop="app_first_deposit" label="APP首存次数" width="110" align="center" />
+            <el-table-column prop="active_members" label="活跃会员" width="100" align="center" />
+            <el-table-column prop="total_deposit" label="存款总额" width="120" align="center" />
+            <el-table-column prop="total_bet" label="下注总数" width="120" align="center" />
+            <el-table-column prop="company_profit" label="公司盈利" width="120" align="center" />
+            <el-table-column prop="valid_members" label="有效会员" width="100" align="center" />
+          </el-table>
+        </div>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script>
+const ONLINE_MOCK = [
+  { member_id: 10001, first_name: "test01", balance: 1250.50, ip: "192.168.1.100", location: "上海", website: "www.example.com", platform: "PC", browser: "Chrome 120", os: "Windows 10", login_time: 1722499200 },
+  { member_id: 10002, first_name: "test02", balance: 3200.00, ip: "192.168.1.101", location: "北京", website: "com.example.app", platform: "Android", browser: "WebView", os: "Android 14", login_time: 1722502800 },
+  { member_id: 10003, first_name: "test03", balance: 580.20, ip: "192.168.1.102", location: "广州", website: "www.example.com", platform: "iOS", browser: "Safari 17", os: "iOS 17.5", login_time: 1722506400 },
+  { member_id: 10004, first_name: "test04", balance: 8880.00, ip: "10.0.0.1", location: "深圳", website: "www.example.com", platform: "PC", browser: "Edge 120", os: "Windows 11", login_time: 1722513600 },
+  { member_id: 10005, first_name: "test05", balance: 150.00, ip: "192.168.1.105", location: "杭州", website: "com.example.mini", platform: "WeChat", browser: "XWeb", os: "iOS 17.4", login_time: 1722517200 },
+  { member_id: 10006, first_name: "test06", balance: 0.00, ip: "172.16.0.1", location: "纽约", website: "www.example.com", platform: "PC", browser: "Chrome 121", os: "macOS 14", login_time: 1722524400 },
+  { member_id: 10007, first_name: "test07", balance: 2350.00, ip: "192.168.1.107", location: "武汉", website: "com.example.app", platform: "iOS", browser: "Safari 17", os: "iOS 17.5", login_time: 1722528000 },
+  { member_id: 10008, first_name: "test08", balance: 680.50, ip: "192.168.1.108", location: "南京", website: "m.example.com", platform: "Mobile", browser: "Chrome Mobile", os: "Android 13", login_time: 1722535200 },
+  { member_id: 10009, first_name: "test09", balance: 4200.00, ip: "192.168.1.109", location: "重庆", website: "com.example.app", platform: "Android", browser: "WebView", os: "Android 12", login_time: 1722538800 },
+  { member_id: 10010, first_name: "test10", balance: 920.00, ip: "192.168.1.110", location: "厦门", website: "com.example.app", platform: "iOS", browser: "Safari 17", os: "iOS 17.5", login_time: 1722546000 },
+]
+
+const NEWUSER_MOCK = [
+  { date: "2026-08-01", h5_reg: 45, app_reg: 32, h5_first_deposit: 28, app_first_deposit: 20, active_members: 35, total_deposit: 28500, total_bet: 156000, company_profit: 8200, valid_members: 30 },
+  { date: "2026-08-02", h5_reg: 52, app_reg: 38, h5_first_deposit: 31, app_first_deposit: 24, active_members: 42, total_deposit: 32100, total_bet: 183000, company_profit: 9500, valid_members: 36 },
+  { date: "2026-08-03", h5_reg: 38, app_reg: 29, h5_first_deposit: 22, app_first_deposit: 18, active_members: 30, total_deposit: 19800, total_bet: 112000, company_profit: 6100, valid_members: 25 },
+  { date: "2026-08-04", h5_reg: 61, app_reg: 45, h5_first_deposit: 35, app_first_deposit: 28, active_members: 48, total_deposit: 35600, total_bet: 201000, company_profit: 11200, valid_members: 42 },
+  { date: "2026-08-05", h5_reg: 49, app_reg: 36, h5_first_deposit: 30, app_first_deposit: 22, active_members: 38, total_deposit: 26800, total_bet: 145000, company_profit: 7800, valid_members: 33 },
+  { date: "2026-08-06", h5_reg: 55, app_reg: 41, h5_first_deposit: 33, app_first_deposit: 26, active_members: 44, total_deposit: 34000, total_bet: 192000, company_profit: 10500, valid_members: 38 },
+  { date: "2026-08-07", h5_reg: 42, app_reg: 30, h5_first_deposit: 25, app_first_deposit: 19, active_members: 33, total_deposit: 22000, total_bet: 128000, company_profit: 6900, valid_members: 28 },
+]
+
+export default {
+  name: "UserOnlineSummary",
+  data() {
+    return {
+      activeTab: "online",
+      loading: false,
+      onlineSearchForm: [
+        { label: "会员ID", type: "input", prop: "member_id", clearable: true },
+        { label: "会员昵称", type: "input", prop: "first_name", clearable: true },
+        { label: "IP地址", type: "input", prop: "ip", clearable: true },
+        { label: "平台", type: "select", prop: "platform", options: [
+          { label: "全部", value: "" },
+          { label: "PC", value: "PC" },
+          { label: "Android", value: "Android" },
+          { label: "iOS", value: "iOS" },
+          { label: "Mobile", value: "Mobile" },
+          { label: "WeChat", value: "WeChat" },
+        ], clearable: true },
+      ],
+      newUserSearchForm: [
+        { label: "开始日期", type: "date", prop: "start_date", clearable: true },
+        { label: "结束日期", type: "date", prop: "end_date", clearable: true },
+      ],
+      onlineFilteredData: [],
+      newUserFilteredData: [],
+      allOnlineData: [...ONLINE_MOCK],
+      allNewUserData: [...NEWUSER_MOCK],
+    }
+  },
+  methods: {
+    formatTime(row) {
+      if (!row.login_time) return "--"
+      const d = new Date(row.login_time * 1000)
+      const pad = n => String(n).padStart(2, "0")
+      return d.getFullYear() + "-" + pad(d.getMonth()+1) + "-" + pad(d.getDate()) + " " + pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds())
+    },
+    handleTabClick() {
+      this.onSubmit({})
+    },
+    onValueChange(val) {},
+    onSubmit(params) {
+      this.loading = true
+      params = params || {}
+      if (this.activeTab === "online") {
+        this.filterOnline(params)
+      } else {
+        this.filterNewUser(params)
+      }
+      this.loading = false
+    },
+    filterOnline(params) {
+      let filtered = [...this.allOnlineData]
+      if (params.member_id) filtered = filtered.filter(r => String(r.member_id).includes(params.member_id))
+      if (params.first_name) filtered = filtered.filter(r => r.first_name.includes(params.first_name))
+      if (params.ip) filtered = filtered.filter(r => r.ip.includes(params.ip))
+      if (params.platform) filtered = filtered.filter(r => r.platform === params.platform)
+      this.onlineFilteredData = filtered
+    },
+    filterNewUser(params) {
+      let filtered = [...this.allNewUserData]
+      if (params.start_date) {
+        filtered = filtered.filter(r => r.date >= params.start_date)
+      }
+      if (params.end_date) {
+        filtered = filtered.filter(r => r.date <= params.end_date)
+      }
+      this.newUserFilteredData = filtered
+    }
+  },
+  mounted() {
+    this.onSubmit({})
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.summary-page {
+  padding: 16px;
+  background: #f5f7fa;
+}
+.min-height-100 { min-height: 100%; }
+.content-panel {
+  min-height: calc(100vh - 82px);
+  overflow: hidden;
+  border-radius: 8px;
+  background: #fff;
+}
+
+.summary-tabs {
+  ::v-deep .el-tabs__header {
+    margin: 0;
+    padding: 0 20px;
+  }
+
+  ::v-deep .el-tabs__item {
+    height: 52px;
+    line-height: 52px;
+    font-weight: 500;
+  }
+}
+
+.filter-panel {
+  padding: 16px 20px 0;
+  border-bottom: 1px solid #ebeef5;
+  background: #fff;
+}
+
+.table-section {
+  padding: 18px 20px 20px;
+}
+
+.info-lines {
+  padding: 12px 20px 4px;
+  font-size: 14px;
+  color: #606266;
+}
+.info-line {
+  line-height: 28px;
+}
+.info-line + .info-line {
+  margin-top: 4px;
+}
+</style>
+
+
+
+
+
+
+
+

+ 328 - 0
src/views/user/userStatistics/index.vue

@@ -0,0 +1,328 @@
+<template>
+  <div class="new-app-container layout-vertical statistics-page">
+    <div class="content-panel">
+      <el-tabs v-model="activeTab" class="statistics-tabs" @tab-click="handleTabChange">
+        <el-tab-pane label="走势图" name="trend" />
+        <el-tab-pane label="域名统计" name="domain" />
+      </el-tabs>
+
+      <div class="filter-panel">
+        <el-form inline :model="query" class="filter-form">
+          <el-form-item v-if="activeTab === 'trend'" label="统计类型">
+            <el-radio-group v-model="query.type" size="small" @change="handleTypeChange">
+              <el-radio-button label="today">今日</el-radio-button>
+              <el-radio-button label="all">全部</el-radio-button>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="注册时间">
+            <el-date-picker
+              v-model="query.dateRange"
+              type="daterange"
+              unlink-panels
+              range-separator="至"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              value-format="yyyy-MM-dd"
+              :picker-options="pickerOptions"
+              :clearable="false"
+            />
+          </el-form-item>
+          <el-form-item class="filter-actions">
+            <el-button type="primary" size="small" icon="el-icon-search" :loading="loading" @click="getData">
+              查询
+            </el-button>
+            <el-button size="small" icon="el-icon-refresh-left" @click="resetQuery">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <div v-show="activeTab === 'trend'" v-loading="loading" class="trend-panel">
+        <div class="panel-title">
+          会员注册走势图
+        </div>
+        <div ref="trendChart" class="trend-chart" />
+      </div>
+
+      <div v-show="activeTab === 'domain'" class="domain-panel">
+        <div class="panel-title">
+          域名统计
+        </div>
+        <el-table v-loading="loading" :data="domainRows" border stripe height="calc(100vh - 300px)" empty-text="暂无域名统计数据">
+          <el-table-column prop="domain" label="登录网址" min-width="280" />
+          <el-table-column prop="count" label="登录人数" min-width="180" />
+        </el-table>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+
+const TREND_HOURLY = [
+  { label: '00:00', user: 3, virtual: 1 },
+  { label: '01:00', user: 1, virtual: 0 },
+  { label: '02:00', user: 0, virtual: 0 },
+  { label: '03:00', user: 0, virtual: 0 },
+  { label: '04:00', user: 1, virtual: 0 },
+  { label: '05:00', user: 2, virtual: 1 },
+  { label: '06:00', user: 4, virtual: 2 },
+  { label: '07:00', user: 6, virtual: 3 },
+  { label: '08:00', user: 8, virtual: 4 },
+  { label: '09:00', user: 12, virtual: 5 },
+  { label: '10:00', user: 15, virtual: 6 },
+  { label: '11:00', user: 18, virtual: 7 },
+  { label: '12:00', user: 14, virtual: 5 },
+  { label: '13:00', user: 16, virtual: 6 },
+  { label: '14:00', user: 20, virtual: 8 },
+  { label: '15:00', user: 22, virtual: 9 },
+  { label: '16:00', user: 19, virtual: 7 },
+  { label: '17:00', user: 17, virtual: 6 },
+  { label: '18:00', user: 13, virtual: 5 },
+  { label: '19:00', user: 11, virtual: 4 },
+  { label: '20:00', user: 9, virtual: 3 },
+  { label: '21:00', user: 7, virtual: 2 },
+  { label: '22:00', user: 5, virtual: 1 },
+  { label: '23:00', user: 3, virtual: 1 },
+]
+
+const TREND_DAILY = [
+  { label: '07-26', user: 45, virtual: 8 },
+  { label: '07-27', user: 52, virtual: 12 },
+  { label: '07-28', user: 38, virtual: 6 },
+  { label: '07-29', user: 61, virtual: 15 },
+  { label: '07-30', user: 49, virtual: 10 },
+  { label: '07-31', user: 55, virtual: 14 },
+  { label: '08-01', user: 42, virtual: 9 },
+]
+
+const DOMAIN_MOCK = [
+  { domain: 'www.example.com', count: 156 },
+  { domain: 'm.example.com', count: 89 },
+  { domain: 'com.example.app', count: 234 },
+  { domain: 'www.example.net', count: 67 },
+  { domain: 'com.example.mini', count: 45 },
+  { domain: 'api.example.com', count: 123 },
+]
+
+export default {
+  name: 'UserStatistics',
+  data() {
+    return {
+      activeTab: 'trend',
+      loading: false,
+      chart: null,
+      query: {
+        type: 'today',
+        dateRange: []
+      },
+      trendRows: [],
+      domainRows: [],
+      allDomainData: [...DOMAIN_MOCK],
+      pickerOptions: {
+        disabledDate(time) {
+          return time.getTime() > Date.now()
+        }
+      }
+    }
+  },
+  created() {
+    this.setDefaultDateRange()
+  },
+  mounted() {
+    window.addEventListener('resize', this.resizeChart)
+    this.getData()
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.resizeChart)
+    if (this.chart) this.chart.dispose()
+  },
+  methods: {
+    formatDate(date) {
+      const year = date.getFullYear()
+      const month = String(date.getMonth() + 1).padStart(2, '0')
+      const day = String(date.getDate()).padStart(2, '0')
+      return `${year}-${month}-${day}`
+    },
+    setDefaultDateRange() {
+      const end = new Date()
+      const start = new Date()
+      if (this.activeTab === 'domain' || this.query.type === 'all') {
+        start.setDate(start.getDate() - 29)
+      }
+      this.query.dateRange = [this.formatDate(start), this.formatDate(end)]
+    },
+    handleTypeChange() {
+      this.setDefaultDateRange()
+      this.getData()
+    },
+    handleTabChange() {
+      this.setDefaultDateRange()
+      this.getData()
+    },
+    resetQuery() {
+      this.query.type = 'today'
+      this.setDefaultDateRange()
+      this.getData()
+    },
+    getData() {
+      this.loading = true
+      setTimeout(() => {
+        if (this.activeTab === 'trend') {
+          this.trendRows = this.query.type === 'today' ? [...TREND_HOURLY] : [...TREND_DAILY]
+          this.$nextTick(this.renderChart)
+        } else {
+          this.domainRows = [...this.allDomainData]
+        }
+        this.loading = false
+      }, 300)
+    },
+    renderChart() {
+      if (!this.$refs.trendChart) return
+      if (!this.chart) {
+        this.chart = echarts.init(this.$refs.trendChart)
+      }
+
+      const hasData = this.trendRows.length > 0 && this.trendRows.some(r => r.user > 0 || r.virtual > 0)
+      const isToday = this.query.type === 'today'
+      const userName = isToday ? '今日会员新增' : '会员总数'
+      const virtualName = isToday ? '今日虚拟会员新增' : '虚拟会员总数'
+
+      if (!hasData) {
+        this.chart.clear()
+        this.chart.setOption({
+          title: {
+            text: '暂无数据',
+            left: 'center',
+            top: 'center',
+            textStyle: { color: '#c0c4cc', fontSize: 14, fontWeight: 400 }
+          },
+          xAxis: { show: false },
+          yAxis: { show: false },
+          series: []
+        }, true)
+        return
+      }
+
+      this.chart.setOption({
+        color: ['#409EFF', '#F56C6C'],
+        tooltip: { trigger: 'axis' },
+        legend: { top: 2, data: [userName, virtualName] },
+        grid: { left: 48, right: 28, top: 52, bottom: 42, containLabel: true },
+        xAxis: {
+          type: 'category',
+          boundaryGap: false,
+          data: this.trendRows.map(item => item.label),
+          axisLine: { show: false },
+          axisTick: { show: false },
+          axisLabel: { color: '#909399', fontSize: 12 }
+        },
+        yAxis: {
+          type: 'value',
+          minInterval: 1,
+          splitLine: { lineStyle: { color: '#F0F0F0', type: 'dashed' }},
+          axisLine: { show: false },
+          axisTick: { show: false },
+          axisLabel: { color: '#909399', fontSize: 12 }
+        },
+        series: [
+          {
+            name: userName,
+            type: 'line',
+            smooth: true,
+            showSymbol: false,
+            lineStyle: { width: 2 },
+            areaStyle: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                { offset: 0, color: 'rgba(64,158,255,0.3)' },
+                { offset: 1, color: 'rgba(64,158,255,0.01)' }
+              ])
+            },
+            data: this.trendRows.map(item => item.user)
+          },
+          {
+            name: virtualName,
+            type: 'line',
+            smooth: true,
+            showSymbol: false,
+            lineStyle: { width: 2 },
+            areaStyle: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                { offset: 0, color: 'rgba(245,108,108,0.3)' },
+                { offset: 1, color: 'rgba(245,108,108,0.01)' }
+              ])
+            },
+            data: this.trendRows.map(item => item.virtual)
+          }
+        ]
+      }, true)
+    },
+    resizeChart() {
+      if (this.chart) this.chart.resize()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.statistics-page {
+  padding: 16px;
+  background: #f5f7fa;
+}
+.content-panel {
+  min-height: calc(100vh - 82px);
+  overflow: hidden;
+  border-radius: 8px;
+  background: #fff;
+}
+.statistics-tabs {
+  ::v-deep .el-tabs__header {
+    margin: 0;
+    padding: 0 20px;
+  }
+  ::v-deep .el-tabs__item {
+    height: 52px;
+    line-height: 52px;
+    font-weight: 500;
+  }
+}
+.filter-panel {
+  padding: 16px 20px 0;
+  border-bottom: 1px solid #ebeef5;
+  background: #fff;
+}
+.filter-form {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  ::v-deep .el-form-item__label {
+    color: #303133;
+    font-size: 15px;
+    font-weight: 500;
+  }
+}
+.filter-actions {
+  margin-left: auto;
+}
+.trend-panel,
+.domain-panel {
+  padding: 18px 20px 20px;
+}
+.panel-title {
+  margin-bottom: 6px;
+  color: #303133;
+  font-size: 16px;
+  font-weight: 600;
+}
+.trend-chart {
+  width: 100%;
+  height: calc(100vh - 270px);
+  min-height: 380px;
+}
+@media (max-width: 768px) {
+  .statistics-page { padding: 10px; }
+  .filter-actions { width: 100%; margin-left: 0; }
+  .trend-chart { height: 420px; min-height: 420px; }
+}
+</style>
+

+ 0 - 1
src/views/yuebao/index.vue

@@ -176,7 +176,6 @@ export default {
         {
           label: '创建时间',
           prop: 'create_time',
-          sortable: 'custom',
           method: (row) => this.formatDate(row.create_time),
           width: 160
         },