소스 검색

feat(home): 添加数据看板功能并重构首页

- 移除原有的打字机动画效果
- 添加数据统计卡片展示商品、方案等各项指标
- 集成ECharts实现商品状态分布饼图和产品类型柱状图
- 添加响应式布局适配不同屏幕尺寸
- 新增合同产品关联的API接口和类型定义
- 实现数据实时刷新和图表自适应调整
- 优化页面样式提升用户体验
Lijingyang 1 개월 전
부모
커밋
6492afde3e
3개의 변경된 파일460개의 추가작업 그리고 9개의 파일을 삭제
  1. 63 0
      src/api/product/contracproduct/index.ts
  2. 146 0
      src/api/product/contracproduct/types.ts
  3. 251 9
      src/views/index.vue

+ 63 - 0
src/api/product/contracproduct/index.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ContracproductVO, ContracproductForm, ContracproductQuery } from '@/api/product/contracproduct/types';
+
+/**
+ * 查询合同产品关联列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listContracproduct = (query?: ContracproductQuery): AxiosPromise<ContracproductVO[]> => {
+  return request({
+    url: '/product/contracproduct/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询合同产品关联详细
+ * @param id
+ */
+export const getContracproduct = (id: string | number): AxiosPromise<ContracproductVO> => {
+  return request({
+    url: '/product/contracproduct/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增合同产品关联
+ * @param data
+ */
+export const addContracproduct = (data: ContracproductForm) => {
+  return request({
+    url: '/product/contracproduct',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改合同产品关联
+ * @param data
+ */
+export const updateContracproduct = (data: ContracproductForm) => {
+  return request({
+    url: '/product/contracproduct',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除合同产品关联
+ * @param id
+ */
+export const delContracproduct = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/product/contracproduct/' + id,
+    method: 'delete'
+  });
+};

+ 146 - 0
src/api/product/contracproduct/types.ts

@@ -0,0 +1,146 @@
+export interface ContracproductVO {
+  /**
+   * 主键ID
+   */
+  id: string | number;
+
+  /**
+   * 合同供货编号
+   */
+  contractSupplyNo: string;
+
+  /**
+   * 产品编号
+   */
+  productNo: string;
+
+  /**
+   * 产品id
+   */
+  productId: string | number;
+
+  /**
+   * 供货周期(单位:天/月,根据业务定义)
+   */
+  supplyCycle: number;
+
+  /**
+   * 库存属性
+   */
+  inventoryProperties: string;
+
+  /**
+   * 最小供货量
+   */
+  minSupply: number;
+
+  /**
+   * 报价(支持大文本)
+   */
+  offerPrice: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status: string;
+
+}
+
+export interface ContracproductForm extends BaseEntity {
+  /**
+   * 主键ID
+   */
+  id?: string | number;
+
+  /**
+   * 合同供货编号
+   */
+  contractSupplyNo?: string;
+
+  /**
+   * 产品编号
+   */
+  productNo?: string;
+
+  /**
+   * 产品id
+   */
+  productId?: string | number;
+
+  /**
+   * 供货周期(单位:天/月,根据业务定义)
+   */
+  supplyCycle?: number;
+
+  /**
+   * 库存属性
+   */
+  inventoryProperties?: string;
+
+  /**
+   * 最小供货量
+   */
+  minSupply?: number;
+
+  /**
+   * 报价(支持大文本)
+   */
+  offerPrice?: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+
+}
+
+export interface ContracproductQuery extends PageQuery {
+
+  /**
+   * 合同供货编号
+   */
+  contractSupplyNo?: string;
+
+  /**
+   * 产品编号
+   */
+  productNo?: string;
+
+  /**
+   * 产品id
+   */
+  productId?: string | number;
+
+  /**
+   * 供货周期(单位:天/月,根据业务定义)
+   */
+  supplyCycle?: number;
+
+  /**
+   * 库存属性
+   */
+  inventoryProperties?: string;
+
+  /**
+   * 最小供货量
+   */
+  minSupply?: number;
+
+  /**
+   * 报价(支持大文本)
+   */
+  offerPrice?: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+
+    /**
+     * 日期范围参数
+     */
+    params?: any;
+}
+
+
+

+ 251 - 9
src/views/index.vue

@@ -1,20 +1,217 @@
 <template>
   <div class="app-container home">
     <!-- <el-divider /> -->
-    <div class="index-style">
-      <div class="typewriter-container">
-        <span v-for="(char, index) in 'welcome!'" :key="index" :style="{ animationDelay: `${index * 0.5}s` }" class="typewriter-char">{{
-          char
-        }}</span>
-      </div>
-    </div>
+    <el-row :gutter="12">
+      <el-col :span="24">
+        <div class="dashboard-title">数据看板</div>
+      </el-col>
+
+      <el-col :span="24">
+        <div class="stat-grid">
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-label">商品总数</div>
+            <div class="stat-value">{{ metrics.productTotal }}</div>
+          </el-card>
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-label">自营商品</div>
+            <div class="stat-value">{{ metrics.productSelf }}</div>
+          </el-card>
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-label">上架商品</div>
+            <div class="stat-value">{{ metrics.productOnSale }}</div>
+          </el-card>
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-label">下架商品</div>
+            <div class="stat-value">{{ metrics.productOffSale }}</div>
+          </el-card>
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-label">停售商品</div>
+            <div class="stat-value">{{ metrics.productStopSale }}</div>
+          </el-card>
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-label">精选商品</div>
+            <div class="stat-value">{{ metrics.productFeatured }}</div>
+          </el-card>
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-label">商品总池</div>
+            <div class="stat-value">{{ metrics.poolTotal }}</div>
+          </el-card>
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-label">精选商品池</div>
+            <div class="stat-value">{{ metrics.poolFeatured }}</div>
+          </el-card>
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-label">方案数量</div>
+            <div class="stat-value">{{ metrics.programTotal }}</div>
+          </el-card>
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-label">今日方案新增</div>
+            <div class="stat-value">{{ metrics.programTodayNew }}</div>
+          </el-card>
+        </div>
+      </el-col>
+
+      <el-col :span="12" class="card-box">
+        <el-card shadow="hover" class="chart-card">
+          <template #header>
+            <span>商品状态分布</span>
+          </template>
+          <div ref="productStatusChartRef" class="chart" />
+        </el-card>
+      </el-col>
+
+      <el-col :span="12" class="card-box">
+        <el-card shadow="hover" class="chart-card">
+          <template #header>
+            <span>精选/停售/默认</span>
+          </template>
+          <div ref="productTypeChartRef" class="chart" />
+        </el-card>
+      </el-col>
+    </el-row>
   </div>
 </template>
 
 <script setup name="Index" lang="ts">
-const goTarget = (url: string) => {
-  window.open(url, '__blank');
+import * as echarts from 'echarts';
+import { getProductStatusCount, listBase } from '@/api/product/base';
+import { listProgram } from '@/api/product/program';
+import { listPool } from '@/api/product/pool/index';
+
+const productStatusChartRef = ref();
+const productTypeChartRef = ref();
+let productStatusChartInstance: echarts.ECharts | undefined;
+let productTypeChartInstance: echarts.ECharts | undefined;
+
+const metrics = reactive({
+  productTotal: 0,
+  productSelf: 0,
+  productOnSale: 0,
+  productOffSale: 0,
+  productStopSale: 0,
+  productFeatured: 0,
+  poolTotal: 0,
+  poolFeatured: 0,
+  programTotal: 0,
+  programTodayNew: 0
+});
+
+const safeTotal = (res: any): number => {
+  const total = res?.total;
+  return typeof total === 'number' ? total : 0;
+};
+
+const getTodayRange = () => {
+  const now = new Date();
+  const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
+  const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
+  const fmt = (d: Date) => {
+    const pad = (n: number) => `${n}`.padStart(2, '0');
+    return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+  };
+  return { beginTime: fmt(start), endTime: fmt(end) };
+};
+
+const renderProductStatusChart = () => {
+  if (!productStatusChartRef.value) return;
+  if (!productStatusChartInstance) {
+    productStatusChartInstance = echarts.init(productStatusChartRef.value, 'macarons');
+  }
+  productStatusChartInstance.setOption({
+    tooltip: { trigger: 'item' },
+    legend: { bottom: 0 },
+    series: [
+      {
+        name: '商品状态',
+        type: 'pie',
+        radius: ['45%', '70%'],
+        avoidLabelOverlap: true,
+        itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 },
+        label: { show: false },
+        emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
+        labelLine: { show: false },
+        data: [
+          { value: metrics.productOnSale, name: '上架' },
+          { value: metrics.productOffSale, name: '下架' }
+        ]
+      }
+    ]
+  });
+};
+
+const renderProductTypeChart = () => {
+  if (!productTypeChartRef.value) return;
+  if (!productTypeChartInstance) {
+    productTypeChartInstance = echarts.init(productTypeChartRef.value, 'macarons');
+  }
+  const defaultCount = Math.max(metrics.productTotal - metrics.productFeatured - metrics.productStopSale, 0);
+  productTypeChartInstance.setOption({
+    tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
+    grid: { left: 12, right: 12, top: 20, bottom: 20, containLabel: true },
+    xAxis: { type: 'category', data: ['默认', '精选', '停售'] },
+    yAxis: { type: 'value' },
+    series: [
+      {
+        name: '数量',
+        type: 'bar',
+        barWidth: 36,
+        itemStyle: { borderRadius: [8, 8, 0, 0] },
+        data: [defaultCount, metrics.productFeatured, metrics.productStopSale]
+      }
+    ]
+  });
+};
+
+const refreshMetrics = async () => {
+  const statusRes = await getProductStatusCount();
+  metrics.productTotal = statusRes.data?.total ?? 0;
+  metrics.productOnSale = statusRes.data?.onSale ?? 0;
+  metrics.productOffSale = statusRes.data?.offSale ?? 0;
+
+  const [selfRes, featuredRes, stopSaleRes] = await Promise.all([
+    listBase({ pageNum: 1, pageSize: 1, isSelf: 1 } as any),
+    listBase({ pageNum: 1, pageSize: 1, productCategory: 2 } as any),
+    listBase({ pageNum: 1, pageSize: 1, productCategory: 3 } as any)
+  ]);
+  metrics.productSelf = safeTotal(selfRes);
+  metrics.productFeatured = safeTotal(featuredRes);
+  metrics.productStopSale = safeTotal(stopSaleRes);
+
+  const [poolTotalRes, poolFeaturedRes] = await Promise.all([
+    listPool({ pageNum: 1, pageSize: 1 } as any),
+    listPool({ pageNum: 1, pageSize: 1, type: 1 } as any)
+  ]);
+  metrics.poolTotal = safeTotal(poolTotalRes);
+  metrics.poolFeatured = safeTotal(poolFeaturedRes);
+
+  const programTotalRes = await listProgram({ pageNum: 1, pageSize: 1 } as any);
+  metrics.programTotal = safeTotal(programTotalRes);
+
+  const { beginTime, endTime } = getTodayRange();
+  const programTodayRes = await listProgram({ pageNum: 1, pageSize: 1, params: { beginTime, endTime } } as any);
+  metrics.programTodayNew = safeTotal(programTodayRes);
+
+  renderProductStatusChart();
+  renderProductTypeChart();
+};
+
+const onResize = () => {
+  productStatusChartInstance?.resize();
+  productTypeChartInstance?.resize();
 };
+
+onMounted(async () => {
+  await refreshMetrics();
+  window.addEventListener('resize', onResize);
+});
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', onResize);
+  productStatusChartInstance?.dispose();
+  productTypeChartInstance?.dispose();
+  productStatusChartInstance = undefined;
+  productTypeChartInstance = undefined;
+});
 </script>
 
 <style lang="scss" scoped>
@@ -35,6 +232,45 @@ const goTarget = (url: string) => {
     margin-bottom: 20px;
   }
 
+  .dashboard-title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #1f2937;
+    padding: 4px 2px 10px;
+  }
+
+  .stat-grid {
+    display: grid;
+    grid-template-columns: repeat(4, minmax(0, 1fr));
+    gap: 12px;
+  }
+
+  .stat-card {
+    border-radius: 12px;
+    background: linear-gradient(135deg, #ffffff 0%, #f7fbff 100%);
+  }
+
+  .stat-label {
+    font-size: 13px;
+    color: #6b7280;
+    margin-bottom: 8px;
+  }
+
+  .stat-value {
+    font-size: 28px;
+    font-weight: 700;
+    color: #111827;
+    letter-spacing: 0.5px;
+  }
+
+  .chart-card {
+    border-radius: 12px;
+  }
+
+  .chart {
+    height: 360px;
+  }
+
   ul {
     padding: 0;
     margin: 0;
@@ -143,5 +379,11 @@ const goTarget = (url: string) => {
       transform: scale(1);
     }
   }
+
+  @media (max-width: 1200px) {
+    .stat-grid {
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+    }
+  }
 }
 </style>