Răsfoiți Sursa

feat(供应商商品管理): 新增供应商商品管理功能

后端:
- SupplySystemService 添加 getByAdminId() 方法
- SupplySystemController 添加 /my-system 端点
- AdminProductController.list() 支持 distributionSystemId 过滤
- 新增 SupplierProductController 提供供应商商品 CRUD

前端:
- 新增 SupplierProductManage.vue 页面
- 新增 supplier product API 函数
- 添加路由和侧边栏菜单
- ProductEdit.vue 增强供应商管理员支持(自动锁定体系选择)
Xiaogang Liao 2 luni în urmă
părinte
comite
cdbf463aa1

+ 57 - 0
cfc-web/src/api/admin.js

@@ -1113,6 +1113,63 @@ export function changeSupplySystemMemberRole(memberId, role) {
   })
 }
 
+// ========== 供应商产品管理 API (supplier_admin) ==========
+
+export function getMySupplySystem() {
+  return request({
+    url: '/api/admin/supply-system/my-system',
+    method: 'post'
+  })
+}
+
+export function getSupplierProductList(params) {
+  return request({
+    url: '/api/admin/supplier/product/list',
+    method: 'post',
+    data: params
+  })
+}
+
+export function createSupplierProduct(data) {
+  return request({
+    url: '/api/admin/supplier/product/create',
+    method: 'post',
+    data
+  })
+}
+
+export function updateSupplierProduct(data) {
+  return request({
+    url: '/api/admin/supplier/product/update',
+    method: 'post',
+    data
+  })
+}
+
+export function getSupplierProductDetail(id) {
+  return request({
+    url: '/api/admin/supplier/product/detail',
+    method: 'post',
+    data: { productId: id }
+  })
+}
+
+export function deleteSupplierProduct(id) {
+  return request({
+    url: '/api/admin/supplier/product/delete',
+    method: 'post',
+    data: { productId: id }
+  })
+}
+
+export function shelveSupplierProduct(productId, shelve) {
+  return request({
+    url: '/api/admin/supplier/product/shelve',
+    method: 'post',
+    data: { productId, shelve }
+  })
+}
+
 // 结算管理
 export function getSupplySettlementList(params) {
   return request({

+ 9 - 3
cfc-web/src/router/index.js

@@ -361,9 +361,15 @@ const routes = [
         name: 'SupplySystemDetail',
         component: () => import('@/views/admin/supply-system/Detail.vue'),
         meta: { title: '体系详情', perm: 'system:supply' },
-        props: true
-      },
-        {
+    props: true
+  },
+  {
+    path: 'supplier-products',
+    name: 'SupplierProducts',
+    component: () => import('@/views/admin/SupplierProductManage.vue'),
+    meta: { title: '商品管理', perm: 'commerce:product' }
+  },
+  {
             path: 'supply-system/:id/edit',
             name: 'SupplySystemEdit',
             component: () => import('@/views/admin/supply-system/Form.vue'),

+ 5 - 4
cfc-web/src/views/Layout.vue

@@ -280,10 +280,11 @@ export default {
                 { path: '/supply-system', label: '供应商体系', icon: 'el-icon-s-management', perm: 'system:supply' },
               ]},
             // --- 供应商管理 (supplier_admin) ---
-            { title: '供应商管理', icon: 'el-icon-s-shop', perm: 'supply:manage',
-              children: [
-                { path: '/supply-manage', label: '供应商管理', icon: 'el-icon-s-shop', perm: 'supply:manage' },
-              ]},
+  { title: '供应商管理', icon: 'el-icon-s-shop', perm: 'supply:manage',
+    children: [
+      { path: '/supply-manage', label: '供应商管理', icon: 'el-icon-s-shop', perm: 'supply:manage' },
+      { path: '/supplier-products', label: '商品管理', icon: 'el-icon-s-goods', perm: 'commerce:product' }
+    ]},
             // --- 健康配置 ---
             { title: '健康配置', icon: 'el-icon-first-aid-kit', perm: 'system:config',
               children: [

+ 27 - 6
cfc-web/src/views/admin/ProductEdit.vue

@@ -118,12 +118,8 @@
           </el-col>
         </el-row>
 
-        <el-form-item label="供应商">
-          <el-input v-model="form.vendorName" placeholder="供应商名称(管理员创建时填写)" />
-        </el-form-item>
-
         <el-form-item label="所属体系">
-          <el-select v-model="form.distributionSystemId" placeholder="不属于任何体系" clearable style="width: 100%" @change="handleSystemChange">
+          <el-select v-model="form.distributionSystemId" placeholder="不属于任何体系" clearable :disabled="isSupplierAdmin" style="width: 100%" @change="handleSystemChange">
             <el-option v-for="s in supplySystems" :key="s.id" :label="s.name" :value="s.id"></el-option>
           </el-select>
           <span v-if="selectedSystem" style="font-size: 12px; color: #999; margin-left: 8px">
@@ -131,6 +127,13 @@
           </span>
         </el-form-item>
 
+        <el-form-item v-if="isSupplierAdmin" label="供应商">
+          <el-input v-model="form.vendorName" placeholder="自动填写" disabled />
+        </el-form-item>
+        <el-form-item v-else label="供应商">
+          <el-input v-model="form.vendorName" placeholder="供应商名称(管理员创建时填写)" />
+        </el-form-item>
+
         <el-form-item label="配送方式">
           <el-radio-group v-model="form.deliveryMethod">
             <el-radio :label="1">快递</el-radio>
@@ -200,7 +203,7 @@
 </template>
 
 <script>
-import { getProduct, createProduct, updateProduct, listAllSupplySystems, getPredefinedPurchaseFields, getProductPurchaseFields, saveProductPurchaseFields } from '@/api/admin'
+import { getProduct, createProduct, updateProduct, listAllSupplySystems, getPredefinedPurchaseFields, getProductPurchaseFields, saveProductPurchaseFields, getMySupplySystem } from '@/api/admin'
 import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight'
 import DimensionWeightPicker from '@/components/DimensionWeightPicker.vue'
 import ProductSkuEditor from './components/ProductSkuEditor.vue'
@@ -272,6 +275,11 @@ export default {
     isEdit() {
       return !!this.$route.query.id
     },
+    isSupplierAdmin() {
+      const rolesStr = localStorage.getItem('roles')
+      const roles = rolesStr ? JSON.parse(rolesStr) : []
+      return roles.includes('supplier_admin')
+    },
     uploadActionUrl() {
       const baseURL = process.env.VUE_APP_BASE_API || 'http://localhost:9082'
       return baseURL + '/api/admin/articles/upload/image'
@@ -299,6 +307,19 @@ export default {
       this.editorInstance = editor
     },
     async loadSupplySystems() {
+      if (this.isSupplierAdmin) {
+        try {
+          const res = await getMySupplySystem()
+          if (res.data) {
+            this.supplySystems = [res.data]
+            this.form.distributionSystemId = res.data.id
+            this.handleSystemChange(res.data.id)
+          }
+        } catch (e) {
+          console.error(e)
+        }
+        return
+      }
       try {
         const res = await listAllSupplySystems()
         if (res.data) {

+ 241 - 0
cfc-web/src/views/admin/SupplierProductManage.vue

@@ -0,0 +1,241 @@
+<template>
+<div class="supplier-product-manage admin-page">
+  <el-card>
+    <div slot="header" class="admin-page-header">
+      <span class="admin-page-title">商品管理</span>
+      <div class="admin-page-actions">
+        <el-input
+          v-model="filters.keyword"
+          placeholder="搜索商品名称"
+          prefix-icon="el-icon-search"
+          clearable
+          class="header-search"
+          @clear="loadList"
+          @keyup.enter.native="loadList"
+        />
+        <el-select v-model="filters.status" placeholder="商品状态" @change="loadList" clearable>
+          <el-option label="全部状态" value="" />
+          <el-option label="待上架" value="approved" />
+          <el-option label="已上架" value="on_shelf" />
+          <el-option label="已下架" value="off_shelf" />
+        </el-select>
+        <el-button type="primary" size="mini" @click="loadList">查询</el-button>
+        <el-button type="success" size="mini" icon="el-icon-plus" @click="handleAdd">新增商品</el-button>
+      </div>
+    </div>
+
+    <div class="table-scroll-wrap-sm">
+      <div style="overflow:auto;max-height:calc(100vh - 300px);">
+        <el-table style="max-height:calc(100vh - 300px);" :data="list" v-loading="loading" border stripe>
+          <el-table-column prop="id" label="ID" width="70" />
+          <el-table-column label="商品图片" width="80">
+            <template slot-scope="{ row }">
+              <img
+                v-if="row.coverImage"
+                :src="row.coverImage"
+                class="product-thumb"
+                @click="previewImage(row.coverImage)"
+              />
+              <span v-else style="color:#999">-</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="name" label="商品名称" min-width="150" show-overflow-tooltip />
+          <el-table-column label="类型" width="90">
+            <template slot-scope="{ row }">
+              {{ typeLabel(row.productType) }}
+            </template>
+          </el-table-column>
+          <el-table-column label="售价(元)" width="90">
+            <template slot-scope="{ row }">
+              {{ formatPriceWithSymbol(row.price || 0) }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="stock" label="库存" width="80">
+            <template slot-scope="{ row }">
+              <span :class="{ 'stock-warning': row.stock !== null && row.stock < 10 }">
+                {{ row.stock }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="salesCount" label="销量" width="70" />
+          <el-table-column label="状态" width="90">
+            <template slot-scope="{ row }">
+              <el-tag :type="statusType(row.status)" size="mini">{{ statusLabel(row.status) }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="createdAt" label="创建时间" width="160">
+            <template slot-scope="{ row }">
+              {{ formatTime(row.createdAt) }}
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="200" fixed="right">
+            <template slot-scope="{ row }">
+              <el-button size="mini" type="primary" @click="handleEdit(row)">编辑</el-button>
+              <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
+                <el-button size="mini">
+                  更多<i class="el-icon-arrow-down el-icon--right"></i>
+                </el-button>
+                <el-dropdown-menu slot="dropdown">
+                  <el-dropdown-item v-if="row.status === 'on_shelf'" command="offShelf" icon="el-icon-download">下架</el-dropdown-item>
+                  <el-dropdown-item v-if="row.status === 'off_shelf'" command="onShelf" icon="el-icon-upload2">上架</el-dropdown-item>
+                </el-dropdown-menu>
+              </el-dropdown>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
+
+    <el-pagination
+      @size-change="handleSizeChange"
+      @current-change="handlePageChange"
+      :current-page="pagination.page"
+      :page-sizes="[10, 20, 50]"
+      :page-size="pagination.size"
+      :total="pagination.total"
+      layout="total, sizes, prev, pager, next, jumper"
+      class="pagination-wrap"
+    />
+  </el-card>
+</div>
+</template>
+
+<script>
+import { getSupplierProductList, shelveSupplierProduct } from '@/api/admin'
+
+export default {
+  name: 'SupplierProductManage',
+  data() {
+    return {
+      list: [],
+      loading: false,
+      filters: {
+        keyword: '',
+        status: ''
+      },
+      pagination: {
+        page: 1,
+        size: 20,
+        total: 0
+      }
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const params = {
+          page: this.pagination.page,
+          size: this.pagination.size,
+          status: this.filters.status || null,
+          keyword: this.filters.keyword || null
+        }
+        const res = await getSupplierProductList(params)
+        if (res.code === 200) {
+          this.list = res.data.records || []
+          this.pagination.total = res.data.total || 0
+        }
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSizeChange(size) {
+      this.pagination.size = size
+      this.pagination.page = 1
+      this.loadList()
+    },
+    handlePageChange(page) {
+      this.pagination.page = page
+      this.loadList()
+    },
+    statusType(status) {
+      const map = {
+        approved: 'info',
+        on_shelf: 'success',
+        off_shelf: 'warning'
+      }
+      return map[status] || 'info'
+    },
+    statusLabel(status) {
+      const map = {
+        approved: '待上架',
+        on_shelf: '已上架',
+        off_shelf: '已下架'
+      }
+      return map[status] || status
+    },
+    typeLabel(type) {
+      const map = {
+        physical: '实物商品', 
+        virtual: '虚拟商品', 
+        coupon: '优惠券', 
+        assessment: '测评商品'
+      }
+      return map[type] || type
+    },
+    formatTime(t) {
+      if (!t) return '-'
+      return t.replace ? t.replace('T', ' ').substring(0, 19) : t
+    },
+    formatPriceWithSymbol(price) {
+      if (price === 0) return '免费'
+      return '¥' + (price / 100).toFixed(2)
+    },
+    previewImage(url) {
+      if (!url) return
+      this.$alert('<img src="' + url + '" style="max-width:100%;" />', '商品图片', {
+        dangerouslyUseHTMLString: true,
+        closeOnClickModal: true
+      })
+    },
+    handleAdd() {
+      // 新增商品 - 默认进入编辑页,无需指定ID
+      this.$router.push('/product-edit')
+    },
+    handleEdit(row) {
+      this.$router.push('/product-edit?id=' + row.id)
+    },
+    async handleShelve(row, shelve) {
+      try {
+        await this.$confirm('确认' + (shelve ? '上架' : '下架') + '该商品?', '提示')
+        await shelveSupplierProduct(row.id, { shelve })
+        this.$message.success('操作成功')
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('操作失败')
+      }
+    },
+    handleActionCmd(row, cmd) {
+      switch (cmd) {
+        case 'offShelf': this.handleShelve(row, false); break;
+        case 'onShelf': this.handleShelve(row, true); break;
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.supplier-product-manage {
+  padding: 20px;
+}
+.product-thumb {
+  width: 50px;
+  height: 50px;
+  object-fit: cover;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.stock-warning {
+  color: #f56c6c;
+  font-weight: bold;
+}
+.header-search {
+  width: 180px;
+}
+</style>