Explorar el Código

feat(product): 添加商品轮播图功能并优化品牌搜索组件

- 在商品基础信息页面添加轮播图上传和展示功能
- 实现品牌下拉选择器的远程搜索功能,提升用户体验
- 将税率字段改为下拉选择器,从系统税率列表获取选项
- 优化价格输入字段,添加格式化为两位小数的功能
- 修复UPC条码输入限制,只允许数字输入
- 更新生产环境API基础路径配置
- 调整产品经理字段标签,原产品性质字段改为产品经理选择
- 优化表格列宽度和布局,改进商品信息展示效果
- 添加协议产品价格修改功能,支持批量价格更新
- 修复商品管理页面图片字段映射错误问题
肖路 hace 1 mes
padre
commit
f31d210faa

+ 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 = 'https://one.yoe365.com'
 
 
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 VITE_BUILD_COMPRESS = gzip
 VITE_BUILD_COMPRESS = gzip

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

@@ -199,3 +199,13 @@ export const changeProductType = (data: BaseForm) => {
   });
   });
 };
 };
 
 
+/**
+ * 获取税率列表
+ */
+export const getTaxRateList = () => {
+  return request({
+    url: '/system/taxrate/list',
+    method: 'get'
+  });
+};
+

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

@@ -243,6 +243,11 @@ export interface BaseVO {
    */
    */
   attributesList?: string;
   attributesList?: string;
 
 
+  /**
+   * 商品轮播图URL(逗号分隔)
+   */
+  imageUrl?: string;
+
 }
 }
 
 
 export interface BaseForm extends BaseEntity {
 export interface BaseForm extends BaseEntity {
@@ -576,6 +581,11 @@ export interface BaseForm extends BaseEntity {
    */
    */
   shelfComments?: string;
   shelfComments?: string;
 
 
+  /**
+   * 商品轮播图URL(逗号分隔)
+   */
+  imageUrl?: string;
+
 }
 }
 
 
 export interface BaseQuery extends PageQuery {
 export interface BaseQuery extends PageQuery {

+ 19 - 7
src/api/product/products/index.ts

@@ -1,6 +1,6 @@
 import request from '@/utils/request';
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
 import { AxiosPromise } from 'axios';
-import { ProductsVO, ProductsForm, ProductsQuery } from '@/api/product/products/types';
+import { ProductsVO, ProductsForm, ProductsQuery } from '@/api/product/protocolProducts/types';
 import { BaseQuery, BaseVO } from '@/api/product/base/types';
 import { BaseQuery, BaseVO } from '@/api/product/base/types';
 
 
 /**
 /**
@@ -11,7 +11,7 @@ import { BaseQuery, BaseVO } from '@/api/product/base/types';
 
 
 export const listProducts = (query?: ProductsQuery): AxiosPromise<BaseVO[]> => {
 export const listProducts = (query?: ProductsQuery): AxiosPromise<BaseVO[]> => {
   return request({
   return request({
-    url: '/product/products/list',
+    url: '/product/protocolProducts/list',
     method: 'get',
     method: 'get',
     params: query
     params: query
   });
   });
@@ -23,7 +23,7 @@ export const listProducts = (query?: ProductsQuery): AxiosPromise<BaseVO[]> => {
 * */
 * */
 export const getProtocolProductIds = (protocolId: string | number): AxiosPromise<string[]> => {
 export const getProtocolProductIds = (protocolId: string | number): AxiosPromise<string[]> => {
   return request({
   return request({
-    url: '/product/products/getProductIds/' + protocolId,
+    url: '/product/protocolProducts/getProductIds/' + protocolId,
     method: 'get'
     method: 'get'
   });
   });
 }
 }
@@ -37,7 +37,7 @@ export const getProtocolProductIds = (protocolId: string | number): AxiosPromise
  */
  */
 export const getProducts = (id: string | number): AxiosPromise<ProductsVO> => {
 export const getProducts = (id: string | number): AxiosPromise<ProductsVO> => {
   return request({
   return request({
-    url: '/product/products/' + id,
+    url: '/product/protocolProducts/' + id,
     method: 'get'
     method: 'get'
   });
   });
 };
 };
@@ -48,7 +48,7 @@ export const getProducts = (id: string | number): AxiosPromise<ProductsVO> => {
  */
  */
 export const addProducts = (data: ProductsForm) => {
 export const addProducts = (data: ProductsForm) => {
   return request({
   return request({
-    url: '/product/products',
+    url: '/product/protocolProducts',
     method: 'post',
     method: 'post',
     data: data
     data: data
   });
   });
@@ -60,7 +60,7 @@ export const addProducts = (data: ProductsForm) => {
  */
  */
 export const updateProducts = (data: ProductsForm) => {
 export const updateProducts = (data: ProductsForm) => {
   return request({
   return request({
-    url: '/product/products',
+    url: '/product/protocolProducts',
     method: 'put',
     method: 'put',
     data: data
     data: data
   });
   });
@@ -72,7 +72,19 @@ export const updateProducts = (data: ProductsForm) => {
  */
  */
 export const delProducts = (id: string | number | Array<string | number>) => {
 export const delProducts = (id: string | number | Array<string | number>) => {
   return request({
   return request({
-    url: '/product/products/' + id,
+    url: '/product/protocolProducts/' + id,
     method: 'delete'
     method: 'delete'
   });
   });
 };
 };
+
+/**
+ * 修改协议产品价格
+ * @param data
+ */
+export const updateProductsPrice = (data: ProductsForm) => {
+  return request({
+    url: '/product/protocolProducts/updateProtocolPrice',
+    method: 'put',
+    data: data
+  })
+}

+ 5 - 5
src/api/product/protocolInfo/index.ts

@@ -10,7 +10,7 @@ import { InfoVO, InfoForm, InfoQuery } from '@/api/product/protocolInfo/types';
 
 
 export const listInfo = (query?: InfoQuery): AxiosPromise<InfoVO[]> => {
 export const listInfo = (query?: InfoQuery): AxiosPromise<InfoVO[]> => {
   return request({
   return request({
-    url: '/product/info/list',
+    url: '/product/protocolInfo/list',
     method: 'get',
     method: 'get',
     params: query
     params: query
   });
   });
@@ -24,7 +24,7 @@ export const listInfo = (query?: InfoQuery): AxiosPromise<InfoVO[]> => {
  */
  */
 export const getInfo = (id: string | number): AxiosPromise<InfoVO> => {
 export const getInfo = (id: string | number): AxiosPromise<InfoVO> => {
   return request({
   return request({
-    url: '/product/info/' + id,
+    url: '/product/protocolInfo/' + id,
     method: 'get'
     method: 'get'
   });
   });
 };
 };
@@ -35,7 +35,7 @@ export const getInfo = (id: string | number): AxiosPromise<InfoVO> => {
  */
  */
 export const addInfo = (data: InfoForm) => {
 export const addInfo = (data: InfoForm) => {
   return request({
   return request({
-    url: '/product/info',
+    url: '/product/protocolInfo',
     method: 'post',
     method: 'post',
     data: data
     data: data
   });
   });
@@ -47,7 +47,7 @@ export const addInfo = (data: InfoForm) => {
  */
  */
 export const updateInfo = (data: InfoForm) => {
 export const updateInfo = (data: InfoForm) => {
   return request({
   return request({
-    url: '/product/info',
+    url: '/product/protocolInfo',
     method: 'put',
     method: 'put',
     data: data
     data: data
   });
   });
@@ -59,7 +59,7 @@ export const updateInfo = (data: InfoForm) => {
  */
  */
 export const delInfo = (id: string | number | Array<string | number>) => {
 export const delInfo = (id: string | number | Array<string | number>) => {
   return request({
   return request({
-    url: '/product/info/' + id,
+    url: '/product/protocolInfo/' + id,
     method: 'delete'
     method: 'delete'
   });
   });
 };
 };

+ 214 - 60
src/views/product/base/add.vue

@@ -213,6 +213,7 @@
                     placeholder="请输入UPC(69)条码"
                     placeholder="请输入UPC(69)条码"
                     maxlength="20"
                     maxlength="20"
                     show-word-limit
                     show-word-limit
+                    @input="handleUpcInput"
                   />
                   />
                 </el-form-item>
                 </el-form-item>
               </el-col>
               </el-col>
@@ -245,16 +246,23 @@
               <!-- 商品品牌 -->
               <!-- 商品品牌 -->
                <el-col :span="12">
                <el-col :span="12">
                 <el-form-item label="商品品牌:" prop="brandId" required>
                 <el-form-item label="商品品牌:" prop="brandId" required>
-                  <el-select-v2
+                  <el-select
                     v-model="productForm.brandId"
                     v-model="productForm.brandId"
-                    :options="brandOptionsFormatted"
-                    placeholder="请选择商品品牌"
-                    clearable
+                    placeholder="请输入品牌名称搜索"
                     filterable
                     filterable
-                    class="w-full"
+                    remote
+                    clearable
+                    :remote-method="handleBrandSearch"
                     :loading="brandLoading"
                     :loading="brandLoading"
-                    @visible-change="handleBrandVisibleChange"
-                  />
+                    class="w-full"
+                  >
+                    <el-option
+                      v-for="item in brandOptions"
+                      :key="item.id"
+                      :label="item.brandName"
+                      :value="item.id"
+                    />
+                  </el-select>
                 </el-form-item>
                 </el-form-item>
                </el-col>
                </el-col>
 
 
@@ -277,7 +285,14 @@
             <el-row :gutter="20">
             <el-row :gutter="20">
               <el-col :span="12">
               <el-col :span="12">
                 <el-form-item label="税率:" required>
                 <el-form-item label="税率:" required>
-                  <el-input v-model="productForm.taxRate" placeholder="请输入商品税率" type="number" />
+                  <el-select v-model="productForm.taxRate" placeholder="请选择税率" clearable class="w-full">
+                    <el-option
+                      v-for="option in taxRateOptions"
+                      :key="option.id"
+                      :label="option.taxrateName"
+                      :value="option.taxrate"
+                    />
+                  </el-select>
                 </el-form-item>
                 </el-form-item>
               </el-col>
               </el-col>
               <el-col :span="12">
               <el-col :span="12">
@@ -373,7 +388,7 @@
                 <el-option
                 <el-option
                   v-for="option in supplierOptions"
                   v-for="option in supplierOptions"
                   :key="option.id"
                   :key="option.id"
-                  :label="option.enterpriseName"
+                  :label="`${option.supplierNo},${option.enterpriseName}`"
                   :value="String(option.id)"
                   :value="String(option.id)"
                 />
                 />
               </el-select>
               </el-select>
@@ -426,6 +441,7 @@
                     v-model="productForm.midRangePrice"
                     v-model="productForm.midRangePrice"
                     type="number"
                     type="number"
                     placeholder="请输入市场价"
                     placeholder="请输入市场价"
+                    @blur="formatPrice('midRangePrice')"
                   />
                   />
                 </el-form-item>
                 </el-form-item>
               </el-col>
               </el-col>
@@ -435,6 +451,7 @@
                     v-model="productForm.standardPrice"
                     v-model="productForm.standardPrice"
                     type="number"
                     type="number"
                     placeholder="请输入平台售价"
                     placeholder="请输入平台售价"
+                    @blur="formatPrice('standardPrice')"
                   />
                   />
                 </el-form-item>
                 </el-form-item>
               </el-col>
               </el-col>
@@ -444,6 +461,7 @@
                     v-model="productForm.certificatePrice"
                     v-model="productForm.certificatePrice"
                     type="number"
                     type="number"
                     placeholder="请输入最低售价"
                     placeholder="请输入最低售价"
+                    @blur="formatPrice('certificatePrice')"
                   />
                   />
                 </el-form-item>
                 </el-form-item>
               </el-col>
               </el-col>
@@ -482,6 +500,7 @@
                     v-model="productForm.purchasePrice"
                     v-model="productForm.purchasePrice"
                     type="number"
                     type="number"
                     placeholder="请输入采购价"
                     placeholder="请输入采购价"
+                    @blur="formatPrice('purchasePrice')"
                   />
                   />
                 </el-form-item>
                 </el-form-item>
               </el-col>
               </el-col>
@@ -491,6 +510,7 @@
                     v-model="productForm.estimatedPurchasePrice"
                     v-model="productForm.estimatedPurchasePrice"
                     type="number"
                     type="number"
                     placeholder="请输入暂估采购价"
                     placeholder="请输入暂估采购价"
+                    @blur="formatPrice('estimatedPurchasePrice')"
                   />
                   />
                 </el-form-item>
                 </el-form-item>
               </el-col>
               </el-col>
@@ -507,11 +527,14 @@
           <el-form ref="purchaseInfoFormRef" :model="productForm" :rules="productRules" label-width="120px" class="product-info-form">
           <el-form ref="purchaseInfoFormRef" :model="productForm" :rules="productRules" label-width="120px" class="product-info-form">
             <el-row :gutter="20">
             <el-row :gutter="20">
               <el-col :span="12">
               <el-col :span="12">
-                <el-form-item label="产品性质:" prop="productNature" required>
-                  <el-select v-model="productForm.productNature" placeholder="请选择" clearable class="w-full">
-                    <el-option label="自营" value="1" />
-                    <el-option label="代销" value="2" />
-                    <el-option label="定制" value="3" />
+                <el-form-item label="产品经理:" prop="productNature" required>
+                  <el-select v-model="productForm.productNature" placeholder="请选择" clearable class="w-full" value-key="staffId">
+                    <el-option
+                      v-for="option in staffOptions"
+                      :key="option.staffId"
+                      :label="`${option.staffCode},${option.staffName}`"
+                      :value="String(option.staffId)"
+                    />
                   </el-select>
                   </el-select>
                 </el-form-item>
                 </el-form-item>
               </el-col>
               </el-col>
@@ -521,7 +544,7 @@
                     <el-option
                     <el-option
                       v-for="option in staffOptions"
                       v-for="option in staffOptions"
                       :key="option.staffId"
                       :key="option.staffId"
-                      :label="option.staffName"
+                      :label="`${option.staffCode},${option.staffName}`"
                       :value="String(option.staffId)"
                       :value="String(option.staffId)"
                     />
                     />
                   </el-select>
                   </el-select>
@@ -622,6 +645,27 @@
               </div>
               </div>
             </el-form-item>
             </el-form-item>
 
 
+            <!-- 商品轮播图 -->
+            <el-form-item label="商品轮播图:">
+              <div class="carousel-images-container">
+                <div class="carousel-image-list">
+                  <div v-for="(imgUrl, index) in carouselImages" :key="index" class="carousel-image-item">
+                    <img :src="imgUrl" class="carousel-preview-image" />
+                    <div class="carousel-image-actions">
+                      <el-button size="small" type="danger" @click="removeCarouselImage(index)">删除</el-button>
+                    </div>
+                  </div>
+                  <div class="image-upload-placeholder carousel-add-btn" @click="openCarouselImageSelector">
+                    <el-icon class="upload-icon"><Plus /></el-icon>
+                    <div class="upload-text">添加图片</div>
+                  </div>
+                </div>
+              </div>
+              <div class="form-item-tip">
+                从图片库选择,支持多选,建议尺寸300*300px
+              </div>
+            </el-form-item>
+
             <!-- 商品详情 -->
             <!-- 商品详情 -->
             <el-form-item label="商品详情:">
             <el-form-item label="商品详情:">
               <el-tabs v-model="activeDetailTab" type="border-card">
               <el-tabs v-model="activeDetailTab" type="border-card">
@@ -770,6 +814,14 @@
       title="选择商品主图"
       title="选择商品主图"
       @confirm="handleMainImageSelected"
       @confirm="handleMainImageSelected"
     />
     />
+    <!-- 轮播图文件选择器 -->
+    <FileSelector
+      v-model="carouselImageSelectorVisible"
+      :allowed-types="[1]"
+      :multiple="true"
+      title="选择商品轮播图"
+      @confirm="handleCarouselImagesSelected"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -784,7 +836,8 @@ import { categoryTreeVO } from '@/api/product/category/types';
 import { BrandVO } from '@/api/product/brand/types';
 import { BrandVO } from '@/api/product/brand/types';
 import { BaseForm } from '@/api/product/base/types';
 import { BaseForm } from '@/api/product/base/types';
 import { AttributesVO } from '@/api/product/attributes/types';
 import { AttributesVO } from '@/api/product/attributes/types';
-import { addBase, updateBase, getBase, brandList, categoryTree, categoryAttributeList, getAfterSaleList, getServiceList, getUnitList } from '@/api/product/base';
+import { addBase, updateBase, getBase, categoryTree, categoryAttributeList, getAfterSaleList, getServiceList, getUnitList, getTaxRateList } from '@/api/product/base';
+import { listBrand } from '@/api/product/brand';
 import { listInfo } from '@/api/customer/supplierInfo';
 import { listInfo } from '@/api/customer/supplierInfo';
 import { InfoVO } from '@/api/customer/supplierInfo/types';
 import { InfoVO } from '@/api/customer/supplierInfo/types';
 import { listComStaff } from '@/api/system/comStaff';
 import { listComStaff } from '@/api/system/comStaff';
@@ -807,6 +860,13 @@ const activeDetailTab = ref('pc');
 
 
 // 文件选择器相关
 // 文件选择器相关
 const mainImageSelectorVisible = ref(false);
 const mainImageSelectorVisible = ref(false);
+const carouselImageSelectorVisible = ref(false);
+
+// 轮播图URL数组(UI管理用)
+const carouselImages = ref<string[]>([]);
+
+// 税率选项
+const taxRateOptions = ref<any[]>([]);
 
 
 // 定制说明表单
 // 定制说明表单
 const customForm = reactive({
 const customForm = reactive({
@@ -943,6 +1003,7 @@ const productForm = reactive<BaseForm>({
   bottomCategoryId: undefined,
   bottomCategoryId: undefined,
   unitId: undefined,
   unitId: undefined,
   productImage: undefined,
   productImage: undefined,
+  imageUrl: undefined,
   isSelf: 0,
   isSelf: 0,
   productReviewStatus: 0,
   productReviewStatus: 0,
   homeRecommended: 0,
   homeRecommended: 0,
@@ -993,9 +1054,9 @@ const productRules = {
   standardPrice: [{ required: true, message: '平台售价不能为空', trigger: 'blur' }],
   standardPrice: [{ required: true, message: '平台售价不能为空', trigger: 'blur' }],
   certificatePrice: [{ required: true, message: '最低售价不能为空', trigger: 'blur' }],
   certificatePrice: [{ required: true, message: '最低售价不能为空', trigger: 'blur' }],
   purchasePrice: [{ required: true, message: '采购价不能为空', trigger: 'blur' }],
   purchasePrice: [{ required: true, message: '采购价不能为空', trigger: 'blur' }],
-  productNature: [{ required: true, message: '产品性质不能为空', trigger: 'change' }],
+  productNature: [{ required: true, message: '产品经理不能为空', trigger: 'change' }],
   purchasingPersonnel: [{ required: true, message: '采购人员不能为空', trigger: 'change' }],
   purchasingPersonnel: [{ required: true, message: '采购人员不能为空', trigger: 'change' }],
-  taxRate: [{ required: true, message: '税率不能为空', trigger: 'blur' }],
+  taxRate: [{ required: true, message: '税率不能为空', trigger: 'change' }],
   minOrderQuantity: [{ required: true, message: '最低起订量不能为空', trigger: 'blur' }],
   minOrderQuantity: [{ required: true, message: '最低起订量不能为空', trigger: 'blur' }],
 };
 };
 
 
@@ -1003,12 +1064,7 @@ const productRules = {
 const categoryOptions = ref<categoryTreeVO[]>([]);
 const categoryOptions = ref<categoryTreeVO[]>([]);
 const brandOptions = ref<BrandVO[]>([]);
 const brandOptions = ref<BrandVO[]>([]);
 const brandLoading = ref(false);
 const brandLoading = ref(false);
-const brandOptionsFormatted = computed(() => {
-  return brandOptions.value.slice(0, 500).map(item => ({
-    label: item.brandName,
-    value: item.id
-  }));
-});
+let brandSearchTimer: ReturnType<typeof setTimeout> | null = null;
 
 
 // 商品属性列表
 // 商品属性列表
 const attributesList = ref<AttributesVO[]>([]);
 const attributesList = ref<AttributesVO[]>([]);
@@ -1186,6 +1242,8 @@ const handleSubmit = async () => {
       ...productForm,
       ...productForm,
       // 将服务保障ID数组转换为逗号分隔字符串
       // 将服务保障ID数组转换为逗号分隔字符串
       serviceGuarantee: serviceGuarantees.value.map(id => String(id)).join(','),
       serviceGuarantee: serviceGuarantees.value.map(id => String(id)).join(','),
+      // 轮播图URL逗号分隔
+      imageUrl: carouselImages.value.join(','),
       // 将商品属性值转换为JSON字符串
       // 将商品属性值转换为JSON字符串
       attributesList: JSON.stringify(productAttributesValues.value),
       attributesList: JSON.stringify(productAttributesValues.value),
       customizable: customForm.customizable,
       customizable: customForm.customizable,
@@ -1238,6 +1296,45 @@ const clearMainImage = () => {
   productForm.productImage = undefined;
   productForm.productImage = undefined;
 };
 };
 
 
+// 打开轮播图选择器
+const openCarouselImageSelector = () => {
+  carouselImageSelectorVisible.value = true;
+};
+
+// 处理轮播图选择(多选)
+const handleCarouselImagesSelected = (files: any[]) => {
+  if (files && files.length > 0) {
+    files.forEach(file => {
+      if (!carouselImages.value.includes(file.url)) {
+        carouselImages.value.push(file.url);
+      }
+    });
+  }
+};
+
+// 删除轮播图
+const removeCarouselImage = (index: number) => {
+  carouselImages.value.splice(index, 1);
+};
+
+// UPC(69)条码只允许输入数字
+const handleUpcInput = () => {
+  if (productForm.upcBarcode) {
+    productForm.upcBarcode = productForm.upcBarcode.replace(/\D/g, '');
+  }
+};
+
+// 格式化价格为两位小数
+const formatPrice = (field: string) => {
+  const val = (productForm as any)[field];
+  if (val !== undefined && val !== null && val !== '') {
+    const num = parseFloat(String(val));
+    if (!isNaN(num)) {
+      (productForm as any)[field] = parseFloat(num.toFixed(2));
+    }
+  }
+};
+
 // 获取分类树
 // 获取分类树
 const getCategoryTree = async () => {
 const getCategoryTree = async () => {
   try {
   try {
@@ -1248,27 +1345,31 @@ const getCategoryTree = async () => {
   }
   }
 };
 };
 
 
-// 获取品牌列表(实时请求,每次只加载500条)
-const getBrandList = async () => {
+// 加载品牌选项(默认100条)
+const loadBrandOptions = async (keyword?: string) => {
+  brandLoading.value = true;
   try {
   try {
-    brandLoading.value = true;
-    const res = await brandList({ pageNum: 1, pageSize: 500 });
-    brandOptions.value = res.data || [];
-    // 如果是新增模式且有选项,设置第一个为默认值
-    if (!route.params.id && brandOptions.value.length > 0 && !productForm.brandId) {
-      productForm.brandId = brandOptions.value[0].id;
-    }
+    const res = await listBrand({ pageNum: 1, pageSize: 100, brandName: keyword });
+    brandOptions.value = res.rows || [];
   } catch (error) {
   } catch (error) {
-    console.error('获取品牌列表失败:', error);
+    console.error('加载品牌列表失败:', error);
   } finally {
   } finally {
     brandLoading.value = false;
     brandLoading.value = false;
   }
   }
 };
 };
 
 
+// 品牌远程搜索(防抖)
+const handleBrandSearch = (query: string) => {
+  if (brandSearchTimer) clearTimeout(brandSearchTimer);
+  brandSearchTimer = setTimeout(() => {
+    loadBrandOptions(query || undefined);
+  }, 300);
+};
+
 // 处理品牌下拉框显示/隐藏
 // 处理品牌下拉框显示/隐藏
 const handleBrandVisibleChange = (visible: boolean) => {
 const handleBrandVisibleChange = (visible: boolean) => {
   if (visible && brandOptions.value.length === 0) {
   if (visible && brandOptions.value.length === 0) {
-    getBrandList();
+    loadBrandOptions();
   }
   }
 };
 };
 
 
@@ -1350,6 +1451,16 @@ const getStaffOptions = async () => {
   }
   }
 };
 };
 
 
+// 获取税率列表
+const getTaxRateOptions = async () => {
+  try {
+    const res = await getTaxRateList();
+    taxRateOptions.value = res.rows || [];
+  } catch (error) {
+    console.error('获取税率列表失败:', error);
+  }
+};
+
 // 加载分类属性列表
 // 加载分类属性列表
 const loadCategoryAttributes = async (categoryId: string | number) => {
 const loadCategoryAttributes = async (categoryId: string | number) => {
   try {
   try {
@@ -1407,6 +1518,13 @@ const loadProductDetail = async () => {
       const res = await getBase(id as string);
       const res = await getBase(id as string);
       Object.assign(productForm, res.data);
       Object.assign(productForm, res.data);
 
 
+      // 回显轮播图
+      if (res.data.imageUrl) {
+        carouselImages.value = res.data.imageUrl.split(',').filter((url: string) => url.trim());
+      } else {
+        carouselImages.value = [];
+      }
+
       // 回显分类选择
       // 回显分类选择
       categoryForm.topCategoryId = res.data.topCategoryId;
       categoryForm.topCategoryId = res.data.topCategoryId;
       categoryForm.mediumCategoryId = res.data.mediumCategoryId;
       categoryForm.mediumCategoryId = res.data.mediumCategoryId;
@@ -1474,11 +1592,13 @@ onMounted(async () => {
   await getUnitOptions();
   await getUnitOptions();
   await getAfterSalesOptions();
   await getAfterSalesOptions();
   await getServiceGuaranteeOptions();
   await getServiceGuaranteeOptions();
+  await getTaxRateOptions();
   // 先加载商品详情(如果是编辑模式)
   // 先加载商品详情(如果是编辑模式)
   await loadProductDetail();
   await loadProductDetail();
   // 再加载下拉选项,这样如果详情中没有值,会自动设置第一个
   // 再加载下拉选项,这样如果详情中没有值,会自动设置第一个
   await getSupplierOptions();
   await getSupplierOptions();
   await getStaffOptions();
   await getStaffOptions();
+  loadBrandOptions();
 });
 });
 </script>
 </script>
 
 
@@ -1587,35 +1707,35 @@ onMounted(async () => {
         gap: 8px;
         gap: 8px;
       }
       }
     }
     }
+  }
 
 
-    .image-upload-placeholder {
-      width: 178px;
-      height: 178px;
-      border: 1px dashed #d9d9d9;
-      border-radius: 6px;
-      cursor: pointer;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      justify-content: center;
-      transition: all 0.3s;
-      background-color: #fafafa;
+  .image-upload-placeholder {
+    width: 178px;
+    height: 178px;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    cursor: pointer;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.3s;
+    background-color: #fafafa;
 
 
-      &:hover {
-        border-color: #409eff;
-        background-color: #f5f7fa;
-      }
+    &:hover {
+      border-color: #409eff;
+      background-color: #f5f7fa;
+    }
 
 
-      .upload-icon {
-        font-size: 28px;
-        color: #8c939d;
-        margin-bottom: 8px;
-      }
+    .upload-icon {
+      font-size: 28px;
+      color: #8c939d;
+      margin-bottom: 8px;
+    }
 
 
-      .upload-text {
-        color: #8c939d;
-        font-size: 14px;
-      }
+    .upload-text {
+      color: #8c939d;
+      font-size: 14px;
     }
     }
   }
   }
 
 
@@ -1625,6 +1745,40 @@ onMounted(async () => {
     flex-wrap: wrap;
     flex-wrap: wrap;
   }
   }
 
 
+  .carousel-images-container {
+    width: 100%;
+
+    .carousel-image-list {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px;
+
+      .carousel-image-item {
+        position: relative;
+
+        .carousel-preview-image {
+          width: 120px;
+          height: 120px;
+          display: block;
+          object-fit: cover;
+          border-radius: 6px;
+          border: 1px solid #dcdfe6;
+        }
+
+        .carousel-image-actions {
+          margin-top: 6px;
+          display: flex;
+          justify-content: center;
+        }
+      }
+
+      .carousel-add-btn {
+        width: 120px;
+        height: 120px;
+      }
+    }
+  }
+
   .custom-table {
   .custom-table {
     width: 100%;
     width: 100%;
     margin-top: 10px;
     margin-top: 10px;

+ 56 - 12
src/views/product/base/index.vue

@@ -17,7 +17,25 @@
               </el-col>
               </el-col>
               <el-col :span="6">
               <el-col :span="6">
                 <el-form-item label="商品品牌" prop="brandName">
                 <el-form-item label="商品品牌" prop="brandName">
-                  <el-input v-model="queryParams.brandName" placeholder="请输入商品品牌" clearable @keyup.enter="handleQuery" />
+                  <el-select
+                    v-model="queryParams.brandName"
+                    placeholder="请输入品牌名称搜索"
+                    filterable
+                    remote
+                    clearable
+                    :remote-method="handleBrandSearch"
+                    :loading="brandLoading"
+                    value-key="brandName"
+                    style="width: 100%"
+                    @keyup.enter="handleQuery"
+                  >
+                    <el-option
+                      v-for="item in brandOptions"
+                      :key="item.id"
+                      :label="item.brandName"
+                      :value="item.brandName"
+                    />
+                  </el-select>
                 </el-form-item>
                 </el-form-item>
               </el-col>
               </el-col>
               <el-col :span="6">
               <el-col :span="6">
@@ -113,17 +131,17 @@
             <image-preview :src="scope.row.productImage" :width="60" :height="60" />
             <image-preview :src="scope.row.productImage" :width="60" :height="60" />
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column label="商品信息" align="center" width="250" show-overflow-tooltip>
+        <el-table-column label="商品信息" align="center" min-width="250">
           <template #default="scope">
           <template #default="scope">
             <div class="text-left">
             <div class="text-left">
-              <div>{{ scope.row.itemName }}</div>
+              <div style="white-space: normal; word-break: break-all; line-height: 1.4">{{ scope.row.itemName }}</div>
               <div class="text-gray-500" style="font-size: 12px">品牌: {{ scope.row.brandName || '-' }}</div>
               <div class="text-gray-500" style="font-size: 12px">品牌: {{ scope.row.brandName || '-' }}</div>
             </div>
             </div>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
         <el-table-column label="商品分类" align="center" prop="categoryName" width="120" />
         <el-table-column label="商品分类" align="center" prop="categoryName" width="120" />
-        <el-table-column label="单位" align="center" prop="unitName" width="80" />
-        <el-table-column label="SKU价格" align="center" width="180">
+        <el-table-column label="单位" align="center" prop="unitName" width="60" />
+        <el-table-column label="SKU价格" align="center" width="120">
           <template #default="scope">
           <template #default="scope">
             <div class="text-left" style="font-size: 12px">
             <div class="text-left" style="font-size: 12px">
               <div>
               <div>
@@ -155,19 +173,19 @@
             </div>
             </div>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column label="数据来源" align="center" prop="dataSource" width="100">
+        <el-table-column label="数据来源" align="center" prop="dataSource" width="80">
           <template #default="scope">
           <template #default="scope">
             <span>{{ scope.row.dataSource || '-' }}</span>
             <span>{{ scope.row.dataSource || '-' }}</span>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column label="是否自营" align="center" width="100">
+        <el-table-column label="是否自营" align="center" width="80">
           <template #default="scope">
           <template #default="scope">
             <el-tag v-if="scope.row.isSelf === 1" type="success">是</el-tag>
             <el-tag v-if="scope.row.isSelf === 1" type="success">是</el-tag>
             <el-tag v-else-if="scope.row.isSelf === 0" type="info">否</el-tag>
             <el-tag v-else-if="scope.row.isSelf === 0" type="info">否</el-tag>
             <span v-else>-</span>
             <span v-else>-</span>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column label="审核状态" align="center" prop="productReviewStatus" width="100">
+        <el-table-column label="审核状态" align="center" prop="productReviewStatus" width="90">
           <template #default="scope">
           <template #default="scope">
             <span v-if="scope.row.productReviewStatus === 0">待采购审核</span>
             <span v-if="scope.row.productReviewStatus === 0">待采购审核</span>
             <span v-else-if="scope.row.productReviewStatus === 1">审核通过</span>
             <span v-else-if="scope.row.productReviewStatus === 1">审核通过</span>
@@ -176,7 +194,7 @@
             <span v-else>-</span>
             <span v-else>-</span>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column label="上下架状态" align="center" prop="productStatus" width="120">
+        <el-table-column label="上下架状态" align="center" prop="productStatus" width="100">
           <template #default="scope">
           <template #default="scope">
             <el-tag v-if="scope.row.productStatus === 1" type="success">已上架</el-tag>
             <el-tag v-if="scope.row.productStatus === 1" type="success">已上架</el-tag>
             <el-tag v-else-if="scope.row.productStatus === 0" type="warning">下架</el-tag>
             <el-tag v-else-if="scope.row.productStatus === 0" type="warning">下架</el-tag>
@@ -184,15 +202,15 @@
             <el-tag v-else type="info">未知</el-tag>
             <el-tag v-else type="info">未知</el-tag>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column label="操作" align="center" width="280" fixed="right">
+        <el-table-column label="操作" align="center" width="150" fixed="right">
           <template #default="scope">
           <template #default="scope">
             <!-- 待审核状态:只显示编辑 -->
             <!-- 待审核状态:只显示编辑 -->
-            <div v-if="scope.row.productReviewStatus !== 2" class="flex gap-1 justify-center">
+            <div v-if="scope.row.productReviewStatus !== 1" class="flex gap-1 justify-center">
               <el-link type="primary" :underline="false" @click="handleUpdate(scope.row)">编辑</el-link>
               <el-link type="primary" :underline="false" @click="handleUpdate(scope.row)">编辑</el-link>
             </div>
             </div>
 
 
             <!-- 审核通过 -->
             <!-- 审核通过 -->
-            <div v-else-if="scope.row.productReviewStatus === 2" class="flex flex-col gap-1">
+            <div v-else-if="scope.row.productReviewStatus === 1" class="flex flex-col gap-1">
               <!-- 下架状态:编辑、上架、停售、修改库存 -->
               <!-- 下架状态:编辑、上架、停售、修改库存 -->
               <div v-if="scope.row.productStatus === 0" class="flex gap-1 justify-center">
               <div v-if="scope.row.productStatus === 0" class="flex gap-1 justify-center">
                 <el-link type="primary" :underline="false" @click="handleUpdate(scope.row)">编辑</el-link>
                 <el-link type="primary" :underline="false" @click="handleUpdate(scope.row)">编辑</el-link>
@@ -240,6 +258,7 @@
 import { listBase, getBase, delBase, brandList, categoryTree, shelfReview, changeProductType, getProductStatusCount } from '@/api/product/base';
 import { listBase, getBase, delBase, brandList, categoryTree, shelfReview, changeProductType, getProductStatusCount } from '@/api/product/base';
 import { BaseVO, BaseQuery, BaseForm, StatusCountVo } from '@/api/product/base/types';
 import { BaseVO, BaseQuery, BaseForm, StatusCountVo } from '@/api/product/base/types';
 import { BrandVO } from '@/api/product/brand/types';
 import { BrandVO } from '@/api/product/brand/types';
+import { listBrand } from '@/api/product/brand';
 import { categoryTreeVO } from '@/api/product/category/types';
 import { categoryTreeVO } from '@/api/product/category/types';
 import { useRoute, useRouter } from 'vue-router';
 import { useRoute, useRouter } from 'vue-router';
 
 
@@ -256,6 +275,9 @@ 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 brandOptions = ref<BrandVO[]>([]);
+const brandLoading = ref(false);
+let brandSearchTimer: ReturnType<typeof setTimeout> | null = null;
 const hasMore = ref(true); // 是否还有更多数据
 const hasMore = ref(true); // 是否还有更多数据
 // 页面历史记录,存储每页的第一个id和最后一个id,用于支持双向翻页
 // 页面历史记录,存储每页的第一个id和最后一个id,用于支持双向翻页
 const pageHistory = ref([]);
 const pageHistory = ref([]);
@@ -575,6 +597,27 @@ const getCategoryTree = async () => {
   categoryOptions.value = res.data || [];
   categoryOptions.value = res.data || [];
 };
 };
 
 
+/** 加载品牌选项(默认100条) */
+const loadBrandOptions = async (keyword?: string) => {
+  brandLoading.value = true;
+  try {
+    const res = await listBrand({ pageNum: 1, pageSize: 100, brandName: keyword });
+    brandOptions.value =  res.rows || [];
+  } catch (error) {
+    console.error('加载品牌列表失败:', error);
+  } finally {
+    brandLoading.value = false;
+  }
+};
+
+/** 品牌远程搜索(防抖) */
+const handleBrandSearch = (query: string) => {
+  if (brandSearchTimer) clearTimeout(brandSearchTimer);
+  brandSearchTimer = setTimeout(() => {
+    loadBrandOptions(query || undefined);
+  }, 300);
+};
+
 /** 获取统计信息 */
 /** 获取统计信息 */
 const getStatistics = async () => {
 const getStatistics = async () => {
   try {
   try {
@@ -591,5 +634,6 @@ onMounted(() => {
   getList();
   getList();
   getCategoryTree();
   getCategoryTree();
   getStatistics();
   getStatistics();
+  loadBrandOptions();
 });
 });
 </script>
 </script>

+ 9 - 9
src/views/product/protocolInfo/productManage.vue

@@ -62,7 +62,7 @@
         <el-table-column label="产品编号" align="center" prop="productNo" width="120" />
         <el-table-column label="产品编号" align="center" prop="productNo" width="120" />
         <el-table-column label="商品图片" align="center" width="100">
         <el-table-column label="商品图片" align="center" width="100">
           <template #default="scope">
           <template #default="scope">
-            <image-preview :src="scope.row.productImageUrl" :width="60" :height="60"/>
+            <image-preview :src="scope.row.productImage" :width="60" :height="60"/>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
         <el-table-column label="商品名称" align="center" min-width="180">
         <el-table-column label="商品名称" align="center" min-width="180">
@@ -85,7 +85,6 @@
             </div>
             </div>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column label="毛利预估" align="center" prop="tempGrossMargin" width="100" />
         <el-table-column label="产品来源" align="center" prop="dataSource" width="100" />
         <el-table-column label="产品来源" align="center" prop="dataSource" width="100" />
         <el-table-column label="状态" align="center" width="80">
         <el-table-column label="状态" align="center" width="80">
           <template #default="scope">
           <template #default="scope">
@@ -94,7 +93,7 @@
             </span>
             </span>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column label="协议供货价" align="center" width="120">
+        <el-table-column label="协议供货价" align="center" width="140">
           <template #default="scope">
           <template #default="scope">
             <el-input-number
             <el-input-number
               v-model="scope.row.agreementPrice"
               v-model="scope.row.agreementPrice"
@@ -159,7 +158,7 @@
           <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
           <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
           <el-table-column label="商品图片" align="center" prop="productImageUrl" width="100">
           <el-table-column label="商品图片" align="center" prop="productImageUrl" width="100">
             <template #default="scope">
             <template #default="scope">
-              <image-preview :src="scope.row.productImageUrl" :width="60" :height="60"/>
+              <image-preview :src="scope.row.productImage " :width="60" :height="60"/>
             </template>
             </template>
           </el-table-column>
           </el-table-column>
           <el-table-column label="商品信息" align="center" min-width="200">
           <el-table-column label="商品信息" align="center" min-width="200">
@@ -239,7 +238,7 @@
 </template>
 </template>
 
 
 <script setup name="ProductManage" lang="ts">
 <script setup name="ProductManage" lang="ts">
-import { listProducts, delProducts, updateProducts, addProducts, getProtocolProductIds } from '@/api/product/products';
+import { listProducts, delProducts, updateProducts, addProducts, getProtocolProductIds, updateProductsPrice } from '@/api/product/products';
 import { ProductsQuery, ProductsForm } from '@/api/product/products/types';
 import { ProductsQuery, ProductsForm } from '@/api/product/products/types';
 import { getInfo } from '@/api/product/protocolInfo';
 import { getInfo } from '@/api/product/protocolInfo';
 import { listBrand } from '@/api/product/brand';
 import { listBrand } from '@/api/product/brand';
@@ -604,9 +603,10 @@ const handleDelete = async (row: any) => {
 /** 协议供货价变更 */
 /** 协议供货价变更 */
 const handlePriceChange = async (row: any) => {
 const handlePriceChange = async (row: any) => {
   try {
   try {
-    await updateProducts({
-      id: row.id,
-      agreementPrice: row.agreementPrice
+    await updateProductsPrice({
+      agreementPrice: row.agreementPrice,
+      productId: row.id,
+      protocolId: route.query.protocolId as string | number
     });
     });
     proxy?.$modal.msgSuccess('价格更新成功');
     proxy?.$modal.msgSuccess('价格更新成功');
   } catch (error) {
   } catch (error) {
@@ -619,7 +619,7 @@ const calculateMargin = (row: any) => {
   if (!row.agreementPrice || !row.purchasingPrice || row.purchasingPrice === 0) {
   if (!row.agreementPrice || !row.purchasingPrice || row.purchasingPrice === 0) {
     return '-';
     return '-';
   }
   }
-  const margin = ((row.agreementPrice - row.purchasingPrice) / row.agreementPrice * 100).toFixed(2);
+  const margin = ((row.agreementPrice - row.purchasingPrice) / row.agreementPrice * 100).toFixed(2)+'%';
   return margin;
   return margin;
 };
 };