Bladeren bron

对接第三方电商

Lijingyang 1 maand geleden
bovenliggende
commit
8a3ef0885d

+ 9 - 1
src/api/external/product/index.ts

@@ -97,7 +97,6 @@ export const batchPushProduct = (ids: string | number | Array<string | number>)
   });
 };
 
-
 /**
  * 商品上下架 状态变更
  * @param data 审核信息(包含id、productStatus、shelfComments)
@@ -108,4 +107,13 @@ export const shelfReview = (data: ProductForm) => {
     method: 'post',
     data: data
   });
+};
+
+export const batchInsertExternalProduct = (itemId: string | number, boList: any[]): AxiosPromise<any> => {
+  return request({
+    url: '/external/product/batch/insert',
+    method: 'post',
+    params: { itemId },
+    data: boList
+  });
 };

+ 2 - 0
src/api/external/product/types.ts

@@ -402,6 +402,8 @@ export interface ThirdProductVO {
    */
   categoryName?: string;
 
+  externalCategoryName?: string;
+
   /**
    * 单位名称
    */

+ 10 - 0
src/api/system/addressArea/index.ts

@@ -0,0 +1,10 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { AddressAreaVo } from './types';
+
+export const getChinaArea = (): AxiosPromise<AddressAreaVo> => {
+  return request({
+    url: '/system/addressarea/getchina/area',
+    method: 'get'
+  });
+};

+ 11 - 0
src/api/system/addressArea/types.ts

@@ -0,0 +1,11 @@
+export interface AddressAreaVo {
+    id: number;
+    areaCode: string;
+    areaName: string;
+    parentCode: number;
+    simpleName: string;
+    level: number;
+    pinYin: string;
+    dataSource: string;
+    children?: AddressAreaVo[];
+}

+ 44 - 13
src/views/external/product/index.vue

@@ -6,13 +6,13 @@
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
             <el-row :gutter="20">
               <el-col :span="6">
-                <el-form-item label="项目" prop="itemKey">
-                  <el-select v-model="queryParams.itemKey" placeholder="请选择项目" clearable @change="handleQuery" style="width: 200px">
+                <el-form-item label="项目" prop="itemId">
+                  <el-select v-model="queryParams.itemId" placeholder="请选择项目" clearable @change="handleItemChange" style="width: 200px">
                     <el-option
                       v-for="item in itemList"
-                      :key="item.itemKey"
+                      :key="item.id"
                       :label="item.itemName"
-                      :value="item.itemKey"
+                      :value="item.id"
                     />
                   </el-select>
                 </el-form-item>
@@ -31,6 +31,7 @@
             <el-form-item>
               <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
               <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+              <el-button type="success" :disabled="multiple" @click="handleBatchPush">批量推送</el-button>
             </el-form-item>
           </el-form>
         </el-card>
@@ -38,7 +39,7 @@
     </transition>
 
     <el-card shadow="never">
-      <el-table v-loading="loading" border :data="productList" @selection-change="handleSelectionChange">
+      <el-table ref="productTableRef" v-loading="loading" border :data="productList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="序号" type="index" width="80" align="center" />
         <el-table-column label="图片" align="center" prop="productImage" width="100">
@@ -57,7 +58,7 @@
         <el-table-column label="我方分类" align="center" prop="categoryName" width="120" show-overflow-tooltip />
         <el-table-column label="第三方分类" align="center" width="120" show-overflow-tooltip>
           <template #default="scope">
-            <span>{{ scope.row.categoryName || '-' }}</span>
+            <span>{{ scope.row.standardCatalogName || '-' }}</span>
           </template>
         </el-table-column>
         <el-table-column label="品牌" align="center" prop="brandName" width="120" />
@@ -65,6 +66,7 @@
         <el-table-column label="市场价" align="center" prop="marketPrice" width="100" />
         <el-table-column label="平档价" align="center" prop="standardPrice" width="100" />
         <el-table-column label="供应价" align="center" prop="purchasePrice" width="100" />
+        <el-table-column label="第三方售价" align="center" prop="externalPrice" width="100" />
         <el-table-column label="限定库存" align="center" prop="availableStock" width="100" />
         <el-table-column label="可用库存" align="center" prop="availableStock" width="100" />
         <el-table-column label="总订单" align="center" width="100">
@@ -142,7 +144,7 @@
 <script setup name="Product" lang="ts">
 import { getThirdProductPage, getProduct, updateProduct, shelfReview, batchPushProduct } from '@/api/external/product';
 import { ThirdProductVO, ProductQuery, ProductVO, ProductForm } from '@/api/external/product/types';
-import { listItem } from '@/api/external/item';
+import { listItem } from '@/api/external/item/index';
 import { ItemVO } from '@/api/external/item/types';
 import { getProductCategoryTree } from '@/api/external/productCategory';
 import { ProductCategoryVO } from '@/api/external/productCategory/types';
@@ -161,6 +163,7 @@ const total = ref(0);
 
 const queryFormRef = ref<ElFormInstance>();
 const productFormRef = ref<ElFormInstance>();
