Просмотр исходного кода

feat(home): 更新首页为数据统计面板

- 替换欢迎动画为供应商统计数据卡片
- 添加图表展示供应商结构、核心指标和趋势
- 集成 ECharts 实现饼图、柱状图和折线图
- 添加响应式布局支持不同屏幕尺寸
- 实现图表窗口大小调整自适应功能
- 添加供应商、合同、商品等关键数据统计

feat(product): 增加产品分类和合同产品相关接口

- 新增 getProductCategoryList 接口获取产品分类列表
- 添加 scmListContracproduct 接口查询合同产品
- 扩展 Contracproduct 类型定义字段
- 修改 customer info 页面兼容新接口返回格式

chore(typescript): 调整模块解析策略为 Node 兼容模式

- 将 tsconfig.json 中 moduleResolution 从 Bundler 改为 Node
- 确保与现有模块导入导出方式兼容
Lijingyang 1 месяц назад
Родитель
Сommit
c8be076cc0

+ 13 - 0
src/api/product/category/index.ts

@@ -83,3 +83,16 @@ export const setCategoryReviewer = (categoryId: string | number, reviewerUserId:
     }
   });
 };
+
+/**
+ * 获取产品分类列表
+ */
+export const getProductCategoryList = () => {
+  return request({
+    url: '/product/category/getProductCategoryList',
+    method: 'get'
+  });
+};
+
+
+

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

@@ -16,6 +16,14 @@ export const listContracproduct = (query?: ContracproductQuery): AxiosPromise<Co
   });
 };
 
+export const scmListContracproduct = (query?: ContracproductQuery): AxiosPromise<ContracproductVO[]> => {
+  return request({
+    url: '/product/contracproduct/scm/list',
+    method: 'get',
+    params: query
+  });
+};
+
 /**
  * 查询合同产品关联详细
  * @param id

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

@@ -9,6 +9,11 @@ export interface ContracproductVO {
    */
   contractSupplyNo: string;
 
+  /**
+   * 合同供货Id
+   */
+  contractSupplyId?: string | number;
+
   /**
    * 产品编号
    */
