|
|
@@ -84,62 +84,57 @@
|
|
|
<view class="section-title">销售规格</view>
|
|
|
<view class="row">
|
|
|
<text class="label required">销售规格</text>
|
|
|
- <u-radio-group :value="form.specMode" @change="onSpecModeChange">
|
|
|
- <u-radio name="single" active-color="#449AFF">单规格</u-radio>
|
|
|
- <u-radio name="multi" active-color="#449AFF" :disabled="specLocked">多规格</u-radio>
|
|
|
- </u-radio-group>
|
|
|
+ <SpecModeSwitch
|
|
|
+ ref="specSwitch"
|
|
|
+ :locked="specLocked"
|
|
|
+ :value="specMode"
|
|
|
+ @change="onSpecModeChange"
|
|
|
+ @locked="onSpecLockedTip"
|
|
|
+ />
|
|
|
</view>
|
|
|
- <view class="spec-tip" v-if="specLocked">当前归属为团餐或已设为套餐,仅支持单规格</view>
|
|
|
-
|
|
|
- <!-- 单规格价格 -->
|
|
|
- <template v-if="form.specMode === 'single'">
|
|
|
- <view class="row">
|
|
|
- <text class="label required">售价 (元)</text>
|
|
|
- <input class="input" type="digit" v-model="form.price" placeholder="请输入" />
|
|
|
- </view>
|
|
|
- <view class="row">
|
|
|
- <text class="label required">原价 (元)</text>
|
|
|
- <input class="input" type="digit" v-model="form.sellingPrice" placeholder="请输入" />
|
|
|
- </view>
|
|
|
- <view class="row">
|
|
|
- <text class="label required">可售库存</text>
|
|
|
- <input class="input" type="number" v-model="form.stock" placeholder="请输入" />
|
|
|
- </view>
|
|
|
- <view class="row">
|
|
|
- <text class="label">成本价 (元)</text>
|
|
|
- <input class="input" type="digit" v-model="form.costPrice" placeholder="请输入 (选填)" />
|
|
|
- </view>
|
|
|
- </template>
|
|
|
+ <view class="spec-tip" v-if="specLocked && specMode !== 'multi'">当前归属为团餐或已设为套餐,仅支持单规格</view>
|
|
|
|
|
|
- <!-- 多规格 -->
|
|
|
- <template v-else>
|
|
|
- <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>
|
|
|
+ <!-- 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="请输入" />
|
|
|
</view>
|
|
|
- <view class="tags">
|
|
|
- <view
|
|
|
- v-for="(v, vi) in spec.values"
|
|
|
- :key="v.id"
|
|
|
- :class="['tag', v.selected ? 'on' : '']"
|
|
|
- @click="v.selected = !v.selected"
|
|
|
- >{{ v.name }}</view>
|
|
|
+ <view class="row">
|
|
|
+ <text class="label required">原价 (元)</text>
|
|
|
+ <input class="input" type="digit" :value="form.sellingPrice" @input="onPriceInput('sellingPrice', $event)" placeholder="请输入" />
|
|
|
</view>
|
|
|
+ <view class="row">
|
|
|
+ <text class="label required">可售库存</text>
|
|
|
+ <input class="input" type="number" :value="form.stock" @input="onPriceInput('stock', $event)" placeholder="请输入" />
|
|
|
+ </view>
|
|
|
+ <view class="row">
|
|
|
+ <text class="label">成本价 (元)</text>
|
|
|
+ <input class="input" type="digit" :value="form.costPrice" @input="onPriceInput('costPrice', $event)" placeholder="请输入 (选填)" />
|
|
|
+ </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 }}</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="add-spec" @click="addSpec">+ 添加规格</view>
|
|
|
</view>
|
|
|
- <view class="add-spec" @click="addSpec">+ 添加规格</view>
|
|
|
- </template>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
|
|
|
- <view class="footer">
|
|
|
- <u-button
|
|
|
- :key="'next-' + form.specMode + '-' + (canNext ? 1 : 0)"
|
|
|
- type="primary"
|
|
|
- shape="circle"
|
|
|
- :disabled="!canNext"
|
|
|
- @click="onNext"
|
|
|
- >下一步</u-button>
|
|
|
- </view>
|
|
|
+ <FormNextBtn ref="nextBtn" @click="onNext" />
|
|
|
|
|
|
<!-- 规格操作菜单 -->
|
|
|
<u-action-sheet :list="specMenuList" v-model="specMenuShow" @click="onSpecMenu"></u-action-sheet>
|
|
|
@@ -200,6 +195,7 @@
|
|
|
|
|
|
<script>
|
|
|
import { buildSpecCombos } from '../utils.js';
|
|
|
+import session from '../session.js';
|
|
|
import configService from '@/common/service/config.service';
|
|
|
import merchantDishApi, {
|
|
|
belongingToKey,
|
|
|
@@ -207,8 +203,8 @@ import merchantDishApi, {
|
|
|
COMBO_API_TO_CAT,
|
|
|
PRODUCT_TYPE_TO_CATEGORY
|
|
|
} from '@/api/merchantDish.js';
|
|
|
-
|
|
|
-const DRAFT_KEY = 'merchantDish_draft';
|
|
|
+import SpecModeSwitch from './components/SpecModeSwitch.vue';
|
|
|
+import FormNextBtn from './components/FormNextBtn.vue';
|
|
|
|
|
|
const PRODUCT_TYPES = [
|
|
|
{ label: '早', value: 'breakfast' },
|
|
|
@@ -255,6 +251,10 @@ const EMPTY_COMBO = [
|
|
|
];
|
|
|
|
|
|
export default {
|
|
|
+ components: {
|
|
|
+ SpecModeSwitch,
|
|
|
+ FormNextBtn
|
|
|
+ },
|
|
|
data() {
|
|
|
return {
|
|
|
isEdit: false,
|
|
|
@@ -284,6 +284,10 @@ export default {
|
|
|
costPrice: '',
|
|
|
specs: JSON.parse(JSON.stringify(DEFAULT_SPECS))
|
|
|
},
|
|
|
+ // 顶层字段:面板延后切换;单选/下一步在子组件内更新,避免整页重渲染卡顿
|
|
|
+ specMode: 'single',
|
|
|
+ panelMode: 'single',
|
|
|
+ _panelTimer: null,
|
|
|
categoryShow: false,
|
|
|
typeShow: false,
|
|
|
categorySheet: [],
|
|
|
@@ -317,20 +321,21 @@ export default {
|
|
|
},
|
|
|
comboCount() {
|
|
|
return (this.form.comboItems || []).reduce((n, c) => n + (c.items || []).filter((i) => i.name).length, 0);
|
|
|
- },
|
|
|
- canNext() {
|
|
|
- const f = this.form;
|
|
|
- if (!f.belong || !f.category || !f.name || !f.productType || !f.cover || !f.detail) return false;
|
|
|
- if (Number(f.isCombo) === 1 && !this.comboCount) return false;
|
|
|
- if (this.specLocked && f.specMode !== 'single') return false;
|
|
|
- if (f.specMode === 'single') {
|
|
|
- return f.price !== '' && f.sellingPrice !== '' && f.stock !== '';
|
|
|
- }
|
|
|
- const hasSelected = (f.specs || []).some((s) => (s.values || []).some((v) => v.selected));
|
|
|
- return hasSelected;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ 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) {
|
|
|
@@ -339,27 +344,92 @@ export default {
|
|
|
this.loadEdit();
|
|
|
} else {
|
|
|
this.form.belong = 'store';
|
|
|
+ this.applySpecLock();
|
|
|
}
|
|
|
- this.applySpecLock();
|
|
|
+ this.$nextTick(() => this.refreshNextEnabled());
|
|
|
});
|
|
|
},
|
|
|
onShow() {
|
|
|
this.initGroupMealFlag();
|
|
|
- const combo = uni.getStorageSync('merchantDish_combo_temp');
|
|
|
- if (combo) {
|
|
|
- this.form.comboItems = combo;
|
|
|
- uni.removeStorageSync('merchantDish_combo_temp');
|
|
|
- }
|
|
|
+ this.refreshNextEnabled();
|
|
|
},
|
|
|
methods: {
|
|
|
- initGroupMealFlag() {
|
|
|
- const cache = uni.getStorageSync('merchantDish_applyStatus');
|
|
|
- if (cache !== '' && cache !== undefined && cache !== null) {
|
|
|
- this.groupMealOpened = Number(cache) === 1;
|
|
|
+ /** 返回未填项提示文案,通过则返回空字符串 */
|
|
|
+ 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 '';
|
|
|
}
|
|
|
- if (!this.groupMealOpened && this.form.belong === 'group' && !this.isEdit) {
|
|
|
- this.form.belong = 'store';
|
|
|
+ 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;
|
|
|
@@ -370,7 +440,6 @@ export default {
|
|
|
else if (apiStatus == 1) localStatus = 1;
|
|
|
else if (apiStatus == 2) localStatus = 2;
|
|
|
}
|
|
|
- uni.setStorageSync('merchantDish_applyStatus', localStatus);
|
|
|
this.groupMealOpened = localStatus === 1;
|
|
|
if (!this.groupMealOpened && this.form.belong === 'group' && !this.isEdit) {
|
|
|
this.form.belong = 'store';
|
|
|
@@ -453,22 +522,25 @@ export default {
|
|
|
: '';
|
|
|
this.form.stock = item.dailyStock != null ? String(item.dailyStock) : '';
|
|
|
this.form.costPrice = item.costPrice != null ? String(item.costPrice) : '';
|
|
|
- this.form.specMode = Number(item.salesSpecType) === 2 ? 'multi' : 'single';
|
|
|
this.form.comboItems = this.mapDetailsToCombo(item.gmMerchantPackageDetails);
|
|
|
- if (this.form.specMode === 'multi') {
|
|
|
+ // 以接口 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 / saleTimes 供提交合并
|
|
|
- uni.setStorageSync('merchantDish_edit_extra', {
|
|
|
+ // 编辑详情中的 skus 放内存,多规格下一步回填价格
|
|
|
+ session.setDishEditExtra({
|
|
|
skus: item.skus || [],
|
|
|
saleTimes: item.saleTimes || [],
|
|
|
saleDateType: item.saleDateType
|
|
|
});
|
|
|
- this.applySpecLock();
|
|
|
+ this.$nextTick(() => this.refreshNextEnabled(specMode));
|
|
|
})
|
|
|
.finally(() => uni.hideLoading());
|
|
|
},
|
|
|
@@ -517,17 +589,25 @@ export default {
|
|
|
this.applySpecLock();
|
|
|
},
|
|
|
onSpecModeChange(val) {
|
|
|
- // 同步更新,避免 u-radio-group 双向绑定延迟导致下一步按钮置灰滞后
|
|
|
const mode = val || 'single';
|
|
|
+ if (mode === this.specMode && mode === this.panelMode) return;
|
|
|
if (this.specLocked && mode === 'multi') {
|
|
|
- this.$set(this.form, 'specMode', 'single');
|
|
|
+ this.onSpecLockedTip();
|
|
|
return;
|
|
|
}
|
|
|
- this.$set(this.form, 'specMode', mode);
|
|
|
+ // 先瞬时更新按钮置灰状态(子组件),再延后切换面板
|
|
|
+ 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.$set(this.form, 'specMode', 'single');
|
|
|
+ this.syncSpecMode('single', true);
|
|
|
}
|
|
|
},
|
|
|
pickCategory() {
|
|
|
@@ -543,7 +623,6 @@ export default {
|
|
|
this.form.productType = e.value;
|
|
|
},
|
|
|
goCombo() {
|
|
|
- uni.setStorageSync('merchantDish_combo_editing', this.form.comboItems || []);
|
|
|
uni.navigateTo({ url: '/pages/merchantDish/dish/combo' });
|
|
|
},
|
|
|
openSpecMenu(si) {
|
|
|
@@ -579,6 +658,7 @@ export default {
|
|
|
success: (res) => {
|
|
|
if (!res.confirm) return;
|
|
|
this.form.specs.splice(this.activeSpecIndex, 1);
|
|
|
+ this.refreshNextEnabled();
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
@@ -626,6 +706,7 @@ export default {
|
|
|
}
|
|
|
this.form.specs[this.activeSpecIndex].values = this.editingValues;
|
|
|
this.valueListShow = false;
|
|
|
+ this.refreshNextEnabled();
|
|
|
},
|
|
|
addSpec() {
|
|
|
const id = `s${Date.now()}`;
|
|
|
@@ -637,25 +718,63 @@ export default {
|
|
|
{ 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() {
|
|
|
- if (!this.canNext) {
|
|
|
- uni.showToast({ title: '请完善必填信息', icon: 'none' });
|
|
|
+ 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: this.form.specMode === 'multi' ? buildSpecCombos(this.form.specs) : []
|
|
|
+ combos
|
|
|
};
|
|
|
- uni.setStorageSync(DRAFT_KEY, draft);
|
|
|
- if (this.form.specMode === 'multi') {
|
|
|
+ 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' });
|
|
|
@@ -780,13 +899,16 @@ export default {
|
|
|
color: #449aff;
|
|
|
font-size: 28rpx;
|
|
|
}
|
|
|
-.footer {
|
|
|
- position: fixed;
|
|
|
- left: 0;
|
|
|
- right: 0;
|
|
|
- bottom: 0;
|
|
|
- padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
|
|
|
- background: #fff;
|
|
|
+.spec-panels {
|
|
|
+ .panel {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+ &.single .panel-single {
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+ &.multi .panel-multi {
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
}
|
|
|
.modal-body {
|
|
|
padding: 30rpx;
|