Explorar el Código

feat(product): 更新品牌编辑页面并添加图片上传组件

- 将 image-upload 组件替换为 upload-image 组件,并添加 limit、width、height、imageText 属性
- 新增 UploadImage 组件用于图片上传功能,支持单张和多张图片上传
- 添加 common.ts 工具函数,包含 isUrl、img 和 deepClone 方法
- 在 .env.production 中将 VITE_APP_BASE_API 的值从 '/prod-api' 更改为 'http://one.yoe365.com'
- 更新路由配置,将首页重定向改为项目选择页面,并调整统计页面路由
- 在品牌编辑页面添加 brand/edit 路由配置
- 修复外部接口参数命名,将 itemKey 替换为 itemId
- 添加 changeItemStatus API 接口用于修改第三方对接项目管理状态
- 添加 getProjectProductPage API 接口用于获取项目商品列表
- 实现完整的图片上传组件功能,包括图片选择、预览、删除、重命名等操作
- 添加图片分类管理和文件搜索功能
肖路 hace 3 semanas
padre
commit
5000ce00f2

+ 1 - 1
.env.production

@@ -15,7 +15,7 @@ VITE_APP_MONITOR_ADMIN = '/admin/applications'
 VITE_APP_SNAILJOB_ADMIN = '/snail-job'
 VITE_APP_SNAILJOB_ADMIN = '/snail-job'
 
 
 # 生产环境
 # 生产环境
-VITE_APP_BASE_API = '/prod-api'
+VITE_APP_BASE_API = 'http://one.yoe365.com'
 
 
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 VITE_BUILD_COMPRESS = gzip
 VITE_BUILD_COMPRESS = gzip

+ 13 - 0
src/api/external/item/index.ts

@@ -61,3 +61,16 @@ export const delItem = (id: string | number | Array<string | number>) => {
     method: 'delete'
     method: 'delete'
   });
   });
 };
 };
+
+/**
+ * 修改第三方对接项目管理状态
+ * @param id
+ * @param status
+ */
+export const changeItemStatus = (id: string | number, status: string) => {
+  return request({
+    url: '/external/item',
+    method: 'put',
+    data: { id, status }
+  });
+};

+ 52 - 8
src/api/external/item/types.ts

