Bläddra i källkod

feat(admin): 商品编辑-线上服务者简化为服务商下拉选择

iwt 2 månader sedan
förälder
incheckning
90591abf71

+ 33 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminGuideController.java

@@ -4,13 +4,19 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.Guide;
 import com.etotem.cfc.entity.GuideApplication;
 import com.etotem.cfc.entity.GuidePackage;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.GuideApplicationService;
 import com.etotem.cfc.service.GuidePackageService;
 import com.etotem.cfc.service.GuideService;
+import io.swagger.v3.oas.annotations.Operation;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 @RestController
 @RequestMapping("/api/admin/guide")
@@ -25,6 +31,9 @@ public class AdminGuideController {
     @Resource
     private GuidePackageService guidePackageService;
 
+    @Resource
+    private UserMapper userMapper;
+
     @PostMapping("/applications/pending")
     public Result<List<GuideApplication>> getPendingApplications() {
         return Result.success(guideApplicationService.getPendingApplications());
@@ -57,6 +66,30 @@ public class AdminGuideController {
         return Result.success(guidePackageService.list());
     }
 
+    @Operation(summary = "获取已入驻的服务商列表(用于商品编辑时下拉选择)")
+    @PostMapping("/guides/approved")
+    public Result<List<Map<String, Object>>> getApprovedGuides() {
+        List<Guide> guides = guideService.getActiveGuides();
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (Guide g : guides) {
+            User u = userMapper.selectById(g.getUserId());
+            if (u == null || !"approved".equals(u.getVendorStatus())) continue;
+            Map<String, Object> item = new HashMap<>();
+            item.put("userId", g.getUserId());
+            item.put("nickname", u.getNickname());
+            item.put("realName", u.getRealName());
+            item.put("avatar", u.getAvatar());
+            item.put("title", g.getTitle());
+            item.put("bio", g.getBio());
+            item.put("intro", g.getIntro());
+            item.put("specialty", g.getSpecialty());
+            item.put("level", g.getLevel());
+            item.put("guideId", g.getId());
+            result.add(item);
+        }
+        return Result.success(result);
+    }
+
     @PostMapping("/packages/create")
     public Result<Boolean> createPackage(@RequestBody GuidePackage pkg) {
         return Result.success(guidePackageService.save(pkg));

+ 24 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/ProductDeliveryConfigService.java

@@ -23,6 +23,7 @@ public class ProductDeliveryConfigService {
     @Resource private ProductServicePersonSlotMapper servicePersonSlotMapper;
     @Resource private ProductOnlineSlotMapper onlineSlotMapper;
     @Resource private UserMapper userMapper;
+    @Resource private GuideService guideService;
 
     // ========== 商品详情组装 ==========
 
@@ -189,10 +190,29 @@ public class ProductDeliveryConfigService {
                         + "」未通过服务商入驻审核,无法添加为线上服务者");
             }
             person.setUserId(providerUserId);
-            person.setName((String) personData.get("name"));
-            person.setTitle((String) personData.get("title"));
-            person.setIntro((String) personData.get("intro"));
-            person.setAvatar((String) personData.get("avatar"));
+            // 从服务商资料自动代入 name/title/intro/avatar(未显式提供时)
+            if (personData.get("name") == null) {
+                person.setName(provider.getRealName() != null ? provider.getRealName() : provider.getNickname());
+            } else {
+                person.setName((String) personData.get("name"));
+            }
+            if (personData.get("title") == null) {
+                Guide guide = guideService.getByUserId(provider.getId());
+                person.setTitle(guide != null ? guide.getTitle() : "");
+            } else {
+                person.setTitle((String) personData.get("title"));
+            }
+            if (personData.get("intro") == null) {
+                Guide guide = guideService.getByUserId(provider.getId());
+                person.setIntro(guide != null ? guide.getIntro() : null);
+            } else {
+                person.setIntro((String) personData.get("intro"));
+            }
+            if (personData.get("avatar") == null) {
+                person.setAvatar(provider.getAvatar());
+            } else {
+                person.setAvatar((String) personData.get("avatar"));
+            }
             Object price = personData.get("price");
             if (price != null) person.setPrice(Integer.valueOf(price.toString()));
             Object origPrice = personData.get("originalPrice");

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

@@ -2011,3 +2011,11 @@ export function getCfValueAllBalances(data) {
     data
   })
 }
+
+// 获取已入驻的服务商列表(用于商品编辑下拉选择)
+export function getApprovedGuides() {
+  return request({
+    url: '/api/admin/guide/guides/approved',
+    method: 'post'
+  })
+}

+ 76 - 50
cfc-web/src/views/admin/ProductEdit.vue

@@ -186,49 +186,47 @@
           </el-radio-group>
         </el-form-item>
 
-        <!-- 线上-可选人(4):服务者配置(每个服务者=一个服务商,可独立定价+可预约时段) -->
+        <!-- 线上-可选人(4):从已入驻服务商中选择 -->
         <el-form-item v-if="form.deliveryMethod === 4" label="线上服务者">
           <div class="service-person-list">
+            <!-- 添加服务者下拉 -->
+            <div class="add-person-row">
+              <el-select v-model="selectedGuideId" placeholder="选择服务商(已入驻)" style="width: 260px"
+                         filterable @change="onSelectGuide">
+                <el-option v-for="g in filteredGuides" :key="g.userId"
+                  :label="g.realName || g.nickname"
+                  :value="g.userId">
+                  <span style="float:left">{{ g.realName || g.nickname }}</span>
+                  <span style="float:right;color:#999;font-size:12px">{{ g.title || g.specialty || '' }}</span>
+                </el-option>
+              </el-select>
+              <el-button type="primary" size="mini" style="margin-left:8px" @click="addSelectedGuide">添加</el-button>
+              <div class="delivery-config-hint" style="margin-top:4px">时段由服务商在「服务商管理」中统一配置</div>
+            </div>
+            <!-- 已选服务者列表 -->
             <div v-for="(person, pIdx) in servicePersons" :key="pIdx" class="service-person-card">
               <div class="sp-header">
-                <span class="sp-title">服务者 {{ pIdx + 1 }}</span>
+                <span class="sp-title">{{ person.name || '服务者 ' + (pIdx+1) }}</span>
                 <el-button type="text" size="mini" @click="removeServicePerson(pIdx)">删除</el-button>
               </div>
               <el-row :gutter="10">
-                <el-col :span="8">
-                  <el-input v-model="person.userId" placeholder="服务商用户ID" />
-                </el-col>
-                <el-col :span="8">
-                  <el-input v-model="person.name" placeholder="展示名称" />
+                <el-col :span="12">
+                  <el-input v-model="person.name" placeholder="展示名称" size="small" />
                 </el-col>
-                <el-col :span="8">
-                  <el-input v-model="person.title" placeholder="头衔(如高级营养师)" />
+                <el-col :span="12">
+                  <el-input v-model="person.title" placeholder="头衔" size="small" />
                 </el-col>
               </el-row>
-              <el-row :gutter="10" style="margin-top: 8px">
-                <el-col :span="8">
-                  <el-input v-model="person.avatar" placeholder="头像URL" />
+              <el-row :gutter="10" style="margin-top:8px">
+                <el-col :span="12">
+                  <el-input v-model.number="person.price" placeholder="专属价格(元)" size="small" type="number" />
                 </el-col>
-                <el-col :span="8">
-                  <el-input v-model.number="person.price" placeholder="专属价格(元)" />
-                </el-col>
-                <el-col :span="8">
-                  <el-input v-model.number="person.originalPrice" placeholder="原价(元)" />
+                <el-col :span="12">
+                  <el-input v-model.number="person.originalPrice" placeholder="原价(元)" size="small" type="number" />
                 </el-col>
               </el-row>
-              <el-input v-model="person.intro" type="textarea" :rows="2" placeholder="个人介绍" style="margin-top: 8px" />
-              <div class="sp-slots">
-                <div v-for="(slot, sIdx) in person.slots" :key="sIdx" class="sp-slot-row">
-                  <el-date-picker v-model="slot.startTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始时间" style="width: 190px" />
-                  <el-date-picker v-model="slot.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束时间" style="width: 190px; margin-left: 8px" />
-                  <el-input v-model.number="slot.maxCapacity" placeholder="容量" style="width: 90px; margin-left: 8px" />
-                  <el-button type="text" size="mini" @click="removeServicePersonSlot(pIdx, sIdx)">删除</el-button>
-                </div>
-                <el-button type="text" size="mini" @click="addServicePersonSlot(pIdx)">+ 添加时段</el-button>
-              </div>
+              <el-input v-model="person.intro" type="textarea" :rows="2" placeholder="个人介绍(可留空,自动从服务商资料代入)" size="small" style="margin-top:8px" />
             </div>
-            <el-button type="primary" plain size="mini" @click="addServicePerson">+ 添加服务者</el-button>
-            <div class="delivery-config-hint">服务者必须已通过服务商入驻审核(userId 为系统用户ID)</div>
           </div>
         </el-form-item>
 
@@ -313,7 +311,7 @@
 </template>
 
 <script>
-import { getProduct, createProduct, updateProduct, listAllSupplySystems, getPredefinedPurchaseFields, getProductPurchaseFields, saveProductPurchaseFields, getMySupplySystem, saveProductGiftRule, listProductGiftRules, listMembershipLevels, getGrowthArchiveProduct, saveGrowthArchiveProduct, listDeliveryServicePersons, saveDeliveryServicePersons, listDeliveryOnlineSlots, saveDeliveryOnlineSlots, getProductList, getProductSkuList, saveBundleItems, getBundleItems } from '@/api/admin'
+import { getProduct, createProduct, updateProduct, listAllSupplySystems, getPredefinedPurchaseFields, getProductPurchaseFields, saveProductPurchaseFields, getMySupplySystem, saveProductGiftRule, listProductGiftRules, listMembershipLevels, getGrowthArchiveProduct, saveGrowthArchiveProduct, listDeliveryServicePersons, saveDeliveryServicePersons, listDeliveryOnlineSlots, saveDeliveryOnlineSlots, getProductList, getProductSkuList, saveBundleItems, getBundleItems, getApprovedGuides } from '@/api/admin'
 import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight'
 import DimensionWeightPicker from '@/components/DimensionWeightPicker.vue'
 import ProductSkuEditor from './components/ProductSkuEditor.vue'
@@ -350,7 +348,10 @@ export default {
         sortOrder: 0
       },
       bundleItems: [], // 套餐组成: [{childProductId, childSkuId, quantity, productOptions:[], skuOptions:[]}]
-      servicePersons: [], // 线上服务者(deliveryMethod=4): [{userId,name,title,intro,avatar,price,originalPrice,slots:[{startTime,endTime,maxCapacity}]}]
+      servicePersons: [], // 线上服务者(deliveryMethod=4): [{userId,name,title,intro,avatar,price,originalPrice}]
+      approvedGuides: [], // 已入驻服务商列表
+      selectedGuideId: '',
+      guideSearchQuery: '',
       onlineSlots: [], // 单人线上时段(deliveryMethod=5): [{startTime,endTime,maxCapacity}]
       growthArchive: {
         enabled: false,
@@ -485,9 +486,22 @@ export default {
     selectedSystem() {
       if (!this.form.distributionSystemId || !this.supplySystems.length) return null
       return this.supplySystems.find(s => s.id === this.form.distributionSystemId)
+    },
+    filteredGuides() {
+      if (!this.approvedGuides.length) return this.approvedGuides
+      if (!this.guideSearchQuery) return this.approvedGuides
+      var q = this.guideSearchQuery.toLowerCase()
+      return this.approvedGuides.filter(function(g) {
+        return (g.realName || '').toLowerCase().indexOf(q) !== -1
+          || (g.nickname || '').toLowerCase().indexOf(q) !== -1
+          || (g.title || '').toLowerCase().indexOf(q) !== -1
+          || (g.specialty || '').toLowerCase().indexOf(q) !== -1
+      })
     }
   },
   created() {
+    this.loadApprovedGuides()
+  },
     this.editorConfig.MENU_CONF.uploadImage.server = this.uploadActionUrl
     this.editorConfig.MENU_CONF.uploadImage.headers = this.uploadHeaders
   },
@@ -679,12 +693,7 @@ async loadGiftRule(productId) {
               intro: p.intro || '',
               avatar: p.avatar || '',
               price: (p.price != null) ? p.price / 100 : null,
-              originalPrice: (p.originalPrice != null) ? p.originalPrice / 100 : null,
-              slots: (p.slots || []).map(s => ({
-                startTime: (s.startTime || '').replace('T', ' ').substring(0, 19),
-                endTime: (s.endTime || '').replace('T', ' ').substring(0, 19),
-                maxCapacity: s.maxCapacity || 1
-              }))
+              originalPrice: (p.originalPrice != null) ? p.originalPrice / 100 : null
             }))
           }
         } catch (e) {
@@ -706,17 +715,11 @@ async loadGiftRule(productId) {
       }
     },
     addServicePerson() {
-      this.servicePersons.push({ userId: '', name: '', title: '', intro: '', avatar: '', price: null, originalPrice: null, slots: [] })
+      this.servicePersons.push({ userId: '', name: '', title: '', intro: '', avatar: '', price: null, originalPrice: null })
     },
     removeServicePerson(idx) {
       this.servicePersons.splice(idx, 1)
     },