@@ -44,6 +49,34 @@ export interface ContracproductVO {
    */
   status: string;
 
+  productName?: string;
+  productImage?: string;
+  unitName?: string;
+  minOrderQuantity?: number;
+  bottomCategoryName?: string;
+  marketPrice?: number;
+  memberPrice?: number;
+  minSellingPrice?: number;
+  totalInventory?: number;
+  nowInventory?: number;
+  virtualInventory?: number;
+  brandId?: string | number;
+  brandName?: string;
+  enterpriseName?: string;
+  provinceName?: string;
+  cityName?: string;
+
+  province?: string;
+  city?: string;
+
+  remark?: string;
+
+  offerEndTime?: string;
+  grossMarginRate?: number;
+
+  endTime?: string;
+  grossProfitRate?: number;
+
 }
 
 export interface ContracproductForm extends BaseEntity {
@@ -57,6 +90,11 @@ export interface ContracproductForm extends BaseEntity {
    */
   contractSupplyNo?: string;
 
+  /**
+   * 合同供货Id
+   */
+  contractSupplyId?: string | number;
+
   /**
    * 产品编号
    */
@@ -92,6 +130,15 @@ export interface ContracproductForm extends BaseEntity {
    */
   status?: string;
 
+  remark?: string;
+
+  productName?: string;
+  enterpriseName?: string;
+  provinceName?: string;
+  cityName?: string;
+  province?: string;
+  city?: string;
+
 }
 
 export interface ContracproductQuery extends PageQuery {
@@ -101,6 +148,11 @@ export interface ContracproductQuery extends PageQuery {
    */
   contractSupplyNo?: string;
 
+  /**
+   * 合同供货Id
+   */
+  contractSupplyId?: string | number;
+
   /**
    * 产品编号
    */
@@ -136,6 +188,15 @@ export interface ContracproductQuery extends PageQuery {
    */
   status?: string;
 
+  productName?: string;
+  enterpriseName?: string;
+  provinceName?: string;
+  cityName?: string;
+  province?: string;
+  city?: string;
+
+  remark?: string;
+
     /**
      * 日期范围参数
      */

+ 1 - 0
src/views/customer/info/components/SupplyInfoTab.vue

@@ -103,6 +103,7 @@
 
 <script setup lang="ts">
 import { useRouter } from 'vue-router';
+import { computed } from 'vue';
 const props = defineProps<{
   isViewMode: boolean;
   productCategoryList: any[];

+ 2 - 1
src/views/customer/info/detail.vue

@@ -649,6 +649,7 @@ import RegionCascader from '@/components/RegionCascader/index.vue';
 import FileUpload from '@/components/FileUpload/index.vue';
 import Pagination from '@/components/Pagination/index.vue';
 import { getInfo, addInfo, updateInfo, scmEditInfo, getStaffListSplice, getSupplierStaffIds, getContactListById, getSupplierCategories, getSupplierContractsById, getBankBySupplierId, getAuthorizeDetailList, savePurchaseInfo, getDictData, getTaxRateList, getSettlementMethodList, getInvoiceTypeList, listInfo } from '@/api/customer/info';
+import { getProductCategoryList } from '@/api/product/category';
 import { getBank, updateBank, addBank } from '@/api/customer/bank';
 import { BankForm } from '@/api/customer/bank/types';
 import { getContact, addContact, updateContact } from '@/api/customer/contact';
@@ -1634,7 +1635,7 @@ const handleContactSubmit = async () => {
 const getProductCategories = async () => {
   try {
     const res = await getProductCategoryList();
-    productCategoryList.value = res.data || [];
+    productCategoryList.value = res.rows || res.data || [];
     console.log('产品分类列表:', productCategoryList.value);
 
     // 获取分类列表后,再获取已选择的品目

+ 343 - 10
src/views/index.vue

@@ -1,24 +1,357 @@
 <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="6">
+        <div class="kpi-stack">
+          <div class="kpi-card kpi-blue">
+            <div class="kpi-title">供应商总数</div>
+            <div class="kpi-value">{{ stats.totalSuppliers }}</div>
+          </div>
+          <div class="kpi-card kpi-red">
+            <div class="kpi-title">不合作供应商</div>
+            <div class="kpi-value">{{ stats.nonCooperativeSuppliers }}</div>
+          </div>
+          <div class="kpi-card kpi-orange">
+            <div class="kpi-title">待审核供应商</div>
+            <div class="kpi-value">{{ stats.pendingSuppliers }}</div>
+          </div>
+          <div class="kpi-card kpi-purple">
+            <div class="kpi-title">协议供货单数</div>
+            <div class="kpi-value">{{ stats.contractSupplies }}</div>
+          </div>
+          <div class="kpi-card kpi-teal">
+            <div class="kpi-title">协议供货商品数</div>
+            <div class="kpi-value">{{ stats.contractSupplyProducts }}</div>
+          </div>
+          <div class="kpi-card kpi-green">
+            <div class="kpi-title">合同总数</div>
+            <div class="kpi-value">{{ stats.contractTotal }}</div>
+          </div>
+          <div class="kpi-card kpi-indigo">
+            <div class="kpi-title">商品总数</div>
+            <div class="kpi-value">{{ stats.productTotal }}</div>
+          </div>
+          <div class="kpi-card kpi-gray">
+            <div class="kpi-title">品牌数量</div>
+            <div class="kpi-value">{{ stats.brandTotal }}</div>
+          </div>
+        </div>
+      </el-col>
+
+      <el-col :span="18">
+        <el-row :gutter="12">
+          <el-col :span="10">
+            <el-card shadow="hover" class="chart-card">
+              <div class="chart-title">供应商结构</div>
+              <div ref="pieRef" class="chart-box"></div>
+            </el-card>
+          </el-col>
+          <el-col :span="14">
+            <el-card shadow="hover" class="chart-card">
+              <div class="chart-title">核心指标</div>
+              <div ref="barRef" class="chart-box"></div>
+            </el-card>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="12" style="margin-top: 12px;">
+          <el-col :span="24">
+            <el-card shadow="hover" class="chart-card">
+              <div class="chart-title">近七日新增趋势</div>
+              <div ref="lineRef" class="chart-box"></div>
+            </el-card>
+          </el-col>
+        </el-row>
+      </el-col>
+    </el-row>
   </div>
 </template>
 
 <script setup name="Index" lang="ts">
-const goTarget = (url: string) => {
-  window.open(url, '__blank');
+import { onMounted, onBeforeUnmount, reactive, ref, nextTick } from 'vue';
+import * as echarts from 'echarts';
+import { getInfoList, getApproveList } from '@/api/customer/info';
+import { listContractsupply } from '@/api/supplier/contractsupply';
+import { scmListContracproduct } from '@/api/product/contracproduct';
+import { listContact } from '@/api/supplier/contact';
+import { listContract } from '@/api/supplier/contract';
+import { listBase } from '@/api/product/base';
+import { listBrand } from '@/api/product/brand';
+
+const pieRef = ref();
+const barRef = ref();
+const lineRef = ref();
+let pieInstance: echarts.ECharts | null = null;
+let barInstance: echarts.ECharts | null = null;
+let lineInstance: echarts.ECharts | null = null;
+
+const stats = reactive({
+  totalSuppliers: 0,
+  nonCooperativeSuppliers: 0,
+  pendingSuppliers: 0,
+  contractSupplies: 0,
+  contractSupplyProducts: 0,
+  contractTotal: 0,
+  productTotal: 0,
+  brandTotal: 0
+});
+
+const trend = reactive({
+  days: [] as string[],
+  contactAdds: [] as number[],
+  contractAdds: [] as number[]
+});
+
+const formatDay = (d: Date) => {
+  const y = d.getFullYear();
+  const m = String(d.getMonth() + 1).padStart(2, '0');
+  const day = String(d.getDate()).padStart(2, '0');
+  return `${y}-${m}-${day}`;
+};
+
+const buildLast7Days = () => {
+  const days: string[] = [];
+  const today = new Date();
+  for (let i = 6; i >= 0; i--) {
+    const d = new Date(today);
+    d.setDate(today.getDate() - i);
+    days.push(formatDay(d));
+  }
+  return days;
+};
+
+const fetchTrend = async () => {
+  const days = buildLast7Days();
+  trend.days = days;
+
+  const contactReqs = days.map((day) => {
+    return listContact({
+      pageNum: 1,
+      pageSize: 1,
+      params: {
+        beginTime: `${day} 00:00:00`,
+        endTime: `${day} 23:59:59`
+      }
+    } as any);
+  });
+
+  const contractReqs = days.map((day) => {
+    return listContract({
+      pageNum: 1,
+      pageSize: 1,
+      params: {
+        beginTime: `${day} 00:00:00`,
+        endTime: `${day} 23:59:59`
+      }
+    } as any);
+  });
+
+  const [contactRes, contractRes] = await Promise.all([
+    Promise.all(contactReqs),
+    Promise.all(contractReqs)
+  ]);
+
+  trend.contactAdds = contactRes.map((r: any) => r?.total ?? 0);
+  trend.contractAdds = contractRes.map((r: any) => r?.total ?? 0);
+};
+
+const fetchStats = async () => {
+  const [supplierRes, nonCoopRes, pendingRes, contractRes, productRes, contractTotalRes, productTotalRes, brandTotalRes] = await Promise.all([
+    getInfoList({ pageNum: 1, pageSize: 1 } as any),
+    getInfoList({ pageNum: 1, pageSize: 1, cooperative: 0 } as any),
+    getApproveList({ pageNum: 1, pageSize: 1 } as any),
+    listContractsupply({ pageNum: 1, pageSize: 1 } as any),
+    scmListContracproduct({ pageNum: 1, pageSize: 1 } as any),
+    listContract({ pageNum: 1, pageSize: 1 } as any),
+    listBase({ pageNum: 1, pageSize: 1 } as any),
+    listBrand({ pageNum: 1, pageSize: 1 } as any)
+  ]);
+
+  stats.totalSuppliers = supplierRes?.total ?? 0;
+  stats.nonCooperativeSuppliers = nonCoopRes?.total ?? 0;
+  stats.pendingSuppliers = pendingRes?.total ?? 0;
+  stats.contractSupplies = contractRes?.total ?? 0;
+  stats.contractSupplyProducts = productRes?.total ?? 0;
+  stats.contractTotal = contractTotalRes?.total ?? 0;
+  stats.productTotal = productTotalRes?.total ?? 0;
+  stats.brandTotal = brandTotalRes?.total ?? 0;
 };
+
+const renderCharts = () => {
+  if (pieRef.value) {
+    pieInstance?.dispose();
+    pieInstance = echarts.init(pieRef.value, 'macarons');
+    pieInstance.setOption({
+      tooltip: { trigger: 'item' },
+      legend: { bottom: 0, left: 'center' },
+      series: [
+        {
+          type: 'pie',
+          radius: ['55%', '75%'],
+          center: ['50%', '45%'],
+          avoidLabelOverlap: true,
+          label: { show: false },
+          labelLine: { show: false },
+          data: [
+            { name: '不合作', value: stats.nonCooperativeSuppliers },
+            { name: '待审核', value: stats.pendingSuppliers },
+            {
+              name: '其他',
+              value: Math.max(0, stats.totalSuppliers - stats.nonCooperativeSuppliers - stats.pendingSuppliers)
+            }
+          ]
+        }
+      ]
+    });
+  }
+
+  if (barRef.value) {
+    barInstance?.dispose();
+    barInstance = echarts.init(barRef.value, 'macarons');
+    barInstance.setOption({
+      tooltip: { trigger: 'axis' },
+      grid: { left: 40, right: 20, top: 20, bottom: 30 },
+      xAxis: {
+        type: 'category',
+        data: ['供应商', '不合作', '待审核', '供货单', '供货商品', '合同', '商品', '品牌'],
+        axisTick: { show: false }
+      },
+      yAxis: { type: 'value', splitLine: { lineStyle: { type: 'dashed' } } },
+      series: [
+        {
+          type: 'bar',
+          data: [
+            stats.totalSuppliers,
+            stats.nonCooperativeSuppliers,
+            stats.pendingSuppliers,
+            stats.contractSupplies,
+            stats.contractSupplyProducts,
+            stats.contractTotal,
+            stats.productTotal,
+            stats.brandTotal
+          ],
+          barWidth: 22,
+          itemStyle: {
+            borderRadius: [6, 6, 0, 0],
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: '#3b82f6' },
+              { offset: 1, color: '#60a5fa' }
+            ])
+          }
+        }
+      ]
+    });
+  }
+
+  if (lineRef.value) {
+    lineInstance?.dispose();
+    lineInstance = echarts.init(lineRef.value, 'macarons');
+    lineInstance.setOption({
+      tooltip: { trigger: 'axis' },
+      legend: { top: 0, right: 10 },
+      grid: { left: 40, right: 20, top: 40, bottom: 30 },
+      xAxis: { type: 'category', data: trend.days },
+      yAxis: { type: 'value', splitLine: { lineStyle: { type: 'dashed' } } },
+      series: [
+        {
+          name: '联系人新增',
+          type: 'line',
+          data: trend.contactAdds,
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 6
+        },
+        {
+          name: '合同新增',
+          type: 'line',
+          data: trend.contractAdds,
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 6
+        }
+      ]
+    });
+  }
+};
+
+const resizeChart = () => {
+  pieInstance?.resize();
+  barInstance?.resize();
+  lineInstance?.resize();
+};
+
+onMounted(async () => {
+  await Promise.all([fetchStats(), fetchTrend()]);
+  await nextTick();
+  renderCharts();
+  window.addEventListener('resize', resizeChart);
+});
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', resizeChart);
+  pieInstance?.dispose();
+  pieInstance = null;
+  barInstance?.dispose();
+  barInstance = null;
+  lineInstance?.dispose();
+  lineInstance = null;
+});
 </script>
 
 <style lang="scss" scoped>
 .home {
+  .kpi-stack {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .kpi-card {
+    padding: 14px 16px;
+    border-radius: 12px;
+    color: #fff;
+    box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
+    border: 1px solid rgba(255, 255, 255, 0.25);
+  }
+
+  .kpi-title {
+    font-size: 13px;
+    opacity: 0.9;
+  }
+
+  .kpi-value {
+    margin-top: 8px;
+    font-size: 28px;
+    font-weight: 800;
+    line-height: 1;
+    letter-spacing: 0.5px;
+  }
+
+  .kpi-blue { background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%); }
+  .kpi-red { background: linear-gradient(135deg, #ef4444 0%, #fb7185 100%); }
+  .kpi-orange { background: linear-gradient(135deg, #f97316 0%, #fdba74 100%); }
+  .kpi-purple { background: linear-gradient(135deg, #7c3aed 0%, #c4b5fd 100%); }
+  .kpi-teal { background: linear-gradient(135deg, #0ea5e9 0%, #67e8f9 100%); }
+  .kpi-green { background: linear-gradient(135deg, #16a34a 0%, #86efac 100%); }
+  .kpi-indigo { background: linear-gradient(135deg, #4f46e5 0%, #a5b4fc 100%); }
+  .kpi-gray { background: linear-gradient(135deg, #334155 0%, #94a3b8 100%); }
+
+  .chart-card {
+    border-radius: 12px;
+  }
+
+  .chart-title {
+    font-size: 14px;
+    font-weight: 700;
+    color: #303133;
+    margin-bottom: 10px;
+  }
+
+  .chart-box {
+    width: 100%;
+    height: 320px;
+  }
+
   blockquote {
     padding: 10px 20px;
     margin: 0 0 20px;

+ 1 - 1
tsconfig.json

@@ -5,7 +5,7 @@
     // https://vite.dev/config/build-options.html#build-target
     "target": "ES2020",
     "module": "ESNext",
-    "moduleResolution": "Bundler",
+    "moduleResolution": "Node",
     "lib": ["ESNext", "DOM", "DOM.Iterable"],
     "skipLibCheck": true,
     // This setting lets you specify a file for storing incremental compilation information as a part of composite projects which enables faster building of larger TypeScript codebases.