@@ -3,6 +3,10 @@ export interface ItemVO {
    * 项目id
    * 项目id
    */
    */
   id: string | number;
   id: string | number;
+  /**
+   * 项目logo
+   */
+  logo: string;
 
 
   /**
   /**
    * 项目名
    * 项目名
@@ -39,6 +43,45 @@ export interface ItemVO {
    */
    */
   remark: string;
   remark: string;
 
 
+  /**
+   * 商品数量
+   */
+  productCount: number | null;
+
+  /**
+   * 订单总数
+   */
+  orderCount: number | null;
+
+  /**
+   * 已完成的数量
+   */
+  completedCount: number | null;
+
+  /**
+   * 待付款的数量
+   */
+  waitPayCount: number | null;
+
+  /**
+   * 待发货的数量
+   */
+  waitDeliverCount: number | null;
+
+  /**
+   * 售后订单数量
+   */
+  refundCount: number | null;
+
+  /**
+   * 订单总金额
+   */
+  orderAmount: number | null;
+
+  /**
+   * 售后订单金额
+   */
+  refundAmount: number | null;
 }
 }
 
 
 export interface ItemForm extends BaseEntity {
 export interface ItemForm extends BaseEntity {
@@ -47,6 +90,11 @@ export interface ItemForm extends BaseEntity {
    */
    */
   id?: string | number;
   id?: string | number;
 
 
+  /**
+   * 项目logo
+   */
+  logo: string;
+
   /**
   /**
    * 项目名
    * 项目名
    */
    */
@@ -81,7 +129,6 @@ export interface ItemForm extends BaseEntity {
    * 备注
    * 备注
    */
    */
   remark?: string;
   remark?: string;
-
 }
 }
 
 
 export interface ItemQuery extends PageQuery {
 export interface ItemQuery extends PageQuery {
@@ -121,11 +168,8 @@ export interface ItemQuery extends PageQuery {
    */
    */
   platformCode?: string;
   platformCode?: string;
 
 
-    /**
-     * 日期范围参数
-     */
-    params?: any;
+  /**
+   * 日期范围参数
+   */
+  params?: any;
 }
 }
-
-
-

+ 6 - 6
src/api/external/productBrand/index.ts

@@ -19,13 +19,13 @@ export const listProductBrand = (query?: ProductBrandQuery): AxiosPromise<Produc
 /**
 /**
  * 查询第三方产品品牌信息详细
  * 查询第三方产品品牌信息详细
  * @param id
  * @param id
- * @param itemKey
+ * @param itemId
  */
  */
-export const getProductBrand = (id: string | number, itemKey?: string): AxiosPromise<ProductBrandVO> => {
+export const getProductBrand = (id: string | number, itemId?: string): AxiosPromise<ProductBrandVO> => {
   return request({
   return request({
     url: '/external/productBrand/' + id,
     url: '/external/productBrand/' + id,
     method: 'get',
     method: 'get',
-    params: { itemKey }
+    params: { itemId }
   });
   });
 };
 };
 
 
@@ -56,12 +56,12 @@ export const updateProductBrand = (data: ProductBrandForm) => {
 /**
 /**
  * 删除第三方产品品牌信息
  * 删除第三方产品品牌信息
  * @param id
  * @param id
- * @param itemKey
+ * @param itemId
  */
  */
-export const delProductBrand = (id: string | number | Array<string | number>, itemKey?: string) => {
+export const delProductBrand = (id: string | number | Array<string | number>, itemId?: string) => {
   return request({
   return request({
     url: '/external/productBrand/' + id,
     url: '/external/productBrand/' + id,
     method: 'delete',
     method: 'delete',
-    params: { itemKey }
+    params: { itemId }
   });
   });
 };
 };

+ 9 - 9
src/api/external/productCategory/index.ts

@@ -19,13 +19,13 @@ export const listProductCategory = (query?: Partial<ProductCategoryQuery>): Axio
 /**
 /**
  * 查询产品分类详细
  * 查询产品分类详细
  * @param id
  * @param id
- * @param itemKey
+ * @param itemId
  */
  */
-export const getProductCategory = (id: string | number, itemKey?: string): AxiosPromise<ProductCategoryVO> => {
+export const getProductCategory = (id: string | number, itemId?: string): AxiosPromise<ProductCategoryVO> => {
   return request({
   return request({
     url: '/external/productCategory/' + id,
     url: '/external/productCategory/' + id,
     method: 'get',
     method: 'get',
-    params: { itemKey }
+    params: { itemId }
   });
   });
 };
 };
 
 
@@ -56,26 +56,26 @@ export const updateProductCategory = (data: ProductCategoryForm) => {
 /**
 /**
  * 删除产品分类
  * 删除产品分类
  * @param id
  * @param id
- * @param itemKey
+ * @param itemId
  */
  */
-export const delProductCategory = (id: string | number | Array<string | number>, itemKey?: string) => {
+export const delProductCategory = (id: string | number | Array<string | number>, itemId?: string) => {
   return request({
   return request({
     url: '/external/productCategory/' + id,
     url: '/external/productCategory/' + id,
     method: 'delete',
     method: 'delete',
-    params: { itemKey }
+    params: { itemId }
   });
   });
 };
 };
 
 
 /**
 /**
  * 查询产品分类列表(排除指定节点及其子节点)
  * 查询产品分类列表(排除指定节点及其子节点)
  * @param id
  * @param id
- * @param itemKey
+ * @param itemId
  */
  */
-export const listProductCategoryExcludeChild = (id: string | number, itemKey?: string): AxiosPromise<ProductCategoryVO[]> => {
+export const listProductCategoryExcludeChild = (id: string | number, itemId?: string): AxiosPromise<ProductCategoryVO[]> => {
   return request({
   return request({
     url: '/external/productCategory/list/exclude/' + id,
     url: '/external/productCategory/list/exclude/' + id,
     method: 'get',
     method: 'get',
-    params: { itemKey }
+    params: { itemId }
   });
   });
 };
 };
 
 

+ 37 - 5
src/api/external/productCategory/types.ts

@@ -74,6 +74,11 @@ export interface ProductCategoryVO {
    */
    */
   children?: ProductCategoryVO[];
   children?: ProductCategoryVO[];
 
 
+  /**
+   * 折扣率
+   */
+  discountRate?: number;
+
 }
 }
 
 
 export interface ProductCategoryForm extends BaseEntity {
 export interface ProductCategoryForm extends BaseEntity {
@@ -142,10 +147,36 @@ export interface ProductCategoryForm extends BaseEntity {
    */
    */
   remark?: string;
   remark?: string;
 
 
+  /**
+   * 折扣率
+   */
+  discountRate?: number;
+
 }
 }
 
 
-export interface ProductCategoryQuery extends PageQuery {
+export interface ProductCategoryDiscountForm {
+  /**
+   * 分类ID
+   */
+  id: string | number;
+
+  /**
+   * 项目ID
+   */
+  itemId?: string | number;
 
 
+  /**
+   * 分类名称
+   */
+  categoryName?: string;
+
+  /**
+   * 折扣率
+   */
+  discountRate?: number;
+}
+
+export interface ProductCategoryQuery extends PageQuery {
   /**
   /**
    * 项目id
    * 项目id
    */
    */
@@ -211,11 +242,12 @@ export interface ProductCategoryQuery extends PageQuery {
    */
    */
   itemKey?: string;
   itemKey?: string;
 
 
-    /**
-     * 日期范围参数
-     */
-    params?: any;
+  /**
+   * 日期范围参数
+   */
+  params?: any;
 }
 }
 
 
 
 
 
 
+

+ 6 - 6
src/api/external/productChangeLog/index.ts

@@ -19,13 +19,13 @@ export const listProductChangeLog = (query?: ProductChangeLogQuery): AxiosPromis
 /**
 /**
  * 查询商品变更消息记录详细
  * 查询商品变更消息记录详细
  * @param id
  * @param id
- * @param itemKey
+ * @param itemId
  */
  */
-export const getProductChangeLog = (id: string | number, itemKey?: string): AxiosPromise<ProductChangeLogVO> => {
+export const getProductChangeLog = (id: string | number, itemId?: string): AxiosPromise<ProductChangeLogVO> => {
   return request({
   return request({
     url: '/external/productChangeLog/' + id,
     url: '/external/productChangeLog/' + id,
     method: 'get',
     method: 'get',
-    params: { itemKey }
+    params: { itemId }
   });
   });
 };
 };
 
 
@@ -56,12 +56,12 @@ export const updateProductChangeLog = (data: ProductChangeLogForm) => {
 /**
 /**
  * 删除商品变更消息记录
  * 删除商品变更消息记录
  * @param id
  * @param id
- * @param itemKey
+ * @param itemId
  */
  */
-export const delProductChangeLog = (id: string | number | Array<string | number>, itemKey?: string) => {
+export const delProductChangeLog = (id: string | number | Array<string | number>, itemId?: string) => {
   return request({
   return request({
     url: '/external/productChangeLog/' + id,
     url: '/external/productChangeLog/' + id,
     method: 'delete',
     method: 'delete',
-    params: { itemKey }
+    params: { itemId }
   });
   });
 };
 };

+ 6 - 6
src/api/external/pushPoolLog/index.ts

@@ -19,13 +19,13 @@ export const listPushPoolLog = (query?: PushPoolLogQuery): AxiosPromise<PushPool
 /**
 /**
  * 查询商品池推送记录详细
  * 查询商品池推送记录详细
  * @param id
  * @param id
- * @param itemKey
+ * @param itemId
  */
  */
-export const getPushPoolLog = (id: string | number, itemKey?: string): AxiosPromise<PushPoolLogVO> => {
+export const getPushPoolLog = (id: string | number, itemId?: string): AxiosPromise<PushPoolLogVO> => {
   return request({
   return request({
     url: '/external/pushPoolLog/' + id,
     url: '/external/pushPoolLog/' + id,
     method: 'get',
     method: 'get',
-    params: { itemKey }
+    params: { itemId }
   });
   });
 };
 };
 
 
@@ -56,12 +56,12 @@ export const updatePushPoolLog = (data: PushPoolLogForm) => {
 /**
 /**
  * 删除商品池推送记录
  * 删除商品池推送记录
  * @param id
  * @param id
- * @param itemKey
+ * @param itemId
  */
  */
-export const delPushPoolLog = (id: string | number | Array<string | number>, itemKey?: string) => {
+export const delPushPoolLog = (id: string | number | Array<string | number>, itemId?: string) => {
   return request({
   return request({
     url: '/external/pushPoolLog/' + id,
     url: '/external/pushPoolLog/' + id,
     method: 'delete',
     method: 'delete',
-    params: { itemKey }
+    params: { itemId }
   });
   });
 };
 };

+ 11 - 0
src/api/product/base/index.ts

@@ -179,3 +179,14 @@ export const shelfReview = (data: BaseForm) => {
   });
   });
 };
 };
 
 
+/**
+ * 获取项目商品列表
+ * */
+export const getProjectProductPage = (query?: BaseQuery): AxiosPromise<BaseVO[]> => {
+  return request({
+    url: '/product/base/getItemProductPage',
+    method: 'get',
+    params: query
+  });
+};
+

+ 5 - 0
src/api/product/base/types.ts

@@ -615,6 +615,11 @@ export interface BaseForm extends BaseEntity {
 
 
 export interface BaseQuery extends PageQuery {
 export interface BaseQuery extends PageQuery {
 
 
+  /**
+   * 项目ID
+   */
+  itemId?: string | number;
+
   /**
   /**
    * 搜索文本(商品名称/商品编号)
    * 搜索文本(商品名称/商品编号)
    */
    */

+ 1177 - 0
src/components/upload-image/index.vue

@@ -0,0 +1,1177 @@
+<template>
+  <div>
+    <div class="flex flex-wrap">
+      <template v-if="limit == 1">
+        <div
+          class="rounded cursor-pointer overflow-hidden relative border border-solid border-color image-wrap mr-[10px]"
+          :class="{ 'rounded-full': type == 'avatar' }"
+          :style="style"
+        >
+          <div class="w-full h-full relative" v-if="imagesData && imagesData.length > 0 && imagesData[0] != ''">
+            <div class="w-full h-full flex items-center justify-center">
+              <el-image class="w-full h-full" :src="imagesData[0].indexOf('data:image') != -1 ? imagesData[0] : img(imagesData[0])"></el-image>
+            </div>
+            <div class="absolute z-[1] flex items-center justify-center w-full h-full inset-0 bg-black bg-opacity-60 operation">
+              <icon name="element ZoomIn" color="#fff" size="18px" class="mr-[10px]" @click="previewImage(imagesData, 0)" />
+              <icon name="element Delete" color="#fff" size="18px" @click.stop="removeImage" />
+            </div>
+          </div>
+          <div class="w-full h-full flex items-center justify-center flex-col content-wrap" v-else @click="openDialog">
+            <icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
+            <div class="leading-none text-xs mt-[10px] text-secondary">{{ imageText || '上传图片' }}</div>
+          </div>
+        </div>
+      </template>
+      <template v-else>
+        <div class="flex flex-wrap" ref="imgListRef">
+          <template v-for="(item, index) in imagesData" :key="item + index">
+            <div
+              v-if="item && item != ''"
+              class="rounded cursor-pointer overflow-hidden relative border border-solid border-color image-wrap mr-[10px] mb-[10px]"
+              :style="style"
+            >
+              <div class="w-full h-full relative">
+                <div class="w-full h-full flex items-center justify-center">
+                  <el-image :src="img(item)" fit="contain"></el-image>
+                </div>
+                <div class="absolute z-[1] flex items-center justify-center w-full h-full inset-0 bg-black bg-opacity-60 operation">
+                  <icon name="element ZoomIn" color="#fff" size="18px" class="mr-[10px]" @click="previewImage(imagesData, index)" />
+                  <icon name="element Delete" color="#fff" size="18px" @click="removeImage(index)" />
+                </div>
+              </div>
+            </div>
+          </template>
+          <div
+            class="rounded cursor-pointer overflow-hidden relative border border-solid border-color image-wrap mr-[10px] mb-[10px]"
+            :style="style"
+            v-if="imagesData.length < limit"
+          >
+            <div class="w-full h-full flex items-center justify-center flex-col content-wrap" @click="openDialog">
+              <icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
+              <div class="leading-none text-xs mt-[10px] text-secondary">{{ imageText || '上传图片' }}</div>
+            </div>
+          </div>
+        </div>
+      </template>
+    </div>
+    <!-- 选择图片 -->
+    <el-dialog
+      v-model="dialogVisible"
+      title="选择图片"
+      width="1400"
+      :close-on-click-modal="false"
+      class="file-selector-dialog"
+      :before-close="closeDialog"
+    >
+      <div class="dialog-bos">
+        <!-- 工具栏 -->
+        <div class="toolbar">
+          <div class="toolbar-left">
+            <el-upload
+              ref="uploadRef"
+              :action="uploadUrl"
+              :headers="uploadHeaders"
+              :before-upload="beforeUpload"
+              :on-success="onUploadSuccess"
+              :on-error="onUploadError"
+              :show-file-list="false"
+              :accept="getUploadFileAccept()"
+            >
+              <el-button type="primary">
+                <el-icon><Plus /></el-icon>
+                上传图片
+              </el-button>
+            </el-upload>
+          </div>
+          <div class="toolbar-right">
+            <el-input v-model="queryParams.name" placeholder="请输入图片名称" style="width: 200px" clearable @input="handleSearch">
+              <template #prefix>
+                <el-icon><Search /></el-icon>
+              </template>
+            </el-input>
+            <div class="view-toggle">
+              <el-button :type="viewMode === 'grid' ? 'primary' : 'default'" size="small" @click="viewMode = 'grid'">
+                <el-icon><Grid /></el-icon>
+              </el-button>
+              <el-button :type="viewMode === 'list' ? 'primary' : 'default'" size="small" @click="viewMode = 'list'">
+                <el-icon><List /></el-icon>
+              </el-button>
+            </div>
+          </div>
+        </div>
+        <div class="content-wrapper">
+          <!-- 左侧分类导航 -->
+          <div class="sidebar">
+            <!-- 全部文件 -->
+            <div @click="handleTreeNodeClick({ id: '' })" class="category-item" :class="{ active: queryParams.categoryId == '' }">
+              <el-icon class="category-icon"><Folder /></el-icon>
+              <span>全部图片</span>
+              <el-dropdown trigger="click" @click.stop class="node-actions">
+                <el-icon class="more-icon"><MoreFilled /></el-icon>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item @click="openClassify({}, 'add')" command="addRoot">添加分类</el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </div>
+            <!-- 树形控件 -->
+            <el-tree
+              ref="categoryTreeRef"
+              :data="filteredCategoryTree"
+              :props="treeProps"
+              :expand-on-click-node="false"
+              :current-node-key="queryParams.categoryId"
+              node-key="id"
+              @node-click="handleTreeNodeClick"
+              class="category-tree"
+            >
+              <template #default="{ data }">
+                <div class="tree-node-content">
+                  <el-icon class="category-icon"><Folder /></el-icon>
+                  <span class="node-label">{{ data.name }}</span>
+                  <el-dropdown trigger="click" @click.stop class="node-actions pr-[10px]">
+                    <el-icon class="more-icon"><MoreFilled /></el-icon>
+                    <template #dropdown>
+                      <el-dropdown-menu>
+                        <el-dropdown-item @click="openClassify(data, 'add')">添加分类</el-dropdown-item>
+                        <el-dropdown-item @click="openClassify(data, 'edit')">编辑分类</el-dropdown-item>
+                        <el-dropdown-item @click="closeClassify(data)">删除分类</el-dropdown-item>
+                      </el-dropdown-menu>
+                    </template>
+                  </el-dropdown>
+                </div>
+              </template>
+            </el-tree>
+          </div>
+          <!-- 右侧文件展示区 -->
+          <div class="content-area">
+            <!-- 网格视图 -->
+            <div v-if="viewMode === 'grid'" class="file-grid">
+              <div
+                v-for="(file, index) in fileList"
+                :key="index"
+                class="file-item"
+                :class="{
+                  selected: selectedFiles.some((row: any) => row.id == file.id)
+                }"
+                @click="toggleFileSelection(file)"
+              >
+                <div class="file-wrapper">
+                  <el-image :src="getImageUrl(file)" fit="cover" class="file-thumbnail" :preview-disabled="true" lazy>
+                    <template #error>
+                      <div class="file-error">
+                        <el-icon size="24" color="#c0c4cc">
+                          <Picture />
+                        </el-icon>
+                      </div>
+                    </template>
+                    <template #placeholder>
+                      <div class="file-loading">
+                        <el-icon size="24" color="#409eff">
+                          <Loading />
+                        </el-icon>
+                      </div>
+                    </template>
+                  </el-image>
+                  <div class="file-checkbox">
+                    <el-checkbox :model-value="selectedFiles.some((row: any) => row.id == file.id)" />
+                  </div>
+                </div>
+                <div class="file-info">
+                  <div class="file-name">{{ file.name || file.originalName }}</div>
+                  <div class="file-actions">
+                    <el-button
+                      link
+                      size="small"
+                      @click.stop="
+                        previewImage(
+                          fileList.map((row: any) => row.url),
+                          index
+                        )
+                      "
+                      >预览</el-button
+                    >
+                    <el-button link size="small" @click.stop="handleRename(file)">重命名</el-button>
+                  </div>
+                </div>
+              </div>
+            </div>
+            <!-- 列表视图 -->
+            <div v-else class="file-list">
+              <!-- @selection-change="handleSelectionChange" -->
+              <el-table height="600" v-loading="loading" :data="fileList" style="width: 100%">
+                <!-- <el-table-column type="selection" width="55" /> -->
+                <el-table-column label="预览" width="80">
+                  <template #default="{ row }">
+                    <el-image :src="getImageUrl(row)" style="width: 50px; height: 50px; border-radius: 4px" fit="cover" :preview-disabled="true" lazy>
+                      <template #error>
+                        <div class="list-image-error">
+                          <el-icon size="20" color="#c0c4cc">
+                            <Picture />
+                          </el-icon>
+                        </div>
+                      </template>
+                      <template #placeholder>
+                        <div class="list-image-loading">
+                          <el-icon size="20" color="#409eff">
+                            <Loading />
+                          </el-icon>
+                        </div>
+                      </template>
+                    </el-image>
+                  </template>
+                </el-table-column>
+                <el-table-column label="文件名" prop="name" min-width="200">
+                  <template #default="{ row }">
+                    {{ row.name || row.originalName }}
+                  </template>
+                </el-table-column>
+                <el-table-column label="大小" width="100">
+                  <template #default="{ row }">
+                    {{ formatFileSize(row.size) }}
+                  </template>
+                </el-table-column>
+                <el-table-column label="类型" prop="type" width="120" />
+                <el-table-column label="上传时间" width="180">
+                  <template #default="{ row }">
+                    {{ formatTime(row.createTime) }}
+                  </template>
+                </el-table-column>
+                <el-table-column label="操作" width="150" fixed="right">
+                  <template #default="scope">
+                    <el-button
+                      link
+                      @click="
+                        previewImage(
+                          fileList.map((row: any) => row.url),
+                          scope.$index
+                        )
+                      "
+                      >预览</el-button
+                    >
+                    <el-button link @click="handleRename(scope.row)">重命名</el-button>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </div>
+            <!-- 分页 -->
+            <div class="pagination">
+              <el-pagination
+                v-model:current-page="queryParams.pageNum"
+                v-model:page-size="queryParams.pageSize"
+                :total="total"
+                :page-sizes="[21, 28, 35, 42]"
+                layout="total, sizes, prev, pager, next, jumper"
+                @size-change="getList"
+                @current-change="getList"
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="closeDialog">取消</el-button>
+          <el-button type="primary" @click="confirmSelection" :disabled="selectedFiles.length > 0 ? false : true">
+            确认选择({{ selectedFiles.length }})
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 添加编辑分类 -->
+    <el-dialog v-model="classifyDialog.dialog" :title="classifyDialog.title" width="600">
+      <el-form ref="categoryFormRef" :model="categoryForm" :rules="categoryRules" label-width="100px">
+        <!-- 分类名称 -->
+        <el-form-item label="分类名称" prop="name" required>
+          <el-input v-model="categoryForm.name" placeholder="请输入分类名称" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="classifyDialog.dialog = false">取消</el-button>
+          <el-button type="primary" @click="submitCategory" :loading="classifyDialog.loading">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 重命名对话框 -->
+    <el-dialog v-model="renameDialogVisible" title="重命名文件" width="400px" append-to-body :close-on-click-modal="false">
+      <el-form ref="renameFormRef" :model="renameForm" label-width="80px">
+        <el-form-item label="原文件名">
+          <el-input v-model="renameForm.originalName" readonly />
+        </el-form-item>
+        <el-form-item
+          label="新文件名"
+          prop="name"
+          :rules="[
+            { required: true, message: '请输入新文件名', trigger: 'blur' },
+            { min: 1, max: 100, message: '文件名长度在 1 到 100 个字符', trigger: 'blur' }
+          ]"
+        >
+          <el-input v-model="renameForm.name" placeholder="请输入新文件名" />
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="renameDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitRename">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 图片放大 -->
+    <el-image-viewer
+      :url-list="previewImageList"
+      v-if="imageViewer.show"
+      @close="imageViewer.show = false"
+      :initial-index="imageViewer.index"
+      :zoom-rate="1"
+    />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { img } from '@/utils/common';
+import { listFileInfo, delFileInfo, addFileInfo, updateDownloadCount, updateFileInfo } from '@/api/file/info';
+import { listFileCategoryTree, addFileCategory, updateFileCategory, delFileCategory } from '@/api/file/category';
+import { globalHeaders } from '@/utils/request';
+const props = defineProps({
+  type: {
+    type: String,
+    default: 'image'
+  },
+  modelValue: {
+    type: String || Array,
+    default: ''
+  },
+  width: {
+    type: String,
+    default: '100px'
+  },
+  height: {
+    type: String,
+    default: '100px'
+  },
+  imageText: {
+    type: String
+  },
+  limit: {
+    type: Number,
+    default: 1
+  }
+});
+const imagesData = ref<any>([]);
+const previewImageList = ref<any>([]);
+watch(
+  () => props.modelValue,
+  () => {
+    if (props.limit == 1) {
+      imagesData.value = [props.modelValue];
+    } else {
+      if (Array.isArray(props.modelValue)) {
+        imagesData.value = props.modelValue;
+      } else {
+        imagesData.value = props.modelValue.split(',');
+        imagesData.value = imagesData.value.filter((item: any) => item !== '');
+      }
+    }
+  },
+  { immediate: true }
+);
+
+const emit = defineEmits(['update:modelValue', 'change']);
+
+const dialogVisible = ref<any>(false);
+const viewMode = ref<any>('grid');
+// 图片列表
+const loading = ref<any>(false);
+const fileList = ref([]);
+const total = ref(0);
+const selectedFiles = ref([]);
+
+// 查询参数
+const queryParams = ref<any>({
+  pageNum: 1,
+  pageSize: 20,
+  name: null,
+  categoryId: '',
+  categoryType: 1
+});
+
+// 分类相关数据
+const filteredCategoryTree = ref<any>([]);
+const categoryTreeRef = ref<any>(null);
+const treeProps = {
+  label: 'name',
+  children: 'children'
+};
+const classifyDialog = ref<any>({
+  dialog: false,
+  title: '添加分类',
+  loading: false
+});
+// 分类表单数据
+const categoryFormRef = ref<any>(null);
+const categoryForm = ref({
+  id: null,
+  name: '',
+  code: '',
+  parentId: null,
+  type: 1,
+  sort: 1,
+  description: '',
+  status: 0
+});
+// 分类表单验证规则
+const categoryRules = ref<any>({
+  name: [
+    { required: true, message: '请输入分类名称', trigger: 'blur' },
+    { min: 2, max: 50, message: '分类名称长度在 2 到 50 个字符', trigger: 'blur' }
+  ]
+});
+
+// 重命名相关数据
+const renameDialogVisible = ref(false);
+const renameForm = ref({
+  id: null,
+  name: '',
+  originalName: '',
+  currentFile: null
+});
+const renameFormRef = ref();
+
+// 上传配置
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
+const uploadHeaders = ref(globalHeaders());
+const uploadFileList = ref([]);
+
+// 打开弹窗
+const openDialog = () => {
+  getCategoryTree();
+  getList();
+  selectedFiles.value = [];
+  dialogVisible.value = true;
+};
+
+//关闭弹窗
+const closeDialog = () => {
+  dialogVisible.value = false;
+};
+
+//确定
+const confirmSelection = () => {
+  if (props.limit == 1) {
+    emit('update:modelValue', selectedFiles.value[0].url);
+    emit('change', selectedFiles.value[0]);
+  } else {
+    const result = selectedFiles.value.map((item: any) => item.url);
+    let resultArray = [];
+    let resultString = '';
+    if (Array.isArray(props.modelValue)) {
+      resultArray = [...result, ...imagesData.value];
+      emit('update:modelValue', resultArray);
+      emit('change', resultArray);
+    } else {
+      resultString = imagesData.value + ',' + result.join(',');
+      emit('update:modelValue', resultString);
+      emit('change', resultString);
+    }
+  }
+  closeDialog();
+};
+
+// 搜索文件
+const handleSearch = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+// 获取文件列表
+const getList = async () => {
+  try {
+    loading.value = true;
+    const response = (await listFileInfo(queryParams.value)) as any;
+    const data = response?.data ?? response;
+    if (data && data.rows) {
+      fileList.value = data.rows as any[];
+      total.value = data.total || 0;
+    } else {
+      fileList.value = [];
+      total.value = 0;
+    }
+  } catch (error) {
+    console.error('获取文件列表失败:', error);
+    ElMessage.error('获取文件列表失败');
+    fileList.value = [];
+    total.value = 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleSelectionChange = (res: any) => {};
+// 切换文件选择状态
+const toggleFileSelection = (res: any) => {
+  if (props.limit == 1) {
+    selectedFiles.value = [res];
+  } else {
+    // 多选模式,实现切换逻辑
+    const index = selectedFiles.value.findIndex((item: any) => item.id === res.id);
+    // 计算当前总选择数量(已选文件 + 已上传图片)
+    const currentTotalCount = selectedFiles.value.length + imagesData.value.length;
+    if (index > -1) {
+      // 如果已选中,则取消选中(删除)
+      selectedFiles.value.splice(index, 1);
+    } else {
+      // 如果未选中,检查是否超过限制
+      if (currentTotalCount >= props.limit) {
+        ElMessage.warning(`最多只能选择 ${props.limit} 张图片`);
+        return;
+      }
+      // 未超过限制,添加选中
+      selectedFiles.value.push(res);
+    }
+  }
+};
+
+// 获取图片URL
+const getImageUrl = (file) => {
+  // 优先使用url字段
+  if (file.url) {
+    // 如果是完整的URL,直接返回
+    if (file.url.startsWith('http://') || file.url.startsWith('https://')) {
+      return file.url;
+    }
+    // 如果是相对路径,添加基础URL
+    if (file.url.startsWith('/')) {
+      return import.meta.env.VITE_APP_BASE_API + file.url;
+    }
+    return file.url;
+  }
+
+  // 备选方案:使用path字段
+  if (file.path) {
+    if (file.path.startsWith('http://') || file.path.startsWith('https://')) {
+      return file.path;
+    }
+    if (file.path.startsWith('/')) {
+      return import.meta.env.VITE_APP_BASE_API + file.path;
+    }
+    return file.path;
+  }
+
+  // 如果都没有,返回空字符串,触发错误处理
+  return '';
+};
+
+// 获取分类树
+const getCategoryTree = () => {
+  listFileCategoryTree().then((res) => {
+    if (res.code == 200) {
+      if (res.data.length > 0) {
+        res.data.forEach((item: any) => {
+          if (item.type == 1 && item.code == 'IMAGE') {
+            filteredCategoryTree.value = item.children;
+          }
+        });
+      }
+    }
+  });
+};
+
+// 添加分类
+const openClassify = (res: any, type: any) => {
+  console.log(res, 'res');
+  if (type == 'add') {
+    classifyDialog.value.title = '添加分类';
+    categoryForm.value.name = '';
+    categoryForm.value.id = null;
+    if (res.id) {
+      categoryForm.value.parentId = res.id;
+    } else {
+      categoryForm.value.parentId = 1;
+    }
+  } else {
+    classifyDialog.value.title = '编辑分类';
+    categoryForm.value.name = res.name;
+    categoryForm.value.id = res.id;
+    categoryForm.value.parentId = null;
+  }
+
+  classifyDialog.value.dialog = true;
+};
+
+// 新增编辑分类
+const submitCategory = async () => {
+  try {
+    await categoryFormRef.value?.validate();
+    classifyDialog.value.loading = true;
+
+    const categoryData = {
+      ...categoryForm.value,
+      tenantId: '000000'
+    };
+
+    if (categoryForm.value.id) {
+      await updateFileCategory(categoryData);
+    } else {
+      await addFileCategory(categoryData);
+    }
+
+    ElMessage.success(categoryForm.value.id ? '更新成功' : '添加成功');
+    classifyDialog.value.loading = false;
+    getCategoryTree();
+  } catch (error) {
+    console.error('保存分类失败:', error);
+    ElMessage.error('保存分类失败');
+  } finally {
+    classifyDialog.value.loading = false;
+    classifyDialog.value.dialog = false;
+  }
+};
+
+// 删除分类
+const closeClassify = async (data: any) => {
+  if (data.children && data.children.length > 0) {
+    ElMessage.warning('该分类下有子分类,请先删除子分类');
+    return;
+  }
+  try {
+    await ElMessageBox.confirm(`确定要删除分类"${data.name}"吗?`, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    } as any);
+    await delFileCategory(data.id);
+    ElMessage.success('删除成功');
+    getCategoryTree();
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('删除分类失败:', error);
+      ElMessage.error('删除分类失败');
+    }
+  }
+};
+
+// 树节点点击事件
+const handleTreeNodeClick = (res: any) => {
+  queryParams.value.categoryId = res.id;
+  if (res.id == '') {
+    categoryTreeRef.value.setCurrentKey(null);
+  }
+  handleSearch();
+};
+
+// 图片放大
+const imageViewer = reactive({
+  show: false,
+  index: 0
+});
+
+const previewImage = (list: any, index: any) => {
+  previewImageList.value = list;
+  imageViewer.index = index ? index : 0;
+  imageViewer.show = true;
+};
+
+/**
+ * 删除图片
+ * @param index
+ */
+const removeImage = (index?: any) => {
+  if (props.limit == 1) {
+    emit('update:modelValue', '');
+    emit('change', '');
+  } else {
+    const list = [...imagesData.value];
+    list.splice(index, 1);
+    if (Array.isArray(props.modelValue)) {
+      emit('update:modelValue', list);
+      emit('change', list);
+    } else {
+      emit('update:modelValue', list.join(','));
+      emit('change', list.join(','));
+    }
+  }
+};
+
+// 重命名文件
+const handleRename = (file: any) => {
+  renameForm.value = {
+    id: file.id,
+    name: '',
+    originalName: file.name || file.originalName || '',
+    currentFile: file // 保存完整的文件信息
+  };
+  renameDialogVisible.value = true;
+};
+
+// 提交重命名
+const submitRename = async () => {
+  try {
+    await renameFormRef.value?.validate();
+
+    if (!renameForm.value.name.trim()) {
+      ElMessage.error('请输入新文件名');
+      return;
+    }
+
+    if (renameForm.value.name === renameForm.value.originalName) {
+      ElMessage.warning('新文件名与原文件名相同');
+      return;
+    }
+
+    // 调用重命名API
+    const file = renameForm.value.currentFile;
+    await updateFileInfo({
+      id: renameForm.value.id,
+      name: renameForm.value.name.trim(),
+      originalName: file.originalName,
+      path: file.path,
+      url: file.url,
+      size: file.size,
+      type: file.type,
+      extension: file.extension,
+      categoryId: file.categoryId,
+      description: file.description
+    });
+
+    ElMessage.success('重命名成功');
+    renameDialogVisible.value = false;
+
+    // 刷新文件列表
+    handleSearch();
+
+    console.log('重命名文件:', {
+      id: renameForm.value.id,
+      oldName: renameForm.value.originalName,
+      newName: renameForm.value.name.trim()
+    });
+  } catch (error) {
+    console.error('重命名失败:', error);
+    ElMessage.error('重命名失败');
+  }
+};
+
+// 格式化文件大小
+const formatFileSize = (size) => {
+  if (!size) return '0 B';
+  const units = ['B', 'KB', 'MB', 'GB'];
+  let index = 0;
+  let fileSize = size;
+  while (fileSize >= 1024 && index < units.length - 1) {
+    fileSize /= 1024;
+    index++;
+  }
+  return fileSize.toFixed(2) + ' ' + units[index];
+};
+
+// 格式化时间
+const formatTime = (time) => {
+  if (!time) return '';
+  return new Date(time).toLocaleString();
+};
+
+// 上传前检查
+const beforeUpload = (file) => {
+  // 获取准确的MIME类型
+  const actualMimeType = getFileMimeType(file);
+  const fileName = file.name || '';
+  const extension = fileName.split('.').pop()?.toLowerCase();
+
+  let isValidType = false;
+  let fileTypeText = '';
+
+  isValidType = actualMimeType.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension);
+  fileTypeText = '图片';
+
+  if (!isValidType) {
+    ElMessage.error(`只能上传${fileTypeText}文件! 检测到的文件类型: ${actualMimeType}`);
+    return false;
+  }
+
+  // 检查文件大小 (50MB)
+  const isLtSize = file.size / 1024 / 1024 < 50;
+  if (!isLtSize) {
+    ElMessage.error('上传文件大小不能超过 50MB!');
+    return false;
+  }
+
+  // 非图片文件或非图片分类,直接上传
+  return true;
+};
+
+// 上传成功
+const onUploadSuccess = (response, file) => {
+  if (response.code === 200) {
+    // 获取文件MIME类型和扩展名
+    const mimeType = getFileMimeType(file);
+    const fileName = file.name || '';
+    const extension = fileName.split('.').pop()?.toLowerCase() || '';
+    const datas = {
+      categoryId: queryParams.value.categoryId,
+      categoryType: 1,
+      description: '',
+      downloadCount: 0,
+      extension: extension,
+      isPublic: 1,
+      name: file.name,
+      originalName: file.name,
+      ossId: response.data?.ossId || '',
+      path: response.data?.url || '',
+      size: file.size,
+      status: 0,
+      type: mimeType,
+      uploadStatus: 1,
+      url: response.data?.url || '',
+      viewCount: 0
+    };
+    addFileInfo(datas).then((res) => {
+      if (res.code == 200) {
+        ElMessage.success('文件上传成功');
+        handleSearch();
+      }
+    });
+  } else {
+    ElMessage.error('上传失败:' + response.msg);
+  }
+};
+
+// 上传失败
+const onUploadError = (error) => {
+  console.error('上传失败:', error);
+  ElMessage.error('上传失败');
+};
+
+// 获取上传文件接受类型
+const getUploadFileAccept = () => {
+  return '.jpg,.jpeg,.png,.gif,.bmp,.webp';
+};
+
+// 获取文件MIME类型
+const getFileMimeType = (file) => {
+  // 优先使用浏览器检测的MIME类型
+  if (file.type) {
+    return file.type;
+  }
+
+  // 如果浏览器无法检测,根据文件扩展名推断
+  const fileName = file.name || '';
+  const extension = fileName.split('.').pop()?.toLowerCase();
+
+  const mimeTypeMap = {
+    // 图片类型
+    'jpg': 'image/jpeg',
+    'jpeg': 'image/jpeg',
+    'png': 'image/png',
+    'gif': 'image/gif',
+    'bmp': 'image/bmp',
+    'webp': 'image/webp',
+    'svg': 'image/svg+xml',
+
+    // 视频类型
+    'mp4': 'video/mp4',
+    'avi': 'video/x-msvideo',
+    'mov': 'video/quicktime',
+    'wmv': 'video/x-ms-wmv',
+    'flv': 'video/x-flv',
+    'mkv': 'video/x-matroska',
+    'webm': 'video/webm',
+
+    // 音频类型
+    'mp3': 'audio/mpeg',
+    'wav': 'audio/wav',
+    'flac': 'audio/flac',
+    'aac': 'audio/aac',
+    'ogg': 'audio/ogg',
+    'm4a': 'audio/mp4',
+
+    // 文档类型
+    'pdf': 'application/pdf',
+    'doc': 'application/msword',
+    'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+    'xls': 'application/vnd.ms-excel',
+    'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+    'ppt': 'application/vnd.ms-powerpoint',
+    'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+    'txt': 'text/plain',
+
+    // 压缩文件
+    'zip': 'application/zip',
+    'rar': 'application/vnd.rar',
+    '7z': 'application/x-7z-compressed',
+    'tar': 'application/x-tar',
+    'gz': 'application/gzip',
+
+    // 其他常见类型
+    'json': 'application/json',
+    'xml': 'application/xml',
+    'csv': 'text/csv',
+    'html': 'text/html',
+    'css': 'text/css',
+    'js': 'application/javascript'
+  };
+
+  return mimeTypeMap[extension] || 'application/octet-stream';
+};
+
+const style = computed(() => {
+  return {
+    width: props.width,
+    height: props.height
+  };
+});
+</script>
+
+<style lang="scss" scoped>
+.image-wrap {
+  .operation {
+    display: none;
+  }
+
+  &:hover {
+    .operation {
+      display: flex;
+    }
+  }
+}
+.border-color {
+  border-color: #e5e7eb;
+}
+
+.dialog-bos {
+  height: 750px;
+  background: #f5f7fa;
+  padding: 10px;
+  border-radius: 0 0 8px 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  margin-left: 0px !important;
+  display: flex;
+  flex-direction: column;
+  .toolbar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+    padding: 16px 20px;
+    background: white;
+    .toolbar-left {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+    }
+    .toolbar-right {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+      .view-toggle {
+        display: flex;
+        gap: 0;
+      }
+    }
+  }
+  .content-wrapper {
+    display: flex;
+    gap: 20px;
+    height: 0;
+    flex: 1;
+    .sidebar {
+      width: 280px;
+      background: white;
+      border-radius: 8px;
+      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+      overflow: auto;
+      display: flex;
+      flex-direction: column;
+      height: 100%;
+      padding: 10px;
+      .category-item {
+        display: flex;
+        align-items: center;
+        padding: 0px 15px;
+        border-radius: 6px;
+        cursor: pointer;
+        margin-bottom: 8px;
+        background: #f5f7fa;
+        transition: background-color 0.3s;
+        height: 40px;
+        .category-icon {
+          margin-right: 10px;
+          font-size: 18px;
+        }
+        &.active {
+          background: #409eff;
+          color: white;
+          .more-icon {
+            color: rgba(255, 255, 255, 0.8);
+            &:hover {
+              color: white;
+              background: rgba(255, 255, 255, 0.2);
+            }
+          }
+        }
+      }
+      /* 树形控件样式 */
+      .category-tree {
+        background: transparent;
+        :deep(.el-tree-node__content) {
+          height: 40px;
+          border-radius: 6px;
+          margin-bottom: 4px;
+          background: #f5f7fa;
+          transition: background-color 0.3s;
+        }
+        :deep(.el-tree-node__content:hover) {
+          background: #ebeef5;
+        }
+        :deep(.el-tree-node.is-current > .el-tree-node__content) {
+          background: #409eff;
+          color: white;
+          .more-icon {
+            color: rgba(255, 255, 255, 0.8);
+            &:hover {
+              color: white;
+              background: rgba(255, 255, 255, 0.2);
+            }
+          }
+        }
+        :deep(.el-tree-node.is-current > .el-tree-node__content:hover) {
+          background: #66b1ff;
+        }
+      }
+      .tree-node-content {
+        display: flex;
+        align-items: center;
+        width: 100%;
+        padding: 0 5px;
+        .category-icon {
+          margin-right: 8px;
+          font-size: 16px;
+        }
+        .node-label {
+          flex: 1;
+          font-size: 14px;
+        }
+      }
+      .node-actions {
+        margin-left: auto;
+        .more-icon {
+          font-size: 16px;
+          color: #909399;
+          cursor: pointer;
+          transition: color 0.3s;
+          border-radius: 2px;
+          &:hover {
+            color: #409eff;
+            background: rgba(64, 158, 255, 0.1);
+          }
+        }
+      }
+    }
+
+    .content-area {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      height: 100%;
+      overflow: hidden;
+      .file-grid {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 15px 10px;
+        padding: 10px;
+        flex: 1;
+        overflow-y: auto;
+        min-height: 0;
+        .file-item {
+          flex: 0 0 calc((100% - 30px) / 4);
+          overflow: hidden;
+          position: relative;
+          cursor: pointer;
+          border: 1px solid #ebeef5;
+          border-radius: 8px;
+          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+          transition:
+            transform 0.3s ease,
+            box-shadow 0.3s ease;
+          height: 223px;
+          &:hover {
+            transform: translateY(-5px);
+            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+          }
+          &.selected {
+            border: 2px solid #409eff;
+            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+          }
+          .file-wrapper {
+            position: relative;
+            width: 100%;
+            height: 150px; /* Fixed height for grid view */
+            overflow: hidden;
+            .file-thumbnail {
+              width: 100%;
+              height: 100%;
+              object-fit: cover;
+              .file-error {
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                width: 100%;
+                height: 100%;
+                background: #f5f7fa;
+                color: #c0c4cc;
+              }
+
+              .file-loading {
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                width: 100%;
+                height: 100%;
+                background: #f0f9ff;
+                color: #409eff;
+              }
+            }
+            .file-checkbox {
+              position: absolute;
+              top: 8px;
+              right: 8px;
+              z-index: 10;
+            }
+          }
+          .file-info {
+            padding: 5px 5px 10px 5px;
+            background: #f5f7fa;
+            border-top: 1px solid #ebeef5;
+            border-radius: 0 0 8px 8px;
+            .file-name {
+              font-size: 14px;
+              color: #303133;
+              overflow: hidden;
+              text-overflow: ellipsis;
+              white-space: nowrap;
+              margin-bottom: 5px;
+            }
+            .file-actions {
+              display: flex;
+              justify-content: space-around;
+              gap: 8px;
+            }
+          }
+        }
+      }
+    }
+
+    .pagination {
+      display: flex;
+      justify-content: center;
+      padding: 10px;
+      background: white;
+      flex-shrink: 0;
+    }
+  }
+}
+</style>

+ 25 - 1
src/permission.ts

@@ -8,6 +8,8 @@ import { isRelogin } from '@/utils/request';
 import { useUserStore } from '@/store/modules/user';
 import { useUserStore } from '@/store/modules/user';
 import { useSettingsStore } from '@/store/modules/settings';
 import { useSettingsStore } from '@/store/modules/settings';
 import { usePermissionStore } from '@/store/modules/permission';
 import { usePermissionStore } from '@/store/modules/permission';
+import { useAppStore } from '@/store/modules/app';
+import cache from '@/plugins/cache';
 import { ElMessage } from 'element-plus/es';
 import { ElMessage } from 'element-plus/es';
 
 
 NProgress.configure({ showSpinner: false });
 NProgress.configure({ showSpinner: false });
@@ -23,11 +25,33 @@ router.beforeEach(async (to, from, next) => {
     to.meta.title && useSettingsStore().setTitle(to.meta.title as string);
     to.meta.title && useSettingsStore().setTitle(to.meta.title as string);
     /* has token*/
     /* has token*/
     if (to.path === '/login') {
     if (to.path === '/login') {
-      next({ path: '/' });
+      // 登录后检查是否已选择项目
+      const currentProjectId = cache.local.get('currentProjectId');
+      if (currentProjectId) {
+        next({ path: '/index' });
+      } else {
+        next({ path: '/project' });
+      }
       NProgress.done();
       NProgress.done();
     } else if (isWhiteList(to.path)) {
     } else if (isWhiteList(to.path)) {
       next();
       next();
     } else {
     } else {
+      // 根据路由控制侧边栏显示
+      const appStore = useAppStore();
+      if (to.path === '/project') {
+        appStore.toggleSideBarHide(true);
+      } else {
+        // 非项目选择页面,检查是否已选择项目
+        const currentProjectId = cache.local.get('currentProjectId');
+        if (!currentProjectId && to.path !== '/project') {
+          // 未选择项目,重定向到项目选择页面
+          next({ path: '/project' });
+          NProgress.done();
+          return;
+        }
+        appStore.toggleSideBarHide(false);
+      }
+      
       if (useUserStore().roles.length === 0) {
       if (useUserStore().roles.length === 0) {
         isRelogin.show = true;
         isRelogin.show = true;
         // 判断当前用户是否已拉取完user_info信息
         // 判断当前用户是否已拉取完user_info信息

+ 15 - 2
src/router/index.ts

@@ -65,13 +65,20 @@ export const constantRoutes: RouteRecordRaw[] = [
   {
   {
     path: '',
     path: '',
     component: Layout,
     component: Layout,
-    redirect: '/index',
+    redirect: '/project',
+    meta: { title: '首页' },
     children: [
     children: [
+      {
+        path: '/project',
+        component: () => import('@/views/external/item/index.vue'),
+        name: 'ProjectSelect',
+        meta: { title: '项目选择', icon: 'dashboard', affix: true }
+      },
       {
       {
         path: '/index',
         path: '/index',
         component: () => import('@/views/index.vue'),
         component: () => import('@/views/index.vue'),
         name: 'Index',
         name: 'Index',
-        meta: { title: '首页', icon: 'dashboard', affix: true }
+        meta: { title: '统计', icon: 'dashboard', affix: true }
       }
       }
     ]
     ]
   },
   },
@@ -100,6 +107,12 @@ export const constantRoutes: RouteRecordRaw[] = [
         component: () => import('@/views/product/base/detail.vue'),
         component: () => import('@/views/product/base/detail.vue'),
         name: 'ProductBaseDetail',
         name: 'ProductBaseDetail',
         meta: { title: '产品详情', activeMenu: '/product/base' }
         meta: { title: '产品详情', activeMenu: '/product/base' }
+      },
+      {
+        path: 'brand/edit',
+        component: () => import('@/views/product/brand/edit.vue'),
+        name: 'BrandEdit',
+        meta: { title: '品牌编辑', activeMenu: '/product/brand' }
       }
       }
     ]
     ]
   }
   }

+ 51 - 0
src/utils/common.ts

@@ -0,0 +1,51 @@
+/**
+ * 判断是否是url
+ * @param str
+ * @returns
+ */
+export function isUrl(str: string): boolean {
+  return str.indexOf('http://') != -1 || str.indexOf('https://') != -1;
+}
+
+const isArray = (value: any) => {
+  if (typeof Array.isArray === 'function') {
+    return Array.isArray(value);
+  }
+  return Object.prototype.toString.call(value) === '[object Array]';
+};
+
+/**
+ * 图片输出
+ * @param path
+ * @returns
+ */
+export function img(path: string): string {
+  let imgDomain = import.meta.env.VITE_IMG_DOMAIN || location.origin;
+
+  if (typeof path == 'string' && path.startsWith('/')) path = path.replace(/^\//, '');
+  if (typeof imgDomain == 'string' && imgDomain.endsWith('/')) imgDomain = imgDomain.slice(0, -1);
+  if (path) {
+    return isUrl(path) ? path : `${imgDomain}/${path}`;
+  }
+}
+
+/**
+ * @description 深度克隆
+ * @param {object} obj 需要深度克隆的对象
+ * @returns {*} 克隆后的对象或者原值(不是对象)
+ */
+export function deepClone(obj: any) {
+  // 对常见的“非”值,直接返回原来值
+  if ([null, undefined, NaN, false].includes(obj)) return obj;
+  if (typeof obj !== 'object' && typeof obj !== 'function') {
+    // 原始类型直接返回
+    return obj;
+  }
+  const o = isArray(obj) ? [] : {};
+  for (const i in obj) {
+    if (obj.hasOwnProperty(i)) {
+      o[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i];
+    }
+  }
+  return o;
+}

+ 731 - 0
src/views/ProjectList.vue

@@ -0,0 +1,731 @@
+<template>
+  <div class="project-page">
+    <!-- 顶部导航栏 -->
+    <header class="navbar">
+      <div class="nav-left">
+        <el-icon class="logo-icon" :size="24"><Platform /></el-icon>
+        <span class="system-name">API对接项目中心</span>
+      </div>
+      <nav class="nav-menu">
+        <a href="#" class="nav-item active">项目管理</a>
+      </nav>
+      <div class="nav-right">
+        <el-dropdown>
+          <div class="user-info">
+            <el-avatar :size="32" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
+            <span class="username">张三</span>
+            <el-icon><CaretBottom /></el-icon>
+          </div>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item>个人中心</el-dropdown-item>
+              <el-dropdown-item divided>退出登录</el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </div>
+    </header>
+
+    <!-- 主体内容 -->
+    <main class="content-container">
+      <div class="page-header">
+        <div class="stats-info">
+          共计项目数 <span class="count-highlight">{{ totalProjects }}</span> 个
+        </div>
+        <div class="header-actions">
+          <el-input
+            v-model="searchQuery"
+            placeholder="搜索项目名称..."
+            class="search-input"
+            clearable
+          >
+            <template #prefix>
+              <el-icon><Search /></el-icon>
+            </template>
+          </el-input>
+          <el-button type="primary" icon="Plus" @click="handleAdd">新建项目</el-button>
+        </div>
+      </div>
+
+      <!-- 项目卡片网格 -->
+      <div v-if="pagedProjects.length > 0" class="project-grid-wrapper">
+        <el-row :gutter="24" class="project-grid">
+          <el-col 
+            v-for="item in pagedProjects" 
+            :key="item.id" 
+            :xs="24" :sm="12" :md="12" :lg="8" :xl="6"
+            class="grid-item"
+          >
+            <div 
+              class="project-card" 
+              :class="{ 'is-selected': selectedId === item.id }"
+              @click="selectProject(item.id)"
+              @dblclick="enterProject"
+            >
+              <!-- 选中标记 -->
+              <div v-if="selectedId === item.id" class="selected-badge">
+                <el-icon><Check /></el-icon>
+              </div>
+
+              <!-- 卡片内容 -->
+              <div class="card-body">
+                <div class="project-logo">
+                  <el-image :src="item.logo" fit="cover">
+                    <template #error>
+                      <div class="image-placeholder">
+                        <el-icon :size="40"><OfficeBuilding /></el-icon>
+                      </div>
+                    </template>
+                  </el-image>
+                </div>
+                
+                <el-tooltip
+                  effect="dark"
+                  :content="item.name"
+                  placement="top"
+                  :show-after="500"
+                >
+                  <h3 class="project-name">{{ item.name }}</h3>
+                </el-tooltip>
+                
+                <div class="stats-grid">
+                  <div class="stat-item">
+                    <span class="label">商品数</span>
+                    <span class="value">{{ item.productCount }}</span>
+                  </div>
+                  <div class="stat-item">
+                    <span class="label">订单数</span>
+                    <span class="value">{{ item.orderCount }}</span>
+                  </div>
+                  <div class="stat-item success">
+                    <span class="label">已完成</span>
+                    <span class="value">{{ item.completedCount }}</span>
+                  </div>
+                  <div class="stat-item warning">
+                    <span class="label">待付款</span>
+                    <span class="value">{{ item.unpaidCount }}</span>
+                  </div>
+                  <div class="stat-item info">
+                    <span class="label">待发货</span>
+                    <span class="value">{{ item.unshippedCount }}</span>
+                  </div>
+                  <div class="stat-item danger">
+                    <span class="label">退款订单</span>
+                    <span class="value">{{ item.refundCount }}</span>
+                  </div>
+                  <div class="stat-item primary">
+                    <span class="label">订单金额</span>
+                    <span class="value">¥{{ item.orderAmount.toLocaleString() }}</span>
+                  </div>
+                  <div class="stat-item danger">
+                    <span class="label">退款金额</span>
+                    <span class="value">¥{{ item.refundAmount.toLocaleString() }}</span>
+                  </div>
+                </div>
+              </div>
+
+              <div class="card-footer">
+                <el-button link type="primary" icon="Edit" @click.stop="handleEdit(item)">编辑</el-button>
+                <el-button link type="danger" icon="Delete" @click.stop="handleDelete(item)">删除</el-button>
+                <el-button link type="primary" icon="Right" @click.stop="enterProject">进入项目</el-button>
+              </div>
+            </div>
+          </el-col>
+        </el-row>
+      </div>
+
+      <!-- 空状态 -->
+      <div v-else class="empty-container">
+        <el-empty 
+          description="暂无相关项目信息" 
+          :image-size="200"
+        >
+          <template #extra>
+            <el-button type="primary" plain icon="Refresh" @click="searchQuery = ''">清除搜索</el-button>
+          </template>
+        </el-empty>
+      </div>
+
+      <!-- 分页栏 -->
+      <div class="pagination-wrapper">
+        <el-pagination
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :page-sizes="[12, 24, 48]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="totalProjects"
+          background
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </main>
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog
+      v-model="dialogVisible"
+      :title="dialogTitle"
+      width="550px"
+      destroy-on-close
+      class="project-dialog"
+    >
+      <el-form
+        ref="formRef"
+        :model="form"
+        :rules="rules"
+        label-width="100px"
+        label-position="right"
+        style="padding: 20px 40px 0 20px"
+      >
+        <el-form-item label="项目名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入项目名称" />
+        </el-form-item>
+        <el-form-item label="Logo 地址" prop="logo">
+          <el-input v-model="form.logo" placeholder="请输入 Logo 图片 URL" />
+        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="商品数" prop="productCount">
+              <el-input-number v-model="form.productCount" :min="0" controls-position="right" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="订单数" prop="orderCount">
+              <el-input-number v-model="form.orderCount" :min="0" controls-position="right" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="待付款" prop="unpaidCount" label-width="70px">
+              <el-input-number v-model="form.unpaidCount" :min="0" controls-position="right" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="待发货" prop="unshippedCount" label-width="70px">
+              <el-input-number v-model="form.unshippedCount" :min="0" controls-position="right" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="已完成" prop="completedCount" label-width="70px">
+              <el-input-number v-model="form.completedCount" :min="0" controls-position="right" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="退款订单" prop="refundCount">
+              <el-input-number v-model="form.refundCount" :min="0" controls-position="right" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="退款金额" prop="refundAmount">
+              <el-input-number v-model="form.refundAmount" :min="0" :precision="2" controls-position="right" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="项目金额" prop="orderAmount">
+          <el-input-number v-model="form.orderAmount" :min="0" :precision="2" style="width: 100%" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitForm">确定并保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, reactive } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+// 模拟数据接口
+interface Project {
+  id: number
+  name: string
+  logo: string
+  productCount: number
+  orderCount: number
+  orderAmount: number
+  refundCount: number
+  refundAmount: number
+  unpaidCount: number
+  unshippedCount: number
+  completedCount: number
+}
+
+// 按钮点击事件
+const handleAdd = () => {
+  resetForm()
+  dialogTitle.value = '新建项目'
+  dialogVisible.value = true
+}
+
+const handleEdit = (item: Project) => {
+  dialogTitle.value = '编辑项目'
+  Object.assign(form, item)
+  dialogVisible.value = true
+}
+
+const handleDelete = (item: Project) => {
+  ElMessageBox.confirm(
+    `您确定要删除项目 "${item.name}" 吗?此操作不可恢复。`,
+    '系统提示',
+    {
+      confirmButtonText: '确定删除',
+      cancelButtonText: '取消',
+      type: 'warning',
+      buttonSize: 'default'
+    }
+  ).then(() => {
+    allProjects.value = allProjects.value.filter(p => p.id !== item.id)
+    ElMessage.success('项目已成功删除')
+  }).catch(() => {})
+}
+
+// 表单逻辑
+const dialogVisible = ref(false)
+const dialogTitle = ref('新建项目')
+const formRef = ref()
+const form = reactive({
+  id: 0,
+  name: '',
+  logo: '',
+  productCount: 0,
+  orderCount: 0,
+  orderAmount: 0,
+  refundCount: 0,
+  refundAmount: 0,
+  unpaidCount: 0,
+  unshippedCount: 0,
+  completedCount: 0
+})
+
+const rules = {
+  name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
+  logo: [{ required: true, message: '请输入 Logo 地址', trigger: 'blur' }]
+}
+
+const resetForm = () => {
+  form.id = 0
+  form.name = ''
+  form.logo = ''
+  form.productCount = 0
+  form.orderCount = 0
+  form.orderAmount = 0
+  form.refundCount = 0
+  form.refundAmount = 0
+  form.unpaidCount = 0
+  form.unshippedCount = 0
+  form.completedCount = 0
+}
+
+const submitForm = () => {
+  formRef.value.validate((valid: boolean) => {
+    if (valid) {
+      if (form.id === 0) {
+        // 新增
+        const newProject = {
+          ...form,
+          id: Math.max(...allProjects.value.map(p => p.id)) + 1
+        }
+        allProjects.value.unshift(newProject)
+        ElMessage.success('项目创建成功')
+      } else {
+        // 修改
+        const index = allProjects.value.findIndex(p => p.id === form.id)
+        if (index !== -1) {
+          allProjects.value[index] = { ...form }
+          ElMessage.success('项目修改已保存')
+        }
+      }
+      dialogVisible.value = false
+    }
+  })
+}
+
+// 模拟数据生成
+const generateData = () => {
+  const data: Project[] = []
+  const companies = ['武汉钢铁集团有限公司', '中建三局第一建设工程有限公司', '长飞光纤光缆股份有限公司', '东风汽车集团有限公司', '人福医药集团股份公司', '斗鱼网络科技有限公司']
+  
+  for (let i = 1; i <= 36; i++) {
+    const baseCompany = companies[i % companies.length]
+    const orderCount = Math.floor(Math.random() * 500) + 50
+    data.push({
+      id: i,
+      name: `${baseCompany} - 项目部 ${String.fromCharCode(64 + (i % 26 + 1))}`,
+      logo: `https://picsum.photos/seed/${i + 100}/120/120`,
+      productCount: Math.floor(Math.random() * 100) + 10,
+      orderCount: orderCount,
+      orderAmount: Math.floor(Math.random() * 100000) + 10000,
+      refundCount: Math.floor(Math.random() * 10),
+      refundAmount: Math.floor(Math.random() * 5000),
+      unpaidCount: Math.floor(orderCount * 0.2),
+      unshippedCount: Math.floor(orderCount * 0.3),
+      completedCount: Math.floor(orderCount * 0.4)
+    })
+  }
+  return data
+}
+
+const allProjects = ref<Project[]>(generateData())
+const selectedId = ref<number | null>(null)
+const searchQuery = ref('')
+const currentPage = ref(1)
+const pageSize = ref(12)
+
+const totalProjects = computed(() => {
+  return allProjects.value.filter(p => p.name.includes(searchQuery.value)).length
+})
+
+const pagedProjects = computed(() => {
+  const start = (currentPage.value - 1) * pageSize.value
+  const end = start + pageSize.value
+  return allProjects.value
+    .filter(p => p.name.includes(searchQuery.value))
+    .slice(start, end)
+})
+
+const selectProject = (id: number) => {
+  selectedId.value = id
+}
+
+const enterProject = () => {
+  window.open('https://www.baidu.com', '_blank')
+}
+
+const handleSizeChange = (val: number) => {
+  pageSize.value = val
+}
+
+const handleCurrentChange = (val: number) => {
+  currentPage.value = val
+}
+</script>
+
+<style scoped lang="scss">
+.project-page {
+  min-height: 100vh;
+  background-color: #f5f7fa;
+}
+
+/* 顶部导航 */
+.navbar {
+  height: 60px;
+  background: #ffffff;
+  display: flex;
+  align-items: center;
+  padding: 0 40px;
+  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+  position: sticky;
+  top: 0;
+  z-index: 100;
+
+  .nav-left {
+    display: flex;
+    align-items: center;
+    margin-right: 60px;
+
+    .logo-icon {
+      color: #1890ff;
+      margin-right: 12px;
+    }
+
+    .system-name {
+      font-size: 18px;
+      font-weight: 600;
+      color: #333;
+    }
+  }
+
+  .nav-menu {
+    flex: 1;
+    display: flex;
+    gap: 32px;
+
+    .nav-item {
+      text-decoration: none;
+      color: #666;
+      font-weight: 500;
+      font-size: 15px;
+      padding: 4px 0;
+      position: relative;
+      transition: color 0.3s;
+
+      &:hover {
+        color: #1890ff;
+      }
+
+      &.active {
+        color: #1890ff;
+        
+        &::after {
+          content: '';
+          position: absolute;
+          bottom: -15px;
+          left: 0;
+          width: 100%;
+          height: 3px;
+          background: #1890ff;
+          border-radius: 2px;
+        }
+      }
+    }
+  }
+
+  .nav-right {
+    .user-info {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      cursor: pointer;
+      padding: 4px 8px;
+      border-radius: 4px;
+      transition: background 0.3s;
+
+      &:hover {
+        background: #f0f2f5;
+      }
+
+      .username {
+        font-size: 14px;
+        color: #333;
+      }
+    }
+  }
+}
+
+/* 内容区域 */
+.content-container {
+  max-width: 1400px;
+  margin: 0 auto;
+  padding: 24px 40px;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24px;
+
+  .stats-info {
+    font-size: 16px;
+    color: #606266;
+
+    .count-highlight {
+      font-weight: bold;
+      color: #1890ff;
+      font-size: 18px;
+      margin: 0 4px;
+    }
+  }
+
+  .header-actions {
+    display: flex;
+    gap: 16px;
+
+    .search-input {
+      width: 240px;
+    }
+  }
+}
+
+/* 卡片网格 */
+.project-grid {
+  margin-top: 20px;
+}
+
+.grid-item {
+  margin-bottom: 24px;
+}
+
+/* 项目卡片样式 */
+.project-card {
+  background: #ffffff;
+  border-radius: 12px;
+  border: 1px solid #e4e7ed;
+  transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+
+  &:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
+    border-color: #1890ff;
+  }
+
+  &.is-selected {
+    border-color: #1890ff;
+    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
+    background-color: #f0f7ff;
+  }
+
+  .selected-badge {
+    position: absolute;
+    top: 0;
+    right: 0;
+    width: 0;
+    height: 0;
+    border-style: solid;
+    border-width: 0 32px 32px 0;
+    border-color: transparent #1890ff transparent transparent;
+    z-index: 10;
+
+    .el-icon {
+      position: absolute;
+      top: 4px;
+      right: -28px;
+      color: #ffffff;
+      font-size: 14px;
+      font-weight: bold;
+    }
+  }
+
+  .card-body {
+    padding: 24px;
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+
+    .project-logo {
+      width: 80px;
+      height: 80px;
+      margin-bottom: 16px;
+      border-radius: 50%;
+      overflow: hidden;
+      background: #f5f7fa;
+      border: 2px solid #fff;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.05);
+
+      .el-image {
+        width: 100%;
+        height: 100%;
+      }
+
+      .image-placeholder {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: #909399;
+      }
+    }
+
+    .project-name {
+      margin: 0 0 20px 0;
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+      text-align: center;
+      display: -webkit-box;
+      -webkit-line-clamp: 1;
+      -webkit-box-orient: vertical;
+      overflow: hidden;
+    }
+
+    .stats-grid {
+      width: 100%;
+      display: grid;
+      grid-template-columns: repeat(3, 1fr);
+      gap: 12px;
+
+      .stat-item {
+        display: flex;
+        flex-direction: column;
+        
+        .label {
+          font-size: 11px;
+          color: #909399;
+          margin-bottom: 2px;
+        }
+
+        .value {
+          font-size: 13px;
+          color: #303133;
+          font-weight: 500;
+        }
+
+        &.primary .value { color: #1890ff; font-weight: bold; }
+        &.danger .value { color: #f56c6c; }
+        &.success .value { color: #67c23a; }
+        &.warning .value { color: #e6a23c; }
+        &.info .value { color: #909399; }
+      }
+
+      /* 最后两个金额占满一行或调整比例 */
+      .stat-item:nth-last-child(2),
+      .stat-item:last-child {
+        grid-column: span 3;
+        border-top: 1px dashed #ebeef5;
+        padding-top: 8px;
+        margin-top: 4px;
+        flex-direction: row;
+        justify-content: space-between;
+        align-items: center;
+
+        .label { margin-bottom: 0; font-size: 12px; }
+        .value { font-size: 15px; }
+      }
+    }
+  }
+
+  .card-footer {
+    padding: 12px 16px;
+    border-top: 1px solid #ebeef5;
+    background: #fafafa;
+    display: flex;
+    justify-content: space-around;
+    
+    .el-button {
+      font-size: 13px;
+    }
+  }
+}
+
+.empty-container {
+  padding: 100px 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: #fff;
+  border-radius: 12px;
+  margin-top: 20px;
+}
+
+.pagination-wrapper {
+  margin-top: 40px;
+  display: flex;
+  justify-content: center;
+}
+
+/* 响应式适配 */
+@media (max-width: 768px) {
+  .navbar {
+    padding: 0 20px;
+    .nav-menu { display: none; }
+    .nav-left { margin-right: 0; flex: 1; }
+  }
+  .content-container {
+    padding: 16px 20px;
+  }
+  .page-header {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 16px;
+    .header-actions {
+      width: 100%;
+      .search-input { flex: 1; }
+    }
+  }
+}
+</style>

+ 487 - 93
src/views/external/item/index.vue

@@ -1,66 +1,172 @@
 <template>
 <template>
-  <div class="p-2">
-    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
-      <div v-show="showSearch" class="mb-[10px]">
-        <el-card shadow="hover">
-          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="项目名" prop="itemName">
-              <el-input v-model="queryParams.itemName" placeholder="请输入项目名" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="项目key" prop="itemKey">
-              <el-input v-model="queryParams.itemKey" placeholder="请输入项目key" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item>
-              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-            </el-form-item>
-          </el-form>
-        </el-card>
+  <div class="project-page">
+    <!-- 主体内容 -->
+    <main class="content-container">
+      <div class="page-header">
+        <div class="stats-info">
+          共计项目数 <span class="count-highlight">{{ total }}</span> 个
+        </div>
+        <div class="header-actions">
+          <el-input
+            v-model="queryParams.itemName"
+            placeholder="搜索项目名称..."
+            class="search-input"
+            clearable
+            @keyup.enter="handleQuery"
+            @clear="handleQuery"
+          >
+            <template #prefix>
+              <el-icon><Search /></el-icon>
+            </template>
+          </el-input>
+          <el-button type="primary" icon="Plus" @click="handleAdd">新建项目</el-button>
+        </div>
       </div>
       </div>
-    </transition>
 
 
-    <el-card shadow="never">
-      <template #header>
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">删除</el-button>
+      <!-- 项目卡片网格 -->
+      <div v-loading="loading" class="project-grid-wrapper">
+        <el-row v-if="itemList.length > 0" :gutter="24" class="project-grid">
+          <el-col 
+            v-for="item in itemList" 
+            :key="item.id" 
+            :xs="24" :sm="12" :md="12" :lg="8" :xl="6"
+            class="grid-item"
+          >
+            <div 
+              class="project-card" 
+              :class="{ 'is-selected': selectedId === item.id }"
+              @click="selectProject(item.id)"
+              @dblclick="handleEnterProject(item)"
+            >
+              <!-- 选中标记 -->
+              <div v-if="selectedId === item.id" class="selected-badge">
+                <el-icon><Check /></el-icon>
+              </div>
+
+              <!-- 状态标记 -->
+              <div class="status-badge" :class="item.status === '1' ? 'is-disabled' : 'is-enabled'">
+                {{ item.status === '1' ? '已关闭' : '运行中' }}
+              </div>
+
+              <!-- 卡片内容 -->
+              <div class="card-body">
+                <div class="project-logo">
+                  <el-image :src="getLogoUrl(item.logo)" fit="cover">
+                    <template #error>
+                      <div class="image-placeholder">
+                        <el-icon :size="40"><OfficeBuilding /></el-icon>
+                      </div>
+                    </template>
+                  </el-image>
+                </div>
+                
+                <el-tooltip
+                  effect="dark"
+                  :content="item.itemName"
+                  placement="top"
+                  :show-after="500"
+                >
+                  <h3 class="project-name">{{ item.itemName }}</h3>
+                </el-tooltip>
+                
+                <div class="stats-grid">
+                  <div class="stat-item">
+                    <span class="label">商品数</span>
+                    <span class="value">{{ item.productCount ?? 0 }}</span>
+                  </div>
+                  <div class="stat-item">
+                    <span class="label">订单数</span>
+                    <span class="value">{{ item.orderCount ?? 0 }}</span>
+                  </div>
+                  <div class="stat-item success">
+                    <span class="label">已完成</span>
+                    <span class="value">{{ item.completedCount ?? 0 }}</span>
+                  </div>
+                  <div class="stat-item warning">
+                    <span class="label">待付款</span>
+                    <span class="value">{{ item.waitPayCount ?? 0 }}</span>
+                  </div>
+                  <div class="stat-item info">
+                    <span class="label">待发货</span>
+                    <span class="value">{{ item.waitDeliverCount ?? 0 }}</span>
+                  </div>
+                  <div class="stat-item danger">
+                    <span class="label">售后订单</span>
+                    <span class="value">{{ item.refundCount ?? 0 }}</span>
+                  </div>
+                  <div class="stat-item primary">
+                    <span class="label">订单金额</span>
+                    <span class="value">¥{{ (item.orderAmount ?? 0).toLocaleString() }}</span>
+                  </div>
+                  <div class="stat-item danger">
+                    <span class="label">售后金额</span>
+                    <span class="value">¥{{ (item.refundAmount ?? 0).toLocaleString() }}</span>
+                  </div>
+                </div>
+              </div>
+
+              <div class="card-footer">
+                <el-button link type="primary" icon="Edit" @click.stop="handleUpdate(item)">编辑</el-button>
+                <el-button
+                  link
+                  :type="item.status === '0' ? 'warning' : 'success'"
+                  :icon="item.status === '0' ? 'VideoStop' : 'VideoPlay'"
+                  @click.stop="handleChangeStatus(item)"
+                >{{ item.status === '0' ? '关闭' : '开启' }}</el-button>
+                <el-button link type="success" icon="Right" @click.stop="handleEnterProject(item)">进入项目</el-button>
+              </div>
+            </div>
           </el-col>
           </el-col>
-          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
         </el-row>
         </el-row>
-      </template>
 
 
-      <el-table v-loading="loading" border :data="itemList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="项目id" align="center" prop="id" v-if="true" />
-        <el-table-column label="项目名" align="center" prop="itemName" />
-        <el-table-column label="项目key" align="center" prop="itemKey" />
-        <el-table-column label="项目用户名" align="center" prop="userName" />
-        <el-table-column label="项目密码" align="center" prop="password" />
-        <el-table-column label="项目url" align="center" prop="url" />
-        <el-table-column label="备注" align="center" prop="remark" />
-        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-          <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
-            </el-tooltip>
-            <el-tooltip content="删除" placement="top">
-              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
-            </el-tooltip>
-          </template>
-        </el-table-column>
-      </el-table>
-
-      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
-    </el-card>
+        <!-- 空状态 -->
+        <div v-else-if="!loading" class="empty-container">
+          <el-empty 
+            description="暂无相关项目信息" 
+            :image-size="200"
+          >
+            <template #extra>
+              <el-button type="primary" plain icon="Refresh" @click="resetQuery">清除搜索</el-button>
+            </template>
+          </el-empty>
+        </div>
+      </div>
+
+      <!-- 分页栏 -->
+      <div class="pagination-wrapper">
+        <el-pagination
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
+          :page-sizes="[12, 24, 48]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          background
+          @size-change="getList"
+          @current-change="getList"
+        />
+      </div>
+    </main>
+
     <!-- 添加或修改第三方对接项目管理对话框 -->
     <!-- 添加或修改第三方对接项目管理对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
-      <el-form ref="itemFormRef" :model="form" :rules="rules" label-width="80px">
+    <el-dialog 
+      :title="dialog.title" 
+      v-model="dialog.visible" 
+      width="550px" 
+      append-to-body
+      destroy-on-close
+      class="project-dialog"
+    >
+      <el-form 
+        ref="itemFormRef" 
+        :model="form" 
+        :rules="rules" 
+        label-width="100px"
+        label-position="right"
+        style="padding: 20px 40px 0 20px"
+      >
+        <el-form-item label="Logo地址" prop="logo">
+          <FileUpload v-model="form.logo" :limit="1" :file-type="['png', 'jpg', 'jpeg', 'gif', 'webp']" />
+        </el-form-item>
         <el-form-item label="项目名" prop="itemName">
         <el-form-item label="项目名" prop="itemName">
           <el-input v-model="form.itemName" placeholder="请输入项目名" />
           <el-input v-model="form.itemName" placeholder="请输入项目名" />
         </el-form-item>
         </el-form-item>
@@ -81,31 +187,33 @@
         </el-form-item>
         </el-form-item>
       </el-form>
       </el-form>
       <template #footer>
       <template #footer>
-        <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
-        </div>
+        <span class="dialog-footer">
+          <el-button @click="cancel">取消</el-button>
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确定并保存</el-button>
+        </span>
       </template>
       </template>
     </el-dialog>
     </el-dialog>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup name="Item" lang="ts">
 <script setup name="Item" lang="ts">
-import { listItem, getItem, delItem, addItem, updateItem } from '@/api/external/item';
+import { listItem, getItem, addItem, updateItem, changeItemStatus } from '@/api/external/item';
 import { ItemVO, ItemQuery, ItemForm } from '@/api/external/item/types';
 import { ItemVO, ItemQuery, ItemForm } from '@/api/external/item/types';
+import { useAppStore } from '@/store/modules/app';
+import cache from '@/plugins/cache';
+import { listByIds } from '@/api/system/oss';
 
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const router = useRouter();
+const appStore = useAppStore();
 
 
 const itemList = ref<ItemVO[]>([]);
 const itemList = ref<ItemVO[]>([]);
 const buttonLoading = ref(false);
 const buttonLoading = ref(false);
 const loading = ref(true);
 const loading = ref(true);
-const showSearch = ref(true);
-const ids = ref<Array<string | number>>([]);
-const single = ref(true);
-const multiple = ref(true);
 const total = ref(0);
 const total = ref(0);
+const selectedId = ref<string | number | null>(null);
+const logoUrlMap = ref<Record<string, string>>({});
 
 
-const queryFormRef = ref<ElFormInstance>();
 const itemFormRef = ref<ElFormInstance>();
 const itemFormRef = ref<ElFormInstance>();
 
 
 const dialog = reactive<DialogOption>({
 const dialog = reactive<DialogOption>({
@@ -115,6 +223,7 @@ const dialog = reactive<DialogOption>({
 
 
 const initFormData: ItemForm = {
 const initFormData: ItemForm = {
   id: undefined,
   id: undefined,
+  logo: '',
   itemName: undefined,
   itemName: undefined,
   itemKey: undefined,
   itemKey: undefined,
   userName: undefined,
   userName: undefined,
@@ -127,7 +236,7 @@ const data = reactive<PageData<ItemForm, ItemQuery>>({
   form: {...initFormData},
   form: {...initFormData},
   queryParams: {
   queryParams: {
     pageNum: 1,
     pageNum: 1,
-    pageSize: 10,
+    pageSize: 12,
     itemName: undefined,
     itemName: undefined,
     itemKey: undefined,
     itemKey: undefined,
     userName: undefined,
     userName: undefined,
@@ -154,12 +263,6 @@ const data = reactive<PageData<ItemForm, ItemQuery>>({
     url: [
     url: [
       { required: true, message: "项目url不能为空", trigger: "blur" }
       { required: true, message: "项目url不能为空", trigger: "blur" }
     ],
     ],
-    status: [
-      { required: true, message: "状态不能为空", trigger: "change" }
-    ],
-    remark: [
-      { required: true, message: "备注不能为空", trigger: "blur" }
-    ],
   }
   }
 });
 });
 
 
@@ -172,6 +275,24 @@ const getList = async () => {
   itemList.value = res.rows;
   itemList.value = res.rows;
   total.value = res.total;
   total.value = res.total;
   loading.value = false;
   loading.value = false;
+  // 解析 logo ossId 为 URL
+  const ossIds = itemList.value
+    .filter(item => item.logo && !item.logo.startsWith('http'))
+    .map(item => item.logo);
+  if (ossIds.length > 0) {
+    const uniqueIds = [...new Set(ossIds)].join(',');
+    const ossRes = await listByIds(uniqueIds);
+    ossRes.data.forEach((oss: any) => {
+      logoUrlMap.value[oss.ossId] = oss.url;
+    });
+  }
+}
+
+/** 获取 logo 显示 URL */
+const getLogoUrl = (logo: string) => {
+  if (!logo) return '';
+  if (logo.startsWith('http')) return logo;
+  return logoUrlMap.value[logo] || '';
 }
 }
 
 
 /** 取消按钮 */
 /** 取消按钮 */
@@ -194,32 +315,31 @@ const handleQuery = () => {
 
 
 /** 重置按钮操作 */
 /** 重置按钮操作 */
 const resetQuery = () => {
 const resetQuery = () => {
-  queryFormRef.value?.resetFields();
+  queryParams.value.itemName = undefined;
   handleQuery();
   handleQuery();
 }
 }
 
 
-/** 多选框选中数据 */
-const handleSelectionChange = (selection: ItemVO[]) => {
-  ids.value = selection.map(item => item.id);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
+/** 选中项目 */
+const selectProject = (id: string | number) => {
+  selectedId.value = id;
 }
 }
 
 
 /** 新增按钮操作 */
 /** 新增按钮操作 */
 const handleAdd = () => {
 const handleAdd = () => {
   reset();
   reset();
   dialog.visible = true;
   dialog.visible = true;
-  dialog.title = "添加第三方对接项目管理";
+  dialog.title = "新建项目";
 }
 }
 
 
 /** 修改按钮操作 */
 /** 修改按钮操作 */
 const handleUpdate = async (row?: ItemVO) => {
 const handleUpdate = async (row?: ItemVO) => {
   reset();
   reset();
-  const _id = row?.id || ids.value[0]
+  const _id = row?.id
+  if (!_id) return;
   const res = await getItem(_id);
   const res = await getItem(_id);
   Object.assign(form.value, res.data);
   Object.assign(form.value, res.data);
   dialog.visible = true;
   dialog.visible = true;
-  dialog.title = "修改第三方对接项目管理";
+  dialog.title = "编辑项目";
 }
 }
 
 
 /** 提交按钮 */
 /** 提交按钮 */
@@ -239,23 +359,297 @@ const submitForm = () => {
   });
   });
 }
 }
 
 
-/** 删除按钮操作 */
-const handleDelete = async (row?: ItemVO) => {
-  const _ids = row?.id || ids.value;
-  await proxy?.$modal.confirm('是否确认删除第三方对接项目管理编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
-  await delItem(_ids);
-  proxy?.$modal.msgSuccess("删除成功");
+/** 开启/关闭项目 */
+const handleChangeStatus = async (row?: ItemVO) => {
+  if (!row?.id) return;
+  const isEnabled = row.status === '0';
+  const actionText = isEnabled ? '关闭' : '开启';
+  await proxy?.$modal.confirm(`您确定要${actionText}项目 "${row.itemName}" 吗?`);
+  const newStatus = isEnabled ? '1' : '0';
+  await changeItemStatus(row.id, newStatus);
+  proxy?.$modal.msgSuccess(`${actionText}成功`);
   await getList();
   await getList();
 }
 }
 
 
-/** 导出按钮操作 */
-const handleExport = () => {
-  proxy?.download('external/item/export', {
-    ...queryParams.value
-  }, `item_${new Date().getTime()}.xlsx`)
+/** 进入项目 */
+const handleEnterProject = (row: ItemVO) => {
+  if (!row?.id) return;
+  // 存储项目ID和项目信息到本地缓存
+  cache.local.setJSON('currentProjectId', row.id);
+  cache.local.setJSON('currentProject', {
+    id: row.id,
+    itemName: row.itemName,
+    itemKey: row.itemKey
+  });
+  // 显示侧边栏
+  appStore.toggleSideBarHide(false);
+  // 跳转到首页
+  router.push('/index');
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
+  // 隐藏侧边栏
+  appStore.toggleSideBarHide(true);
   getList();
   getList();
 });
 });
 </script>
 </script>
+
+<style scoped lang="scss">
+.project-page {
+  min-height: 100vh;
+  background-color: #f5f7fa;
+}
+
+/* 内容区域 */
+.content-container {
+  max-width: 1400px;
+  margin: 0 auto;
+  padding: 24px 40px;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24px;
+
+  .stats-info {
+    font-size: 16px;
+    color: #606266;
+
+    .count-highlight {
+      font-weight: bold;
+      color: #1890ff;
+      font-size: 18px;
+      margin: 0 4px;
+    }
+  }
+
+  .header-actions {
+    display: flex;
+    gap: 16px;
+
+    .search-input {
+      width: 240px;
+    }
+  }
+}
+
+/* 卡片网格 */
+.project-grid-wrapper {
+  min-height: 400px;
+}
+
+.project-grid {
+  margin-top: 20px;
+}
+
+.grid-item {
+  margin-bottom: 24px;
+}
+
+/* 项目卡片样式 */
+.project-card {
+  background: #ffffff;
+  border-radius: 12px;
+  border: 1px solid #e4e7ed;
+  transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+
+  &:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
+    border-color: #1890ff;
+  }
+
+  &.is-selected {
+    border-color: #1890ff;
+    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
+    background-color: #f0f7ff;
+  }
+
+  .selected-badge {
+    position: absolute;
+    top: 0;
+    right: 0;
+    width: 0;
+    height: 0;
+    border-style: solid;
+    border-width: 0 32px 32px 0;
+    border-color: transparent #1890ff transparent transparent;
+    z-index: 10;
+
+    .el-icon {
+      position: absolute;
+      top: 4px;
+      right: -28px;
+      color: #ffffff;
+      font-size: 14px;
+      font-weight: bold;
+    }
+  }
+
+  .card-body {
+    padding: 24px;
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+
+    .project-logo {
+      width: 80px;
+      height: 80px;
+      margin-bottom: 16px;
+      border-radius: 50%;
+      overflow: hidden;
+      background: #f5f7fa;
+      border: 2px solid #fff;
+      box-shadow: 0 2px 8px rgba(0,0,0,0.05);
+
+      .el-image {
+        width: 100%;
+        height: 100%;
+      }
+
+      .image-placeholder {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: #909399;
+      }
+    }
+
+    .project-name {
+      margin: 0 0 20px 0;
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+      text-align: center;
+      display: -webkit-box;
+      -webkit-line-clamp: 1;
+      line-clamp: 1;
+      -webkit-box-orient: vertical;
+      overflow: hidden;
+    }
+
+    .stats-grid {
+      width: 100%;
+      display: grid;
+      grid-template-columns: repeat(3, 1fr);
+      gap: 12px;
+
+      .stat-item {
+        display: flex;
+        flex-direction: column;
+        
+        .label {
+          font-size: 11px;
+          color: #909399;
+          margin-bottom: 2px;
+        }
+
+        .value {
+          font-size: 13px;
+          color: #303133;
+          font-weight: 500;
+        }
+
+        &.primary .value { color: #1890ff; font-weight: bold; }
+        &.danger .value { color: #f56c6c; }
+        &.success .value { color: #67c23a; }
+        &.warning .value { color: #e6a23c; }
+        &.info .value { color: #909399; }
+      }
+
+      /* 最后两个金额占满一行或调整比例 */
+      .stat-item:nth-last-child(2),
+      .stat-item:last-child {
+        grid-column: span 3;
+        border-top: 1px dashed #ebeef5;
+        padding-top: 8px;
+        margin-top: 4px;
+        flex-direction: row;
+        justify-content: space-between;
+        align-items: center;
+
+        .label { margin-bottom: 0; font-size: 12px; }
+        .value { font-size: 15px; }
+      }
+    }
+  }
+
+  .card-footer {
+    padding: 12px 16px;
+    border-top: 1px solid #ebeef5;
+    background: #fafafa;
+    display: flex;
+    justify-content: space-around;
+    
+    .el-button {
+      font-size: 13px;
+    }
+  }
+
+  .status-badge {
+    position: absolute;
+    top: 10px;
+    left: 10px;
+    padding: 2px 8px;
+    border-radius: 10px;
+    font-size: 11px;
+    font-weight: 500;
+    z-index: 10;
+
+    &.is-enabled {
+      background-color: #f0f9eb;
+      color: #67c23a;
+      border: 1px solid #b3e19d;
+    }
+
+    &.is-disabled {
+      background-color: #fef0f0;
+      color: #f56c6c;
+      border: 1px solid #fbc4c4;
+    }
+  }
+}
+
+.empty-container {
+  padding: 100px 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: #fff;
+  border-radius: 12px;
+  margin-top: 20px;
+}
+
+.pagination-wrapper {
+  margin-top: 40px;
+  display: flex;
+  justify-content: center;
+}
+
+/* 响应式适配 */
+@media (max-width: 768px) {
+  .content-container {
+    padding: 16px 20px;
+  }
+  .page-header {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 16px;
+    .header-actions {
+      width: 100%;
+      .search-input { flex: 1; }
+    }
+  }
+}
+</style>

+ 97 - 41
src/views/external/product/index.vue

@@ -5,18 +5,6 @@
         <el-card shadow="hover">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
             <el-row :gutter="20">
             <el-row :gutter="20">
-              <el-col :span="6">
-                <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.id"
-                      :label="item.itemName"
-                      :value="item.id"
-                    />
-                  </el-select>
-                </el-form-item>
-              </el-col>
               <el-col :span="6">
               <el-col :span="6">
                 <el-form-item label="商品编号" prop="productNo">
                 <el-form-item label="商品编号" prop="productNo">
                   <el-input v-model="queryParams.productNo" placeholder="请输入商品编号" clearable @keyup.enter="handleQuery" />
                   <el-input v-model="queryParams.productNo" placeholder="请输入商品编号" clearable @keyup.enter="handleQuery" />
@@ -96,7 +84,10 @@
               <el-button link type="primary" @click="handleToggleStatus(scope.row)">{{ scope.row.productStatus == '1' ? '下架' : '上架' }}</el-button>
               <el-button link type="primary" @click="handleToggleStatus(scope.row)">{{ scope.row.productStatus == '1' ? '下架' : '上架' }}</el-button>
             </el-tooltip>
             </el-tooltip>
             <el-tooltip content="推送" placement="top">
             <el-tooltip content="推送" placement="top">
-              <el-button link type="primary" @click="handlePush(scope.row)">推送</el-button>
+              <el-button link type="primary" :disabled="scope.row.productStatus != '1'" @click="handlePush(scope.row)">推送</el-button>
+            </el-tooltip>
+            <el-tooltip content="查看日志" placement="top">
+              <el-button link type="primary" @click="handleViewLog(scope.row)">查看日志</el-button>
             </el-tooltip>
             </el-tooltip>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
@@ -104,6 +95,29 @@
 
 
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
     </el-card>
+    <!-- 查看日志侧边栏 -->
+    <el-drawer v-model="logDrawer.visible" title="推送日志" size="1000px" append-to-body>
+      <div v-loading="logDrawer.loading">
+        <el-table v-if="logDrawer.data.length > 0" :data="logDrawer.data" border stripe>
+          <el-table-column label="序号" type="index" width="55" align="center" />
+          <el-table-column label="推送状态" align="center" width="90">
+            <template #default="scope">
+              <el-tag v-if="scope.row.pushStatus == '0'" type="success">成功</el-tag>
+              <el-tag v-else-if="scope.row.pushStatus == '1'" type="danger">失败</el-tag>
+              <el-tag v-else type="info">-</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="失败原因" align="center" prop="reason" show-overflow-tooltip>
+            <template #default="scope">
+              {{ scope.row.reason || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
+        </el-table>
+        <el-empty v-else description="暂无日志数据" />
+      </div>
+    </el-drawer>
+
     <!-- 添加或修改对外部推送商品对话框 -->
     <!-- 添加或修改对外部推送商品对话框 -->
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
       <el-form ref="productFormRef" :model="form" :rules="rules" label-width="130px">
       <el-form ref="productFormRef" :model="form" :rules="rules" label-width="130px">
@@ -144,15 +158,15 @@
 <script setup name="Product" lang="ts">
 <script setup name="Product" lang="ts">
 import { getThirdProductPage, getProduct, updateProduct, shelfReview, batchPushProduct } from '@/api/external/product';
 import { getThirdProductPage, getProduct, updateProduct, shelfReview, batchPushProduct } from '@/api/external/product';
 import { ThirdProductVO, ProductQuery, ProductVO, ProductForm } from '@/api/external/product/types';
 import { ThirdProductVO, ProductQuery, ProductVO, ProductForm } from '@/api/external/product/types';
-import { listItem } from '@/api/external/item/index';
-import { ItemVO } from '@/api/external/item/types';
 import { getProductCategoryTree } from '@/api/external/productCategory';
 import { getProductCategoryTree } from '@/api/external/productCategory';
 import { ProductCategoryVO } from '@/api/external/productCategory/types';
 import { ProductCategoryVO } from '@/api/external/productCategory/types';
+import { listPushPoolLog } from '@/api/external/pushPoolLog';
+import { PushPoolLogVO } from '@/api/external/pushPoolLog/types';
+import cache from '@/plugins/cache';
 
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 
 const productList = ref<ThirdProductVO[]>([]);
 const productList = ref<ThirdProductVO[]>([]);
-const itemList = ref<ItemVO[]>([]);
 const buttonLoading = ref(false);
 const buttonLoading = ref(false);
 const loading = ref(true);
 const loading = ref(true);
 const showSearch = ref(true);
 const showSearch = ref(true);
@@ -160,6 +174,8 @@ const ids = ref<Array<string | number>>([]);
 const single = ref(true);
 const single = ref(true);
 const multiple = ref(true);
 const multiple = ref(true);
 const total = ref(0);
 const total = ref(0);
+// 已上架商品数量(用于批量推送)
+const onShelfCount = ref(0);
 
 
 const queryFormRef = ref<ElFormInstance>();
 const queryFormRef = ref<ElFormInstance>();
 const productFormRef = ref<ElFormInstance>();
 const productFormRef = ref<ElFormInstance>();
@@ -170,6 +186,13 @@ const dialog = reactive<DialogOption>({
   title: ''
   title: ''
 });
 });
 
 
+// 日志侧边栏数据
+const logDrawer = reactive({
+  visible: false,
+  loading: false,
+  data: [] as PushPoolLogVO[]
+});
+
 // 编辑模式标识
 // 编辑模式标识
 const isEditMode = ref(false);
 const isEditMode = ref(false);
 
 
@@ -242,32 +265,33 @@ const getList = async () => {
   loading.value = false;
   loading.value = false;
 }
 }
 
 
-/** 获取项目列表 */
-const getItemList = async () => {
-  const res = await listItem();
-  console.log('listItem response:', res);
-  // 兼容后端返回结构:rows 或 data
-  itemList.value = (res as any).rows || res.data || [];
-  console.log('itemList.value:', itemList.value);
-  // 默认选择第一个项目
-  if (itemList.value.length > 0 && !queryParams.value.itemId) {
-    queryParams.value.itemId = itemList.value[0].id;
-    // 自动触发查询
-    await getList();
-    await initCategoryData();
+/** 初始化项目ID(从缓存获取) */
+const initProjectId = () => {
+  const currentProject = cache.local.getJSON('currentProject');
+  if (currentProject && currentProject.id) {
+    queryParams.value.itemId = currentProject.id;
   }
   }
 }
 }
 
 
 const handleBatchPush = async () => {
 const handleBatchPush = async () => {
   if (multiple.value) return;
   if (multiple.value) return;
+  // 检查是否有已上架商品
+  if (onShelfCount.value === 0) {
+    proxy?.$modal.msgWarning('请选择已上架的商品进行推送');
+    return;
+  }
   try {
   try {
-    await proxy?.$modal.confirm('确认推送选中的商品吗?');
-    await batchPushProduct(ids.value);
+    await proxy?.$modal.confirm(`确认推送选中的 ${onShelfCount.value} 个已上架商品吗?`);
+    // 只推送已上架商品的ID
+    const selection = productTableRef.value?.getSelectionRows() || [];
+    const onShelfIds = selection.filter((item: ThirdProductVO) => item.productStatus == '1').map((item: ThirdProductVO) => item.id);
+    await batchPushProduct(onShelfIds);
     proxy?.$modal.msgSuccess('推送成功');
     proxy?.$modal.msgSuccess('推送成功');
     productTableRef.value?.clearSelection?.();
     productTableRef.value?.clearSelection?.();
     ids.value = [];
     ids.value = [];
     multiple.value = true;
     multiple.value = true;
     single.value = true;
     single.value = true;
+    onShelfCount.value = 0;
     await getList();
     await getList();
   } catch (error) {
   } catch (error) {
     if (error !== 'cancel') {
     if (error !== 'cancel') {
@@ -277,11 +301,6 @@ const handleBatchPush = async () => {
   }
   }
 }
 }
 
 
-const handleItemChange = async () => {
-  await initCategoryData();
-  handleQuery();
-}
-
 /** 取消按钮 */
 /** 取消按钮 */
 const cancel = () => {
 const cancel = () => {
   reset();
   reset();
@@ -320,6 +339,8 @@ const handleSelectionChange = (selection: ThirdProductVO[]) => {
   ids.value = selection.map(item => item.id);
   ids.value = selection.map(item => item.id);
   single.value = selection.length != 1;
   single.value = selection.length != 1;
   multiple.value = !selection.length;
   multiple.value = !selection.length;
+  // 统计已上架商品数量
+  onShelfCount.value = selection.filter(item => item.productStatus == '1').length;
 }
 }
 
 
 /** 新增按钮操作 */
 /** 新增按钮操作 */
@@ -424,14 +445,19 @@ const handleToggleStatus = async (row: ThirdProductVO) => {
 
 
 /** 推送操作 */
 /** 推送操作 */
 const handlePush = async (row: ThirdProductVO) => {
 const handlePush = async (row: ThirdProductVO) => {
+  // 检查是否已上架
+  if (row.productStatus != '1') {
+    proxy?.$modal.msgWarning('只有已上架的商品才能推送');
+    return;
+  }
   try {
   try {
     await proxy?.$modal.confirm('确认推送该商品吗?');
     await proxy?.$modal.confirm('确认推送该商品吗?');
-    
+
     // 调用批量推送接口,传入单个商品ID
     // 调用批量推送接口,传入单个商品ID
     await batchPushProduct(row.id);
     await batchPushProduct(row.id);
-    
+
     proxy?.$modal.msgSuccess('推送成功');
     proxy?.$modal.msgSuccess('推送成功');
-    
+
     // 刷新列表
     // 刷新列表
     await getList();
     await getList();
   } catch (error) {
   } catch (error) {
@@ -442,6 +468,32 @@ const handlePush = async (row: ThirdProductVO) => {
   }
   }
 }
 }
 
 
+/** 查看日志操作 */
+const handleViewLog = async (row: ThirdProductVO) => {
+  logDrawer.visible = true;
+  logDrawer.loading = true;
+  logDrawer.data = [];
+  
+  try {
+    // 获取项目ID
+    const itemId = queryParams.value.itemId;
+    if (!itemId) {
+      proxy?.$modal.msgWarning('请先选择项目');
+      logDrawer.visible = false;
+      return;
+    }
+    
+    // 调用列表接口,传入项目id和商品池id
+    const res = await listPushPoolLog({ itemId, productId: row.productId, pageNum: 1, pageSize: 100 });
+    logDrawer.data = res.rows || [];
+  } catch (error) {
+    console.error('获取推送日志失败:', error);
+    proxy?.$modal.msgError('获取推送日志失败');
+  } finally {
+    logDrawer.loading = false;
+  }
+}
+
 /** 导出按钮操作 */
 /** 导出按钮操作 */
 const handleExport = () => {
 const handleExport = () => {
   proxy?.download('external/product/export', {
   proxy?.download('external/product/export', {
@@ -450,7 +502,11 @@ const handleExport = () => {
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
-  // 先加载项目列表,内部会自动触发查询
-  getItemList();
+  // 从缓存获取项目ID并初始化
+  initProjectId();
+  if (queryParams.value.itemId) {
+    getList();
+    initCategoryData();
+  }
 });
 });
 </script>
 </script>

+ 13 - 27
src/views/external/productBrand/index.vue

@@ -4,16 +4,6 @@
       <div v-show="showSearch" class="mb-[10px]">
       <div v-show="showSearch" class="mb-[10px]">
         <el-card shadow="hover">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="项目" prop="itemKey">
-              <el-select v-model="queryParams.itemKey" placeholder="请选择项目" clearable @change="handleQuery" style="width: 200px">
-                <el-option
-                  v-for="item in itemList"
-                  :key="item.itemKey"
-                  :label="item.itemName"
-                  :value="item.itemKey"
-                />
-              </el-select>
-            </el-form-item>
             <el-form-item label="官方品牌名称" prop="brandName">
             <el-form-item label="官方品牌名称" prop="brandName">
               <el-input v-model="queryParams.brandName" placeholder="请输入官方品牌名称" clearable @keyup.enter="handleQuery" />
               <el-input v-model="queryParams.brandName" placeholder="请输入官方品牌名称" clearable @keyup.enter="handleQuery" />
             </el-form-item>
             </el-form-item>
@@ -100,15 +90,13 @@ import { listProductBrand, getProductBrand, updateProductBrand } from '@/api/ext
 import { ProductBrandVO, ProductBrandQuery, ProductBrandForm } from '@/api/external/productBrand/types';
 import { ProductBrandVO, ProductBrandQuery, ProductBrandForm } from '@/api/external/productBrand/types';
 import { listBrand } from '@/api/product/brand';
 import { listBrand } from '@/api/product/brand';
 import { BrandVO } from '@/api/product/brand/types';
 import { BrandVO } from '@/api/product/brand/types';
-import { listItem } from '@/api/external/item';
-import { ItemVO } from '@/api/external/item/types';
+import cache from '@/plugins/cache';
 
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 
 const productBrandList = ref<ProductBrandVO[]>([]);
 const productBrandList = ref<ProductBrandVO[]>([]);
 const brandList = ref<BrandVO[]>([]);
 const brandList = ref<BrandVO[]>([]);
 const defaultBrandList = ref<BrandVO[]>([]);
 const defaultBrandList = ref<BrandVO[]>([]);
-const itemList = ref<ItemVO[]>([]);
 const buttonLoading = ref(false);
 const buttonLoading = ref(false);
 const loading = ref(true);
 const loading = ref(true);
 const showSearch = ref(true);
 const showSearch = ref(true);
@@ -139,7 +127,7 @@ const data = reactive<PageData<ProductBrandForm, ProductBrandQuery>>({
     pageSize: 10,
     pageSize: 10,
     brandName: undefined,
     brandName: undefined,
     thirdPartyBrandName: undefined,
     thirdPartyBrandName: undefined,
-    itemKey: undefined,
+    itemId: undefined,
     params: {
     params: {
     }
     }
   },
   },
@@ -196,16 +184,11 @@ const remoteSearchBrand = (query: string) => {
   }, 300);
   }, 300);
 }
 }
 
 
-/** 获取项目列表 */
-const getItemList = async () => {
-  const res = await listItem();
-  // 兼容后端返回结构:rows 或 data
-  itemList.value = (res as any).rows || res.data || [];
-  // 默认选择第一个项目
-  if (itemList.value.length > 0 && !queryParams.value.itemKey) {
-    queryParams.value.itemKey = itemList.value[0].itemKey;
-    // 自动触发查询
-    await getList();
+/** 初始化项目Key(从缓存获取) */
+const initProjectKey = () => {
+  const currentProject = cache.local.getJSON('currentProject');
+  if (currentProject && currentProject.id) {
+    queryParams.value.itemId = currentProject.id;
   }
   }
 }
 }
 
 
@@ -242,7 +225,7 @@ const handleSelectionChange = (selection: ProductBrandVO[]) => {
 const handleUpdate = async (row?: ProductBrandVO) => {
 const handleUpdate = async (row?: ProductBrandVO) => {
   reset();
   reset();
   const _id = row?.id || ids.value[0]
   const _id = row?.id || ids.value[0]
-  const res = await getProductBrand(_id, queryParams.value.itemKey);
+  const res = await getProductBrand(_id, queryParams.value.id);
   Object.assign(form.value, res.data);
   Object.assign(form.value, res.data);
   dialog.visible = true;
   dialog.visible = true;
   dialog.title = "绑定官方品牌";
   dialog.title = "绑定官方品牌";
@@ -279,8 +262,11 @@ const handleExport = () => {
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
-  // 先加载项目列表,内部会自动触发查询
-  getItemList();
+  // 从缓存获取项目Key并初始化
+  initProjectKey();
+  if (queryParams.value.itemId) {
+    getList();
+  }
   brandLoading.value = true;
   brandLoading.value = true;
   getBrandList().finally(() => (brandLoading.value = false));
   getBrandList().finally(() => (brandLoading.value = false));
 });
 });

+ 20 - 34
src/views/external/productCategory/index.vue

@@ -4,16 +4,6 @@
       <div v-show="showSearch" class="mb-[10px]">
       <div v-show="showSearch" class="mb-[10px]">
         <el-card shadow="hover">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="项目" prop="itemKey">
-              <el-select v-model="queryParams.itemKey" placeholder="请选择项目" clearable @change="handleQuery" style="width: 200px">
-                <el-option
-                  v-for="item in itemList"
-                  :key="item.itemKey"
-                  :label="item.itemName"
-                  :value="item.itemKey"
-                />
-              </el-select>
-            </el-form-item>
             <el-form-item label="分类名称" prop="categoryName">
             <el-form-item label="分类名称" prop="categoryName">
               <el-input v-model="queryParams.categoryName" placeholder="请输入分类名称" clearable @keyup.enter="handleQuery" />
               <el-input v-model="queryParams.categoryName" placeholder="请输入分类名称" clearable @keyup.enter="handleQuery" />
             </el-form-item>
             </el-form-item>
@@ -51,9 +41,9 @@
             <span>{{ scope.row.classLevel }}级</span>
             <span>{{ scope.row.classLevel }}级</span>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        
-        
-      
+
+
+
         <el-table-column fixed="right" align="center" label="操作">
         <el-table-column fixed="right" align="center" label="操作">
           <template #default="scope">
           <template #default="scope">
             <el-tooltip content="新增分类" placement="top">
             <el-tooltip content="新增分类" placement="top">
@@ -107,8 +97,7 @@
 <script setup name="ProductCategory" lang="ts">
 <script setup name="ProductCategory" lang="ts">
 import { listProductCategory, getProductCategory, delProductCategory, addProductCategory, updateProductCategory, listProductCategoryExcludeChild } from '@/api/external/productCategory';
 import { listProductCategory, getProductCategory, delProductCategory, addProductCategory, updateProductCategory, listProductCategoryExcludeChild } from '@/api/external/productCategory';
 import { ProductCategoryVO, ProductCategoryQuery, ProductCategoryForm } from '@/api/external/productCategory/types';
 import { ProductCategoryVO, ProductCategoryQuery, ProductCategoryForm } from '@/api/external/productCategory/types';
-import { listItem } from '@/api/external/item';
-import { ItemVO } from '@/api/external/item/types';
+import cache from '@/plugins/cache';
 
 
 interface CategoryOptionsType {
 interface CategoryOptionsType {
   id: number | string;
   id: number | string;
@@ -119,7 +108,6 @@ interface CategoryOptionsType {
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 
 const productCategoryList = ref<ProductCategoryVO[]>([]);
 const productCategoryList = ref<ProductCategoryVO[]>([]);
-const itemList = ref<ItemVO[]>([]);
 const loading = ref(true);
 const loading = ref(true);
 const showSearch = ref(true);
 const showSearch = ref(true);
 const categoryOptions = ref<CategoryOptionsType[]>([]);
 const categoryOptions = ref<CategoryOptionsType[]>([]);
@@ -147,7 +135,7 @@ const data = reactive<PageData<ProductCategoryForm, Partial<ProductCategoryQuery
   form: {...initFormData},
   form: {...initFormData},
   queryParams: {
   queryParams: {
     categoryName: undefined,
     categoryName: undefined,
-    itemKey: undefined
+    itemId: undefined
   },
   },
   rules: {
   rules: {
     categoryName: [
     categoryName: [
@@ -178,16 +166,11 @@ const getList = async () => {
   loading.value = false;
   loading.value = false;
 }
 }
 
 
-/** 获取项目列表 */
-const getItemList = async () => {
-  const res = await listItem();
-  // 兼容后端返回结构:rows 或 data
-  itemList.value = (res as any).rows || res.data || [];
-  // 默认选择第一个项目
-  if (itemList.value.length > 0 && !queryParams.value.itemKey) {
-    queryParams.value.itemKey = itemList.value[0].itemKey;
-    // 自动触发查询
-    await getList();
+/** 初始化项目Key(从缓存获取) */
+const initProjectKey = () => {
+  const currentProject = cache.local.getJSON('currentProject');
+  if (currentProject && currentProject.id) {
+    queryParams.value.itemId = currentProject.id;
   }
   }
 }
 }
 
 
@@ -196,7 +179,7 @@ const loadChildren = async (row: ProductCategoryVO, treeNode: any, resolve: (dat
   // 存储加载状态,便于刷新时重新加载
   // 存储加载状态,便于刷新时重新加载
   lazyTreeNodeMap.value.set(row.id, { tree: row, treeNode, resolve });
   lazyTreeNodeMap.value.set(row.id, { tree: row, treeNode, resolve });
   try {
   try {
-    const res = await listProductCategory({ parentId: row.id, itemKey: queryParams.value.itemKey });
+    const res = await listProductCategory({ parentId: row.id, itemId: queryParams.value.itemId });
     const data = (res as any).rows || res.data || [];
     const data = (res as any).rows || res.data || [];
     // 标记子节点是否还有子级
     // 标记子节点是否还有子级
     const children = data.map((item: ProductCategoryVO) => ({
     const children = data.map((item: ProductCategoryVO) => ({
@@ -247,7 +230,7 @@ const refreshLazyNodes = () => {
 /** 新增按钮操作 */
 /** 新增按钮操作 */
 const handleAdd = async (row?: ProductCategoryVO) => {
 const handleAdd = async (row?: ProductCategoryVO) => {
   reset();
   reset();
-  const res = await listProductCategory({ itemKey: queryParams.value.itemKey });
+  const res = await listProductCategory({ itemId: queryParams.value.itemId });
   const responseData = (res as any).rows || res.data || [];
   const responseData = (res as any).rows || res.data || [];
   const data = proxy?.handleTree<CategoryOptionsType>(responseData, 'id');
   const data = proxy?.handleTree<CategoryOptionsType>(responseData, 'id');
   if (data) {
   if (data) {
@@ -263,9 +246,9 @@ const handleAdd = async (row?: ProductCategoryVO) => {
 /** 修改按钮操作 */
 /** 修改按钮操作 */
 const handleUpdate = async (row: ProductCategoryVO) => {
 const handleUpdate = async (row: ProductCategoryVO) => {
   reset();
   reset();
-  const res = await getProductCategory(row.id, queryParams.value.itemKey);
+  const res = await getProductCategory(row.id, queryParams.value.itemId);
   form.value = res.data;
   form.value = res.data;
-  const response = await listProductCategory({ itemKey: queryParams.value.itemKey });
+  const response = await listProductCategory({ itemId: queryParams.value.itemId });
   // 根据后端返回结构,如果是 TableDataInfo 则用 rows,如果是直接数组则用 data
   // 根据后端返回结构,如果是 TableDataInfo 则用 rows,如果是直接数组则用 data
   const responseData = (response as any).rows || response.data || [];
   const responseData = (response as any).rows || response.data || [];
   const data = proxy?.handleTree<CategoryOptionsType>(responseData, 'id');
   const data = proxy?.handleTree<CategoryOptionsType>(responseData, 'id');
@@ -291,13 +274,16 @@ const submitForm = () => {
 /** 删除按钮操作 */
 /** 删除按钮操作 */
 const handleDelete = async (row: ProductCategoryVO) => {
 const handleDelete = async (row: ProductCategoryVO) => {
   await proxy?.$modal.confirm('是否确认删除名称为"' + row.categoryName + '"的数据项?');
   await proxy?.$modal.confirm('是否确认删除名称为"' + row.categoryName + '"的数据项?');
-  await delProductCategory(row.id, queryParams.value.itemKey);
+  await delProductCategory(row.id, queryParams.value.itemId);
   await getList();
   await getList();
   proxy?.$modal.msgSuccess('删除成功');
   proxy?.$modal.msgSuccess('删除成功');
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
-  // 先加载项目列表,内部会自动触发查询
-  getItemList();
+  // 从缓存获取项目Key并初始化
+  initProjectKey();
+  if (queryParams.value.itemId) {
+    getList();
+  }
 });
 });
 </script>
 </script>

+ 19 - 56
src/views/external/productChangeLog/index.vue

@@ -4,16 +4,6 @@
       <div v-show="showSearch" class="mb-[10px]">
       <div v-show="showSearch" class="mb-[10px]">
         <el-card shadow="hover">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="项目" prop="itemKey">
-              <el-select v-model="queryParams.itemKey" placeholder="请选择项目" clearable @change="handleQuery" style="width: 200px">
-                <el-option
-                  v-for="item in itemList"
-                  :key="item.itemKey"
-                  :label="item.itemName"
-                  :value="item.itemKey"
-                />
-              </el-select>
-            </el-form-item>
             <el-form-item label="项目id" prop="itemId">
             <el-form-item label="项目id" prop="itemId">
               <el-input v-model="queryParams.itemId" placeholder="请输入项目id" clearable @keyup.enter="handleQuery" />
               <el-input v-model="queryParams.itemId" placeholder="请输入项目id" clearable @keyup.enter="handleQuery" />
             </el-form-item>
             </el-form-item>
@@ -121,13 +111,11 @@ type=16商品
 <script setup name="ProductChangeLog" lang="ts">
 <script setup name="ProductChangeLog" lang="ts">
 import { listProductChangeLog, getProductChangeLog, delProductChangeLog, addProductChangeLog, updateProductChangeLog } from '@/api/external/productChangeLog';
 import { listProductChangeLog, getProductChangeLog, delProductChangeLog, addProductChangeLog, updateProductChangeLog } from '@/api/external/productChangeLog';
 import { ProductChangeLogVO, ProductChangeLogQuery, ProductChangeLogForm } from '@/api/external/productChangeLog/types';
 import { ProductChangeLogVO, ProductChangeLogQuery, ProductChangeLogForm } from '@/api/external/productChangeLog/types';
-import { listItem } from '@/api/external/item';
-import { ItemVO } from '@/api/external/item/types';
+import cache from '@/plugins/cache';
 
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 
 const productChangeLogList = ref<ProductChangeLogVO[]>([]);
 const productChangeLogList = ref<ProductChangeLogVO[]>([]);
-const itemList = ref<ItemVO[]>([]);
 const buttonLoading = ref(false);
 const buttonLoading = ref(false);
 const loading = ref(true);
 const loading = ref(true);
 const showSearch = ref(true);
 const showSearch = ref(true);
@@ -153,7 +141,7 @@ const initFormData: ProductChangeLogForm = {
   remark: undefined,
   remark: undefined,
 }
 }
 const data = reactive<PageData<ProductChangeLogForm, ProductChangeLogQuery>>({
 const data = reactive<PageData<ProductChangeLogForm, ProductChangeLogQuery>>({
-  form: {...initFormData},
+  form: { ...initFormData },
   queryParams: {
   queryParams: {
     pageNum: 1,
     pageNum: 1,
     pageSize: 10,
     pageSize: 10,
@@ -162,41 +150,18 @@ const data = reactive<PageData<ProductChangeLogForm, ProductChangeLogQuery>>({
     type: undefined,
     type: undefined,
     status: undefined,
     status: undefined,
     platformCode: undefined,
     platformCode: undefined,
-    itemKey: undefined,
-    params: {
-    }
+    itemId: undefined,
+    params: {}
   },
   },
   rules: {
   rules: {
     itemId: [
     itemId: [
-      { required: true, message: "项目id不能为空", trigger: "blur" }
+      { required: true, message: "项目 id 不能为空", trigger: "blur" }
     ],
     ],
     productId: [
     productId: [
-      { required: true, message: "商品id不能为空", trigger: "blur" }
+      { required: true, message: "商品 id 不能为空", trigger: "blur" }
     ],
     ],
     type: [
     type: [
-      { required: true, message: "type=2商品
-价格变更,后
-续会调用价格
-接口。
-type=4代表
-商品上下架变
-更消息,后续
-会调用上下架
-状态接口。
-type=6代表
-添加、删除商
-品池内的商
-品,触发保存
-商品流程,依
-次调用商品详
-情等接口获取
-商品信息。
-type=16商品
-介绍及规格参
-数变更消息,
-调用商品详情
-等接口更新商
-品信息。不能为空", trigger: "change" }
+      { required: true, message: "type=2 商品价格变更,后续会调用价格接口。type=4 代表商品上下架变更消息,后续会调用上下架状态接口。type=6 代表添加、删除商品池内的商品,触发保存商品流程,依次调用商品详情等接口获取商品信息。type=16 商品介绍及规格参数变更消息,调用商品详情等接口更新商品信息。不能为空", trigger: "change" }
     ],
     ],
     status: [
     status: [
       { required: true, message: "状态不能为空", trigger: "change" }
       { required: true, message: "状态不能为空", trigger: "change" }
@@ -218,16 +183,11 @@ const getList = async () => {
   loading.value = false;
   loading.value = false;
 }
 }
 
 
-/** 获取项目列表 */
-const getItemList = async () => {
-  const res = await listItem();
-  // 兼容后端返回结构:rows 或 data
-  itemList.value = (res as any).rows || res.data || [];
-  // 默认选择第一个项目
-  if (itemList.value.length > 0 && !queryParams.value.itemKey) {
-    queryParams.value.itemKey = itemList.value[0].itemKey;
-    // 自动触发查询
-    await getList();
+/** 初始化项目Key(从缓存获取) */
+const initProjectKey = () => {
+  const currentProject = cache.local.getJSON('currentProject');
+  if (currentProject && currentProject.id) {
+    queryParams.value.itemId = currentProject.id;
   }
   }
 }
 }
 
 
@@ -273,7 +233,7 @@ const handleAdd = () => {
 const handleUpdate = async (row?: ProductChangeLogVO) => {
 const handleUpdate = async (row?: ProductChangeLogVO) => {
   reset();
   reset();
   const _id = row?.id || ids.value[0]
   const _id = row?.id || ids.value[0]
-  const res = await getProductChangeLog(_id, queryParams.value.itemKey);
+  const res = await getProductChangeLog(_id, queryParams.value.itemId);
   Object.assign(form.value, res.data);
   Object.assign(form.value, res.data);
   dialog.visible = true;
   dialog.visible = true;
   dialog.title = "修改商品变更消息记录";
   dialog.title = "修改商品变更消息记录";
@@ -300,7 +260,7 @@ const submitForm = () => {
 const handleDelete = async (row?: ProductChangeLogVO) => {
 const handleDelete = async (row?: ProductChangeLogVO) => {
   const _ids = row?.id || ids.value;
   const _ids = row?.id || ids.value;
   await proxy?.$modal.confirm('是否确认删除商品变更消息记录编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
   await proxy?.$modal.confirm('是否确认删除商品变更消息记录编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
-  await delProductChangeLog(_ids, queryParams.value.itemKey);
+  await delProductChangeLog(_ids, queryParams.value.itemId);
   proxy?.$modal.msgSuccess("删除成功");
   proxy?.$modal.msgSuccess("删除成功");
   await getList();
   await getList();
 }
 }
@@ -313,7 +273,10 @@ const handleExport = () => {
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
-  // 先加载项目列表,内部会自动触发查询
-  getItemList();
+  // 从缓存获取项目Key并初始化
+  initProjectKey();
+  if (queryParams.value.itemId) {
+    getList();
+  }
 });
 });
 </script>
 </script>

+ 13 - 27
src/views/external/pushPoolLog/index.vue

@@ -4,16 +4,6 @@
       <div v-show="showSearch" class="mb-[10px]">
       <div v-show="showSearch" class="mb-[10px]">
         <el-card shadow="hover">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="项目" prop="itemKey">
-              <el-select v-model="queryParams.itemKey" placeholder="请选择项目" clearable @change="handleQuery" style="width: 200px">
-                <el-option
-                  v-for="item in itemList"
-                  :key="item.itemKey"
-                  :label="item.itemName"
-                  :value="item.itemKey"
-                />
-              </el-select>
-            </el-form-item>
             <el-form-item label="项目id" prop="itemId">
             <el-form-item label="项目id" prop="itemId">
               <el-input v-model="queryParams.itemId" placeholder="请输入项目id" clearable @keyup.enter="handleQuery" />
               <el-input v-model="queryParams.itemId" placeholder="请输入项目id" clearable @keyup.enter="handleQuery" />
             </el-form-item>
             </el-form-item>
@@ -113,13 +103,11 @@
 <script setup name="PushPoolLog" lang="ts">
 <script setup name="PushPoolLog" lang="ts">
 import { listPushPoolLog, getPushPoolLog, delPushPoolLog, addPushPoolLog, updatePushPoolLog } from '@/api/external/pushPoolLog';
 import { listPushPoolLog, getPushPoolLog, delPushPoolLog, addPushPoolLog, updatePushPoolLog } from '@/api/external/pushPoolLog';
 import { PushPoolLogVO, PushPoolLogQuery, PushPoolLogForm } from '@/api/external/pushPoolLog/types';
 import { PushPoolLogVO, PushPoolLogQuery, PushPoolLogForm } from '@/api/external/pushPoolLog/types';
-import { listItem } from '@/api/external/item';
-import { ItemVO } from '@/api/external/item/types';
+import cache from '@/plugins/cache';
 
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 
 const pushPoolLogList = ref<PushPoolLogVO[]>([]);
 const pushPoolLogList = ref<PushPoolLogVO[]>([]);
-const itemList = ref<ItemVO[]>([]);
 const buttonLoading = ref(false);
 const buttonLoading = ref(false);
 const loading = ref(true);
 const loading = ref(true);
 const showSearch = ref(true);
 const showSearch = ref(true);
@@ -198,16 +186,11 @@ const getList = async () => {
   loading.value = false;
   loading.value = false;
 }
 }
 
 
-/** 获取项目列表 */
-const getItemList = async () => {
-  const res = await listItem();
-  // 兼容后端返回结构:rows 或 data
-  itemList.value = (res as any).rows || res.data || [];
-  // 默认选择第一个项目
-  if (itemList.value.length > 0 && !queryParams.value.itemKey) {
-    queryParams.value.itemKey = itemList.value[0].itemKey;
-    // 自动触发查询
-    await getList();
+/** 初始化项目Key(从缓存获取) */
+const initProjectKey = () => {
+  const currentProject = cache.local.getJSON('currentProject');
+  if (currentProject && currentProject.id) {
+    queryParams.value.itemId = currentProject.id;
   }
   }
 }
 }
 
 
@@ -253,7 +236,7 @@ const handleAdd = () => {
 const handleUpdate = async (row?: PushPoolLogVO) => {
 const handleUpdate = async (row?: PushPoolLogVO) => {
   reset();
   reset();
   const _id = row?.id || ids.value[0]
   const _id = row?.id || ids.value[0]
-  const res = await getPushPoolLog(_id, queryParams.value.itemKey);
+  const res = await getPushPoolLog(_id, queryParams.value.itemId);
   Object.assign(form.value, res.data);
   Object.assign(form.value, res.data);
   dialog.visible = true;
   dialog.visible = true;
   dialog.title = "修改商品池推送记录";
   dialog.title = "修改商品池推送记录";
@@ -280,7 +263,7 @@ const submitForm = () => {
 const handleDelete = async (row?: PushPoolLogVO) => {
 const handleDelete = async (row?: PushPoolLogVO) => {
   const _ids = row?.id || ids.value;
   const _ids = row?.id || ids.value;
   await proxy?.$modal.confirm('是否确认删除商品池推送记录编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
   await proxy?.$modal.confirm('是否确认删除商品池推送记录编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
-  await delPushPoolLog(_ids, queryParams.value.itemKey);
+  await delPushPoolLog(_ids, queryParams.value.itemId);
   proxy?.$modal.msgSuccess("删除成功");
   proxy?.$modal.msgSuccess("删除成功");
   await getList();
   await getList();
 }
 }
@@ -293,7 +276,10 @@ const handleExport = () => {
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
-  // 先加载项目列表,内部会自动触发查询
-  getItemList();
+  // 从缓存获取项目Key并初始化
+  initProjectKey();
+  if (queryParams.value.itemId) {
+    getList();
+  }
 });
 });
 </script>
 </script>

+ 62 - 19
src/views/index.vue

@@ -5,9 +5,13 @@
         <span class="title-text">概览数据看板</span>
         <span class="title-text">概览数据看板</span>
         <span class="title-desc">Overview Dashboard</span>
         <span class="title-desc">Overview Dashboard</span>
       </div>
       </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 class="dashboard-actions">
+        <div class="current-project-info">
+          <span class="project-label">当前项目:</span>
+          <span class="project-name">{{ currentProjectName }}</span>
+        </div>
+        <el-button type="warning" plain icon="Switch" @click="handleSwitchProject">切换项目</el-button>
+      </div>
     </div>
     </div>
 
 
     <!-- Stats Cards -->
     <!-- Stats Cards -->
@@ -187,14 +191,17 @@
 import { ref, reactive, computed, onMounted, onBeforeUnmount, watch } from 'vue';
 import { ref, reactive, computed, onMounted, onBeforeUnmount, watch } from 'vue';
 import { Box, Goods, PriceTag, Tickets } from '@element-plus/icons-vue';
 import { Box, Goods, PriceTag, Tickets } from '@element-plus/icons-vue';
 import * as echarts from 'echarts';
 import * as echarts from 'echarts';
+import cache from '@/plugins/cache';
+import { useAppStore } from '@/store/modules/app';
+
+const router = useRouter();
+const appStore = useAppStore();
 
 
 type ProjectKey = 'zhongzhi' | 'zhongche';
 type ProjectKey = 'zhongzhi' | 'zhongche';
 
 
+// 当前项目信息
+const currentProjectName = ref<string>('');
 const activeProject = ref<ProjectKey>('zhongzhi');
 const activeProject = ref<ProjectKey>('zhongzhi');
-const projectOptions: Array<{ label: string; value: ProjectKey }> = [
-  { label: '中直', value: 'zhongzhi' },
-  { label: '中车', value: 'zhongche' }
-];
 
 
 const dashboardData = reactive<Record<ProjectKey, any>>({
 const dashboardData = reactive<Record<ProjectKey, any>>({
   zhongzhi: {
   zhongzhi: {
@@ -384,11 +391,36 @@ const resizeCharts = () => {
   afterSalePieChart?.resize();
   afterSalePieChart?.resize();
 };
 };
 
 
+/** 初始化项目信息(从缓存获取) */
+const initProjectInfo = () => {
+  const currentProject = cache.local.getJSON('currentProject');
+  if (currentProject) {
+    currentProjectName.value = currentProject.itemName || '';
+    // 根据 itemKey 确定当前项目
+    const itemKey = currentProject.itemKey;
+    if (itemKey === 'zhongzhi' || itemKey === 'zhongche') {
+      activeProject.value = itemKey;
+    }
+  }
+};
+
 watch(activeProject, () => {
 watch(activeProject, () => {
   renderCharts();
   renderCharts();
 });
 });
 
 
+/** 切换项目 */
+const handleSwitchProject = () => {
+  // 清除当前项目缓存
+  cache.local.remove('currentProjectId');
+  cache.local.remove('currentProject');
+  // 隐藏侧边栏
+  appStore.toggleSideBarHide(true);
+  // 跳转到项目选择页面
+  router.push('/project');
+};
+
 onMounted(() => {
 onMounted(() => {
+  initProjectInfo();
   renderCharts();
   renderCharts();
   window.addEventListener('resize', resizeCharts);
   window.addEventListener('resize', resizeCharts);
 });
 });
@@ -427,18 +459,29 @@ onMounted(() => {
     }
     }
   }
   }
 
 
-  .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);
-      }
+  .dashboard-actions {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+  }
+
+  .current-project-info {
+    display: flex;
+    align-items: center;
+    background: #fff;
+    padding: 8px 20px;
+    border-radius: 20px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+    
+    .project-label {
+      font-size: 14px;
+      color: #909399;
+    }
+    
+    .project-name {
+      font-size: 15px;
+      font-weight: 600;
+      color: #303133;
     }
     }
   }
   }
 }
 }

+ 96 - 108
src/views/product/base/index.vue

@@ -20,7 +20,6 @@
               <el-col :span="24" class="text-left">
               <el-col :span="24" class="text-left">
                 <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                 <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                 <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                 <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-                <el-button type="success" :disabled="multiple" @click="handleBatchAddToExternal">添加到对接产品库</el-button>
               </el-col>
               </el-col>
             </el-row>
             </el-row>
           </el-form>
           </el-form>
@@ -32,7 +31,7 @@
 
 
 
 
     <el-card shadow="never">
     <el-card shadow="never">
-      <el-table ref="baseTableRef" v-loading="loading" border :data="baseList" @selection-change="handleSelectionChange">
+      <el-table v-loading="loading" border :data="baseList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="图片" align="center" prop="productImage" width="100" fixed="left">
         <el-table-column label="图片" align="center" prop="productImage" width="100" fixed="left">
           <template #default="scope">
           <template #default="scope">
@@ -73,6 +72,12 @@
             <span>{{ scope.row.minOrderQuantity || '1' }}</span>
             <span>{{ scope.row.minOrderQuantity || '1' }}</span>
           </template>
           </template>
         </el-table-column>
         </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">
         <el-table-column label="操作" align="center" width="180" fixed="right">
           <template #default="scope">
           <template #default="scope">
             <el-button link type="primary" @click="handleConnect(scope.row)">对接</el-button>
             <el-button link type="primary" @click="handleConnect(scope.row)">对接</el-button>
@@ -107,12 +112,12 @@
             @change="handleCategoryChange"
             @change="handleCategoryChange"
           />
           />
         </el-form-item>
         </el-form-item>
+        <el-form-item label="折扣率:">
+          <el-text type="info">{{ discountRateDisplay }}</el-text>
+        </el-form-item>
         <el-form-item label="第三方平台售价:" prop="externalPrice">
         <el-form-item label="第三方平台售价:" prop="externalPrice">
           <el-input-number v-model="connectForm.externalPrice" :precision="2" :min="0" :controls="false" style="width: 100%" />
           <el-input-number v-model="connectForm.externalPrice" :precision="2" :min="0" :controls="false" style="width: 100%" />
         </el-form-item>
         </el-form-item>
-        <el-form-item label="折扣率:">
-          <el-text type="info">{{ discountRate }}</el-text>
-        </el-form-item>
         <el-form-item label="市场价:">
         <el-form-item label="市场价:">
           <el-text>{{ connectForm.marketPrice }}</el-text>
           <el-text>{{ connectForm.marketPrice }}</el-text>
         </el-form-item>
         </el-form-item>
@@ -120,7 +125,7 @@
           <el-text>{{ connectForm.memberPrice }}</el-text>
           <el-text>{{ connectForm.memberPrice }}</el-text>
         </el-form-item>
         </el-form-item>
         <el-form-item label="起订量:">
         <el-form-item label="起订量:">
-          <el-input-number v-model="connectForm.minOrderQuantity" :min="1" :controls="false" style="width: 100%" disabled/>
+          <el-input-number v-model="connectForm.minOrderQuantity" :min="1" :controls="false" style="width: 100%" />
         </el-form-item>
         </el-form-item>
         <el-form-item label="最低售价:">
         <el-form-item label="最低售价:">
           <el-text type="danger">¥{{ connectForm.minSellingPrice }}</el-text>
           <el-text type="danger">¥{{ connectForm.minSellingPrice }}</el-text>
@@ -137,44 +142,26 @@
         </div>
         </div>
       </template>
       </template>
     </el-dialog>
     </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>
   </div>
 </template>
 </template>
 
 
 <script setup name="Base" lang="ts">
 <script setup name="Base" lang="ts">
-import { listBase, getBase, delBase, brandList, categoryTree } from '@/api/product/base';
+import { getProjectProductPage, getBase, delBase, brandList, categoryTree } from '@/api/product/base';
 import { BaseVO, BaseQuery, BaseForm } from '@/api/product/base/types';
 import { BaseVO, BaseQuery, BaseForm } from '@/api/product/base/types';
 import { BrandVO } from '@/api/product/brand/types';
 import { BrandVO } from '@/api/product/brand/types';
 import { categoryTreeVO } from '@/api/product/category/types';
 import { categoryTreeVO } from '@/api/product/category/types';
-import { addProduct, batchInsertExternalProduct } from '@/api/external/product';
+import { addProduct, updateProduct, getProductByProductId } from '@/api/external/product';
 import { ProductForm } from '@/api/external/product/types';
 import { ProductForm } from '@/api/external/product/types';
-import { listProductCategory } from '@/api/external/productCategory';
+import { listProductCategory, getProductCategory } from '@/api/external/productCategory';
 import { ProductCategoryVO } from '@/api/external/productCategory/types';
 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';
 import { useRoute, useRouter } from 'vue-router';
+import cache from '@/plugins/cache';
 
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const router = useRouter();
 const router = useRouter();
 const route = useRoute();
 const route = useRoute();
 
 
 const baseList = ref<BaseVO[]>([]);
 const baseList = ref<BaseVO[]>([]);
-const selectedRows = ref<BaseVO[]>([]);
 const buttonLoading = ref(false);
 const buttonLoading = ref(false);
 const loading = ref(true);
 const loading = ref(true);
 const showSearch = ref(true);
 const showSearch = ref(true);
@@ -183,7 +170,6 @@ const single = ref(true);
 const multiple = ref(true);
 const multiple = ref(true);
 const total = ref(0);
 const total = ref(0);
 const categoryOptions = ref<categoryTreeVO[]>([]);
 const categoryOptions = ref<categoryTreeVO[]>([]);
-const itemList = ref<ItemVO[]>([]);
 const hasMore = ref(true); // 是否还有更多数据
 const hasMore = ref(true); // 是否还有更多数据
 // 页面历史记录,存储每页的第一个id和最后一个id,用于支持双向翻页
 // 页面历史记录,存储每页的第一个id和最后一个id,用于支持双向翻页
 const pageHistory = ref([]);
 const pageHistory = ref([]);
@@ -200,8 +186,6 @@ const statistics = ref({
 const queryFormRef = ref<ElFormInstance>();
 const queryFormRef = ref<ElFormInstance>();
 const baseFormRef = ref<ElFormInstance>();
 const baseFormRef = ref<ElFormInstance>();
 const connectFormRef = ref<ElFormInstance>();
 const connectFormRef = ref<ElFormInstance>();
-const batchFormRef = ref<ElFormInstance>();
-const baseTableRef = ref();
 
 
 const dialog = reactive<DialogOption>({
 const dialog = reactive<DialogOption>({
   visible: false,
   visible: false,
@@ -215,13 +199,15 @@ const connectDialog = reactive({
 
 
 // 对接表单
 // 对接表单
 const connectForm = ref({
 const connectForm = ref({
+  id: undefined as string | number | undefined, // 对接记录ID(编辑时使用)
   productId: undefined as string | number | undefined,
   productId: undefined as string | number | undefined,
   externalCategoryId: undefined as string | number | undefined,
   externalCategoryId: undefined as string | number | undefined,
   externalPrice: 0,
   externalPrice: 0,
   marketPrice: 0,
   marketPrice: 0,
   memberPrice: 0,
   memberPrice: 0,
   minSellingPrice: 0,
   minSellingPrice: 0,
-  minOrderQuantity: 1
+  minOrderQuantity: 1,
+  discountRate: 0 // 折扣率
 });
 });
 
 
 // 外部分类列表(懒加载模式只需要初始化为空数组)
 // 外部分类列表(懒加载模式只需要初始化为空数组)
@@ -241,15 +227,19 @@ const cascaderProps = {
       // level > 0 表示子节点,根据父节点ID加载子分类
       // level > 0 表示子节点,根据父节点ID加载子分类
       const parentId = level === 0 ? 0 : value;
       const parentId = level === 0 ? 0 : value;
       
       
-      const res = await listProductCategory({ parentId });
+      // 从缓存中获取项目ID
+      const currentProject = cache.local.getJSON('currentProject');
+      const itemId = currentProject?.id;
+
+      const res = await listProductCategory({ parentId, itemId });
       const responseData = (res as any).rows || res.data || [];
       const responseData = (res as any).rows || res.data || [];
-      
+
       // 为每个节点添加 leaf 属性,判断是否有子节点
       // 为每个节点添加 leaf 属性,判断是否有子节点
       const nodes = responseData.map((item: ProductCategoryVO) => ({
       const nodes = responseData.map((item: ProductCategoryVO) => ({
         ...item,
         ...item,
         leaf: !item.hasChildren && level >= 2 // 如果没有子节点或已经是三级分类,则标记为叶子节点
         leaf: !item.hasChildren && level >= 2 // 如果没有子节点或已经是三级分类,则标记为叶子节点
       }));
       }));
-      
+
       resolve(nodes);
       resolve(nodes);
     } catch (error) {
     } catch (error) {
       console.error('加载分类数据失败:', error);
       console.error('加载分类数据失败:', error);
@@ -262,20 +252,6 @@ const cascaderProps = {
 // 对接加载状态
 // 对接加载状态
 const connectLoading = ref(false);
 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 = {
 const connectRules = {
   externalCategoryId: [{ required: true, message: '请选择第三方产品分类', trigger: 'change' }],
   externalCategoryId: [{ required: true, message: '请选择第三方产品分类', trigger: 'change' }],
@@ -296,13 +272,12 @@ const connectRules = {
   ]
   ]
 };
 };
 
 
-// 计算折扣率
-const discountRate = computed(() => {
-  if (!connectForm.value.externalCategoryId) {
+// 折扣率显示值(从分类获取)
+const discountRateDisplay = computed(() => {
+  if (!connectForm.value.discountRate && connectForm.value.discountRate !== 0) {
     return '请先选择第三方产品分类';
     return '请先选择第三方产品分类';
   }
   }
-  const rate = ((connectForm.value.externalPrice / connectForm.value.marketPrice) * 100).toFixed(2);
-  return rate + '%';
+  return (connectForm.value.discountRate * 100).toFixed(2) + '%';
 });
 });
 
 
 const initFormData: BaseForm = {
 const initFormData: BaseForm = {
@@ -347,6 +322,7 @@ const data = reactive<PageData<BaseForm, BaseQuery>>({
     productStatus: undefined,
     productStatus: undefined,
     lastSeenId: undefined, // 游标分页的lastSeenId
     lastSeenId: undefined, // 游标分页的lastSeenId
     way: undefined,
     way: undefined,
+    auditStatus: 2,
     params: {}
     params: {}
   },
   },
   rules: {
   rules: {
@@ -379,6 +355,12 @@ const getList = async () => {
     const params = { ...queryParams.value };
     const params = { ...queryParams.value };
     const currentPageNum = queryParams.value.pageNum;
     const currentPageNum = queryParams.value.pageNum;
 
 
+    // 从缓存中获取项目ID
+    const currentProject = cache.local.getJSON('currentProject');
+    if (currentProject?.id) {
+      params.itemId = currentProject.id;
+    }
+
     // 第一页不需要游标参数
     // 第一页不需要游标参数
     if (currentPageNum === 1) {
     if (currentPageNum === 1) {
       delete params.lastSeenId;
       delete params.lastSeenId;
@@ -402,7 +384,7 @@ const getList = async () => {
       }
       }
     }
     }
 
 
-    const res = await listBase(params);
+    const res = await getProjectProductPage(params);
     baseList.value = res.rows || [];
     baseList.value = res.rows || [];
 
 
     // 判断是否还有更多数据
     // 判断是否还有更多数据
@@ -497,54 +479,10 @@ const resetQuery = () => {
 /** 多选框选中数据 */
 /** 多选框选中数据 */
 const handleSelectionChange = (selection: BaseVO[]) => {
 const handleSelectionChange = (selection: BaseVO[]) => {
   ids.value = selection.map((item) => item.id);
   ids.value = selection.map((item) => item.id);
-  selectedRows.value = selection;
   single.value = selection.length != 1;
   single.value = selection.length != 1;
   multiple.value = !selection.length;
   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 = () => {
 const handleAdd = () => {
   router.push('/product/base/add');
   router.push('/product/base/add');
@@ -585,31 +523,74 @@ const handleView = (row: BaseVO) => {
 const handleConnect = async (row: BaseVO) => {
 const handleConnect = async (row: BaseVO) => {
   // 重置表单
   // 重置表单
   connectForm.value = {
   connectForm.value = {
+    id: undefined,
     productId: row.id,
     productId: row.id,
     externalCategoryId: undefined,
     externalCategoryId: undefined,
     externalPrice: 0,
     externalPrice: 0,
     marketPrice: row.marketPrice || 0,
     marketPrice: row.marketPrice || 0,
     memberPrice: row.memberPrice || 0,
     memberPrice: row.memberPrice || 0,
     minSellingPrice: row.minSellingPrice || 0,
     minSellingPrice: row.minSellingPrice || 0,
-    minOrderQuantity: row.minOrderQuantity || 1
+    minOrderQuantity: row.minOrderQuantity || 1,
+    discountRate: 0
   };
   };
 
 
-  // 懒加载模式不需要预先加载数据,打开弹框即可
+  // 查询是否已有对接信息
+  try {
+    const res = await getProductByProductId(row.id);
+    if (res.data) {
+      // 已有对接信息,回显数据
+      const existData = res.data;
+      connectForm.value.id = existData.id;
+      connectForm.value.externalCategoryId = existData.externalCategoryId;
+      connectForm.value.externalPrice = existData.externalPrice || 0;
+      
+      // 如果有分类,获取折扣率
+      if (existData.externalCategoryId) {
+        const currentProject = cache.local.getJSON('currentProject');
+        const itemId = currentProject?.id;
+        const categoryRes = await getProductCategory(existData.externalCategoryId, itemId);
+        if (categoryRes.data) {
+          connectForm.value.discountRate = categoryRes.data.discountRate || 0;
+        }
+      }
+    }
+  } catch (error) {
+    console.log('暂无对接信息,将新增');
+  }
+
   connectDialog.visible = true;
   connectDialog.visible = true;
 };
 };
 
 
 
 
 
 
 /** 分类变化处理 */
 /** 分类变化处理 */
-const handleCategoryChange = (value: string | number) => {
+const handleCategoryChange = async (value: string | number) => {
   if (!value) {
   if (!value) {
     connectForm.value.externalPrice = 0;
     connectForm.value.externalPrice = 0;
+    connectForm.value.discountRate = 0;
     return;
     return;
   }
   }
 
 
-  // 根据选择的分类计算第三方价格(这里可以根据实际业务逻辑调整)
-  // 默认使用会员价作为第三方价格
-  connectForm.value.externalPrice = connectForm.value.memberPrice;
+  // 获取选中分类的详情,包含折扣率
+  try {
+    const currentProject = cache.local.getJSON('currentProject');
+    const itemId = currentProject?.id;
+    const res = await getProductCategory(value, itemId);
+    const category = res.data;
+    
+    // 设置折扣率
+    const discountRate = category?.discountRate || 0;
+    connectForm.value.discountRate = discountRate;
+    
+    // 第三方价格默认为平台价 * 折扣率
+    const defaultPrice = Number((connectForm.value.memberPrice * discountRate).toFixed(2));
+    connectForm.value.externalPrice = defaultPrice;
+  } catch (error) {
+    console.error('获取分类详情失败:', error);
+    // 如果获取失败,默认使用会员价作为第三方价格
+    connectForm.value.externalPrice = connectForm.value.memberPrice;
+    connectForm.value.discountRate = 0;
+  }
 };
 };
 
 
 /** 取消对接 */
 /** 取消对接 */
@@ -627,14 +608,22 @@ const submitConnect = async () => {
       connectLoading.value = true;
       connectLoading.value = true;
       try {
       try {
         const data: ProductForm = {
         const data: ProductForm = {
+          id: connectForm.value.id,
           productId: connectForm.value.productId,
           productId: connectForm.value.productId,
           externalCategoryId: connectForm.value.externalCategoryId,
           externalCategoryId: connectForm.value.externalCategoryId,
           externalPrice: connectForm.value.externalPrice,
           externalPrice: connectForm.value.externalPrice,
+          minOrderQuantity: connectForm.value.minOrderQuantity,
           pushStatus: 0 // 0=未推送
           pushStatus: 0 // 0=未推送
         };
         };
 
 
-        await addProduct(data);
-        proxy?.$modal.msgSuccess('对接成功');
+        // 判断是新增还是编辑
+        if (connectForm.value.id) {
+          await updateProduct(data);
+          proxy?.$modal.msgSuccess('编辑成功');
+        } else {
+          await addProduct(data);
+          proxy?.$modal.msgSuccess('对接成功');
+        }
         connectDialog.visible = false;
         connectDialog.visible = false;
 
 
         // 刷新列表
         // 刷新列表
@@ -697,6 +686,5 @@ const getCategoryTree = async () => {
 onMounted(() => {
 onMounted(() => {
   getList();
   getList();
   getCategoryTree();
   getCategoryTree();
-  getItemList();
 });
 });
 </script>
 </script>

+ 11 - 10
src/views/product/brand/edit.vue

@@ -32,12 +32,12 @@
           </el-col>
           </el-col>
           <el-col :span="12">
           <el-col :span="12">
             <el-form-item label="品牌推荐系数" prop="recommendValue">
             <el-form-item label="品牌推荐系数" prop="recommendValue">
-              <el-input-number 
-                v-model="form.recommendValue" 
-                :min="0" 
-                :max="9999" 
-                controls-position="right" 
-                style="width: 100%" 
+              <el-input-number
+                v-model="form.recommendValue"
+                :min="0"
+                :max="9999"
+                controls-position="right"
+                style="width: 100%"
                 placeholder="请输入推荐系数"
                 placeholder="请输入推荐系数"
               />
               />
             </el-form-item>
             </el-form-item>
@@ -49,7 +49,7 @@
         </el-form-item>
         </el-form-item>
 
 
         <el-form-item label="品牌LOGO" prop="brandLogo">
         <el-form-item label="品牌LOGO" prop="brandLogo">
-          <image-upload v-model="form.brandLogo" />
+          <upload-image v-model="form.brandLogo" :limit="1" width="120px" height="120px" imageText="添加图片" />
         </el-form-item>
         </el-form-item>
 
 
         <el-form-item label="品牌故事" prop="brandStory">
         <el-form-item label="品牌故事" prop="brandStory">
@@ -77,18 +77,18 @@
         <el-row :gutter="20">
         <el-row :gutter="20">
           <el-col :span="12">
           <el-col :span="12">
             <el-form-item label="上传营业执照" prop="license">
             <el-form-item label="上传营业执照" prop="license">
-              <image-upload v-model="form.license" />
+              <upload-image v-model="form.license" :limit="1" width="120px" height="120px" imageText="添加图片" />
             </el-form-item>
             </el-form-item>
           </el-col>
           </el-col>
           <el-col :span="12">
           <el-col :span="12">
             <el-form-item label="上传商标注册证" prop="registrationCertificate">
             <el-form-item label="上传商标注册证" prop="registrationCertificate">
-              <image-upload v-model="form.registrationCertificate" />
+              <upload-image v-model="form.registrationCertificate" :limit="1" width="120px" height="120px" imageText="添加图片" />
             </el-form-item>
             </el-form-item>
           </el-col>
           </el-col>
         </el-row>
         </el-row>
 
 
         <el-form-item label="到期时间" prop="expireTime">
         <el-form-item label="到期时间" prop="expireTime">
-          <el-date-picker 
+          <el-date-picker
             v-model="form.expireTime"
             v-model="form.expireTime"
             type="datetime"
             type="datetime"
             value-format="YYYY-MM-DD HH:mm:ss"
             value-format="YYYY-MM-DD HH:mm:ss"
@@ -131,6 +131,7 @@
 <script setup lang="ts" name="BrandEdit">
 <script setup lang="ts" name="BrandEdit">
 import { getBrand, addBrand, updateBrand } from '@/api/product/brand';
 import { getBrand, addBrand, updateBrand } from '@/api/product/brand';
 import { BrandForm } from '@/api/product/brand/types';
 import { BrandForm } from '@/api/product/brand/types';
+import UploadImage from '@/components/upload-image/index.vue';
 
 
 const route = useRoute();
 const route = useRoute();
 const router = useRouter();
 const router = useRouter();

+ 8 - 8
src/views/product/warehouseInventory/index.vue

@@ -44,17 +44,17 @@
       <template #header>
       <template #header>
         <el-row :gutter="10" class="mb8">
         <el-row :gutter="10" class="mb8">
           <el-col :span="1.5">
           <el-col :span="1.5">
-            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['product:warehouseInventory:add']">新增</el-button>
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" >新增</el-button>
           </el-col>
           </el-col>
           <el-col :span="1.5">
           <el-col :span="1.5">
-            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['product:warehouseInventory:edit']">修改</el-button>
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" >修改</el-button>
           </el-col>
           </el-col>
           <el-col :span="1.5">
           <el-col :span="1.5">
-            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['product:warehouseInventory:remove']">删除</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['product:warehouseInventory:export']">导出</el-button>
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" >删除</el-button>
           </el-col>
           </el-col>
+<!--          <el-col :span="1.5">-->
+<!--            <el-button type="warning" plain icon="Download" @click="handleExport" >导出</el-button>-->
+<!--          </el-col>-->
           <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
           <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
         </el-row>
         </el-row>
       </template>
       </template>
@@ -75,10 +75,10 @@
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template #default="scope">
           <template #default="scope">
             <el-tooltip content="修改" placement="top">
             <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['product:warehouseInventory:edit']"></el-button>
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" ></el-button>
             </el-tooltip>
             </el-tooltip>
             <el-tooltip content="删除" placement="top">
             <el-tooltip content="删除" placement="top">
-              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['product:warehouseInventory:remove']"></el-button>
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" ></el-button>
             </el-tooltip>
             </el-tooltip>
           </template>
           </template>
         </el-table-column>
         </el-table-column>

+ 0 - 1
src/views/system/tenant/index.vue

@@ -210,7 +210,6 @@ import {
   syncTenantDict,
   syncTenantDict,
   syncTenantConfig
   syncTenantConfig
 } from '@/api/system/tenant';
 } from '@/api/system/tenant';
-import FileSelector from '@/components/FileSelector/index.vue';
 import { selectTenantPackage } from '@/api/system/tenantPackage';
 import { selectTenantPackage } from '@/api/system/tenantPackage';
 import { useUserStore } from '@/store/modules/user';
 import { useUserStore } from '@/store/modules/user';
 import { TenantForm, TenantQuery, TenantVO } from '@/api/system/tenant/types';
 import { TenantForm, TenantQuery, TenantVO } from '@/api/system/tenant/types';