luzhenxing hace 2 meses
padre
commit
448e8d32d7

+ 1 - 1
.vscode/settings.json

@@ -92,7 +92,7 @@
       "source.fixAll.eslint": "explicit",
       "source.fixAll.stylelint": "explicit"
     },
-    "editor.defaultFormatter": "octref.vetur"
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "i18n-ally.localesPaths": ["src/locales/lang"],
   "i18n-ally.keystyle": "nested",

+ 71 - 65
src/views/dashboard/workbench/components/ChartsAreaLine.vue

@@ -1,79 +1,85 @@
 <template>
   <Flex justify="space-between">
-    <span style="opacity: .6;">{{ label }}</span>
+    <span style="opacity: 0.6">{{ label }}</span>
     <span :style="{ color: rate >= 0 ? '#10B981' : '#df3c2f' }">
-      <ArrowUpOutlined v-if="rate > 0" style="font-size: .9em;" />
-      <ArrowDownOutlined v-else-if="rate < 0" style="font-size: .9em;" />
+      <ArrowUpOutlined v-if="rate > 0" style="font-size: 0.9em" />
+      <ArrowDownOutlined v-else-if="rate < 0" style="font-size: 0.9em" />
       {{ Math.abs(rate) }}%
     </span>
   </Flex>
   <div ref="echartsLineRef" class="echarts-box"></div>
 </template>
+
 <script lang="ts" setup>
-import { Ref, ref, watchEffect } from 'vue';
-import { Flex } from 'ant-design-vue'
-import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons-vue'
-import { useECharts } from '@/hooks/web/useECharts';
+  import { Ref, ref, watchEffect } from 'vue';
+  import { Flex } from 'ant-design-vue';
+  import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons-vue';
+  import { useECharts } from '@/hooks/web/useECharts';
+
+  const echartsLineRef = ref<HTMLDivElement | null>(null);
+  const { setOptions, resize } = useECharts(echartsLineRef as Ref<HTMLDivElement>);
+  const { label, labels, rate, title, data } = defineProps<{
+    label: string;
+    labels: string[];
+    rate: number;
+    title: string;
+    data: number[];
+  }>();
 
