| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262 |
- <template>
- <div class="product-edit admin-page">
- <el-card v-loading="pageLoading">
- <div slot="header" class="admin-page-header">
- <span class="admin-page-title">{{ isEdit ? '编辑商品' : '新增商品' }}</span>
- <div class="admin-page-actions">
- <el-button size="mini" @click="$router.back()">返回</el-button>
- </div>
- </div>
- <el-form ref="form" :model="form" :rules="rules" label-width="100px">
- <el-form-item label="商品名称" prop="name">
- <el-input v-model="form.name" placeholder="请输入商品名称" maxlength="100" />
- </el-form-item>
- <el-form-item label="商品类型" prop="productType">
- <el-select v-model="form.productType" placeholder="请选择商品类型" style="width: 200px;">
- <el-option label="实物商品" value="physical" />
- <el-option label="虚拟商品" value="virtual" />
- <el-option label="优惠券" value="coupon" />
- <el-option label="测评商品" value="assessment" />
- <el-option label="套餐" value="bundle" />
- </el-select>
- </el-form-item>
- <!-- 测评商品扩展信息 -->
- <template v-if="form.productType === 'assessment'">
- <el-form-item label="测评类型">
- <el-select v-model="form.assessmentType" placeholder="请选择测评类型" style="width: 200px;">
- <el-option label="单包" value="single" />
- <el-option label="组合" value="bundle" />
- </el-select>
- </el-form-item>
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="测评次数">
- <el-input-number v-model="form.totalSessions" :min="1" :max="999" :precision="0" style="width: 100%;" controls />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="有效期(天)">
- <el-input-number v-model="form.validityDays" :min="1" :max="3650" :precision="0" :default-value="365" style="width: 100%;" controls />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="规划师范围">
- <el-select v-model="form.guideScope" placeholder="不限" style="width: 100%;">
- <el-option label="不限" value="all" />
- <el-option label="指定规划师" value="assigned" />
- <el-option label="自动分配" value="auto" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- </template>
- <!-- 套餐商品扩展信息 -->
- <template v-if="form.productType === 'bundle'">
- <el-divider content-position="left">套餐组成</el-divider>
- <el-form-item label="组成商品">
- <div v-for="(item, idx) in bundleItems" :key="'bi' + idx" style="display:flex; align-items:center; margin-bottom:8px; gap:8px; flex-wrap:wrap;">
- <el-select v-model="item.childProductId" filterable remote placeholder="搜索子商品" :remote-method="(q) => searchBundleProduct(q, idx)" style="width:220px;" @change="(pid) => onBundleProductChange(pid, idx)">
- <el-option v-for="p in item.productOptions" :key="p.id" :label="(p.name || 'ID:' + p.id) + ' (库存:' + (p.stock || 0) + ')'" :value="p.id" />
- </el-select>
- <el-select v-if="item.skuOptions.length" v-model="item.childSkuId" placeholder="选择规格" clearable style="width:160px;">
- <el-option v-for="s in item.skuOptions" :key="s.id" :label="(s.specs || '默认') + ' (库存:' + (s.stock || 0) + ')'" :value="s.id" />
- </el-select>
- <el-input-number v-model="item.quantity" :min="1" :precision="0" size="small" style="width:110px;" />
- <el-button type="danger" size="mini" @click="removeBundleItem(idx)">删除</el-button>
- </div>
- <el-button type="primary" size="mini" plain @click="addBundleItem">+ 添加子商品</el-button>
- <div style="margin-top:8px; font-size:12px; color:#999;">套餐出库时自动完成所有组成商品出库(数量 × 套餐数量)</div>
- </el-form-item>
- </template>
- <!-- 关联成长档案 -->
- <el-divider content-position="left">关联成长档案</el-divider>
- <el-form-item label="关联成长档案">
- <el-switch
- v-model="growthArchive.enabled"
- active-text="开启"
- inactive-text="关闭"
- />
- <span style="margin-left: 10px; font-size: 12px; color: #999;">在成长档案上传页推荐该商品</span>
- </el-form-item>
- <el-form-item label="商品简介" prop="intro">
- <el-input v-model="form.intro" type="textarea" :rows="2" placeholder="简短描述" maxlength="200" />
- </el-form-item>
- <el-form-item label="排序值">
- <el-input-number v-model="form.sortOrder" :min="0" :precision="0" style="width: 200px;" controls />
- <span style="margin-left: 10px; font-size: 12px; color: #999;">越小越靠前</span>
- </el-form-item>
- <el-form-item label="商品描述" prop="description">
- <div class="rich-editor">
- <Toolbar :editor="editorInstance" :defaultConfig="toolbarConfig" />
- <Editor :defaultConfig="editorConfig" v-model="form.description" @onCreated="handleEditorCreated" style="min-height: 300px;" />
- </div>
- </el-form-item>
- <el-form-item label="封面图片" prop="coverImage">
- <div class="cover-uploader">
- <el-upload
- :action="uploadActionUrl"
- :headers="uploadHeaders"
- :show-file-list="false"
- :on-success="handleCoverSuccess"
- :before-upload="beforeUpload"
- accept="image/*"
- >
- <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview" />
- <i v-else class="el-icon-plus cover-icon"></i>
- </el-upload>
- <span class="cover-tip">点击上传封面图,建议尺寸 800x800</span>
- </div>
- </el-form-item>
- <el-form-item label="商品图片">
- <div class="images-uploader">
- <div v-for="(img, idx) in imageList" :key="idx" class="image-item">
- <img :src="img" class="image-preview" />
- <span class="image-remove" @click="removeImage(idx)">×</span>
- </div>
- <el-upload
- :action="uploadActionUrl"
- :headers="uploadHeaders"
- :show-file-list="false"
- :on-success="handleImageSuccess"
- :before-upload="beforeUpload"
- accept="image/*"
- multiple
- >
- <i class="el-icon-plus image-add-icon"></i>
- </el-upload>
- </div>
- </el-form-item>
- <el-row :gutter="20">
- <el-col :span="6">
- <el-form-item label="售价(元)" prop="price">
- <el-input-number v-model="form.price" :min="0" :precision="2" placeholder="0.00" style="width: 100%;" controls />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="会员价(元)">
- <el-input-number v-model="form.memberPrice" :min="0" :precision="2" :step="0.01" placeholder="留空等于普通价" style="width: 100%;" controls />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="库存" prop="stock">
- <el-input-number v-model="form.stock" :min="0" :precision="0" placeholder="0" style="width: 100%;" controls />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="最小购买数">
- <el-input-number v-model="form.minQuantity" :min="1" :precision="0" placeholder="1" style="width: 100%;" controls />
- </el-form-item>
- </el-col>
- </el-row>
- <el-form-item label="所属体系">
- <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">
- 平台留利: {{ selectedSystem.platformProfitRate }}% → {{ selectedSystem.platformProfitRate * 10 }}‰
- </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>
- <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 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">{{ person.name || '服务者 ' + (pIdx+1) }}</span>
- <el-button type="text" size="mini" @click="removeServicePerson(pIdx)">删除</el-button>
- </div>
- <el-row :gutter="10">
- <el-col :span="12">
- <el-input v-model="person.name" placeholder="展示名称" size="small" />
- </el-col>
- <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="12">
- <el-input v-model.number="person.price" placeholder="专属价格(元)" size="small" type="number" />
- </el-col>
- <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="个人介绍(可留空,自动从服务商资料代入)" size="small" style="margin-top:8px" />
- </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>
- <el-radio label="yes">是</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="商品维度">
- <dimension-weight-picker v-model="form.dimensionWeights" :show-presets="true" />
- </el-form-item>
- <el-form-item label="SKU规格">
- <ProductSkuEditor ref="skuEditor" :productId="form.id" />
- </el-form-item>
- <!-- 购买信息采集 -->
- <el-form-item label="购买信息采集">
- <div class="purchase-field-section">
- <el-checkbox v-model="purchaseFieldsEnabled" @change="handlePurchaseFieldsToggle">
- 启用购买信息采集(用户购买时需填写/确认以下信息)
- </el-checkbox>
- <div v-if="purchaseFieldsEnabled && predefinedFields.length" class="purchase-field-list">
- <div
- v-for="(pf, idx) in predefinedFields"
- :key="pf.fieldKey"
- class="purchase-field-item"
- >
- <el-checkbox
- v-model="pf.selected"
- @change="syncPurchaseFieldOrder"
- >
- {{ pf.fieldName }}
- </el-checkbox>
- <el-checkbox
- v-if="pf.selected"
- v-model="pf.required"
- class="purchase-field-required"
- size="mini"
- >
- 必填
- </el-checkbox>
- <span v-if="pf.selected" class="purchase-field-drag">
- <i class="el-icon-rank"></i>
- </span>
- </div>
- </div>
- </div>
- </el-form-item>
- <el-form-item label="赠送会员">
- <el-switch v-model="giftRuleEnabled" active-text="启用" inactive-text="停用" />
- <el-select v-if="giftRuleEnabled" v-model="giftRuleLevel" placeholder="选择赠送等级" style="margin-left: 12px; width: 200px">
- <el-option v-for="lv in membershipLevels" :key="lv.levelCode" :label="lv.levelName" :value="lv.levelCode" />
- </el-select>
- </el-form-item>
- <el-form-item label="可选赠品">
- <div style="width: 100%">
- <div v-for="lv in membershipLevels" :key="lv.levelCode" class="gift-config-level">
- <div class="gift-config-level-title">{{ lv.levelName }} 赠品</div>
- <div v-for="(item, idx) in getGiftItems(lv.levelCode)" :key="getGiftItemKey(item, idx)" class="gift-config-item">
- <el-select v-model="item.giftType" size="mini" style="width: 110px; margin-right: 8px">
- <el-option label="赠品商品" value="product" />
- <el-option label="CF值" value="cf" />
- </el-select>
- <el-select v-if="item.giftType === 'product'" v-model="item.giftProductId" size="mini" filterable placeholder="选择赠品商品" style="width: 260px; margin-right: 8px">
- <el-option v-for="p in onShelfProducts" :key="p.id" :label="p.name" :value="p.id" />
- </el-select>
- <el-input v-if="item.giftType === 'cf'" v-model.number="item.cfValue" size="mini" placeholder="CF值数量" type="number" style="width: 140px; margin-right: 8px" />
- <el-button size="mini" type="text" @click="removeGiftItem(lv.levelCode, idx)">删除</el-button>
- </div>
- <el-button size="mini" type="text" @click="addGiftItem(lv.levelCode)">+ 添加赠品</el-button>
- </div>
- </div>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" :loading="saving" @click="handleSave">
- {{ isEdit ? '保存修改' : '创建商品' }}
- </el-button>
- <el-button @click="$router.back()">取消</el-button>
- </el-form-item>
- </el-form>
- </el-card>
- </div>
- </template>
- <script>
- import { getProduct, createProduct, updateProduct, listAllSupplySystems, getPredefinedPurchaseFields, getProductPurchaseFields, saveProductPurchaseFields, getMySupplySystem, saveProductGiftRule, listProductGiftRules, listMembershipLevels, getGrowthArchiveProduct, saveGrowthArchiveProduct, listDeliveryServicePersons, saveDeliveryServicePersons, listDeliveryOnlineSlots, saveDeliveryOnlineSlots, getProductList, getProductSkuList, saveBundleItems, getBundleItems, getApprovedGuides, saveProductGift, getProductGiftList } from '@/api/admin'
- import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight'
- import DimensionWeightPicker from '@/components/DimensionWeightPicker.vue'
- import ProductSkuEditor from './components/ProductSkuEditor.vue'
- import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
- import '@wangeditor/editor/dist/css/style.css'
- export default {
- name: 'ProductEdit',
- components: { DimensionWeightPicker, ProductSkuEditor, Editor, Toolbar },
- data() {
- return {
- form: {
- name: '',
- intro: '',
- description: '',
- coverImage: '',
- images: '',
- productType: 'physical',
- price: null,
- memberPrice: null,
- stock: null,
- minQuantity: 1,
- vendorName: '',
- vendorId: null,
- distributionSystemId: null,
- deliveryMethod: 1,
- memberEligible: 'no',
- dimensionWeights: [],
- // 测评商品扩展
- assessmentType: 'single',
- totalSessions: 1,
- validityDays: 365,
- guideScope: 'all',
- sortOrder: 0
- },
- bundleItems: [], // 套餐组成: [{childProductId, childSkuId, quantity, productOptions:[], skuOptions:[]}]
- servicePersons: [], // 线上服务者(deliveryMethod=4): [{userId,name,title,intro,avatar,price,originalPrice}]
- approvedGuides: [], // 已入驻服务商列表
- selectedGuideId: '',
- guideSearchQuery: '',
- onlineSlots: [], // 单人线上时段(deliveryMethod=5): [{startTime,endTime,maxCapacity}]
- growthArchive: {
- enabled: false,
- sortOrder: 0
- },
- imageList: [],
- supplySystems: [],
- giftRuleEnabled: false,
- giftRuleLevel: '',
- membershipLevels: [],
- // 可选赠品配置:{ [levelCode]: [{giftType,giftProductId,cfValue,sortOrder}] }
- giftConfigsByLevel: {},
- onShelfProducts: [],
- rules: {
- name: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
- productType: [{ required: true, message: '请选择商品类型', trigger: 'change' }],
- price: [{ required: true, message: '请输入售价', trigger: 'blur' }],
- stock: [{ required: true, message: '请输入库存', trigger: 'blur' }]
- },
- saving: false,
- pageLoading: false,
- purchaseFieldsEnabled: false,
- predefinedFields: [],
- editorConfig: {
- MENU_CONF: {
- uploadImage: {
- server: '',
- headers: {},
- maxNumberOfFiles: 1,
- maxFileSize: 5 * 1024 * 1024,
- fieldName: 'file',
- customAlert: (msg, type) => {
- this.$message && this.$message({ message: msg, type: type || 'error' })
- },
- onError: (file, err, res) => {
- const msg = (file && file.name || '图片') + ' 上传失败,图片不能超过 5MB'
- this.$message && this.$message({ message: msg, type: 'error' })
- },
- customInsert(res, insertFn) {
- if (res.code === 200) {
- const baseURL = process.env.VUE_APP_BASE_API || ''
- const url = res.data && res.data.url ? res.data.url : res.data
- insertFn(baseURL + url)
- }
- }
- }
- }
- },
- addBundleItem() {
- this.bundleItems.push({
- childProductId: null,
- childSkuId: null,
- quantity: 1,
- productOptions: [],
- skuOptions: []
- })
- },
- removeBundleItem(idx) {
- this.bundleItems.splice(idx, 1)
- },
- async searchBundleProduct(query, idx) {
- if (!query || query.length < 1) return
- try {
- var res = await getProductList({ keyword: query, page: 1, size: 20 })
- if (res.code === 200 && res.data && Array.isArray(res.data.records)) {
- this.bundleItems[idx].productOptions = res.data.records
- }
- } catch (e) { /* ignore */ }
- },
- async onBundleProductChange(productId, idx) {
- var item = this.bundleItems[idx]
- item.childSkuId = null
- item.skuOptions = []
- if (!productId) return
- try {
- var res = await getProductSkuList(productId)
- if (res.code === 200 && Array.isArray(res.data)) {
- item.skuOptions = res.data
- }
- } catch (e) { /* ignore */ }
- },
- buildBundleItemPayload() {
- return this.bundleItems
- .filter(function(b) { return b.childProductId })
- .map(function(b) {
- return {
- childProductId: b.childProductId,
- childSkuId: b.childSkuId || null,
- quantity: b.quantity || 1
- }
- })
- },
- async loadBundleItems(productId) {
- try {
- var res = await getBundleItems(productId)
- if (res.code === 200 && Array.isArray(res.data)) {
- this.bundleItems = res.data.map(function(b) {
- return {
- childProductId: b.childProductId,
- childSkuId: b.childSkuId || null,
- quantity: b.quantity || 1,
- productOptions: [],
- skuOptions: []
- }
- })
- this.bundleItems.forEach(function(item, idx) {
- if (item.childProductId) {
- this.onBundleProductChange(item.childProductId, idx)
- }
- }, this)
- }
- } catch (e) { /* ignore */ }
- },
- toolbarConfig: {},
- editorInstance: null
- }
- },
- computed: {
- isEdit() {
- return !!this.$route.query.id
- },
- isSupplierAdmin() {
- const rolesStr = localStorage.getItem('roles')
- const roles = rolesStr ? JSON.parse(rolesStr) : []
- return roles.includes('supplier') || roles.includes('supplier_admin')
- },
- uploadActionUrl() {
- const baseURL = process.env.VUE_APP_BASE_API || ''
- return baseURL + '/api/admin/articles/upload/image'
- },
- uploadHeaders() {
- const token = localStorage.getItem('token')
- return { Authorization: 'Bearer ' + token }
- },
- 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
- },
- mounted() {
- this.loadSupplySystems()
- this.loadPredefinedFields()
- this.ensureMembershipLevels()
- this.loadOnShelfProducts()
- if (this.isEdit) {
- this.loadProduct(this.$route.query.id)
- }
- },
- methods: {
- handleEditorCreated(editor) {
- 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) {
- this.supplySystems = res.data
- }
- } catch (e) {
- console.error(e)
- }
- },
- handleSystemChange(systemId) {
- if (!systemId) return
- const system = this.supplySystems.find(s => s.id === systemId)
- if (system && system.platformProfitRate != null) {
- this.form.profitRate = Math.round(system.platformProfitRate * 10)
- }
- },
- async loadProduct(id) {
- this.pageLoading = true
- try {
- const res = await getProduct(id)
- if (res.code === 200 && res.data) {
- const p = res.data
- this.form = {
- id: p.id,
- name: p.name || '',
- intro: p.intro || '',
- description: p.description || '',
- coverImage: p.coverImage || '',
- images: p.images || '',
- productType: p.productType || 'physical',
- price: (p.price != null) ? p.price / 100 : null,
- memberPrice: (p.memberPrice != null && p.memberPrice > 0) ? p.memberPrice / 100 : null,
- stock: (p.stock != null) ? p.stock : null,
- minQuantity: p.minQuantity || null,
- vendorName: p.vendorName || '',
- vendorId: p.vendorId || null,
- distributionSystemId: p.distributionSystemId || null,
- deliveryMethod: p.deliveryMethod || 1,
- memberEligible: p.memberEligible || 'no',
- dimensionWeights: [],
- assessmentType: p.assessmentType || 'single',
- totalSessions: p.totalSessions || 1,
- validityDays: p.validityDays || 365,
- guideScope: p.guideScope || 'all',
- sortOrder: p.sortOrder != null ? p.sortOrder : 0
- }
- if (p.images) {
- try {
- this.imageList = JSON.parse(p.images)
- } catch (e) {
- this.imageList = []
- }
- }
- // Load dimension weights
- this.loadDimensionWeights(p.id)
- // Load purchase fields config
- this.loadPurchaseFields(p.id)
- // 赠送会员规则回显
- this.loadGiftRule(p.id)
- this.loadGiftConfigs(p.id)
- // Load growth archive config
- this.loadGrowthArchive(p.id)
- // Load delivery config (service persons / online slots)
- this.loadDeliveryConfig(p.id)
- // Load bundle items config(套餐商品)
- if (p.productType === 'bundle') {
- this.loadBundleItems(p.id)
- }
- // v-model 已绑定 form.description,editor 内容自动更新
- }
- } catch (e) {
- this.$message.error('加载商品失败')
- } finally {
- this.pageLoading = false
- }
- },
- async loadDimensionWeights(productId) {
- try {
- const res = await getDimensionWeights('product', productId)
- if (res.code === 200 && res.data) {
- this.form.dimensionWeights = res.data.map(w => ({
- dimension: w.dimension,
- weight: w.weight,
- enabled: w.enabled
- }))
- }
- } catch (e) {
- // ignore
- }
- },
- async loadPredefinedFields() {
- try {
- const res = await getPredefinedPurchaseFields()
- if (res.code === 200 && Array.isArray(res.data)) {
- this.predefinedFields = res.data.map(f => ({
- fieldKey: f.fieldKey,
- fieldName: f.fieldName,
- selected: false,
- required: true
- }))
- }
- } catch (e) {
- console.warn('加载预定义字段失败', e)
- }
- },
- async loadPurchaseFields(productId) {
- try {
- const res = await getProductPurchaseFields(productId)
- if (res.code === 200 && Array.isArray(res.data)) {
- const configs = res.data
- this.purchaseFieldsEnabled = configs.length > 0
- this.predefinedFields.forEach(pf => {
- const match = configs.find(c => c.fieldKey === pf.fieldKey)
- pf.selected = !!match
- pf.required = match ? match.isRequired === 1 : false
- })
- }
- } catch (e) {
- console.warn('加载购买字段配置失败', e)
- }
- },
- async loadGiftRule(productId) {
- try {
- const lvRes = await listMembershipLevels()
- if (lvRes.code === 200 && Array.isArray(lvRes.data)) {
- this.membershipLevels = lvRes.data.filter(l => l.levelCode !== 'FREE')
- }
- const ruleRes = await listProductGiftRules(productId)
- if (ruleRes.code === 200 && Array.isArray(ruleRes.data) && ruleRes.data.length > 0) {
- const active = ruleRes.data.find(r => r.status === 1)
- if (active) {
- this.giftRuleEnabled = true
- this.giftRuleLevel = active.levelCode
- }
- }
- } catch (e) {
- console.warn('加载赠送会员规则失败', e)
- }
- },
- getGiftItems(levelCode) {
- return this.giftConfigsByLevel[levelCode] || []
- },
- getGiftItemKey(item, idx) {
- return (item && item.giftType ? item.giftType : 'item') + '-' + idx
- },
- addGiftItem(levelCode) {
- var arr = this.giftConfigsByLevel[levelCode]
- if (!arr) {
- this.$set(this.giftConfigsByLevel, levelCode, [])
- arr = this.giftConfigsByLevel[levelCode]
- }
- arr.push({ giftType: 'product', giftProductId: null, cfValue: 0, sortOrder: arr.length })
- },
- removeGiftItem(levelCode, idx) {
- var arr = this.giftConfigsByLevel[levelCode]
- if (arr) arr.splice(idx, 1)
- },
- async ensureMembershipLevels() {
- if (this.membershipLevels.length === 0) {
- try {
- const lvRes = await listMembershipLevels()
- if (lvRes.code === 200 && Array.isArray(lvRes.data)) {
- this.membershipLevels = lvRes.data.filter(l => l.levelCode !== 'FREE')
- }
- } catch (e) {
- console.warn('加载会员等级失败', e)
- }
- }
- },
- async loadOnShelfProducts() {
- try {
- const res = await getProductList({ status: 'on_shelf', page: 1, size: 200 })
- if (res.code === 200) {
- const data = res.data || {}
- this.onShelfProducts = data.records || (Array.isArray(data) ? data : [])
- }
- } catch (e) {
- console.warn('加载上架商品失败', e)
- }
- },
- async loadGiftConfigs(productId) {
- try {
- await this.ensureMembershipLevels()
- await this.loadOnShelfProducts()
- // 初始化每等级空数组
- var that = this
- this.membershipLevels.forEach(function(lv) {
- that.$set(that.giftConfigsByLevel, lv.levelCode, [])
- })
- const res = await getProductGiftList(productId)
- if (res.code === 200 && Array.isArray(res.data)) {
- res.data.forEach(function(cfg) {
- if (cfg && cfg.levelCode) {
- that.$set(that.giftConfigsByLevel, cfg.levelCode, (cfg.items || []).map(function(it) {
- return {
- giftType: it.giftType,
- giftProductId: it.giftProductId,
- cfValue: it.cfValue || 0,
- sortOrder: it.sortOrder || 0
- }
- }))
- }
- })
- }
- } catch (e) {
- console.warn('加载赠品配置失败', e)
- }
- },
- async saveGiftConfigs(productId) {
- try {
- for (var i = 0; i < this.membershipLevels.length; i++) {
- var lv = this.membershipLevels[i]
- var items = (this.giftConfigsByLevel[lv.levelCode] || []).map(function(it, idx) {
- return {
- giftType: it.giftType,
- giftProductId: it.giftType === 'product' ? it.giftProductId : null,
- cfValue: it.giftType === 'cf' ? (it.cfValue || 0) : 0,
- sortOrder: idx
- }
- })
- var status = items.length > 0 ? 1 : 0
- await saveProductGift({ productId: productId, levelCode: lv.levelCode, status: status, items: items })
- }
- } catch (e) {
- console.warn('保存赠品配置失败', e)
- }
- },
- async loadGrowthArchive(productId) {
- try {
- const res = await getGrowthArchiveProduct(productId)
- if (res.code === 200 && res.data) {
- this.growthArchive = {
- enabled: res.data.status === 1,
- sortOrder: res.data.sortOrder || 0
- }
- }
- } catch (e) {
- 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
- }))
- }
- } 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 })
- },
- removeServicePerson(idx) {
- this.servicePersons.splice(idx, 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
- }))
- },
- 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
- }))
- },
- 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 })
- }
- },
- syncPurchaseFieldOrder() {
- // sortOrder is derived from selection order
- },
- buildPurchaseFieldPayload() {
- if (!this.purchaseFieldsEnabled) return []
- return this.predefinedFields
- .filter(pf => pf.selected)
- .map((pf, idx) => ({
- fieldKey: pf.fieldKey,
- fieldName: pf.fieldName,
- isRequired: pf.required ? 1 : 0,
- sortOrder: idx
- }))
- },
- beforeUpload(file) {
- const isImage = file.type.startsWith('image/')
- const isLt5M = file.size / 1024 / 1024 < 5
- if (!isImage) {
- this.$message.error('只能上传图片文件')
- return false
- }
- if (!isLt5M) {
- this.$message.error('图片大小不能超过 5MB')
- return false
- }
- return true
- },
- handleCoverSuccess(res) {
- if (res.code === 200 && res.data) {
- this.form.coverImage = res.data.url || res.data
- this.$message.success('封面上传成功')
- } else {
- this.$message.error(res.message || '上传失败')
- }
- },
- handleImageSuccess(res) {
- if (res.code === 200 && res.data) {
- const url = res.data.url || res.data
- this.imageList.push(url)
- this.form.images = JSON.stringify(this.imageList)
- this.$message.success('图片上传成功')
- } else {
- this.$message.error(res.message || '上传失败')
- }
- },
- removeImage(idx) {
- this.imageList.splice(idx, 1)
- this.form.images = JSON.stringify(this.imageList)
- },
- async handleSave() {
- this.$refs.form.validate(async (valid) => {
- if (!valid) return
- this.saving = true
- try {
- const productData = {
- ...this.form,
- price: (this.form.price != null) ? Math.round(this.form.price * 100) : 0,
- memberPrice: (this.form.memberPrice != null && this.form.memberPrice > 0) ? Math.round(this.form.memberPrice * 100) : 0,
- images: JSON.stringify(this.imageList)
- }
- // Extract dimensionWeights before sending to product API
- const dimensionWeights = productData.dimensionWeights
- delete productData.dimensionWeights
- // Build ProductCreateRequest payload
- const requestPayload = { product: productData }
- if (productData.productType === 'assessment') {
- requestPayload.assessmentExt = {
- assessmentType: productData.assessmentType,
- totalSessions: productData.totalSessions,
- validityDays: productData.validityDays,
- guideScope: productData.guideScope
- }
- }
- // Remove assessment ext fields from product object
- delete productData.assessmentType
- delete productData.totalSessions
- delete productData.validityDays
- delete productData.guideScope
- let res
- let productId = this.form.id
- if (this.isEdit) {
- res = await updateProduct(requestPayload)
- productId = this.form.id
- } else {
- res = await createProduct(requestPayload)
- if (res.code === 200 && res.data) {
- productId = res.data.id || res.data
- this.form.id = productId
- }
- }
- if (res.code === 200) {
- // Save dimension weights
- if (productId && Array.isArray(dimensionWeights)) {
- try {
- await saveDimensionWeights('product', productId, dimensionWeights)
- } catch (e) {
- console.warn('保存维度权重失败', e)
- }
- }
- // Save purchase fields config
- if (productId) {
- try {
- const fields = this.buildPurchaseFieldPayload()
- await saveProductPurchaseFields(productId, fields)
- } catch (e) {
- console.warn('保存购买字段配置失败', e)
- }
- }
- // 保存赠送会员规则
- if (productId) {
- try {
- await saveProductGiftRule(productId, this.giftRuleEnabled ? 1 : 0,
- this.giftRuleEnabled ? this.giftRuleLevel : '')
- } catch (e) {
- console.warn('保存赠送会员规则失败', e)
- }
- }
- // 保存可选赠品配置
- if (productId) {
- try {
- await this.saveGiftConfigs(productId)
- } catch (e) {
- console.warn('保存赠品配置失败', e)
- }
- }
- // Save growth archive config (only for existing products)
- if (this.form.id) {
- try {
- await saveGrowthArchiveProduct({
- productId: this.form.id,
- status: this.growthArchive.enabled ? 1 : 0
- })
- } catch (e) {
- this.$message.error('保存成长档案关联失败')
- 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)
- }
- }
- // Save bundle items config(套餐商品组成)
- if (productId) {
- try {
- var bundlePayload = this.buildBundleItemPayload()
- await saveBundleItems(productId, bundlePayload)
- } catch (e) {
- this.$message.error('保存套餐组成失败')
- console.warn('保存套餐组成失败', e)
- }
- }
- this.$message.success(this.isEdit ? '保存成功' : '创建成功')
- this.$router.push('/product-manage')
- } else {
- this.$message.error(res.message || '操作失败')
- }
- } catch (e) {
- this.$message.error('操作失败')
- } finally {
- this.saving = false
- }
- })
- }
- }
- }
- </script>
- <style scoped>
- .product-edit {
- padding: 20px;
- display: block;
- min-height: auto;
- }
- .product-edit > .el-card {
- display: block;
- min-height: auto;
- }
- .product-edit > .el-card /deep/ .el-card__body {
- display: block;
- min-height: auto;
- overflow: visible;
- }
- .cover-uploader {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .cover-preview {
- width: 100px;
- height: 100px;
- object-fit: cover;
- border-radius: 4px;
- border: 1px solid #ddd;
- }
- .cover-icon {
- width: 100px;
- height: 100px;
- line-height: 100px;
- text-align: center;
- font-size: 28px;
- color: #8c939d;
- border: 1px dashed #d9d9d9;
- border-radius: 4px;
- display: block;
- cursor: pointer;
- }
- .cover-icon:hover {
- border-color: #409eff;
- color: #409eff;
- }
- .cover-tip {
- font-size: 12px;
- color: #999;
- }
- .images-uploader {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
- .image-item {
- position: relative;
- width: 80px;
- height: 80px;
- }
- .image-preview {
- width: 80px;
- height: 80px;
- object-fit: cover;
- border-radius: 4px;
- border: 1px solid #ddd;
- }
- .image-remove {
- position: absolute;
- top: -6px;
- right: -6px;
- width: 18px;
- height: 18px;
- background: #f56c6c;
- color: #fff;
- border-radius: 50%;
- text-align: center;
- line-height: 18px;
- font-size: 12px;
- cursor: pointer;
- }
- .image-add-icon {
- width: 80px;
- height: 80px;
- line-height: 80px;
- text-align: center;
- font-size: 24px;
- color: #8c939d;
- border: 1px dashed #d9d9d9;
- border-radius: 4px;
- display: block;
- cursor: pointer;
- }
- .image-add-icon:hover {
- border-color: #409eff;
- color: #409eff;
- }
- .product-edit .el-card {
- width: 100%;
- }
- .product-edit .el-form {
- width: 100%;
- }
- .rich-editor {
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- }
- .rich-editor /deep/ .w-e-toolbar {
- flex-wrap: wrap;
- }
- .rich-editor /deep/ .w-e-text-container {
- min-height: 300px;
- max-height: 500px;
- overflow-y: auto;
- }
- .purchase-field-section {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- .purchase-field-list {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- padding: 12px;
- background: #fafafa;
- border-radius: 4px;
- border: 1px solid #eee;
- }
- .purchase-field-item {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 6px 12px;
- background: #fff;
- border: 1px solid #e8e8e8;
- border-radius: 4px;
- }
- .purchase-field-required {
- font-size: 12px;
- }
- .purchase-field-drag {
- color: #ccc;
- cursor: move;
- }
- .gift-config-level {
- margin-bottom: 12px;
- padding: 8px 0;
- border-bottom: 1px dashed #eee;
- }
- .gift-config-level-title {
- font-size: 13px;
- color: #606266;
- margin-bottom: 6px;
- }
- .gift-config-item {
- display: flex;
- align-items: center;
- margin-bottom: 6px;
- }
- .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>
|