+const productTableRef = ref();
 
 const dialog = reactive<DialogOption>({
   visible: false,
@@ -186,7 +189,7 @@ const data = reactive<PageData<ProductForm, ProductQuery>>({
     pageSize: 10,
     productNo: undefined,
     itemName: undefined,
-    itemKey: undefined,
+    itemId: undefined,
     params: {},
     externalPrice: 0
   },
@@ -213,7 +216,12 @@ const cascaderProps = {
 /** 初始化第三方分类数据 */
 const initCategoryData = async () => {
   try {
-    const res = await getProductCategoryTree();
+    const itemId = queryParams.value.itemId;
+    if (!itemId) {
+      externalCategoryList.value = [];
+      return;
+    }
+    const res = await getProductCategoryTree({ itemId });
     externalCategoryList.value = res.data || [];
   } catch (error) {
     console.error('加载分类树失败:', error);
@@ -242,13 +250,38 @@ const getItemList = async () => {
   itemList.value = (res as any).rows || res.data || [];
   console.log('itemList.value:', itemList.value);
   // 默认选择第一个项目
-  if (itemList.value.length > 0 && !queryParams.value.itemKey) {
-    queryParams.value.itemKey = itemList.value[0].itemKey;
+  if (itemList.value.length > 0 && !queryParams.value.itemId) {
+    queryParams.value.itemId = itemList.value[0].id;
     // 自动触发查询
     await getList();
+    await initCategoryData();
   }
 }
 
+const handleBatchPush = async () => {
+  if (multiple.value) return;
+  try {
+    await proxy?.$modal.confirm('确认推送选中的商品吗?');
+    await batchPushProduct(ids.value);
+    proxy?.$modal.msgSuccess('推送成功');
+    productTableRef.value?.clearSelection?.();
+    ids.value = [];
+    multiple.value = true;
+    single.value = true;
+    await getList();
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('批量推送失败:', error);
+      proxy?.$modal.msgError('推送失败');
+    }
+  }
+}
+
+const handleItemChange = async () => {
+  await initCategoryData();
+  handleQuery();
+}
+
 /** 取消按钮 */
 const cancel = () => {
   reset();
@@ -419,7 +452,5 @@ const handleExport = () => {
 onMounted(() => {
   // 先加载项目列表,内部会自动触发查询
   getItemList();
-  // 初始化第三方分类数据
-  initCategoryData();
 });
 </script>

+ 40 - 4
src/views/external/productBrand/index.vue

@@ -64,8 +64,13 @@
             v-model="form.productBrandId"
             placeholder="请选择官方品牌"
             filterable
+            remote
+            :remote-method="remoteSearchBrand"
+            :loading="brandLoading"
+            :reserve-keyword="false"
             clearable
             style="width: 100%"
+            @clear="handleBrandClear"
             @change="handleBrandChange"
           >
             <el-option
@@ -102,6 +107,7 @@ const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const productBrandList = ref<ProductBrandVO[]>([]);
 const brandList = ref<BrandVO[]>([]);
+const defaultBrandList = ref<BrandVO[]>([]);
 const itemList = ref<ItemVO[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
@@ -109,6 +115,9 @@ const showSearch = ref(true);
 const ids = ref<Array<string | number>>([]);
 const total = ref(0);
 
+const brandLoading = ref(false);
+let brandSearchTimer: ReturnType<typeof setTimeout> | undefined;
+
 const queryFormRef = ref<ElFormInstance>();
 const productBrandFormRef = ref<ElFormInstance>();
 
@@ -156,9 +165,35 @@ const getList = async () => {
 }
 
 /** 获取官方品牌列表 */
-const getBrandList = async () => {
-  const res = await listBrand();
-  brandList.value = res.data;
+const getBrandList = async (brandName?: string) => {
+  const res = await listBrand({ pageNum: 1, pageSize: 500, brandName } as any);
+  const list = (res as any).rows || res.data || [];
+  brandList.value = list;
+  if (!brandName) {
+    defaultBrandList.value = list;
+  }
+}
+
+const handleBrandClear = () => {
+  if (brandSearchTimer) {
+    clearTimeout(brandSearchTimer);
+    brandSearchTimer = undefined;
+  }
+  brandList.value = defaultBrandList.value || [];
+}
+
+const remoteSearchBrand = (query: string) => {
+  const q = (query || '').trim();
+  if (!q) {
+    brandList.value = defaultBrandList.value || [];
+    return;
+  }
+
+  if (brandSearchTimer) clearTimeout(brandSearchTimer);
+  brandSearchTimer = setTimeout(async () => {
+    brandLoading.value = true;
+    await getBrandList(q).finally(() => (brandLoading.value = false));
+  }, 300);
 }
 
 /** 获取项目列表 */
@@ -246,6 +281,7 @@ const handleExport = () => {
 onMounted(() => {
   // 先加载项目列表,内部会自动触发查询
   getItemList();
-  getBrandList();
+  brandLoading.value = true;
+  getBrandList().finally(() => (brandLoading.value = false));
 });
 </script>

+ 1 - 5
src/views/external/productCategory/index.vue

@@ -52,11 +52,7 @@
           </template>
         </el-table-column>
         
-        <el-table-column prop="platform" align="center" label="平台" width="150">
-          <template #default="scope">
-            <span>{{ scope.row.platform === 0 ? 'Web' : scope.row.platform === 1 ? '小程序' : '未知' }}</span>
-          </template>
-        </el-table-column>
+        
       
         <el-table-column fixed="right" align="center" label="操作">
           <template #default="scope">

+ 592 - 111
src/views/index.vue

@@ -1,147 +1,628 @@
 <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 class="app-container home p-6">
+    <div class="dashboard-header">
+      <div class="dashboard-title">
+        <span class="title-text">概览数据看板</span>
+        <span class="title-desc">Overview Dashboard</span>
       </div>
+      <el-select v-model="activeProject" placeholder="请选择项目" class="project-select" filterable>
+        <el-option v-for="opt in projectOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+      </el-select>
     </div>
+
+    <!-- Stats Cards -->
+    <el-row :gutter="20" class="kpi-row">
+      <el-col :xs="12" :sm="12" :md="6">
+        <el-card shadow="hover" class="kpi-card custom-card primary-card">
+          <div class="kpi-inner">
+            <div class="kpi-content">
+              <div class="kpi-label">订单总量</div>
+              <div class="kpi-value">
+                <span class="num">{{ currentStats.orderCount }}</span>
+                <span class="unit">单</span>
+              </div>
+            </div>
+            <div class="kpi-icon-wrapper">
+              <el-icon class="kpi-icon"><Tickets /></el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="12" :sm="12" :md="6">
+        <el-card shadow="hover" class="kpi-card custom-card success-card">
+          <div class="kpi-inner">
+            <div class="kpi-content">
+              <div class="kpi-label">商品数量</div>
+              <div class="kpi-value">
+                <span class="num">{{ currentStats.productCount }}</span>
+                <span class="unit">件</span>
+              </div>
+            </div>
+            <div class="kpi-icon-wrapper">
+              <el-icon class="kpi-icon"><Goods /></el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="12" :sm="12" :md="6">
+        <el-card shadow="hover" class="kpi-card custom-card warning-card">
+          <div class="kpi-inner">
+            <div class="kpi-content">
+              <div class="kpi-label">热销商品</div>
+              <div class="kpi-value">
+                <span class="num">{{ currentStats.hotProductCount }}</span>
+                <span class="unit">款</span>
+              </div>
+            </div>
+            <div class="kpi-icon-wrapper">
+              <el-icon class="kpi-icon"><PriceTag /></el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="12" :sm="12" :md="6">
+        <el-card shadow="hover" class="kpi-card custom-card danger-card">
+          <div class="kpi-inner">
+            <div class="kpi-content">
+              <div class="kpi-label">售后订单</div>
+              <div class="kpi-value">
+                <span class="num">{{ currentStats.afterSaleCount }}</span>
+                <span class="unit">单</span>
+              </div>
+            </div>
+            <div class="kpi-icon-wrapper">
+              <el-icon class="kpi-icon"><Box /></el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- Charts Row -->
+    <el-row :gutter="20" class="board-row">
+      <el-col :xs="24" :md="14">
+        <el-card shadow="hover" class="board-card custom-card border-card">
+          <template #header>
+            <div class="board-card-title">
+              <span class="dot primary-dot"></span>
+              <span>订单趋势线</span>
+            </div>
+          </template>
+          <div ref="orderTrendRef" class="chart chart-large" />
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :md="10">
+        <el-card shadow="hover" class="board-card custom-card border-card">
+          <template #header>
+            <div class="board-card-title">
+              <span class="dot warning-dot"></span>
+              <span>售后状态占比</span>
+            </div>
+          </template>
+          <div ref="afterSalePieRef" class="chart chart-large" />
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- Tables Row -->
+    <el-row :gutter="20" class="board-row">
+      <el-col :xs="24" :md="14">
+        <el-card shadow="hover" class="board-card custom-card border-card">
+          <template #header>
+            <div class="board-card-title">
+              <span class="dot success-dot"></span>
+              <span>热销商品排行榜 TOP 5</span>
+            </div>
+          </template>
+          <el-table 
+            :data="currentStats.hotProducts" 
+            style="width: 100%" 
+            :header-cell-style="{ background: '#f8f9fa', color: '#333', fontWeight: 'bold' }"
+            stripe
+          >
+            <el-table-column label="排名" type="index" width="80" align="center">
+              <template #default="scope">
+                <span :class="['rank-badge', `rank-${scope.$index + 1}`]">
+                  {{ scope.$index + 1 }}
+                </span>
+              </template>
+            </el-table-column>
+            <el-table-column label="商品名称" prop="name" show-overflow-tooltip>
+              <template #default="scope">
+                <span class="product-name">{{ scope.row.name }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="近期销量" prop="sales" width="120" align="center">
+              <template #default="scope">
+                <el-tag type="danger" effect="light" size="small">{{ scope.row.sales }} 件</el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+      
+      <el-col :xs="24" :md="10">
+        <el-card shadow="hover" class="board-card custom-card border-card">
+          <template #header>
+            <div class="board-card-title">
+              <span class="dot danger-dot"></span>
+              <span>近期售后动态</span>
+            </div>
+            <div class="mini-kpi-wrap">
+              <div class="mini-kpi">
+                <div class="m-label">待处理</div>
+                <div class="m-value text-danger">{{ currentStats.afterSalePending }}</div>
+              </div>
+              <div class="mini-v-divider"></div>
+              <div class="mini-kpi">
+                <div class="m-label">已完成</div>
+                <div class="m-value text-success">{{ currentStats.afterSaleDone }}</div>
+              </div>
+            </div>
+          </template>
+          
+          <el-table 
+            :data="currentStats.recentAfterSales" 
+            style="width: 100%"
+            :header-cell-style="{ background: '#f8f9fa' }"
+          >
+            <el-table-column label="售后单号" prop="no" show-overflow-tooltip min-width="140" />
+            <el-table-column label="状态" prop="status" width="90" align="center">
+              <template #default="scope">
+                <el-tag :type="scope.row.status === '已完成' ? 'success' : 'warning'" size="small">
+                  {{ scope.row.status }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="申请时间" prop="time" width="110" />
+          </el-table>
+        </el-card>
+      </el-col>
+    </el-row>
+
   </div>
 </template>
 
 <script setup name="Index" lang="ts">
-const goTarget = (url: string) => {
-  window.open(url, '__blank');
-};
-</script>
+import { ref, reactive, computed, onMounted, onBeforeUnmount, watch } from 'vue';
+import { Box, Goods, PriceTag, Tickets } from '@element-plus/icons-vue';
+import * as echarts from 'echarts';
 
-<style lang="scss" scoped>
-.home {
-  blockquote {
-    padding: 10px 20px;
-    margin: 0 0 20px;
-    font-size: 17.5px;
-    border-left: 5px solid #eee;
-  }
-  hr {
-    margin-top: 20px;
-    margin-bottom: 20px;
-    border: 0;
-    border-top: 1px solid #eee;
-  }
-  .col-item {
-    margin-bottom: 20px;
-  }
+type ProjectKey = 'zhongzhi' | 'zhongche';
 
-  ul {
-    padding: 0;
-    margin: 0;
+const activeProject = ref<ProjectKey>('zhongzhi');
+const projectOptions: Array<{ label: string; value: ProjectKey }> = [
+  { label: '中直', value: 'zhongzhi' },
+  { label: '中车', value: 'zhongche' }
+];
+
+const dashboardData = reactive<Record<ProjectKey, any>>({
+  zhongzhi: {
+    orderCount: 1280,
+    productCount: 3560,
+    hotProductCount: 56,
+    afterSaleCount: 38,
+    afterSalePending: 9,
+    afterSaleDone: 29,
+    hotProducts: [
+      { name: '中直-热销商品型号 2024款', sales: 320 },
+      { name: '高配自选升级包 豪华版', sales: 265 },
+      { name: '专业定制服务 年卡版', sales: 210 },
+      { name: '中直-专属增值服务包', sales: 154 },
+      { name: '标准配件套装 (多色)', sales: 112 }
+    ],
+    recentAfterSales: [
+      { no: 'AS20250303001', status: '待处理', time: '03-03 10:20' },
+      { no: 'AS20250303002', status: '已完成', time: '03-03 09:12' },
+      { no: 'AS20250303003', status: '待处理', time: '03-02 14:35' },
+      { no: 'AS20250302004', status: '已完成', time: '03-02 11:21' }
+    ],
+    orderTrend: {
+      xAxis: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
+      orderSeries: [120, 160, 140, 220, 260, 200, 180],
+      amountSeries: [18, 26, 22, 35, 42, 31, 28]
+    },
+    afterSalePie: [
+      { name: '待处理', value: 9 },
+      { name: '处理中', value: 6 },
+      { name: '已完成', value: 23 }
+    ]
+  },
+  zhongche: {
+    orderCount: 980,
+    productCount: 2890,
+    hotProductCount: 44,
+    afterSaleCount: 26,
+    afterSalePending: 5,
+    afterSaleDone: 21,
+    hotProducts: [
+      { name: '中车-特供机型 v2', sales: 280 },
+      { name: '工业配件标准件包', sales: 230 },
+      { name: '中车-定制保养套装', sales: 190 },
+      { name: '机电耗材包 (月卡)', sales: 140 },
+      { name: '基础版维修备件', sales: 90 }
+    ],
+    recentAfterSales: [
+      { no: 'AS20250303011', status: '已完成', time: '03-03 14:10' },
+      { no: 'AS20250303012', status: '待处理', time: '03-02 09:30' },
+      { no: 'AS20250303013', status: '已完成', time: '03-01 16:45' },
+      { no: 'AS20250302014', status: '待处理', time: '03-01 11:05' }
+    ],
+    orderTrend: {
+      xAxis: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
+      orderSeries: [80, 110, 95, 140, 170, 150, 130],
+      amountSeries: [12, 18, 15, 22, 28, 24, 20]
+    },
+    afterSalePie: [
+      { name: '待处理', value: 5 },
+      { name: '处理中', value: 4 },
+      { name: '已完成', value: 17 }
+    ]
   }
+});
 
-  font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-  font-size: 13px;
-  color: #676a6c;
-  overflow-x: hidden;
+const currentStats = computed(() => dashboardData[activeProject.value]);
 
-  ul {
-    list-style-type: none;
-  }
+const orderTrendRef = ref<HTMLDivElement>();
+const afterSalePieRef = ref<HTMLDivElement>();
+
+let orderTrendChart: echarts.ECharts | undefined;
+let afterSalePieChart: echarts.ECharts | undefined;
 
-  h4 {
-    margin-top: 0px;
+const renderCharts = () => {
+  const stats = currentStats.value;
+
+  if (orderTrendRef.value) {
+    if (!orderTrendChart) {
+      orderTrendChart = echarts.init(orderTrendRef.value);
+    }
+    orderTrendChart.setOption({
+      backgroundColor: 'transparent',
+      tooltip: { 
+        trigger: 'axis',
+        backgroundColor: 'rgba(255, 255, 255, 0.9)',
+        borderColor: '#eee',
+        padding: 10,
+        textStyle: { color: '#333' }
+      },
+      legend: { 
+        data: ['订单量', '销售额(万)'],
+        top: 0
+      },
+      grid: { left: 40, right: 20, top: 40, bottom: 30, containLabel: true },
+      xAxis: { 
+        type: 'category', 
+        data: stats.orderTrend.xAxis,
+        axisLine: { lineStyle: { color: '#ddd' } },
+        axisLabel: { color: '#666' }
+      },
+      yAxis: [
+        { 
+          type: 'value', 
+          name: '订单量',
+          nameTextStyle: { color: '#999' },
+          splitLine: { lineStyle: { type: 'dashed', color: '#eee' } },
+          axisLabel: { color: '#666' }
+        },
+        { 
+          type: 'value', 
+          name: '销售额',
+          nameTextStyle: { color: '#999' },
+          splitLine: { show: false },
+          axisLabel: { color: '#666' }
+        }
+      ],
+      series: [
+        { 
+          name: '订单量', 
+          type: 'line', 
+          smooth: true, 
+          symbolSize: 8,
+          itemStyle: { color: '#409EFF' },
+          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)' }
+            ])
+          },
+          data: stats.orderTrend.orderSeries 
+        },
+        { 
+          name: '销售额(万)', 
+          type: 'bar', 
+          yAxisIndex: 1, 
+          barWidth: 12, 
+          itemStyle: { 
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: '#36cfc9' },
+              { offset: 1, color: '#009688' }
+            ]),
+            borderRadius: [4, 4, 0, 0]
+          },
+          data: stats.orderTrend.amountSeries 
+        }
+      ]
+    });
   }
 
-  h2 {
-    margin-top: 10px;
-    font-size: 26px;
-    font-weight: 100;
+  if (afterSalePieRef.value) {
+    if (!afterSalePieChart) {
+      afterSalePieChart = echarts.init(afterSalePieRef.value);
+    }
+    afterSalePieChart.setOption({
+      backgroundColor: 'transparent',
+      tooltip: { trigger: 'item' },
+      legend: { bottom: 0, left: 'center', itemWidth: 10, itemHeight: 10, icon: 'circle' },
+      color: ['#F56C6C', '#E6A23C', '#67C23A'],
+      series: [
+        {
+          name: '售后状态',
+          type: 'pie',
+          radius: ['45%', '70%'],
+          center: ['50%', '42%'],
+          avoidLabelOverlap: false,
+          itemStyle: {
+            borderRadius: 6,
+            borderColor: '#fff',
+            borderWidth: 2
+          },
+          label: { 
+            show: true,
+            formatter: '{b}\n{d}%',
+            color: '#666'
+          },
+          labelLine: { show: true, length: 15, length2: 10 },
+          data: stats.afterSalePie
+        }
+      ]
+    });
   }
+};
+
+const resizeCharts = () => {
+  orderTrendChart?.resize();
+  afterSalePieChart?.resize();
+};
 
-  p {
-    margin-top: 10px;
+watch(activeProject, () => {
+  renderCharts();
+});
 
-    b {
+onMounted(() => {
+  renderCharts();
+  window.addEventListener('resize', resizeCharts);
+});
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  background-color: #f2f5f9;
+  min-height: calc(100vh - 84px);
+  padding: 20px;
+}
+
+.dashboard-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24px;
+  
+  .dashboard-title {
+    display: flex;
+    align-items: baseline;
+    gap: 12px;
+    
+    .title-text {
+      font-size: 22px;
       font-weight: 700;
+      color: #2c3e50;
+      letter-spacing: 1px;
+    }
+    
+    .title-desc {
+      font-size: 14px;
+      color: #909399;
+      font-weight: 400;
+      letter-spacing: 0.5px;
     }
   }
 
-  .update-log {
-    ol {
-      display: block;
-      list-style-type: decimal;
-      margin-block-start: 1em;
-      margin-block-end: 1em;
-      margin-inline-start: 0;
-      margin-inline-end: 0;
-      padding-inline-start: 40px;
+  .project-select {
+    width: 180px;
+    :deep(.el-input__wrapper) {
+      border-radius: 20px;
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+      background-color: #fff;
+      padding: 2px 15px;
+      transition: all 0.3s;
+      
+      &:hover, &.is-focus {
+        box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
+      }
     }
   }
-  .index-style {
-    font-size: 48px;
-    font-weight: bold;
-    letter-spacing: 15px;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    height: 300px;
-    background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
-    border-radius: 10px;
-    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
+}
 
-    .typewriter-container {
-      display: flex;
-    }
+.kpi-row, .board-row {
+  margin-bottom: 20px;
+}
 
-    .typewriter-char {
-      opacity: 0;
-      transform: translateY(20px) rotate(-5deg);
-      animation:
-        typewriter-animation 0.8s ease forwards,
-        pulse 2s ease-in-out infinite 1s;
-      color: #2d8cf0;
-      text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
-      transition: color 0.3s ease;
-
-      &:hover {
-        color: #f06292;
-        transform: scale(1.1);
-        animation-play-state: paused;
-      }
-    }
+.custom-card {
+  border: none;
+  border-radius: 12px;
+  transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+  background: #fff;
+  
+  &:hover {
+    transform: translateY(-4px);
+    box-shadow: 0 10px 20px rgba(0,0,0,0.08);
   }
+}
 
-  @keyframes typewriter-animation {
-    0% {
-      opacity: 0;
-      transform: translateY(20px) rotate(-5deg);
-    }
-    50% {
-      opacity: 0.5;
-      transform: translateY(10px) rotate(-2deg);
+/* KPI Cards */
+.kpi-card {
+  overflow: hidden;
+  position: relative;
+  
+  .kpi-inner {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 24px 20px;
+    position: relative;
+    z-index: 1;
+  }
+  
+  .kpi-label {
+    font-size: 15px;
+    color: #fff;
+    opacity: 0.9;
+    margin-bottom: 8px;
+    font-weight: 500;
+  }
+  
+  .kpi-value {
+    color: #fff;
+    .num {
+      font-size: 32px;
+      font-weight: 700;
+      line-height: 1;
+      font-family: 'Rubik', Arial, sans-serif;
     }
-    100% {
-      opacity: 1;
-      transform: translateY(0) rotate(0deg);
+    .unit {
+      font-size: 14px;
+      margin-left: 4px;
+      opacity: 0.8;
     }
   }
 
-  @keyframes pulse {
-    0% {
-      text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
-      transform: scale(1);
-    }
-    50% {
-      text-shadow:
-        0 0 15px rgba(45, 140, 240, 0.8),
-        0 0 30px rgba(45, 140, 240, 0.4);
-      transform: scale(1.05);
-    }
-    100% {
-      text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
-      transform: scale(1);
+  .kpi-icon-wrapper {
+    width: 60px;
+    height: 60px;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: rgba(255, 255, 255, 0.2);
+    backdrop-filter: blur(4px);
+    
+    .kpi-icon {
+      font-size: 30px;
+      color: #fff;
     }
   }
+
+  &::after {
+    content: '';
+    position: absolute;
+    right: -20px;
+    top: -20px;
+    width: 120px;
+    height: 120px;
+    border-radius: 50%;
+    background: rgba(255, 255, 255, 0.1);
+    z-index: 0;
+  }
+}
+
+.primary-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
+.success-card { background: linear-gradient(135deg, #20E2D7 0%, #F9FEA5 100%); .kpi-label, .kpi-value {color: #333} .kpi-icon {color: #333} .kpi-icon-wrapper {background: rgba(0,0,0,0.05)}}
+.warning-card { background: linear-gradient(135deg, #f6d365 0%, #fda085 100%); }
+.danger-card { background: linear-gradient(135deg, #ff0844 0%, #ffb199 100%); }
+
+/* Board Cards */
+.border-card {
+  box-shadow: 0 4px 12px rgba(0,0,0,0.03);
+  
+  :deep(.el-card__header) {
+    border-bottom: 1px solid #f0f2f5;
+    padding: 16px 20px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+  :deep(.el-card__body) {
+    padding: 20px;
+  }
+}
+
+.board-card-title {
+  display: flex;
+  align-items: center;
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  
+  .dot {
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    margin-right: 10px;
+    
+    &.primary-dot { background-color: #409EFF; }
+    &.warning-dot { background-color: #E6A23C; }
+    &.success-dot { background-color: #67C23A; }
+    &.danger-dot { background-color: #F56C6C; }
+  }
+}
+
+.chart-large {
+  height: 340px;
+  width: 100%;
+}
+
+/* Tables styling */
+.rank-badge {
+  display: inline-block;
+  width: 24px;
+  height: 24px;
+  line-height: 24px;
+  text-align: center;
+  border-radius: 50%;
+  font-weight: bold;
+  font-size: 12px;
+  background-color: #f4f4f5;
+  color: #909399;
+  
+  &.rank-1 { background-color: #f56c6c; color: white; }
+  &.rank-2 { background-color: #ff9800; color: white; }
+  &.rank-3 { background-color: #e6a23c; color: white; }
+}
+
+.product-name {
+  font-weight: 500;
+  color: #303133;
+}
+
+.mini-kpi-wrap {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.mini-v-divider {
+  width: 1px;
+  height: 24px;
+  background-color: #ebeef5;
+}
+
+.mini-kpi {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  
+  .m-label {
+    font-size: 13px;
+    color: #909399;
+  }
+  
+  .m-value {
+    font-size: 18px;
+    font-weight: 700;
+    font-family: 'Rubik', Arial, sans-serif;
+  }
+  
+  .text-danger { color: #f56c6c; }
+  .text-success { color: #67c23a; }
 }
 </style>

+ 54 - 11
src/views/order/zhongcheReturn/index.vue

@@ -128,14 +128,17 @@
         <el-form-item label="手机号" prop="mobile">
           <el-input v-model="acceptForm.mobile" placeholder="请输入手机号" />
         </el-form-item>
-        <el-form-item label="省编码" prop="provinceId">
-          <el-input v-model="acceptForm.provinceId" placeholder="请输入省编码" />
-        </el-form-item>
-        <el-form-item label="市编码" prop="cityId">
-          <el-input v-model="acceptForm.cityId" placeholder="请输入市编码" />
-        </el-form-item>
-        <el-form-item label="区县编码" prop="countyId">
-          <el-input v-model="acceptForm.countyId" placeholder="请输入区县编码" />
+        <el-form-item label="省市区" prop="areaCodes">
+          <el-cascader
+            v-model="acceptForm.areaCodes"
+            :options="chinaAreaOptions"
+            :props="chinaAreaCascaderProps"
+            :disabled="chinaAreaLoading"
+            clearable
+            placeholder="请选择省/市/区"
+            style="width: 100%"
+            @change="handleAreaCodesChange"
+          />
         </el-form-item>
         
         <el-form-item label="详细地址" prop="address">
@@ -164,12 +167,17 @@
 <script setup name="ZhongcheReturn" lang="ts">
 import { listZhongcheReturn, getZhongcheReturn, delZhongcheReturn, addZhongcheReturn, updateZhongcheReturn, acceptAfterSale, rejectAfterSale, confirmReceived, refundAfterSale } from '@/api/order/zhongcheReturn';
 import { ZhongcheReturnVO, ZhongcheReturnQuery, ZhongcheReturnForm } from '@/api/order/zhongcheReturn/types';
+import { getChinaArea } from '@/api/system/addressArea/index';
+import type { AddressAreaVo } from '@/api/system/addressArea/types';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { return_status } = toRefs<any>(proxy?.useDict('return_status'));
 const { service_type } = toRefs<any>(proxy?.useDict('service_type'));
 const { pick_type } = toRefs<any>(proxy?.useDict('pick_type'));
 
+const chinaAreaTree = ref<AddressAreaVo | undefined>(undefined);
+const chinaAreaLoading = ref(false);
+
 const zhongcheReturnList = ref<ZhongcheReturnVO[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
@@ -192,9 +200,7 @@ const acceptForm = ref<any>({});
 const acceptRules = reactive({
   name: [{ required: true, message: "联系人不能为空", trigger: "blur" }],
   mobile: [{ required: true, message: "手机号不能为空", trigger: "blur" }],
-  provinceId: [{ required: true, message: "省编码不能为空", trigger: "blur" }],
-  cityId: [{ required: true, message: "市编码不能为空", trigger: "blur" }],
-  countyId: [{ required: true, message: "区县编码不能为空", trigger: "blur" }],
+  areaCodes: [{ required: true, message: "省市区不能为空", trigger: "change" }],
   address: [{ required: true, message: "详细地址不能为空", trigger: "blur" }],
   email: [{ required: true, message: "邮箱不能为空", trigger: "blur" }]
 });
@@ -334,6 +340,7 @@ const handleMerchantProcess = async () => {
       provinceId: undefined,
       cityId: undefined,
       countyId: undefined,
+      areaCodes: [],
       townId: undefined,
       address: undefined,
       zip: undefined,
@@ -436,7 +443,43 @@ const handleExport = () => {
   }, `zhongcheReturn_${new Date().getTime()}.xlsx`)
 }
 
+const loadChinaArea = async () => {
+  chinaAreaLoading.value = true;
+  try {
+    const res = await getChinaArea();
+    chinaAreaTree.value = res.data;
+  } catch (e: any) {
+    proxy?.$modal.msgError(e?.message || '获取地区数据失败');
+  } finally {
+    chinaAreaLoading.value = false;
+  }
+}
+
+const chinaAreaOptions = computed<any[]>(() => {
+  const data: any = chinaAreaTree.value as any;
+  if (!data) return [];
+  if (Array.isArray(data)) return data;
+  if (Array.isArray(data.children)) return data.children;
+  return [data];
+});
+
+const chinaAreaCascaderProps = {
+  value: 'areaCode',
+  label: 'areaName',
+  children: 'children',
+  emitPath: true,
+  checkStrictly: false
+};
+
+const handleAreaCodesChange = (val: any) => {
+  const codes = Array.isArray(val) ? val : [];
+  acceptForm.value.provinceId = codes[0];
+  acceptForm.value.cityId = codes[1];
+  acceptForm.value.countyId = codes[2];
+}
+
 onMounted(() => {
   getList();
+  loadChinaArea();
 });
 </script>

+ 85 - 9
src/views/product/base/index.vue

@@ -20,6 +20,7 @@
               <el-col :span="24" class="text-left">
                 <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                 <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                <el-button type="success" :disabled="multiple" @click="handleBatchAddToExternal">添加到对接产品库</el-button>
               </el-col>
             </el-row>
           </el-form>
@@ -31,7 +32,7 @@
 
 
     <el-card shadow="never">
-      <el-table v-loading="loading" border :data="baseList" @selection-change="handleSelectionChange">
+      <el-table ref="baseTableRef" v-loading="loading" border :data="baseList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="图片" align="center" prop="productImage" width="100" fixed="left">
           <template #default="scope">
@@ -43,7 +44,7 @@
             <el-link type="primary" @click="handleView(scope.row)">{{ scope.row.productNo }}</el-link>
           </template>
         </el-table-column>
-        <el-table-column label="产品名称" align="center" prop="itemName" width="250" show-overflow-tooltip />
+        <el-table-column label="产品名称" align="center" prop="itemName" min-width="250" show-overflow-tooltip />
         <el-table-column label="类别" align="center" prop="categoryName" width="120" />
         <el-table-column label="品牌" align="center" prop="brandName" width="100" />
         <el-table-column label="单位" align="center" prop="unitName" width="80" />
@@ -72,12 +73,6 @@
             <span>{{ scope.row.minOrderQuantity || '1' }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="对接状态" align="center" prop="connectStatus" width="100">
-          <template #default="scope">
-            <el-tag v-if="scope.row.connectStatus === '已对接'" type="success">已对接</el-tag>
-            <el-tag v-else type="warning">未对接</el-tag>
-          </template>
-        </el-table-column>
         <el-table-column label="操作" align="center" width="180" fixed="right">
           <template #default="scope">
             <el-button link type="primary" @click="handleConnect(scope.row)">对接</el-button>
@@ -142,6 +137,22 @@
         </div>
       </template>
     </el-dialog>
+
+    <el-dialog v-model="batchDialog.visible" title="添加到对接产品库" width="500px" append-to-body @close="cancelBatchDialog">
+      <el-form ref="batchFormRef" :model="batchForm" :rules="batchRules" label-width="120px">
+        <el-form-item label="选择项目" prop="itemId">
+          <el-select v-model="batchForm.itemId" placeholder="请选择项目" clearable filterable style="width: 100%">
+            <el-option v-for="item in itemList" :key="item.id" :label="item.itemName" :value="item.id" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="cancelBatchDialog">取 消</el-button>
+          <el-button type="primary" :loading="batchLoading" @click="submitBatchAddToExternal">确 定</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
@@ -150,10 +161,12 @@ import { listBase, getBase, delBase, brandList, categoryTree } from '@/api/produ
 import { BaseVO, BaseQuery, BaseForm } from '@/api/product/base/types';
 import { BrandVO } from '@/api/product/brand/types';
 import { categoryTreeVO } from '@/api/product/category/types';
-import { addProduct } from '@/api/external/product';
+import { addProduct, batchInsertExternalProduct } from '@/api/external/product';
 import { ProductForm } from '@/api/external/product/types';
 import { listProductCategory } from '@/api/external/productCategory';
 import { ProductCategoryVO } from '@/api/external/productCategory/types';
+import { listItem } from '@/api/external/item/index';
+import { ItemVO } from '@/api/external/item/types';
 import { useRoute, useRouter } from 'vue-router';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -161,6 +174,7 @@ const router = useRouter();
 const route = useRoute();
 
 const baseList = ref<BaseVO[]>([]);
+const selectedRows = ref<BaseVO[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
 const showSearch = ref(true);
@@ -169,6 +183,7 @@ const single = ref(true);
 const multiple = ref(true);
 const total = ref(0);
 const categoryOptions = ref<categoryTreeVO[]>([]);
+const itemList = ref<ItemVO[]>([]);
 const hasMore = ref(true); // 是否还有更多数据
 // 页面历史记录,存储每页的第一个id和最后一个id,用于支持双向翻页
 const pageHistory = ref([]);
@@ -185,6 +200,8 @@ const statistics = ref({
 const queryFormRef = ref<ElFormInstance>();
 const baseFormRef = ref<ElFormInstance>();
 const connectFormRef = ref<ElFormInstance>();
+const batchFormRef = ref<ElFormInstance>();
+const baseTableRef = ref();
 
 const dialog = reactive<DialogOption>({
   visible: false,
@@ -245,6 +262,20 @@ const cascaderProps = {
 // 对接加载状态
 const connectLoading = ref(false);
 
+const batchDialog = reactive({
+  visible: false
+});
+
+const batchLoading = ref(false);
+
+const batchForm = ref({
+  itemId: undefined as string | number | undefined
+});
+
+const batchRules = {
+  itemId: [{ required: true, message: '请选择项目', trigger: 'change' }]
+};
+
 // 对接表单验证规则
 const connectRules = {
   externalCategoryId: [{ required: true, message: '请选择第三方产品分类', trigger: 'change' }],
@@ -466,10 +497,54 @@ const resetQuery = () => {
 /** 多选框选中数据 */
 const handleSelectionChange = (selection: BaseVO[]) => {
   ids.value = selection.map((item) => item.id);
+  selectedRows.value = selection;
   single.value = selection.length != 1;
   multiple.value = !selection.length;
 };
 
+const getItemList = async () => {
+  const res = await listItem();
+  itemList.value = (res as any).rows || res.data || [];
+};
+
+const handleBatchAddToExternal = async () => {
+  if (multiple.value) {
+    proxy?.$modal.msgWarning('请先选择要添加的商品');
+    return;
+  }
+  batchForm.value.itemId = undefined;
+  batchDialog.visible = true;
+};
+
+const cancelBatchDialog = () => {
+  batchDialog.visible = false;
+  batchFormRef.value?.resetFields();
+};
+
+const submitBatchAddToExternal = () => {
+  batchFormRef.value?.validate(async (valid: boolean) => {
+    if (!valid) return;
+    if (!selectedRows.value?.length) {
+      proxy?.$modal.msgWarning('请先选择要添加的商品');
+      return;
+    }
+    batchLoading.value = true;
+    try {
+      await batchInsertExternalProduct(batchForm.value.itemId as any, selectedRows.value as any);
+      proxy?.$modal.msgSuccess('添加成功');
+      batchDialog.visible = false;
+      baseTableRef.value?.clearSelection?.();
+      selectedRows.value = [];
+      ids.value = [];
+      multiple.value = true;
+      single.value = true;
+      await getList();
+    } finally {
+      batchLoading.value = false;
+    }
+  });
+};
+
 /** 新增按钮操作 */
 const handleAdd = () => {
   router.push('/product/base/add');
@@ -622,5 +697,6 @@ const getCategoryTree = async () => {
 onMounted(() => {
   getList();
   getCategoryTree();
+  getItemList();
 });
 </script>