浏览代码

提交修改

付晓文。 1 月之前
父节点
当前提交
2cba833671

+ 0 - 2
pages/merchantDish/apply/index.vue

@@ -52,7 +52,6 @@ export default {
 				else if (apiStatus == 1) localStatus = 1;
 				else if (apiStatus == 2) localStatus = 2;
 				this.status = localStatus;
-				uni.setStorageSync('merchantDish_applyStatus', localStatus);
 			});
 		},
 		onSubmit() {
@@ -73,7 +72,6 @@ export default {
 						localStatus = 2;
 					}
 					this.status = localStatus;
-					uni.setStorageSync('merchantDish_applyStatus', localStatus);
 					this.modalText.content = content;
 					this.$refs.myModal.onOpen();
 				}

+ 13 - 2
pages/merchantDish/dish/combo.vue

@@ -81,7 +81,8 @@ export default {
 		};
 	},
 	onLoad() {
-		const editing = uni.getStorageSync('merchantDish_combo_editing');
+		const prev = this.getPrevFormPage();
+		const editing = prev && prev.form ? prev.form.comboItems : null;
 		if (editing && editing.length) {
 			this.categories = editing.map((cat) => ({
 				...cat,
@@ -98,6 +99,11 @@ export default {
 		}
 	},
 	methods: {
+		getPrevFormPage() {
+			const pages = getCurrentPages();
+			const prev = pages[pages.length - 2];
+			return prev && prev.$vm ? prev.$vm : null;
+		},
 		onCollapse(e) {
 			this.activeNames = Array.isArray(e) ? e : e ? [e] : [];
 		},
@@ -130,7 +136,12 @@ export default {
 			this.refreshCollapse();
 		},
 		onConfirm() {
-			uni.setStorageSync('merchantDish_combo_temp', this.categories);
+			const prev = this.getPrevFormPage();
+			if (prev && typeof prev.applyComboItems === 'function') {
+				prev.applyComboItems(this.categories);
+			} else if (prev && prev.form) {
+				prev.form.comboItems = this.categories;
+			}
 			uni.showToast({ title: '提交成功', icon: 'success' });
 			setTimeout(() => uni.navigateBack(), 400);
 		}

+ 50 - 0
pages/merchantDish/dish/components/FormNextBtn.vue

@@ -0,0 +1,50 @@
+<template>
+	<view class="footer">
+		<view :class="['next-btn', enabled ? '' : 'is-disabled']" @click="onClick">下一步</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'FormNextBtn',
+	data() {
+		return {
+			enabled: false
+		};
+	},
+	methods: {
+		/** 只更新本组件,不触发父页面重渲染 */
+		setEnabled(v) {
+			this.enabled = !!v;
+		},
+		onClick() {
+			this.$emit('click');
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+.footer {
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 20;
+	padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
+	background: #fff;
+}
+.next-btn {
+	height: 88rpx;
+	line-height: 88rpx;
+	text-align: center;
+	background: #449aff;
+	color: #fff;
+	border-radius: 44rpx;
+	font-size: 32rpx;
+	transition: none;
+	&.is-disabled {
+		opacity: 0.45;
+	}
+}
+</style>

+ 100 - 0
pages/merchantDish/dish/components/SpecModeSwitch.vue

@@ -0,0 +1,100 @@
+<template>
+	<view class="spec-mode-group">
+		<view :class="['spec-mode-item', mode === 'single' ? 'on' : '']" @click="onPick('single')">
+			<view class="radio-dot"></view>
+			<text>单规格</text>
+		</view>
+		<view
+			:class="['spec-mode-item', mode === 'multi' ? 'on' : '', locked && mode !== 'multi' ? 'disabled' : '']"
+			@click="onPick('multi')"
+		>
+			<view class="radio-dot"></view>
+			<text>多规格</text>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'SpecModeSwitch',
+	props: {
+		locked: {
+			type: Boolean,
+			default: false
+		},
+		value: {
+			type: String,
+			default: 'single'
+		}
+	},
+	data() {
+		return {
+			mode: this.value || 'single'
+		};
+	},
+	watch: {
+		value(v) {
+			if (v && v !== this.mode) this.mode = v;
+		}
+	},
+	methods: {
+		setMode(mode) {
+			this.mode = mode || 'single';
+		},
+		onPick(mode) {
+			if (this.locked && mode === 'multi') {
+				this.$emit('locked');
+				return;
+			}
+			if (mode === this.mode) return;
+			this.mode = mode;
+			this.$emit('change', mode);
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+.spec-mode-group {
+	flex: 1;
+	display: flex;
+	justify-content: flex-end;
+	align-items: center;
+	gap: 32rpx;
+}
+.spec-mode-item {
+	display: flex;
+	align-items: center;
+	font-size: 28rpx;
+	color: #333;
+	.radio-dot {
+		width: 32rpx;
+		height: 32rpx;
+		border-radius: 50%;
+		border: 2rpx solid #c0c4cc;
+		margin-right: 10rpx;
+		box-sizing: border-box;
+		position: relative;
+	}
+	&.on {
+		color: #449aff;
+		.radio-dot {
+			border-color: #449aff;
+			&::after {
+				content: '';
+				position: absolute;
+				left: 50%;
+				top: 50%;
+				width: 16rpx;
+				height: 16rpx;
+				margin: -8rpx 0 0 -8rpx;
+				border-radius: 50%;
+				background: #449aff;
+			}
+		}
+	}
+	&.disabled {
+		opacity: 0.4;
+	}
+}
+</style>

+ 219 - 97
pages/merchantDish/dish/form.vue

@@ -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;

+ 0 - 5
pages/merchantDish/dish/list.vue

@@ -235,10 +235,6 @@ export default {
 			uni.navigateBack({ delta: 1 });
 		},
 		fetchApplyStatus() {
-			const cache = uni.getStorageSync('merchantDish_applyStatus');
-			if (cache !== '' && cache !== undefined && cache !== null) {
-				this.applyStatus = Number(cache);
-			}
 			merchantDishApi.isApply().then((res) => {
 				if (res.data.code !== 200) return;
 				const result = res.data.result;
@@ -250,7 +246,6 @@ export default {
 					else if (apiStatus == 2) localStatus = 2;
 				}
 				this.applyStatus = localStatus;
-				uni.setStorageSync('merchantDish_applyStatus', localStatus);
 			});
 		},
 		loadSpecialCategories() {

+ 4 - 6
pages/merchantDish/dish/sale-time.vue

@@ -69,8 +69,7 @@ import merchantDishApi, {
 	getMerchantId,
 	PRODUCT_TYPE_TO_CATEGORY
 } from '@/api/merchantDish.js';
-
-const DRAFT_KEY = 'merchantDish_draft';
+import session from '../session.js';
 
 export default {
 	data() {
@@ -276,9 +275,9 @@ export default {
 				if (!this.canSubmit) uni.showToast({ title: '请完善售卖时间', icon: 'none' });
 				return;
 			}
-			const draft = uni.getStorageSync(DRAFT_KEY) || {};
+			const draft = session.getDishDraft() || {};
 			if (!draft.name) {
-				uni.showToast({ title: '草稿已失效,请重新填写', icon: 'none' });
+				uni.showToast({ title: '表单已失效,请重新填写', icon: 'none' });
 				return;
 			}
 			const payload = this.buildSubmitPayload(draft);
@@ -290,8 +289,7 @@ export default {
 					const ok = res.data.code === 200;
 					this.$tip[ok ? 'success' : 'error'](res.data.message || (ok ? '送审成功!' : '提交失败'));
 					if (ok) {
-						uni.removeStorageSync(DRAFT_KEY);
-						uni.removeStorageSync('merchantDish_edit_extra');
+						session.clearDishDraft();
 						setTimeout(() => {
 							uni.redirectTo({ url: '/pages/merchantDish/dish/list' });
 						}, 500);

+ 69 - 43
pages/merchantDish/dish/spec-detail.vue

@@ -68,7 +68,7 @@
 </template>
 
 <script>
-const DRAFT_KEY = 'merchantDish_draft';
+import session from '../session.js';
 
 export default {
 	data() {
@@ -89,8 +89,7 @@ export default {
 		}
 	},
 	onLoad() {
-		const draft = uni.getStorageSync(DRAFT_KEY) || {};
-		// 兼容旧草稿 originPrice → sellingPrice
+		const draft = session.getDishDraft() || {};
 		this.combos = (draft.combos || []).map((i) => ({
 			...i,
 			sellingPrice: i.sellingPrice != null && i.sellingPrice !== '' ? i.sellingPrice : i.originPrice || ''
@@ -144,9 +143,9 @@ export default {
 				uni.showToast({ title: '请完善规格价格库存', icon: 'none' });
 				return;
 			}
-			const draft = uni.getStorageSync(DRAFT_KEY) || {};
+			const draft = session.getDishDraft() || {};
 			draft.combos = this.combos;
-			uni.setStorageSync(DRAFT_KEY, draft);
+			session.setDishDraft(draft);
 			uni.navigateTo({ url: '/pages/merchantDish/dish/sale-time?from=multi' });
 		}
 	}
@@ -165,7 +164,7 @@ export default {
 }
 .list {
 	height: calc(100vh - 100rpx);
-	padding: 20rpx 24rpx;
+	padding: 16rpx 24rpx;
 	box-sizing: border-box;
 }
 .card {
@@ -175,46 +174,45 @@ export default {
 	padding: 20rpx;
 	margin-bottom: 16rpx;
 	.check {
+		display: flex;
+		align-items: center;
 		margin-right: 16rpx;
-		padding-top: 8rpx;
+	}
+	.dot {
+		width: 36rpx;
+		height: 36rpx;
+		border-radius: 50%;
+		border: 2rpx solid #ccc;
+		&.on {
+			background: #449aff;
+			border-color: #449aff;
+		}
 	}
 	.body {
 		flex: 1;
 	}
 	.title {
-		font-size: 30rpx;
-		font-weight: 600;
+		font-size: 28rpx;
+		font-weight: 500;
 		margin-bottom: 8rpx;
 	}
-}
-.row {
-	display: flex;
-	align-items: center;
-	min-height: 80rpx;
-	border-bottom: 1rpx solid #f5f5f5;
-	.label {
-		width: 180rpx;
-		font-size: 26rpx;
-		color: #333;
-		&.required::before {
-			content: '*';
-			color: #ff4d4f;
+	.row {
+		display: flex;
+		align-items: center;
+		min-height: 72rpx;
+		.label {
+			width: 180rpx;
+			font-size: 26rpx;
+			&.required::before {
+				content: '*';
+				color: #ff4d4f;
+			}
+		}
+		input {
+			flex: 1;
+			text-align: right;
+			font-size: 26rpx;
 		}
-	}
-	input {
-		flex: 1;
-		text-align: right;
-		font-size: 26rpx;
-	}
-}
-.dot {
-	width: 36rpx;
-	height: 36rpx;
-	border-radius: 50%;
-	border: 2rpx solid #ccc;
-	&.on {
-		background: #ff4d4f;
-		border-color: #ff4d4f;
 	}
 }
 .footer {
@@ -238,33 +236,61 @@ export default {
 		display: flex;
 		align-items: center;
 		font-size: 26rpx;
+		margin-right: 20rpx;
 		.dot {
+			width: 36rpx;
+			height: 36rpx;
+			border-radius: 50%;
+			border: 2rpx solid #ccc;
 			margin-right: 10rpx;
+			&.on {
+				background: #449aff;
+				border-color: #449aff;
+			}
 		}
 	}
 	.b-btn {
-		margin-left: auto;
-		background: #ff4d4f;
-		color: #fff;
-		padding: 16rpx 40rpx;
+		flex: 1;
+		text-align: center;
+		height: 72rpx;
+		line-height: 72rpx;
 		border-radius: 36rpx;
+		background: #449aff;
+		color: #fff;
 		font-size: 28rpx;
 	}
 }
 .batch-sheet {
-	padding: 30rpx;
+	padding: 30rpx 40rpx calc(30rpx + env(safe-area-inset-bottom));
 	.bs-title {
 		text-align: center;
 		font-size: 32rpx;
 		font-weight: 600;
 		margin-bottom: 20rpx;
 	}
+	.row {
+		display: flex;
+		align-items: center;
+		min-height: 88rpx;
+		border-bottom: 1rpx solid #f0f0f0;
+		.label {
+			width: 180rpx;
+			&.required::before {
+				content: '*';
+				color: #ff4d4f;
+			}
+		}
+		input {
+			flex: 1;
+			text-align: right;
+		}
+	}
 	.bs-btn {
 		margin-top: 30rpx;
 		height: 80rpx;
 		line-height: 80rpx;
 		text-align: center;
-		background: #ff4d4f;
+		background: #449aff;
 		color: #fff;
 		border-radius: 40rpx;
 	}

+ 32 - 0
pages/merchantDish/session.js

@@ -0,0 +1,32 @@
+/**
+ * 多步骤表单的页面间内存态(非本地缓存,进程内有效,提交后清空)
+ * 列表/详情/报名等业务数据一律走接口
+ */
+
+let dishDraft = null
+let dishEditExtra = null
+
+const session = {
+	getDishDraft() {
+		return dishDraft
+	},
+	setDishDraft(data) {
+		dishDraft = data || null
+	},
+	clearDishDraft() {
+		dishDraft = null
+		dishEditExtra = null
+	},
+	getDishEditExtra() {
+		return dishEditExtra || {}
+	},
+	setDishEditExtra(data) {
+		dishEditExtra = data || null
+	},
+	clearAll() {
+		dishDraft = null
+		dishEditExtra = null
+	}
+}
+
+export default session

+ 28 - 21
pages/merchantDish/settings/box-bind.vue

@@ -44,38 +44,44 @@ export default {
 	data() {
 		return {
 			boxId: null,
-			isTempBox: false,
+			fromForm: false,
 			keyword: '',
 			cats: ['早餐', '午餐', '晚餐'],
 			catIndex: 0,
 			list: [],
 			selected: [],
 			loading: false,
-			saving: false
+			saving: false,
+			eventChannel: null
 		};
 	},
 	computed: {
 		isAll() {
 			const ids = this.list.map((i) => i.id);
 			return ids.length > 0 && ids.every((id) => this.selected.includes(id));
+		},
+		isTempBox() {
+			return !this.boxId || this.boxId === 'new';
 		}
 	},
 	onLoad(query) {
 		this.boxId = query.id;
-		this.isTempBox = !query.id || query.id === 'new';
+		this.fromForm = query.from === 'form';
+		this.eventChannel = this.getOpenerEventChannel && this.getOpenerEventChannel();
+		if (this.eventChannel && this.eventChannel.on) {
+			this.eventChannel.on('initBind', (data) => {
+				this.selected = [...((data && data.bindIds) || [])];
+			});
+		}
 		this.initSelected().then(() => this.fetchPackages());
 	},
 	methods: {
 		initSelected() {
-			const editing = uni.getStorageSync('merchantDish_box_editing');
-			if (editing && String(editing.id) === String(this.boxId)) {
-				this.selected = [...(editing.bindIds || [])];
-				return Promise.resolve();
-			}
-			if (this.isTempBox) {
-				this.selected = [];
+			// 从表单进入:等待 eventChannel 传入已选;新建无接口数据
+			if (this.fromForm || this.isTempBox) {
 				return Promise.resolve();
 			}
+			// 从列表进入:拉接口已绑定套餐
 			return merchantDishApi.mealBoxPackages(this.boxId).then((res) => {
 				if (res.data.code === 200) {
 					const pkgs = res.data.result || [];
@@ -94,7 +100,6 @@ export default {
 					pageNo: 1,
 					pageSize: 100,
 					productBelonging: 2,
-					// statusType: 1,
 					category: String(this.catIndex + 1),
 					name: this.keyword || undefined
 				})
@@ -123,7 +128,6 @@ export default {
 		},
 		toggleAll() {
 			if (this.isAll) {
-				// 仅取消当前分类下已选
 				const ids = this.list.map((i) => i.id);
 				this.selected = this.selected.filter((id) => !ids.includes(id));
 			} else {
@@ -134,19 +138,22 @@ export default {
 		},
 		onConfirm() {
 			if (this.saving) return;
-			const editing = uni.getStorageSync('merchantDish_box_editing');
-			const fromForm = editing && String(editing.id) === String(this.boxId);
-			// 从餐盒表单进入:只回写草稿,等表单保存时一并提交
-			if (this.isTempBox || fromForm) {
-				uni.setStorageSync('merchantDish_box_bind_temp', {
-					boxId: this.boxId || 'new',
-					bindIds: [...this.selected]
-				});
+			// 从表单进入:回写上一页,随表单保存一起提交接口
+			if (this.fromForm || this.isTempBox) {
+				if (this.eventChannel && this.eventChannel.emit) {
+					this.eventChannel.emit('onBindConfirm', { bindIds: [...this.selected] });
+				} else {
+					const pages = getCurrentPages();
+					const prev = pages[pages.length - 2];
+					if (prev && prev.$vm && typeof prev.$vm.applyBindIds === 'function') {
+						prev.$vm.applyBindIds([...this.selected]);
+					}
+				}
 				uni.showToast({ title: '已选择', icon: 'success' });
 				setTimeout(() => uni.navigateBack(), 400);
 				return;
 			}
-			// 从餐盒列表直接绑定:立即全量替换
+			// 从列表直接绑定:立即调接口全量替换
 			this.saving = true;
 			merchantDishApi
 				.mealBoxBind(this.boxId, this.selected)

+ 18 - 17
pages/merchantDish/settings/box-form.vue

@@ -71,15 +71,12 @@ export default {
 			this.loadDetail(query.id);
 		}
 	},
-	onShow() {
-		const temp = uni.getStorageSync('merchantDish_box_bind_temp');
-		if (temp && String(temp.boxId) === String(this.form.id || 'new')) {
-			this.form.bindIds = temp.bindIds || [];
-			this.form.bindCount = this.form.bindIds.length;
-			uni.removeStorageSync('merchantDish_box_bind_temp');
-		}
-	},
 	methods: {
+		/** 供绑定页回写选中商品 */
+		applyBindIds(ids) {
+			this.form.bindIds = ids || [];
+			this.form.bindCount = this.form.bindIds.length;
+		},
 		loadDetail(id) {
 			uni.showLoading({ title: '加载中' });
 			Promise.all([
@@ -124,11 +121,19 @@ export default {
 		},
 		goBind() {
 			const boxId = this.form.id || 'new';
-			uni.setStorageSync('merchantDish_box_editing', {
-				id: boxId,
-				bindIds: this.form.bindIds || []
+			uni.navigateTo({
+				url: `/pages/merchantDish/settings/box-bind?id=${boxId}&from=form`,
+				success: (res) => {
+					res.eventChannel.emit('initBind', {
+						bindIds: this.form.bindIds || []
+					});
+				},
+				events: {
+					onBindConfirm: (data) => {
+						this.applyBindIds((data && data.bindIds) || []);
+					}
+				}
 			});
-			uni.navigateTo({ url: `/pages/merchantDish/settings/box-bind?id=${boxId}` });
 		},
 		onSave() {
 			if (!this.form.name) {
@@ -148,10 +153,7 @@ export default {
 			const done = (res) => {
 				const ok = res.data.code === 200;
 				this.$tip.toast(res.data.message || (ok ? '保存成功' : '保存失败'));
-				if (ok) {
-					uni.removeStorageSync('merchantDish_box_editing');
-					setTimeout(() => uni.navigateBack(), 400);
-				}
+				if (ok) setTimeout(() => uni.navigateBack(), 400);
 			};
 			if (!this.isEdit) {
 				merchantDishApi
@@ -162,7 +164,6 @@ export default {
 					});
 				return;
 			}
-			// 编辑:先改餐盒信息;绑定用 bindPackages 全量替换(空数组可清空)
 			merchantDishApi
 				.mealBoxEdit({ id: this.form.id, ...base })
 				.then((res) => {