-const echartsLineRef = ref<HTMLDivElement | null>(null);
-const { setOptions, resize } = useECharts(echartsLineRef as Ref<HTMLDivElement>);
-const { label, labels, rate, title, data } = defineProps<{
-  label: string,
-  labels: string[],
-  rate: number
-  title: string,
-  data: number[]
-}>()
-watchEffect(() => {
-  setOptions({
-    title: {
-      text: filterTitle(title)
-    },
-    tooltip: {
-      trigger: 'axis',
-    },
-    grid: {
-      left: 0,
-      right: 0,
-      bottom: 0,
-      top: 0,
-      // containLabel: false
-    },
-    xAxis: {
-      show: false,
-      type: 'category',
-      boundaryGap: false,
-      data: labels
-    },
-    yAxis: {
-      show: false,
-      type: 'value'
-    },
-    series: [{
-      data,
-      type: 'line',
-      smooth: true,
-      color: [rate >= 0 ? '#10B981' : '#df3c2f'],
-      showSymbol: false,
-      areaStyle: {
-        opacity: .1
-      }
-    }]
+  watchEffect(() => {
+    setOptions({
+      title: {
+        text: filterTitle(title),
+      },
+      tooltip: {
+        trigger: 'axis',
+      },
+      grid: {
+        left: 0,
+        right: 0,
+        bottom: 0,
+        top: 0,
+        // containLabel: false
+      },
+      xAxis: {
+        show: false,
+        type: 'category',
+        boundaryGap: false,
+        data: labels,
+      },
+      yAxis: {
+        show: false,
+        type: 'value',
+      },
+      series: [
+        {
+          data,
+          type: 'line',
+          smooth: true,
+          color: [rate >= 0 ? '#10B981' : '#df3c2f'],
+          showSymbol: false,
+          areaStyle: {
+            opacity: 0.1,
+          },
+        },
+      ],
+    });
+    setTimeout(resize, 100);
   });
-  setTimeout(resize, 100)
-});
-function filterTitle(value) {
-  let str = ''
-  // if (value) {
-    str = new Intl.NumberFormat('en-US').format(value) + '次'
-  // }
-  return str
-}
+
+  function filterTitle(value) {
+    let str = '';
+    // if (value) {
+    str = new Intl.NumberFormat('en-US').format(value) + '次';
+    // }
+    return str;
+  }
 </script>
+
 <style lang="scss" scoped>
-.echarts-box {
-  width: 100%;
-  height: 140px;
-  margin-top: 10px;
-}
+  .echarts-box {
+    width: 100%;
+    height: 140px;
+    margin-top: 10px;
+  }
 </style>

+ 39 - 35
src/views/dashboard/workbench/components/ChartsLine.vue

@@ -1,41 +1,45 @@
 <template>
-  <div ref="echartsLineRef" style="height: 400px;"></div>
+  <div ref="echartsLineRef" style="height: 400px"></div>
 </template>
+
 <script lang="ts" setup>
-import { Ref, ref, watchEffect } from 'vue';
-import { useECharts } from '@/hooks/web/useECharts';
+  import { Ref, ref, watchEffect } from 'vue';
+  import { useECharts } from '@/hooks/web/useECharts';
+
+  const echartsLineRef = ref<HTMLDivElement | null>(null);
+  const { setOptions } = useECharts(echartsLineRef as Ref<HTMLDivElement>);
+  const { title, labels, data } = defineProps<{
+    title: string;
+    labels: string[];
+    data: number[];
+  }>();
 
-const echartsLineRef = ref<HTMLDivElement | null>(null);
-const { setOptions } = useECharts(echartsLineRef as Ref<HTMLDivElement>);
-const { title, labels, data } = defineProps<{
-  title: string
-  labels: string[]
-  data: number[]
-}>()
-watchEffect(() => {
-  setOptions({
-    title: {
-      text: title
-    },
-    tooltip: {
-      trigger: 'axis',
-    },
-    grid: {
-      right: 0,
-      containLabel: false
-    },
-    xAxis: {
-      type: 'category',
-      data: labels
-    },
-    yAxis: {
-      type: 'value'
-    },
-    series: [{
-      data,
-      type: 'line',
-      smooth: true,
-    }]
+  watchEffect(() => {
+    setOptions({
+      title: {
+        text: title,
+      },
+      tooltip: {
+        trigger: 'axis',
+      },
+      grid: {
+        right: 0,
+        containLabel: false,
+      },
+      xAxis: {
+        type: 'category',
+        data: labels,
+      },
+      yAxis: {
+        type: 'value',
+      },
+      series: [
+        {
+          data,
+          type: 'line',
+          smooth: true,
+        },
+      ],
+    });
   });
-});
 </script>

+ 41 - 38
src/views/dashboard/workbench/components/ChartsPie.vue

@@ -1,45 +1,48 @@
 <template>
-  <div ref="echartsLineRef" style="height: 400px;"></div>
+  <div ref="echartsLineRef" style="height: 400px"></div>
 </template>
 <script lang="ts" setup>
-import { Ref, ref, watchEffect } from 'vue';
-import { useECharts } from '@/hooks/web/useECharts';
+  import { Ref, ref, watchEffect } from 'vue';
+  import { useECharts } from '@/hooks/web/useECharts';
 
-const echartsLineRef = ref<HTMLDivElement | null>(null);
-const { setOptions } = useECharts(echartsLineRef as Ref<HTMLDivElement>);
-const { title, data } = defineProps<{
-  title: string
-  data: {
-    value: number
-    name: string
-  }[]
-}>()
-watchEffect(() => {
-  setOptions({
-    title: {
-      text: title
-    },
-    tooltip: {
-      trigger: 'item',
-      formatter: '{b} <br/>{c} ({d}%)'
-    },
-    legend: {
-      type: 'scroll',
-      // orient: 'vertical',
-      bottom: 'bottom'
-    },
-    series: [{
-      name: title,
-      type: 'pie',
-      radius: ['45%', '60%'],
-      // avoidLabelOverlap: false,
-      padAngle: 5,
-      label: {
-        formatter: '{b} {d}%',
-        color: 'inherit'
+  const echartsLineRef = ref<HTMLDivElement | null>(null);
+  const { setOptions } = useECharts(echartsLineRef as Ref<HTMLDivElement>);
+  const { title, data } = defineProps<{
+    title: string;
+    data: {
+      value: number;
+      name: string;
+    }[];
+  }>();
+
+  watchEffect(() => {
+    setOptions({
+      title: {
+        text: title,
+      },
+      tooltip: {
+        trigger: 'item',
+        formatter: '{b} <br/>{c} ({d}%)',
+      },
+      legend: {
+        type: 'scroll',
+        // orient: 'vertical',
+        bottom: 'bottom',
       },
-      data
-    }]
+      series: [
+        {
+          name: title,
+          type: 'pie',
+          radius: ['45%', '60%'],
+          // avoidLabelOverlap: false,
+          padAngle: 5,
+          label: {
+            formatter: '{b} {d}%',
+            color: 'inherit',
+          },
+          data,
+        },
+      ],
+    });
   });
-});
 </script>

+ 23 - 25
src/views/dashboard/workbench/components/TableList.vue

@@ -10,43 +10,41 @@
         hideOnSinglePage: true,
         current: currentPage,
         pageSize: 7,
-        onChange: page => currentPage = page,
+        onChange: (page) => (currentPage = page),
       }"
     >
       <template #bodyCell="{ column, text }">
-        <template v-if="column.dataIndex === 'interaction_rate'">
-          {{ text }}%
-        </template>
+        <template v-if="column.dataIndex === 'interaction_rate'">{{ text }}%</template>
       </template>
     </Table>
   </section>
 </template>
 <script setup lang="ts">
-import { ref, watchEffect } from 'vue';
-import { Table } from 'ant-design-vue'
-const { title, columns, data } = defineProps<{
-  title: string
-  columns: object[]
-  data: object[]
-}>()
+  import { ref, watchEffect } from 'vue';
+  import { Table } from 'ant-design-vue';
 
-const currentPage = ref(1)
+  const { title, columns, data } = defineProps<{
+    title: string;
+    columns: object[];
+    data: object[];
+  }>();
 
-watchEffect(() => {
-  console.log('watchEffect')
-  currentPage.value = 1
-})
+  const currentPage = ref(1);
+
+  watchEffect(() => {
+    currentPage.value = 1;
+  });
 </script>
 <style lang="scss" scoped>
-.section {
-  .title {
-    font-size: 18px;
-    font-weight: bold;
-  }
+  .section {
+    .title {
+      font-size: 18px;
+      font-weight: bold;
+    }
 
-  .table {
-    width: 100%;
-    margin-top: 12px;
+    .table {
+      width: 100%;
+      margin-top: 12px;
+    }
   }
-}
 </style>

+ 160 - 88
src/views/dashboard/workbench/index.vue

@@ -1,33 +1,41 @@
 <!-- 微信私域运营数据看板页面 -->
 <template>
-  <PageWrapper class="page-wrapper">
+  <PageWrapper class="dashboard">
     <!-- 页面头部内容 -->
     <template #headerContent>
       <Flex justify="space-between" align="center">
-        <h1 class="title">微信私域用户运营数据看板</h1>
+        <h1 class="dashboard__title">微信私域用户运营数据看板</h1>
         <Space>
           <!-- 快捷日期选择按钮组 -->
-          <div>
-            <a-button type="link" size="small" @click="toDate(7)">近7天</a-button>
-            <a-button type="link" size="small" @click="toDate(1)">昨天</a-button>
-            <a-button type="link" size="small" @click="toDate(0)">今天</a-button>
+          <div class="dashboard__date-shortcuts">
+            <a-button
+              v-for="item in DATE_SHORTCUTS"
+              :key="item.days"
+              type="link"
+              size="small"
+              @click="handleDateShortcut(item.days)"
+            >
+              {{ item.label }}
+            </a-button>
           </div>
+
           <!-- 日期范围选择器 -->
           <RangePicker
             v-model:value="date"
             size="small"
             :allowClear="false"
             inputReadOnly
-            @change="fetchData"
+            @change="handleDateChange"
           />
+
           <!-- 超级管理员可见的组织选择器 -->
           <template v-if="isSuper">
             <UserOutlined />
             <Select
+              v-model:value="organizationId"
               size="small"
               style="width: 100px"
-              v-model:value="organizationId"
-              @change="fetchData"
+              @change="handleOrgChange"
             >
               <SelectOption :value="0">所有账号</SelectOption>
               <SelectOption v-for="item in departmentList" :key="item.id" :value="item.id">
@@ -38,13 +46,13 @@
         </Space>
       </Flex>
     </template>
-    <!-- 数据加载状态 -->
+
+    <!-- 数据展示区域 -->
     <Spin :spinning="loading" size="large">
-      <!-- 图表展示区域 -->
       <Space v-if="charts" direction="vertical" :size="32">
-        <!-- 第一行:数据概览卡片 -->
+        <!-- 数据概览卡片 -->
         <Row :gutter="[32, 32]">
-          <Col :span="6" v-for="item in chartsAreaLineMap" :key="item.key">
+          <Col v-for="item in CHART_CONFIG.AREA_LINE_MAP" :key="item.key" :span="6">
             <ChartsAreaLine
               :label="item.label"
               :rate="charts[item.key].rate"
@@ -54,30 +62,32 @@
             />
           </Col>
         </Row>
-        <!-- 第二行:趋势图表 -->
+
+        <!-- 趋势图表 -->
         <Row :gutter="32">
           <Col :span="12">
             <ChartsLine
               title="Token 消耗趋势"
-              :labels="charts.consume_token.label"
-              :data="charts.consume_token.val"
+              :labels="charts.consume_token.label || []"
+              :data="charts.consume_token.val || []"
             />
           </Col>
           <Col :span="12">
             <ChartsLine
               title="用户增长趋势"
-              :labels="charts.new_user.label"
-              :data="charts.new_user.val"
+              :labels="charts.new_user.label || []"
+              :data="charts.new_user.val || []"
             />
           </Col>
         </Row>
-        <!-- 第三行:标签分布和账号分析 -->
+
+        <!-- 标签分布和账号分析 -->
         <Row :gutter="32">
           <Col :span="12">
             <ChartsPie title="标签分布" :data="charts.label_dist" />
           </Col>
           <Col :span="12">
-            <TableList title="账号分析" :columns="columns" :data="tableData" />
+            <TableList title="账号分析" :columns="TABLE_COLUMNS" :data="tableData" />
           </Col>
         </Row>
       </Space>
@@ -86,91 +96,124 @@
 </template>
 
 <script lang="ts" setup>
-  // 导入所需的组件和工具
-  import { onMounted, ref } from 'vue';
-  import { PageWrapper } from '@/components/Page';
+  // Vue 核心
+  import { onMounted, ref, computed } from 'vue';
+
+  // 第三方组件
   import { Flex, Row, Col, Space, Select, SelectOption, RangePicker, Spin } from 'ant-design-vue';
   import { UserOutlined } from '@ant-design/icons-vue';
+  import dayjs, { Dayjs } from 'dayjs';
+
+  // 自定义组件
+  import { PageWrapper } from '@/components/Page';
   import ChartsAreaLine from './components/ChartsAreaLine.vue';
   import ChartsLine from './components/ChartsLine.vue';
   import ChartsPie from './components/ChartsPie.vue';
   import TableList from './components/TableList.vue';
-  import dayjs from 'dayjs';
-  import { getDashboardCharts, getDashboardWxList } from '/@/api/dashboard/dashboard';
+
+  // API 和工具
+  import { getDashboardCharts, getDashboardWxList } from '@/api/dashboard/dashboard';
   import { getDepartmentList } from '@/api/sys/department';
-  import { useUserStore } from '/@/store/modules/user';
-  import { DepartmentInfo } from '/@/api/sys/model/departmentModel';
+  import { useUserStore } from '@/store/modules/user';
+  import type { DepartmentInfo } from '@/api/sys/model/departmentModel';
 
-  // 用户信息和权限判断
-  const userStore = useUserStore();
-  const userInfo = userStore.getUserInfo;
-  const isSuper = userInfo.roleName.some((str) => str == '超级管理员');
+  // 类型定义
+  interface ChartData {
+    rate: number;
+    count: number;
+    label: string[] | null;
+    val: number[] | null;
+  }
 
-  // 页面状态管理
-  const loading = ref(false);
-  const organizationId = ref(0);
-  const departmentList = ref<DepartmentInfo[]>([]);
-  const charts = ref();
-  const date = ref<[dayjs.Dayjs, dayjs.Dayjs]>();
-  const tableData = ref<object[]>([]);
-
-  // 图表配置数据
-  const chartsAreaLineMap = [
-    { label: 'AI 回复次数', key: 'ai_response' },
-    { label: 'SOP执行次数', key: 'sop_run' },
-    { label: '总好友数', key: 'total_friend' },
-    { label: '总群数', key: 'total_group' },
-    { label: '账户余额', key: 'account_balance' },
-    { label: 'Token消耗', key: 'consume_token' },
-    { label: '活跃用户', key: 'active_user' },
-    { label: '新增好友', key: 'new_user' },
-  ];
+  interface DashboardCharts {
+    ai_response: ChartData;
+    sop_run: ChartData;
+    total_friend: ChartData;
+    total_group: ChartData;
+    account_balance: ChartData;
+    consume_token: ChartData;
+    active_user: ChartData;
+    new_user: ChartData;
+    label_dist: any[];
+  }
+
+  interface TableRecord {
+    nickname: string;
+    total_friend: number;
+    total_group: number;
+    interaction_rate: number;
+  }
 
-  // 表格列配置
-  const columns = [
+  // 常量配置
+  const CHART_CONFIG = {
+    AREA_LINE_MAP: [
+      { label: 'AI 回复次数', key: 'ai_response' },
+      { label: 'SOP执行次数', key: 'sop_run' },
+      { label: '总好友数', key: 'total_friend' },
+      { label: '总群数', key: 'total_group' },
+      { label: '账户余额', key: 'account_balance' },
+      { label: 'Token消耗', key: 'consume_token' },
+      { label: '活跃用户', key: 'active_user' },
+      { label: '新增好友', key: 'new_user' },
+    ],
+  };
+
+  const TABLE_COLUMNS = [
     { title: '账号', dataIndex: 'nickname' },
     { title: '好友数', dataIndex: 'total_friend' },
     { title: '群数量', dataIndex: 'total_group' },
     { title: '互动率', dataIndex: 'interaction_rate' },
   ];
 
-  // 页面初始化
-  onMounted(async () => {
-    toDate(0); // 默认显示今天的数据
-    if (isSuper) {
-      // 如果是超级管理员,加载部门列表
-      const res = await getDepartmentList({ page: 1, pageSize: 100 });
-      departmentList.value = res.data.data;
-    }
+  const DATE_SHORTCUTS = [
+    { label: '近7天', days: 7 },
+    { label: '昨天', days: 1 },
+    { label: '今天', days: 0 },
+  ];
+
+  // 组件逻辑
+  const userStore = useUserStore();
+  const userInfo = userStore.getUserInfo;
+  const isSuper = computed(() => userInfo.roleName.some((str) => str === '超级管理员'));
+
+  // 状态管理
+  const loading = ref<boolean>(false);
+  const organizationId = ref<number>(0);
+  const departmentList = ref<DepartmentInfo[]>([]);
+  const charts = ref<DashboardCharts | null>(null);
+  const date = ref<[Dayjs, Dayjs]>();
+  const tableData = ref<TableRecord[]>([]);
+
+  // 计算属性
+  computed(() => {
+    if (!date.value?.length) return null;
+    return {
+      start_date: date.value[0].format('YYYY-MM-DD'),
+      end_date: date.value[1].format('YYYY-MM-DD'),
+    };
   });
 
-  // 获取图表数据
+  // 方法定义
   async function fetchCharts() {
     try {
       loading.value = true;
-      let start_date = '';
-      let end_date = '';
-      if (date.value && date.value.length) {
-        [start_date, end_date] = date.value.map((o) => o.format('YYYY-MM-DD'));
-      }
-      const res = await getDashboardCharts({
-        start_date,
-        end_date,
+      const params = {
         organizationId: organizationId.value,
-      });
+        start_date: date.value?.[0].format('YYYY-MM-DD') || dayjs().format('YYYY-MM-DD'),
+        end_date: date.value?.[1].format('YYYY-MM-DD') || dayjs().format('YYYY-MM-DD'),
+      };
+      const res = await getDashboardCharts(params);
       charts.value = res.data;
+    } catch (error) {
+      console.error('获取图表数据失败:', error);
     } finally {
       loading.value = false;
     }
   }
 
-  // 获取账号列表数据
   async function fetchList() {
     try {
-      let end_date = dayjs().format('YYYY-MM-DD');
-      if (date.value && date.value.length) {
-        end_date = date.value[1].format('YYYY-MM-DD');
-      }
+      const end_date = date.value?.[1].format('YYYY-MM-DD') || dayjs().format('YYYY-MM-DD');
       const res = await getDashboardWxList({
         page: 1,
         pageSize: 1000,
@@ -179,30 +222,59 @@
       });
       tableData.value = res.data.data;
     } catch (error) {
-      console.error(error);
+      console.error('获取列表数据失败:', error);
     }
   }
 
-  // 刷新所有数据
+  function handleDateShortcut(days: number) {
+    date.value = [dayjs().subtract(days, 'day'), dayjs()];
+    fetchData();
+  }
+
+  function handleDateChange() {
+    fetchData();
+  }
+
+  function handleOrgChange() {
+    fetchData();
+  }
+
   function fetchData() {
     fetchCharts();
     fetchList();
   }
 
-  // 快捷设置日期范围
-  function toDate(day) {
-    date.value = [dayjs().subtract(day, 'day'), dayjs()];
-    fetchData();
-  }
+  // 生命周期
+  onMounted(async () => {
+    handleDateShortcut(0); // 默认显示今天的数据
+
+    if (isSuper.value) {
+      try {
+        const res = await getDepartmentList({ page: 1, pageSize: 100 });
+        departmentList.value = res.data.data;
+      } catch (error) {
+        console.error('获取部门列表失败:', error);
+      }
+    }
+  });
 </script>
 
 <style lang="scss" scoped>
-  // 页面样式
-  .page-wrapper {
-    background: #fff;
-  }
+  .dashboard {
+    &__title {
+      margin: 0;
+      color: #000000d9;
+      font-size: 1.5em;
+      font-weight: 500;
+    }
 
-  .title {
-    font-size: 1.5em;
+    &__date-shortcuts {
+      display: inline-flex;
+      gap: 8px;
+    }
+
+    :deep(.ant-spin-container) {
+      padding: 24px;
+    }
   }
 </style>