| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228 |
- <template>
- <view class="page">
- <u-navbar :title="isEdit ? '编辑菜品' : '新建菜品'" :autoBack="true" :placeholder="true"></u-navbar>
- <view class="card">
- <view class="section-title">基础信息</view>
- <view class="row">
- <text class="label required">菜品归属</text>
- <u-radio-group v-model="form.belong" :disabled="isEdit" @change="onBelongChange">
- <u-radio name="store" active-color="#449AFF">门店</u-radio>
- <u-radio v-if="showGroupBelong" name="group" active-color="#449AFF">团餐</u-radio>
- </u-radio-group>
- </view>
- <view class="row" @click="pickCategory">
- <text class="label required">菜品分类</text>
- <text class="value">{{ categoryName || '请选择' }}</text>
- <image class="arrow-down" src="/static/merchantDish/selete.png" mode="aspectFit"></image>
- </view>
- <view class="row">
- <text class="label required">菜品名称</text>
- <input class="input" v-model="form.name" placeholder="请输入" placeholder-class="ph" />
- </view>
- <view class="row" @click="pickProductType">
- <text class="label required">菜品类型</text>
- <text class="value">{{ productTypeName || '请选择' }}</text>
- <image class="arrow-down" src="/static/merchantDish/selete.png" mode="aspectFit"></image>
- </view>
- <view class="upload-row">
- <text class="label required">菜品封面图</text>
- <view class="tip">支持jpg、png格式,大小不超过2M</view>
- <u-upload
- :action="uploadAction"
- :file-list="fileList.cover"
- :form-data="uploadFormData"
- :max-count="4"
- :max-size="2 * 1024 * 1024"
- :multiple="true"
- :deletable="true"
- :custom-btn="true"
- del-bg-color="#fa3534"
- width="140"
- height="140"
- preview-full-image
- @on-success="onCoverSuccess"
- @on-remove="onCoverRemove"
- @on-oversize="onUploadOversize"
- >
- <view slot="addBtn" class="upload-btn">
- <image class="upload-icon" src="/static/merchantDish/photo.png" mode="aspectFit"></image>
- <text>上传</text>
- </view>
- </u-upload>
- </view>
- <view class="upload-row last">
- <text class="label required">菜品图片详情</text>
- <view class="tip">支持jpg、png格式,大小不超过2M</view>
- <u-upload
- :action="uploadAction"
- :file-list="fileList.detail"
- :form-data="uploadFormData"
- :max-count="20"
- :max-size="2 * 1024 * 1024"
- :multiple="true"
- :deletable="true"
- :custom-btn="true"
- del-bg-color="#fa3534"
- width="140"
- height="140"
- preview-full-image
- @on-success="onDetailSuccess"
- @on-remove="onDetailRemove"
- @on-oversize="onUploadOversize"
- >
- <view slot="addBtn" class="upload-btn">
- <image class="upload-icon" src="/static/merchantDish/photo.png" mode="aspectFit"></image>
- <text>上传</text>
- </view>
- </u-upload>
- </view>
- </view>
- <view class="card">
- <view class="section-title">其他信息</view>
- <view class="row">
- <text class="label required">是否为套餐</text>
- <u-radio-group v-model="form.isCombo" @change="onComboChange">
- <u-radio :name="1" active-color="#449AFF">是</u-radio>
- <u-radio :name="0" active-color="#449AFF">否</u-radio>
- </u-radio-group>
- </view>
- <view class="row" v-if="Number(form.isCombo) === 1" @click="goCombo">
- <text class="label">套餐</text>
- <text class="value link">{{ comboCount ? `共${comboCount}项` : '去添加' }}</text>
- <u-icon name="arrow-right" color="#C7C6CA"></u-icon>
- </view>
- <view class="row">
- <text class="label required">销售规格</text>
- <SpecModeSwitch
- ref="specSwitch"
- :locked="specLocked"
- :value="specMode"
- @change="onSpecModeChange"
- @locked="onSpecLockedTip"
- />
- </view>
- <view class="spec-tip" v-if="specLocked && specMode !== 'multi'">当前归属为团餐或已设为套餐,仅支持单规格</view>
- <!-- panelMode 延后切换,避免与下一步按钮抢同一帧渲染 -->
- <view class="spec-panels" :class="panelMode">
- <view class="panel panel-single">
- <view class="row">
- <text class="label required">售价 (元)</text>
- <input class="input" type="digit" :value="form.price" @input="onPriceInput('price', $event)" placeholder="请输入" placeholder-class="ph" />
- </view>
- <view class="row">
- <text class="label required">原价 (元)</text>
- <input class="input" type="digit" :value="form.sellingPrice" @input="onPriceInput('sellingPrice', $event)" placeholder="请输入" placeholder-class="ph" />
- </view>
- <view class="row">
- <text class="label required">可售库存</text>
- <input class="input" type="number" :value="form.stock" @input="onPriceInput('stock', $event)" placeholder="请输入" placeholder-class="ph" />
- </view>
- <view class="row">
- <text class="label">成本价 (元)</text>
- <input class="input" type="digit" :value="form.costPrice" @input="onPriceInput('costPrice', $event)" placeholder="请输入 (选填)" placeholder-class="ph" />
- </view>
- </view>
- <view class="panel panel-multi">
- <view class="spec-block" v-for="(spec, si) in form.specs" :key="spec.id">
- <view class="spec-head">
- <text>{{ spec.name }}</text>
- <text class="more" @click="openSpecMenu(si)">···</text>
- </view>
- <view class="tags">
- <view
- v-for="(v, vi) in spec.values"
- :key="v.id"
- :class="['tag', v.selected ? 'on' : '']"
- @click="toggleSpecValue(v)"
- >
- {{ v.name }}
- <image v-if="v.selected" class="tag-check" src="/static/merchantDish/selete-true.png" mode="aspectFit"></image>
- </view>
- </view>
- </view>
- <view class="add-spec" @click="addSpec">+ 添加规格</view>
- </view>
- </view>
- </view>
- <FormNextBtn ref="nextBtn" @click="onNext" />
- <!-- 规格操作菜单 -->
- <u-action-sheet :list="specMenuList" v-model="specMenuShow" @click="onSpecMenu"></u-action-sheet>
- <!-- 编辑规格名 -->
- <view class="mask" v-if="nameModal" @click="nameModal = false">
- <view class="spec-dialog" @click.stop>
- <view class="spec-dialog-title">编辑规格名</view>
- <input
- class="spec-dialog-input"
- v-model="editingName"
- placeholder="请输入规格名称"
- placeholder-class="spec-ph"
- />
- <view class="spec-dialog-footer">
- <view class="spec-dialog-btn cancel" @click="nameModal = false">取消</view>
- <view class="spec-dialog-btn save" @click="saveSpecName">保存</view>
- </view>
- </view>
- </view>
- <!-- 编辑规格值 -->
- <view class="mask" v-if="valueListShow" @click="valueListShow = false">
- <view class="spec-dialog spec-value-dialog" @click.stop>
- <view class="spec-dialog-title">编辑规格值</view>
- <view class="sv-list">
- <view class="sv-row" v-for="(v, i) in editingValues" :key="v.id">
- <input
- class="spec-dialog-input sv-input"
- v-model="v.name"
- placeholder="请输入规格值"
- placeholder-class="spec-ph"
- />
- <view class="sv-circle minus" @click="removeSpecValue(i)">−</view>
- </view>
- <view class="sv-row">
- <input
- class="spec-dialog-input sv-input"
- v-model="valueDraft"
- placeholder="请输入规格值"
- placeholder-class="spec-ph"
- />
- <view class="sv-circle plus" @click="addSpecValue">+</view>
- </view>
- </view>
- <view class="spec-dialog-footer">
- <view class="spec-dialog-btn cancel" @click="valueListShow = false">取消</view>
- <view class="spec-dialog-btn save" @click="confirmValues">保存</view>
- </view>
- </view>
- </view>
- <!-- 菜品分类 / 菜品类型:底部弹层 -->
- <view class="pick-mask" v-if="pickShow" @click="closePickPopup">
- <view class="pick-sheet" @click.stop>
- <view class="pick-sheet-title">{{ pickTitle }}</view>
- <view class="pick-options">
- <view
- v-for="item in pickOptions"
- :key="item.value"
- :class="['pick-opt', String(pickTemp) === String(item.value) ? 'on' : '']"
- @click="pickTemp = item.value"
- >
- {{ item.label }}
- </view>
- </view>
- <view class="pick-sheet-footer">
- <view class="pick-btn cancel" @click="closePickPopup">取消</view>
- <view class="pick-btn confirm" @click="confirmPick">确定</view>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script>
- import { buildSpecCombos } from '../utils.js';
- import session from '../session.js';
- import configService from '@/common/service/config.service';
- import merchantDishApi, {
- belongingToKey,
- CATEGORY_TO_PRODUCT_TYPE,
- COMBO_API_TO_CAT,
- PRODUCT_TYPE_TO_CATEGORY
- } from '@/api/merchantDish.js';
- import SpecModeSwitch from './components/SpecModeSwitch.vue';
- import FormNextBtn from './components/FormNextBtn.vue';
- const PRODUCT_TYPES = [
- { label: '早餐', value: 'breakfast' },
- { label: '午餐', value: 'lunch' },
- { label: '晚餐', value: 'dinner' }
- ];
- const DEFAULT_SPECS = [
- {
- id: 's1',
- name: '份量',
- values: [
- { id: 'v1', name: '小份', selected: true },
- { id: 'v2', name: '中份', selected: true },
- { id: 'v3', name: '大份', selected: false }
- ]
- },
- {
- id: 's2',
- name: '口味',
- values: [
- { id: 'v4', name: '微辣', selected: false },
- { id: 'v5', name: '中辣', selected: true },
- { id: 'v6', name: '特辣', selected: false }
- ]
- },
- {
- id: 's3',
- name: '温度',
- values: [
- { id: 'v7', name: '温', selected: false },
- { id: 'v8', name: '冰', selected: false },
- { id: 'v9', name: '热', selected: false }
- ]
- }
- ];
- const EMPTY_COMBO = [
- { id: 'meat', name: '荤菜', items: [] },
- { id: 'veg', name: '素菜', items: [] },
- { id: 'staple', name: '主食', items: [] },
- { id: 'soup', name: '汤', items: [] },
- { id: 'other', name: '其他', items: [] }
- ];
- export default {
- components: {
- SpecModeSwitch,
- FormNextBtn
- },
- data() {
- return {
- isEdit: false,
- editId: null,
- groupMealOpened: false,
- uploadAction: configService.apiUrl + '/sys/common/upload',
- imageHttp: configService.apiUrl + '/',
- uploadFormData: { biz: 'temp' },
- fileList: {
- cover: [],
- detail: []
- },
- specialCategories: [],
- form: {
- belong: 'store',
- category: '',
- name: '',
- productType: 'breakfast',
- cover: '',
- detail: '',
- isCombo: 0,
- comboItems: [],
- specMode: 'single',
- price: '',
- sellingPrice: '',
- stock: '',
- costPrice: '',
- specs: JSON.parse(JSON.stringify(DEFAULT_SPECS))
- },
- // 顶层字段:面板延后切换;单选/下一步在子组件内更新,避免整页重渲染卡顿
- specMode: 'single',
- panelMode: 'single',
- _panelTimer: null,
- pickShow: false,
- pickKind: '', // category | type
- pickTemp: '',
- productTypes: PRODUCT_TYPES,
- specMenuShow: false,
- specMenuList: [{ text: '编辑规格名' }, { text: '编辑规格值' }, { text: '删除规格', color: '#FF4D4F' }],
- activeSpecIndex: 0,
- nameModal: false,
- editingName: '',
- valueListShow: false,
- editingValues: [],
- valueDraft: ''
- };
- },
- computed: {
- showGroupBelong() {
- return this.groupMealOpened;
- },
- categoryName() {
- const hit = this.specialCategories.find((i) => String(i.value) === String(this.form.category));
- return hit ? hit.label : '';
- },
- productTypeName() {
- const hit = PRODUCT_TYPES.find((i) => i.value === this.form.productType);
- return hit ? hit.label : '';
- },
- specLocked() {
- return this.form.belong === 'group' || Number(this.form.isCombo) === 1;
- },
- comboCount() {
- return (this.form.comboItems || []).reduce((n, c) => n + (c.items || []).filter((i) => i.name).length, 0);
- },
- pickTitle() {
- return this.pickKind === 'category' ? '菜品分类' : '菜品类型';
- },
- pickOptions() {
- return this.pickKind === 'category' ? this.specialCategories : this.productTypes;
- }
- },
- watch: {
- 'form.name'() { this.refreshNextEnabled(); },
- 'form.category'() { this.refreshNextEnabled(); },
- 'form.cover'() { this.refreshNextEnabled(); },
- 'form.detail'() { this.refreshNextEnabled(); },
- 'form.isCombo'() { this.refreshNextEnabled(); },
- 'form.comboItems': {
- deep: true,
- handler() { this.refreshNextEnabled(); }
- }
- },
- onLoad(query) {
- session.clearDishDraft();
- this.initGroupMealFlag();
- this.loadSpecialCategories().then(() => {
- if (query.id) {
- this.isEdit = true;
- this.editId = query.id;
- this.loadEdit();
- } else {
- this.form.belong = 'store';
- this.applySpecLock();
- }
- this.$nextTick(() => this.refreshNextEnabled());
- });
- },
- onShow() {
- this.initGroupMealFlag();
- this.refreshNextEnabled();
- },
- methods: {
- /** 返回未填项提示文案,通过则返回空字符串 */
- getNextTip(mode) {
- const f = this.form;
- const specMode = mode || this.specMode;
- if (!f.belong) return '请选择商品归属';
- if (!f.category) return '请选择商品分类';
- if (!f.name) return '请输入商品名称';
- if (!f.productType) return '请选择商品类型';
- if (!f.cover) return '请上传商品封面图';
- if (!f.detail) return '请上传商品详情图';
- if (Number(f.isCombo) === 1 && !this.comboCount) return '请添加套餐菜品';
- if (this.specLocked && specMode !== 'single') return '当前仅支持单规格';
- if (specMode === 'single') {
- if (f.price === '') return '请填写售价';
- if (f.sellingPrice === '') return '请填写原价';
- if (f.stock === '') return '请填写可售库存';
- return '';
- }
- const hasSelected = (f.specs || []).some((s) => (s.values || []).some((v) => v.selected));
- if (!hasSelected) return '请至少选择一个规格值';
- return '';
- },
- refreshNextEnabled(mode) {
- const enabled = !this.getNextTip(mode);
- const apply = () => {
- if (this.$refs.nextBtn && this.$refs.nextBtn.setEnabled) {
- this.$refs.nextBtn.setEnabled(enabled);
- }
- };
- if (this.$refs.nextBtn) apply();
- else this.$nextTick(apply);
- return enabled;
- },
- syncSpecMode(mode, immediatePanel) {
- const nextMode = mode || 'single';
- const touchChildren = () => {
- if (this.$refs.specSwitch && this.$refs.specSwitch.setMode) {
- this.$refs.specSwitch.setMode(nextMode);
- }
- this.refreshNextEnabled(nextMode);
- };
- const applyParent = () => {
- this.specMode = nextMode;
- this.form.specMode = nextMode;
- this.panelMode = nextMode;
- };
- if (immediatePanel) {
- applyParent();
- this.$nextTick(touchChildren);
- return;
- }
- touchChildren();
- if (this._panelTimer) clearTimeout(this._panelTimer);
- this._panelTimer = setTimeout(() => {
- applyParent();
- this._panelTimer = null;
- }, 0);
- },
- onSpecLockedTip() {
- uni.showToast({ title: '当前仅支持单规格', icon: 'none' });
- },
- onPriceInput(key, e) {
- const val = e && e.detail ? e.detail.value : e;
- this.form[key] = val == null ? '' : String(val);
- this.refreshNextEnabled();
- },
- toggleSpecValue(v) {
- v.selected = !v.selected;
- this.refreshNextEnabled();
- },
- /** 供套餐页回写 */
- applyComboItems(categories) {
- this.form.comboItems = categories || [];
- this.refreshNextEnabled();
- },
- initGroupMealFlag() {
- merchantDishApi.isApply().then((res) => {
- if (res.data.code !== 200) return;
- const result = res.data.result;
- let localStatus = 0;
- if (result) {
- const apiStatus = result.status;
- if (apiStatus == 0) localStatus = 3;
- else if (apiStatus == 1) localStatus = 1;
- else if (apiStatus == 2) localStatus = 2;
- }
- this.groupMealOpened = localStatus === 1;
- if (!this.groupMealOpened && this.form.belong === 'group' && !this.isEdit) {
- this.form.belong = 'store';
- }
- });
- },
- loadSpecialCategories() {
- return merchantDishApi.specialCategoryList().then((res) => {
- if (res.data.code !== 200) return;
- const rows = res.data.result || [];
- this.specialCategories = rows.map((i) => ({
- label: i.categoryName,
- value: String(i.id)
- }));
- if (!this.form.category && this.specialCategories.length) {
- this.form.category = this.specialCategories[0].value;
- }
- });
- },
- mapDetailsToCombo(details) {
- const cats = JSON.parse(JSON.stringify(EMPTY_COMBO));
- (details || []).forEach((d, idx) => {
- const catId = COMBO_API_TO_CAT[d.category] || 'other';
- const cat = cats.find((c) => c.id === catId);
- if (!cat) return;
- cat.items.push({
- id: d.id || `d_${idx}`,
- name: d.name || '',
- price: d.price != null ? String(d.price) : '',
- description: d.description || ''
- });
- });
- return cats;
- },
- mapSpecsFromApi(specs) {
- if (!specs || !specs.length) return JSON.parse(JSON.stringify(DEFAULT_SPECS));
- const groupMap = {};
- specs.forEach((s, idx) => {
- const key = s.specName || '规格';
- if (!groupMap[key]) {
- groupMap[key] = {
- id: `g_${key}`,
- name: key,
- values: []
- };
- }
- // isSelected: 1 选中,0 未选中;兼容旧数据无该字段时视为选中
- const selected = s.isSelected == null ? true : Number(s.isSelected) === 1;
- groupMap[key].values.push({
- id: s.id || `${key}_${s.optionName || idx}`,
- persistId: s.id || null,
- name: s.optionName,
- selected
- });
- });
- return Object.values(groupMap);
- },
- loadEdit() {
- uni.showLoading({ title: '加载中' });
- merchantDishApi
- .packageDetail(this.editId)
- .then((res) => {
- if (res.data.code !== 200 || !res.data.result) {
- this.$tip.toast(res.data.message || '加载失败');
- return;
- }
- const item = res.data.result;
- this.form.belong = belongingToKey(item.productBelonging);
- this.form.category = item.specialCategoryId != null ? String(item.specialCategoryId) : '';
- this.form.name = item.name || '';
- this.form.productType = CATEGORY_TO_PRODUCT_TYPE[item.category] || 'breakfast';
- this.form.cover = item.productImage || '';
- this.form.detail = item.detailImages || '';
- this.form.isCombo = Number(item.isPackage) == 1 ? 1 : 0;
- // 售价 price,原价 sellingPrice
- this.form.price = item.price != null ? String(item.price) : '';
- this.form.sellingPrice =
- item.sellingPrice != null
- ? String(item.sellingPrice)
- : item.originalPrice != null
- ? String(item.originalPrice)
- : '';
- this.form.stock = item.dailyStock != null ? String(item.dailyStock) : '';
- this.form.costPrice = item.costPrice != null ? String(item.costPrice) : '';
- this.form.comboItems = this.mapDetailsToCombo(item.gmMerchantPackageDetails);
- // 以接口 salesSpecType 为准:1 单规格,2 多规格(编辑时不强制改回单规格)
- const salesSpecType = Number(item.salesSpecType);
- const specMode = salesSpecType === 2 ? 'multi' : 'single';
- if (specMode === 'multi') {
- this.form.specs = this.mapSpecsFromApi(item.specs);
- }
- this.syncSpecMode(specMode, true);
- this.fileList = {
- cover: this.toFileList(this.form.cover),
- detail: this.toFileList(this.form.detail)
- };
- // 编辑详情中的 skus 放内存,多规格下一步回填价格
- session.setDishEditExtra({
- skus: item.skus || [],
- saleTimes: item.saleTimes || [],
- saleDateType: item.saleDateType
- });
- this.$nextTick(() => this.refreshNextEnabled(specMode));
- })
- .finally(() => uni.hideLoading());
- },
- toFileList(value) {
- if (!value) return [];
- return String(value)
- .split(',')
- .filter(Boolean)
- .map((url) => ({ url }));
- },
- processImageList(lists, formKey) {
- const urls = lists
- .map((item) => {
- if (item.response && item.response.message) {
- return this.imageHttp + item.response.message;
- }
- if (item.url) return item.url;
- return '';
- })
- .filter(Boolean);
- this.form[formKey] = urls.join(',');
- },
- onUploadOversize() {
- uni.showToast({ title: '图片大小不能超过2M', icon: 'none' });
- },
- onCoverSuccess(res, index, lists) {
- this.fileList.cover = lists;
- this.processImageList(lists, 'cover');
- },
- onCoverRemove(index, lists) {
- this.fileList.cover = lists;
- this.processImageList(lists, 'cover');
- },
- onDetailSuccess(res, index, lists) {
- this.fileList.detail = lists;
- this.processImageList(lists, 'detail');
- },
- onDetailRemove(index, lists) {
- this.fileList.detail = lists;
- this.processImageList(lists, 'detail');
- },
- onBelongChange(val) {
- const belong = val || this.form.belong;
- this.form.belong = belong;
- this.applySpecLock();
- },
- onComboChange(val) {
- this.form.isCombo = val === undefined ? this.form.isCombo : val;
- this.applySpecLock();
- },
- onSpecModeChange(val) {
- const mode = val || 'single';
- if (mode === this.specMode && mode === this.panelMode) return;
- if (this.specLocked && mode === 'multi') {
- this.onSpecLockedTip();
- return;
- }
- // 先瞬时更新按钮置灰状态(子组件),再延后切换面板
- this.refreshNextEnabled(mode);
- if (this._panelTimer) clearTimeout(this._panelTimer);
- this._panelTimer = setTimeout(() => {
- this.specMode = mode;
- this.form.specMode = mode;
- this.panelMode = mode;
- this._panelTimer = null;
- }, 0);
- },
- applySpecLock() {
- if (this.specLocked) {
- this.syncSpecMode('single', true);
- }
- },
- pickCategory() {
- if (!this.specialCategories.length) {
- uni.showToast({ title: '暂无分类', icon: 'none' });
- return;
- }
- this.pickKind = 'category';
- this.pickTemp = this.form.category || '';
- this.pickShow = true;
- },
- pickProductType() {
- this.pickKind = 'type';
- this.pickTemp = this.form.productType || 'breakfast';
- this.pickShow = true;
- },
- closePickPopup() {
- this.pickShow = false;
- },
- confirmPick() {
- if (this.pickTemp === '' || this.pickTemp == null) {
- uni.showToast({
- title: this.pickKind === 'category' ? '请选择菜品分类' : '请选择菜品类型',
- icon: 'none'
- });
- return;
- }
- if (this.pickKind === 'category') {
- this.form.category = String(this.pickTemp);
- } else {
- this.form.productType = this.pickTemp;
- }
- this.pickShow = false;
- },
- goCombo() {
- uni.navigateTo({ url: '/pages/merchantDish/dish/combo' });
- },
- openSpecMenu(si) {
- this.activeSpecIndex = si;
- this.specMenuShow = true;
- },
- onSpecMenu(e) {
- const index = typeof e === 'object' ? e.index : e;
- const spec = this.form.specs[this.activeSpecIndex];
- if (!spec) return;
- if (index === 0) {
- this.editingName = spec.name || '';
- this.$nextTick(() => {
- this.nameModal = true;
- });
- } else if (index === 1) {
- this.editingValues = JSON.parse(JSON.stringify(spec.values || []));
- this.valueDraft = '';
- this.$nextTick(() => {
- this.valueListShow = true;
- });
- } else if (index === 2) {
- this.deleteSpec();
- }
- },
- deleteSpec() {
- if ((this.form.specs || []).length <= 1) {
- uni.showToast({ title: '至少保留一个规格', icon: 'none' });
- return;
- }
- uni.showModal({
- content: '确定删除该规格?',
- confirmColor: '#FF4D4F',
- success: (res) => {
- if (!res.confirm) return;
- this.form.specs.splice(this.activeSpecIndex, 1);
- this.refreshNextEnabled();
- }
- });
- },
- saveSpecName() {
- if (!this.editingName.trim()) {
- uni.showToast({ title: '请输入规格名称', icon: 'none' });
- return;
- }
- this.form.specs[this.activeSpecIndex].name = this.editingName.trim();
- this.nameModal = false;
- },
- addSpecValue() {
- const name = (this.valueDraft || '').trim();
- if (!name) {
- uni.showToast({ title: '请输入规格值', icon: 'none' });
- return;
- }
- this.editingValues.push({
- id: `v${Date.now()}`,
- name,
- selected: true
- });
- this.valueDraft = '';
- },
- removeSpecValue(i) {
- if (this.editingValues.length <= 1 && !(this.valueDraft || '').trim()) {
- uni.showToast({ title: '至少保留一个规格值', icon: 'none' });
- return;
- }
- this.editingValues.splice(i, 1);
- },
- confirmValues() {
- const draft = (this.valueDraft || '').trim();
- const list = this.editingValues
- .map((v) => ({ ...v, name: (v.name || '').trim() }))
- .filter((v) => v.name);
- if (draft) {
- list.push({
- id: `v${Date.now()}`,
- name: draft,
- selected: true
- });
- }
- if (!list.length) {
- uni.showToast({ title: '请至少填写一个规格值', icon: 'none' });
- return;
- }
- this.form.specs[this.activeSpecIndex].values = list;
- this.valueDraft = '';
- this.valueListShow = false;
- this.refreshNextEnabled();
- },
- addSpec() {
- const id = `s${Date.now()}`;
- this.form.specs.push({
- id,
- name: '新规格',
- values: [
- { id: `${id}_1`, name: '选项1', selected: true },
- { id: `${id}_2`, name: '选项2', selected: false }
- ]
- });
- this.refreshNextEnabled();
- },
- /** 编辑多规格时,用接口 skus 回填价格/库存 */
- mergeSkusIntoCombos(combos) {
- const extra = session.getDishEditExtra();
- const skus = extra.skus || [];
- if (!skus.length) return combos;
- const skuMap = {};
- skus.forEach((s) => {
- const key = (s.specOptionNames || '').replace(/\//g, ' | ');
- skuMap[key] = s;
- skuMap[s.specOptionNames || ''] = s;
- });
- return (combos || []).map((c) => {
- const hit = skuMap[c.label] || skuMap[(c.label || '').replace(/\s*\|\s*/g, '/')];
- if (!hit) return c;
- return {
- ...c,
- price: hit.price != null ? String(hit.price) : hit.sellingPrice != null ? String(hit.sellingPrice) : c.price,
- sellingPrice:
- hit.sellingPrice != null
- ? String(hit.sellingPrice)
- : hit.originalPrice != null
- ? String(hit.originalPrice)
- : c.sellingPrice,
- stock: hit.stock != null ? String(hit.stock) : c.stock,
- costPrice: hit.costPrice != null ? String(hit.costPrice) : c.costPrice
- };
- });
- },
- onNext() {
- const tip = this.getNextTip(this.specMode);
- if (tip) {
- this.refreshNextEnabled();
- uni.showToast({ title: tip, icon: 'none' });
- return;
- }
- this.form.specMode = this.specMode;
- const coverUrls = (this.form.cover || '').split(',').filter(Boolean);
- let combos = [];
- if (this.specMode === 'multi') {
- combos = buildSpecCombos(this.form.specs);
- if (this.isEdit) combos = this.mergeSkusIntoCombos(combos);
- }
- const draft = {
- ...this.form,
- specMode: this.specMode,
- coverImages: this.form.cover,
- cover: coverUrls[0] || '',
- detailImages: this.form.detail,
- isEdit: this.isEdit,
- editId: this.editId,
- mealCategory: PRODUCT_TYPE_TO_CATEGORY[this.form.productType] || '1',
- combos
- };
- session.setDishDraft(draft);
- if (this.specMode === 'multi') {
- uni.navigateTo({ url: '/pages/merchantDish/dish/spec-detail' });
- } else {
- uni.navigateTo({ url: '/pages/merchantDish/dish/sale-time?from=single' });
- }
- }
- }
- };
- </script>
- <style lang="scss" scoped>
- .page {
- min-height: 100vh;
- background: #f7f8fa;
- padding-bottom: 160rpx;
- }
- .card {
- background: #fff;
- margin: 20rpx 24rpx;
- border-radius: 16rpx;
- padding: 8rpx 24rpx 16rpx;
- }
- .section-title {
- position: relative;
- display: inline-block;
- font-size: 30rpx;
- font-weight: 600;
- color: #222;
- padding: 24rpx 0 16rpx;
- &::after {
- content: '';
- position: absolute;
- left: 0;
- bottom: 8rpx;
- width: 100%;
- height: 4rpx;
- background: #449aff;
- border-radius: 2rpx;
- }
- }
- .row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- min-height: 96rpx;
- border-bottom: 1rpx solid #f0f0f0;
- .label {
- width: 200rpx;
- font-size: 28rpx;
- color: #333;
- flex-shrink: 0;
- &.required::before {
- content: '*';
- color: #ff4d4f;
- margin-right: 4rpx;
- }
- }
- .value {
- flex: 1;
- text-align: right;
- font-size: 28rpx;
- color: #333;
- &.link {
- color: #449aff;
- }
- }
- .input {
- flex: 1;
- text-align: right;
- font-size: 28rpx;
- color: #333;
- }
- .arrow-down {
- width: 25rpx;
- height: 25rpx;
- margin-left: 10rpx;
- flex-shrink: 0;
- }
- }
- .ph {
- color: #c0c4cc;
- }
- .spec-tip {
- font-size: 22rpx;
- color: #999;
- padding: 0 0 16rpx;
- }
- .upload-row {
- padding: 20rpx 0;
- border-bottom: 1rpx solid #f0f0f0;
- &.last {
- border-bottom: none;
- }
- .label {
- font-size: 28rpx;
- color: #333;
- &.required::before {
- content: '*';
- color: #ff4d4f;
- margin-right: 4rpx;
- }
- }
- .tip {
- font-size: 22rpx;
- color: #999;
- margin: 8rpx 0 16rpx;
- }
- }
- .upload-btn {
- width: 140rpx;
- height: 140rpx;
- background: #f5f6f8;
- border-radius: 8rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- color: #999;
- font-size: 24rpx;
- .upload-icon {
- width: 48rpx;
- height: 48rpx;
- margin-bottom: 8rpx;
- }
- }
- .spec-block {
- padding: 16rpx 0;
- border-bottom: 1rpx solid #f5f5f5;
- .spec-head {
- display: flex;
- justify-content: space-between;
- font-size: 28rpx;
- color: #333;
- margin-bottom: 16rpx;
- .more {
- letter-spacing: 2rpx;
- color: #999;
- padding: 0 8rpx;
- }
- }
- .tags {
- display: flex;
- flex-wrap: wrap;
- gap: 16rpx;
- }
- .tag {
- position: relative;
- min-width: 120rpx;
- padding: 16rpx 28rpx;
- border-radius: 8rpx;
- font-size: 26rpx;
- text-align: center;
- background: #fff;
- color: #666;
- border: 1rpx solid #e8e8e8;
- box-sizing: border-box;
- overflow: hidden;
- &.on {
- background: rgba(68, 154, 255, 0.08);
- color: #449aff;
- border-color: #449aff;
- }
- .tag-check {
- position: absolute;
- right: 0;
- bottom: 0;
- width: 28rpx;
- height: 28rpx;
- }
- }
- }
- .add-spec {
- padding: 24rpx 0;
- text-align: center;
- color: #449aff;
- font-size: 28rpx;
- }
- .spec-panels {
- .panel {
- display: none;
- }
- &.single .panel-single {
- display: block;
- }
- &.multi .panel-multi {
- display: block;
- }
- }
- .modal-body {
- padding: 30rpx;
- }
- .modal-input {
- border: 1rpx solid #eee;
- border-radius: 8rpx;
- padding: 16rpx 20rpx;
- font-size: 28rpx;
- }
- .mask {
- position: fixed;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.45);
- z-index: 999;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .spec-dialog {
- width: 600rpx;
- background: #fff;
- border-radius: 20rpx;
- padding: 36rpx 32rpx 32rpx;
- box-sizing: border-box;
- }
- .spec-dialog-title {
- text-align: center;
- font-size: 32rpx;
- font-weight: 600;
- color: #222;
- margin-bottom: 28rpx;
- }
- .spec-dialog-input {
- width: 100%;
- height: 80rpx;
- padding: 0 24rpx;
- background: #f5f6f8;
- border-radius: 12rpx;
- font-size: 28rpx;
- color: #333;
- box-sizing: border-box;
- }
- .spec-ph {
- color: #c0c4cc;
- }
- .spec-dialog-footer {
- display: flex;
- gap: 20rpx;
- margin-top: 32rpx;
- }
- .spec-dialog-btn {
- flex: 1;
- height: 80rpx;
- line-height: 80rpx;
- text-align: center;
- font-size: 30rpx;
- border-radius: 40rpx;
- &.cancel {
- color: #666;
- background: #fff;
- border: 2rpx solid #e5e5e5;
- box-sizing: border-box;
- line-height: 76rpx;
- }
- &.save {
- color: #fff;
- background: linear-gradient( 89deg, #10ABFE 0%, #2260F6 100%);
- }
- }
- .spec-value-dialog {
- max-height: 70vh;
- display: flex;
- flex-direction: column;
- }
- .sv-list {
- overflow-y: auto;
- max-height: 48vh;
- }
- .sv-row {
- display: flex;
- align-items: center;
- gap: 16rpx;
- margin-bottom: 20rpx;
- &:last-child {
- margin-bottom: 0;
- }
- }
- .sv-input {
- flex: 1;
- width: auto;
- margin: 0;
- }
- .sv-circle {
- width: 48rpx;
- height: 48rpx;
- border-radius: 50%;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 36rpx;
- line-height: 1;
- color: #fff;
- font-weight: 500;
- &.minus,
- &.plus {
- background: #7eb6ff;
- }
- }
- .value-popup {
- width: 600rpx;
- padding: 30rpx;
- }
- .pick-mask {
- position: fixed;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.45);
- z-index: 1000;
- display: flex;
- align-items: flex-end;
- }
- .pick-sheet {
- width: 100%;
- background: #fff;
- border-radius: 24rpx 24rpx 0 0;
- padding: 0 32rpx calc(24rpx + env(safe-area-inset-bottom));
- box-sizing: border-box;
- max-height: 70vh;
- display: flex;
- flex-direction: column;
- }
- .pick-sheet-title {
- text-align: center;
- font-size: 32rpx;
- font-weight: 600;
- color: #222;
- padding: 36rpx 0 32rpx;
- flex-shrink: 0;
- }
- .pick-options {
- display: flex;
- flex-wrap: wrap;
- gap: 20rpx;
- margin-bottom: 40rpx;
- overflow-y: auto;
- max-height: 46vh;
- }
- .pick-opt {
- width: calc((100% - 40rpx) / 3);
- height: 72rpx;
- line-height: 70rpx;
- text-align: center;
- font-size: 28rpx;
- color: #666;
- background: #fff;
- border: 1rpx solid #e5e5e5;
- border-radius: 12rpx;
- box-sizing: border-box;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- padding: 0 8rpx;
- &.on {
- color: #449aff;
- background: rgba(68, 154, 255, 0.08);
- border-color: #449aff;
- }
- }
- .pick-sheet-footer {
- display: flex;
- gap: 24rpx;
- padding-bottom: 12rpx;
- flex-shrink: 0;
- }
- .pick-btn {
- flex: 1;
- height: 88rpx;
- line-height: 88rpx;
- text-align: center;
- font-size: 30rpx;
- border-radius: 44rpx;
- &.cancel {
- color: #666;
- background: #f2f3f5;
- }
- &.confirm {
- color: #fff;
- background: linear-gradient( 89deg, #10ABFE 0%, #2260F6 100%);
- }
- }
- ::v-deep .u-radio {
- margin-right: 24rpx;
- }
- </style>
|