-    addServicePersonSlot(pIdx) {
-      this.servicePersons[pIdx].slots.push({ startTime: '', endTime: '', maxCapacity: 1 })
-    },
-    removeServicePersonSlot(pIdx, sIdx) {
-      this.servicePersons[pIdx].slots.splice(sIdx, 1)
-    },
     addOnlineSlot() {
       this.onlineSlots.push({ startTime: '', endTime: '', maxCapacity: 1 })
     },
@@ -734,12 +737,7 @@ async loadGiftRule(productId) {
           intro: p.intro || '',
           avatar: p.avatar || '',
           price: (p.price != null) ? Math.round(p.price * 100) : 0,
-          originalPrice: (p.originalPrice != null) ? Math.round(p.originalPrice * 100) : null,
-          slots: (p.slots || []).filter(s => s.startTime && s.endTime).map(s => ({
-            startTime: s.startTime,
-            endTime: s.endTime,
-            maxCapacity: s.maxCapacity || 1
-          }))
+          originalPrice: (p.originalPrice != null) ? Math.round(p.originalPrice * 100) : null
         }))
     },
     buildOnlineSlotPayload() {
@@ -750,6 +748,34 @@ async loadGiftRule(productId) {
         maxCapacity: s.maxCapacity || 1
       }))
     },
+    async loadApprovedGuides() {
+      try {
+        var res = await getApprovedGuides()
+        if (res.code === 200 && Array.isArray(res.data)) {
+          this.approvedGuides = res.data
+        }
+      } catch (e) {
+        console.warn('加载服务商列表失败', e)
+      }
+    },
+    onSelectGuide(userId) {
+      if (!userId) return
+      var guide = this.approvedGuides.find(function(g) { return g.userId == userId })
+      if (!guide) return
+      this.servicePersons.push({
+        userId: String(userId),
+        name: guide.realName || guide.nickname || '',
+        title: guide.title || '',
+        intro: guide.intro || guide.bio || '',
+        avatar: guide.avatar || '',
+        price: null,
+        originalPrice: null
+      })
+      this.selectedGuideId = ''
+    },
+    addSelectedGuide() {
+      this.onSelectGuide(this.selectedGuideId)
+    },
     handlePurchaseFieldsToggle(val) {
       if (!val) {
         this.predefinedFields.forEach(pf => { pf.selected = false })