|
|
@@ -155,9 +155,70 @@
|
|
|
<el-radio :label="1">快递</el-radio>
|
|
|
<el-radio :label="2">自提</el-radio>
|
|
|
<el-radio :label="3">两者皆可</el-radio>
|
|
|
+ <el-radio :label="4">线上-可选人</el-radio>
|
|
|
+ <el-radio :label="5">线上-单人</el-radio>
|
|
|
</el-radio-group>
|
|
|
</el-form-item>
|
|
|
|
|
|
+ <!-- 线上-可选人(4):服务者配置(每个服务者=一个服务商,可独立定价+可预约时段) -->
|
|
|
+ <el-form-item v-if="form.deliveryMethod === 4" label="线上服务者">
|
|
|
+ <div class="service-person-list">
|
|
|
+ <div v-for="(person, pIdx) in servicePersons" :key="pIdx" class="service-person-card">
|
|
|
+ <div class="sp-header">
|
|
|
+ <span class="sp-title">服务者 {{ 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>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-input v-model="person.title" placeholder="头衔(如高级营养师)" />
|
|
|
+ </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-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>
|
|
|
+ </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>
|
|
|
+ </div>
|
|
|
+ <el-button type="primary" plain size="mini" @click="addServicePerson">+ 添加服务者</el-button>
|
|
|
+ <div class="delivery-config-hint">服务者必须已通过服务商入驻审核(userId 为系统用户ID)</div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 线上-单人(5):固定服务时段配置 -->
|
|
|
+ <el-form-item v-if="form.deliveryMethod === 5" label="线上服务时段">
|
|
|
+ <div class="online-slot-list">
|
|
|
+ <div v-for="(slot, idx) in onlineSlots" :key="idx" 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="removeOnlineSlot(idx)">删除</el-button>
|
|
|
+ </div>
|
|
|
+ <el-button type="primary" plain size="mini" @click="addOnlineSlot">+ 添加时段</el-button>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
<el-form-item label="会员专属">
|
|
|
<el-radio-group v-model="form.memberEligible">
|
|
|
<el-radio label="no">否</el-radio>
|
|
|
@@ -226,7 +287,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { getProduct, createProduct, updateProduct, listAllSupplySystems, getPredefinedPurchaseFields, getProductPurchaseFields, saveProductPurchaseFields, getMySupplySystem, saveProductGiftRule, listProductGiftRules, listMembershipLevels, getGrowthArchiveProduct, saveGrowthArchiveProduct } from '@/api/admin'
|
|
|
+import { getProduct, createProduct, updateProduct, listAllSupplySystems, getPredefinedPurchaseFields, getProductPurchaseFields, saveProductPurchaseFields, getMySupplySystem, saveProductGiftRule, listProductGiftRules, listMembershipLevels, getGrowthArchiveProduct, saveGrowthArchiveProduct, listDeliveryServicePersons, saveDeliveryServicePersons, listDeliveryOnlineSlots, saveDeliveryOnlineSlots } from '@/api/admin'
|
|
|
import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight'
|
|
|
import DimensionWeightPicker from '@/components/DimensionWeightPicker.vue'
|
|
|
import ProductSkuEditor from './components/ProductSkuEditor.vue'
|
|
|
@@ -260,6 +321,8 @@ export default {
|
|
|
validityDays: 365,
|
|
|
guideScope: 'all'
|
|
|
},
|
|
|
+ servicePersons: [], // 线上服务者(deliveryMethod=4): [{userId,name,title,intro,avatar,price,originalPrice,slots:[{startTime,endTime,maxCapacity}]}]
|
|
|
+ onlineSlots: [], // 单人线上时段(deliveryMethod=5): [{startTime,endTime,maxCapacity}]
|
|
|
growthArchive: {
|
|
|
enabled: false,
|
|
|
sortOrder: 0
|
|
|
@@ -418,6 +481,8 @@ export default {
|
|
|
this.loadGiftRule(p.id)
|
|
|
// Load growth archive config
|
|
|
this.loadGrowthArchive(p.id)
|
|
|
+ // Load delivery config (service persons / online slots)
|
|
|
+ this.loadDeliveryConfig(p.id)
|
|
|
// v-model 已绑定 form.description,editor 内容自动更新
|
|
|
}
|
|
|
} catch (e) {
|
|
|
@@ -502,6 +567,89 @@ async loadGiftRule(productId) {
|
|
|
console.warn('加载成长档案关联配置失败', e)
|
|
|
}
|
|
|
},
|
|
|
+ async loadDeliveryConfig(productId) {
|
|
|
+ if (this.form.deliveryMethod === 4) {
|
|
|
+ try {
|
|
|
+ const res = await listDeliveryServicePersons(productId)
|
|
|
+ if (res.code === 200 && Array.isArray(res.data)) {
|
|
|
+ this.servicePersons = res.data.map(p => ({
|
|
|
+ userId: p.userId || '',
|
|
|
+ name: p.name || '',
|
|
|
+ title: p.title || '',
|
|
|
+ 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
|
|
|
+ }))
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('加载线上服务者失败', e)
|
|
|
+ }
|
|
|
+ } else if (this.form.deliveryMethod === 5) {
|
|
|
+ try {
|
|
|
+ const res = await listDeliveryOnlineSlots(productId)
|
|
|
+ if (res.code === 200 && Array.isArray(res.data)) {
|
|
|
+ this.onlineSlots = res.data.map(s => ({
|
|
|
+ startTime: (s.startTime || '').replace('T', ' ').substring(0, 19),
|
|
|
+ endTime: (s.endTime || '').replace('T', ' ').substring(0, 19),
|
|
|
+ maxCapacity: s.maxCapacity || 1
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('加载线上服务时段失败', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ addServicePerson() {
|
|
|
+ this.servicePersons.push({ userId: '', name: '', title: '', intro: '', avatar: '', price: null, originalPrice: null, slots: [] })
|
|
|
+ },
|
|
|
+ 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 })
|
|
|
+ },
|
|
|
+ removeOnlineSlot(idx) {
|
|
|
+ this.onlineSlots.splice(idx, 1)
|
|
|
+ },
|
|
|
+ buildServicePersonPayload() {
|
|
|
+ if (this.form.deliveryMethod !== 4) return []
|
|
|
+ return this.servicePersons
|
|
|
+ .filter(p => p.userId)
|
|
|
+ .map(p => ({
|
|
|
+ userId: Number(p.userId),
|
|
|
+ name: p.name || '',
|
|
|
+ title: p.title || '',
|
|
|
+ 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
|
|
|
+ }))
|
|
|
+ }))
|
|
|
+ },
|
|
|
+ buildOnlineSlotPayload() {
|
|
|
+ if (this.form.deliveryMethod !== 5) return []
|
|
|
+ return this.onlineSlots.filter(s => s.startTime && s.endTime).map(s => ({
|
|
|
+ startTime: s.startTime,
|
|
|
+ endTime: s.endTime,
|
|
|
+ maxCapacity: s.maxCapacity || 1
|
|
|
+ }))
|
|
|
+ },
|
|
|
handlePurchaseFieldsToggle(val) {
|
|
|
if (!val) {
|
|
|
this.predefinedFields.forEach(pf => { pf.selected = false })
|
|
|
@@ -638,6 +786,16 @@ async loadGiftRule(productId) {
|
|
|
console.warn('保存成长档案关联失败', e)
|
|
|
}
|
|
|
}
|
|
|
+ // Save delivery config (service persons / online slots)
|
|
|
+ if (productId) {
|
|
|
+ try {
|
|
|
+ await saveDeliveryServicePersons(productId, this.buildServicePersonPayload())
|
|
|
+ await saveDeliveryOnlineSlots(productId, this.buildOnlineSlotPayload())
|
|
|
+ } catch (e) {
|
|
|
+ this.$message.error('保存线上服务配置失败:' + (e.message || e))
|
|
|
+ console.warn('保存线上服务配置失败', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
this.$message.success(this.isEdit ? '保存成功' : '创建成功')
|
|
|
this.$router.push('/product-manage')
|
|
|
} else {
|
|
|
@@ -794,4 +952,37 @@ async loadGiftRule(productId) {
|
|
|
color: #ccc;
|
|
|
cursor: move;
|
|
|
}
|
|
|
+.service-person-card {
|
|
|
+ border: 1px solid #e8e8e8;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 10px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ background: #fafafa;
|
|
|
+}
|
|
|
+.sp-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+.sp-title {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.sp-slots {
|
|
|
+ margin-top: 8px;
|
|
|
+ padding: 8px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+.sp-slot-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+.delivery-config-hint {
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
</style>
|