付晓文。 1 месяц назад
Родитель
Сommit
50b6740d0e

+ 3 - 1
api/merchantDish.js

@@ -111,7 +111,9 @@ export function mapPackageToListItem(item) {
 		sales: item.sales != null ? item.sales : 0,
 		status,
 		statusName: item.statusName || STATUS_TEXT[status] || '',
-		hasInProgressOrder: item.hasInProgressOrder
+		hasInProgressOrder: item.hasInProgressOrder,
+		ungroupedOrderCount: Number(item.ungroupedOrderCount) || 0,
+		groupedOrderCount: Number(item.groupedOrderCount) || 0
 	}
 }
 

+ 279 - 160
pages/merchantDish/dish/form.vue

@@ -21,7 +21,7 @@
 				<input class="input" v-model="form.name" placeholder="请输入" placeholder-class="ph" />
 			</view>
 			<view class="row" @click="pickProductType">
-				<text class="label required">菜品类</text>
+				<text class="label required">菜品类</text>
 				<text class="value">{{ productTypeName || '请选择' }}</text>
 				<image class="arrow-down" src="/static/merchantDish/selete.png" mode="aspectFit"></image>
 			</view>
@@ -151,55 +151,72 @@
 
 		<!-- 编辑规格名 -->
 		<view class="mask" v-if="nameModal" @click="nameModal = false">
-			<view class="dialog" @click.stop>
-				<view class="dialog-title">
-					编辑规格名
-					<text class="dialog-close" @click="nameModal = false">×</text>
-				</view>
-				<input class="dialog-input" v-model="editingName" placeholder="请输入规格名称" />
-				<view class="dialog-footer">
-					<view class="dialog-btn" @click="nameModal = false">取消</view>
-					<view class="dialog-btn primary" @click="saveSpecName">确认</view>
+			<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="dialog list-dialog" @click.stop>
-				<view class="dialog-title">
-					编辑规格值
-					<text class="dialog-close" @click="valueListShow = false">×</text>
-				</view>
-				<view class="vp-item" v-for="(v, i) in editingValues" :key="v.id">
-					<text class="vp-name" @click="editOneValue(i)">{{ v.name }}</text>
-					<view class="vp-ops">
-						<u-icon name="edit-pen" color="#999" size="32" @click="editOneValue(i)"></u-icon>
-						<u-icon name="trash" color="#FF4D4F" size="32" @click="removeSpecValue(i)"></u-icon>
+			<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 class="vp-add" @click="addSpecValue">+ 添加规格值</view>
-				<view class="vp-confirm" @click="confirmValues">确定</view>
 			</view>
 		</view>
 
-		<!-- 编辑规格值:单条 -->
-		<view class="mask top-mask" v-if="valueEditModal" @click="valueEditModal = false">
-			<view class="dialog" @click.stop>
-				<view class="dialog-title">
-					编辑规格值
-					<text class="dialog-close" @click="valueEditModal = false">×</text>
+		<!-- 菜品分类 / 菜品类型:底部弹层 -->
+		<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>
-				<input class="dialog-input" v-model="editingValueName" placeholder="请输入规格值" focus />
-				<view class="dialog-footer">
-					<view class="dialog-btn" @click="valueEditModal = false">取消</view>
-					<view class="dialog-btn primary" @click="saveOneValue">确认</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>
-
-		<u-action-sheet :list="categorySheet" v-model="categoryShow" @click="onCategoryPick"></u-action-sheet>
-		<u-action-sheet :list="typeSheet" v-model="typeShow" @click="onTypePick"></u-action-sheet>
 	</view>
 </template>
 
@@ -298,10 +315,10 @@ export default {
 			specMode: 'single',
 			panelMode: 'single',
 			_panelTimer: null,
-			categoryShow: false,
-			typeShow: false,
-			categorySheet: [],
-			typeSheet: PRODUCT_TYPES.map((i) => ({ text: i.label, value: i.value })),
+			pickShow: false,
+			pickKind: '', // category | type
+			pickTemp: '',
+			productTypes: PRODUCT_TYPES,
 			specMenuShow: false,
 			specMenuList: [{ text: '编辑规格名' }, { text: '编辑规格值' }, { text: '删除规格', color: '#FF4D4F' }],
 			activeSpecIndex: 0,
@@ -309,9 +326,7 @@ export default {
 			editingName: '',
 			valueListShow: false,
 			editingValues: [],
-			valueEditModal: false,
-			editingValueIndex: 0,
-			editingValueName: ''
+			valueDraft: ''
 		};
 	},
 	computed: {
@@ -331,6 +346,12 @@ export default {
 		},
 		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: {
@@ -464,7 +485,6 @@ export default {
 					label: i.categoryName,
 					value: String(i.id)
 				}));
-				this.categorySheet = this.specialCategories.map((i) => ({ text: i.label, value: i.value }));
 				if (!this.form.category && this.specialCategories.length) {
 					this.form.category = this.specialCategories[0].value;
 				}
@@ -624,16 +644,36 @@ export default {
 			}
 		},
 		pickCategory() {
-			this.categoryShow = true;
-		},
-		onCategoryPick(e) {
-			this.form.category = e.value;
+			if (!this.specialCategories.length) {
+				uni.showToast({ title: '暂无分类', icon: 'none' });
+				return;
+			}
+			this.pickKind = 'category';
+			this.pickTemp = this.form.category || '';
+			this.pickShow = true;
 		},
 		pickProductType() {
-			this.typeShow = true;
+			this.pickKind = 'type';
+			this.pickTemp = this.form.productType || 'breakfast';
+			this.pickShow = true;
+		},
+		closePickPopup() {
+			this.pickShow = false;
 		},
-		onTypePick(e) {
-			this.form.productType = e.value;
+		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' });
@@ -653,6 +693,7 @@ export default {
 				});
 			} else if (index === 1) {
 				this.editingValues = JSON.parse(JSON.stringify(spec.values || []));
+				this.valueDraft = '';
 				this.$nextTick(() => {
 					this.valueListShow = true;
 				});
@@ -683,41 +724,44 @@ export default {
 			this.form.specs[this.activeSpecIndex].name = this.editingName.trim();
 			this.nameModal = false;
 		},
-		editOneValue(i) {
-			this.editingValueIndex = i;
-			this.editingValueName = (this.editingValues[i] && this.editingValues[i].name) || '';
-			this.valueEditModal = true;
-		},
 		addSpecValue() {
-			const id = `v${Date.now()}`;
+			const name = (this.valueDraft || '').trim();
+			if (!name) {
+				uni.showToast({ title: '请输入规格值', icon: 'none' });
+				return;
+			}
 			this.editingValues.push({
-				id,
-				name: `选项${this.editingValues.length + 1}`,
+				id: `v${Date.now()}`,
+				name,
 				selected: true
 			});
-			this.editOneValue(this.editingValues.length - 1);
+			this.valueDraft = '';
 		},
 		removeSpecValue(i) {
-			if (this.editingValues.length <= 1) {
+			if (this.editingValues.length <= 1 && !(this.valueDraft || '').trim()) {
 				uni.showToast({ title: '至少保留一个规格值', icon: 'none' });
 				return;
 			}
 			this.editingValues.splice(i, 1);
 		},
-		saveOneValue() {
-			if (!this.editingValueName.trim()) {
-				uni.showToast({ title: '请输入规格值', icon: 'none' });
-				return;
-			}
-			this.$set(this.editingValues[this.editingValueIndex], 'name', this.editingValueName.trim());
-			this.valueEditModal = false;
-		},
 		confirmValues() {
-			if (!this.editingValues.length) {
-				uni.showToast({ title: '请至少添加一个规格值', icon: 'none' });
+			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 = this.editingValues;
+			this.form.specs[this.activeSpecIndex].values = list;
+			this.valueDraft = '';
 			this.valueListShow = false;
 			this.refreshNextEnabled();
 		},
@@ -995,107 +1039,182 @@ export default {
 	display: flex;
 	align-items: center;
 	justify-content: center;
-	&.top-mask {
-		z-index: 1001;
-	}
 }
-.dialog {
-	width: 560rpx;
+.spec-dialog {
+	width: 600rpx;
 	background: #fff;
-	border-radius: 16rpx;
-	overflow: hidden;
-	.dialog-title {
-		position: relative;
-		text-align: center;
-		font-size: 32rpx;
-		font-weight: 600;
-		color: #222;
-		padding: 36rpx 48rpx 20rpx;
-		.dialog-close {
-			position: absolute;
-			right: 24rpx;
-			top: 24rpx;
-			font-size: 40rpx;
-			color: #999;
-			line-height: 1;
-			font-weight: 400;
-		}
-	}
-	.dialog-input {
-		margin: 10rpx 40rpx 30rpx;
-		height: 80rpx;
-		padding: 0 24rpx;
-		border: 1rpx solid #e5e5e5;
-		border-radius: 8rpx;
-		font-size: 28rpx;
+	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;
 	}
-	.dialog-footer {
-		display: flex;
-		border-top: 1rpx solid #eee;
-		.dialog-btn {
-			flex: 1;
-			height: 96rpx;
-			line-height: 96rpx;
-			text-align: center;
-			font-size: 30rpx;
-			color: #333;
-			&:first-child {
-				border-right: 1rpx solid #eee;
-			}
-			&.primary {
-				color: #449aff;
-			}
-		}
+	&.save {
+		color: #fff;
+		background: linear-gradient(90deg, #77b6ff 0%, #449aff 100%);
 	}
 }
-.list-dialog {
-	padding-bottom: 24rpx;
-	.vp-item {
-		display: flex;
-		justify-content: space-between;
-		align-items: center;
-		padding: 28rpx 40rpx;
-		border-bottom: 1rpx solid #f0f0f0;
-		font-size: 28rpx;
-		color: #333;
-		.vp-name {
-			flex: 1;
-			min-width: 0;
-		}
-		.vp-ops {
-			display: flex;
-			align-items: center;
-			gap: 28rpx;
-			margin-left: 20rpx;
-			flex-shrink: 0;
-		}
-	}
-	.vp-add {
-		margin: 24rpx 40rpx 0;
-		height: 72rpx;
-		line-height: 72rpx;
-		text-align: center;
-		border: 1rpx dashed #449aff;
-		border-radius: 12rpx;
-		color: #449aff;
-		font-size: 28rpx;
+.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;
 	}
-	.vp-confirm {
-		margin: 24rpx 40rpx 10rpx;
-		height: 80rpx;
-		line-height: 80rpx;
-		text-align: center;
-		background: #ff4d4f;
-		color: #fff;
-		border-radius: 40rpx;
-		font-size: 30rpx;
+}
+.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(90deg, #77b6ff 0%, #449aff 100%);
+	}
+}
 ::v-deep .u-radio {
 	margin-right: 24rpx;
 }

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

@@ -132,6 +132,19 @@
 		</view>
 
 		<u-action-sheet :list="pickerList" v-model="pickerShow" @click="onPick"></u-action-sheet>
+
+		<!-- 团餐下架确认 -->
+		<view class="mask" v-if="offShelfShow" @click="closeOffShelf">
+			<view class="confirm-dialog" @click.stop>
+				<view class="confirm-content">
+					该菜品当前有未成团订单{{ (offShelfItem && offShelfItem.ungroupedOrderCount) || 0 }}份,已成团{{ (offShelfItem && offShelfItem.groupedOrderCount) || 0 }}份,是否要确定下架
+				</view>
+				<view class="confirm-footer">
+					<view class="confirm-btn" @click="closeOffShelf">取消</view>
+					<view class="confirm-btn primary" @click="confirmOffShelf">确定</view>
+				</view>
+			</view>
+		</view>
 	</view>
 </template>
 
@@ -184,7 +197,9 @@ export default {
 			statusText: STATUS_TEXT,
 			applyStatus: 0,
 			listVersion: 0,
-			listReady: false
+			listReady: false,
+			offShelfShow: false,
+			offShelfItem: null
 		};
 	},
 	computed: {
@@ -366,11 +381,25 @@ export default {
 			});
 		},
 		async onOffShelf(item) {
-			let content = '确定下架当前商品';
-			if (item.belong === 'group' && item.hasInProgressOrder) {
-				content = '该商品当前有进行中的团餐订单,是否确定下架';
+			// 团餐:展示未成团/已成团订单数确认弹窗
+			if (item.belong === 'group') {
+				this.offShelfItem = item;
+				this.offShelfShow = true;
+				return;
 			}
-			if (!(await this.confirmModal(content))) return;
+			if (!(await this.confirmModal('确定下架当前商品'))) return;
+			this.doOffShelf(item);
+		},
+		closeOffShelf() {
+			this.offShelfShow = false;
+			this.offShelfItem = null;
+		},
+		confirmOffShelf() {
+			const item = this.offShelfItem;
+			this.closeOffShelf();
+			if (item) this.doOffShelf(item);
+		},
+		doOffShelf(item) {
 			merchantDishApi.packageLaunch(item.id, 0).then((res) => {
 				this.$tip.toast(res.data.message || (res.data.code === 200 ? '已下架' : '操作失败'));
 				if (res.data.code === 200) this.reloadList();
@@ -718,4 +747,47 @@ export default {
 		}
 	}
 }
+.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: center;
+	justify-content: center;
+}
+.confirm-dialog {
+	width: 560rpx;
+	background: #fff;
+	border-radius: 20rpx;
+	overflow: hidden;
+}
+.confirm-content {
+	padding: 48rpx 40rpx 40rpx;
+	text-align: center;
+	font-size: 30rpx;
+	color: #222;
+	line-height: 1.7;
+}
+.confirm-footer {
+	display: flex;
+	border-top: 1rpx solid #eee;
+}
+.confirm-btn {
+	flex: 1;
+	height: 96rpx;
+	line-height: 96rpx;
+	text-align: center;
+	font-size: 30rpx;
+	color: #333;
+	&:first-child {
+		border-right: 1rpx solid #eee;
+	}
+	&.primary {
+		color: #449aff;
+	}
+}
 </style>

+ 380 - 86
pages/merchantDish/dish/sale-time.vue

@@ -3,7 +3,7 @@
 		<u-navbar title="售卖时间" :autoBack="true" :placeholder="true"></u-navbar>
 
 		<view class="card">
-			<view class="label required">商品售卖日期(可多选)</view>
+			<view class="label required">菜品售卖日期</view>
 			<u-radio-group v-model="mode" @change="onModeChange">
 				<u-radio name="fixed" active-color="#449AFF">固定日期</u-radio>
 				<u-radio name="custom" active-color="#449AFF">自定义</u-radio>
@@ -11,12 +11,19 @@
 			</u-radio-group>
 		</view>
 
-		<!-- 固定日期:简易月历多选 -->
+		<!-- 固定日期:月历多选 -->
 		<view class="card" v-if="mode === 'fixed'">
 			<view class="cal-head">
-				<u-icon name="arrow-left" @click="changeMonth(-1)"></u-icon>
-				<text>{{ year }}年{{ month }}月</text>
-				<u-icon name="arrow-right" @click="changeMonth(1)"></u-icon>
+				<view class="cal-month" @click="monthPickerShow = true">
+					<text>{{ year }}年{{ month }}月</text>
+					<u-icon name="arrow-down" color="#333" size="24"></u-icon>
+				</view>
+				<view class="cal-all" @click="toggleSelectAll">
+					<text>全选</text>
+					<view :class="['check', isMonthAllSelected ? 'on' : '']">
+						<u-icon v-if="isMonthAllSelected" name="checkmark" color="#fff" size="18"></u-icon>
+					</view>
+				</view>
 			</view>
 			<view class="week">
 				<text v-for="w in weeks" :key="w">{{ w }}</text>
@@ -28,24 +35,22 @@
 					:class="['day', !d ? 'empty' : '', d && selectedDates.includes(d.full) ? 'on' : '']"
 					@click="toggleDate(d)"
 				>
-					<template v-if="d">
-						<text class="n">{{ d.day }}</text>
-					</template>
+					<text v-if="d" class="n">{{ d.day }}</text>
 				</view>
 			</view>
 		</view>
 
 		<!-- 自定义区间 -->
 		<view class="card" v-if="mode === 'custom'">
-			<view class="row" @click="pickDate('start')">
-				<text class="label required">开始日期</text>
-				<text class="value">{{ startDate || '请选择开始日期' }}</text>
-				<u-icon name="arrow-right" color="#C7C6CA"></u-icon>
-			</view>
-			<view class="row" @click="pickDate('end')">
-				<text class="label required">结束日期</text>
-				<text class="value">{{ endDate || '请选择结束日期' }}</text>
-				<u-icon name="arrow-right" color="#C7C6CA"></u-icon>
+			<view class="label">自定义时间</view>
+			<view class="range-row">
+				<view class="range-box" @click="openRangePicker('start')">
+					{{ startDate || '开始日期' }}
+				</view>
+				<text class="range-sep">-</text>
+				<view class="range-box" @click="openRangePicker('end')">
+					{{ endDate || '结束日期' }}
+				</view>
 			</view>
 		</view>
 
@@ -54,11 +59,63 @@
 		</view>
 
 		<view class="footer">
-			<u-button shape="circle" plain type="primary" @click="goPrev">上一步</u-button>
-			<u-button type="primary" shape="circle" :disabled="!canSubmit || submitting" @click="onSubmit">提交审核</u-button>
+			<view class="f-btn outline" @click="goPrev">上一步</view>
+			<view
+				:class="['f-btn', 'primary', !canSubmit || submitting ? 'disabled' : '']"
+				@click="onSubmit"
+			>
+				提交审核
+			</view>
 		</view>
 
-		<u-calendar v-model="calendarShow" mode="date" max-date="2050-12-31" @change="onCalendar"></u-calendar>
+		<!-- 年月切换 -->
+		<view class="pick-mask" v-if="monthPickerShow" @click="monthPickerShow = false">
+			<view class="pick-sheet" @click.stop>
+				<view class="pick-title">选择月份</view>
+				<picker-view class="pick-view" :value="monthPickerValue" @change="onMonthPickerChange">
+					<picker-view-column>
+						<view class="pick-item" v-for="y in yearList" :key="y">{{ y }}年</view>
+					</picker-view-column>
+					<picker-view-column>
+						<view class="pick-item" v-for="m in 12" :key="m">{{ m }}月</view>
+					</picker-view-column>
+				</picker-view>
+				<view class="pick-confirm" @click="confirmMonthPicker">确定</view>
+			</view>
+		</view>
+
+		<!-- 自定义:选择时间(起止) -->
+		<view class="pick-mask" v-if="rangePickerShow" @click="rangePickerShow = false">
+			<view class="pick-sheet" @click.stop>
+				<view class="pick-title">选择时间</view>
+				<view class="range-preview">
+					<view
+						:class="['preview-box', rangeField === 'start' ? 'on' : '']"
+						@click="switchRangeField('start')"
+					>
+						{{ formatCnDate(tempStart) || '开始日期' }}
+					</view>
+					<view
+						:class="['preview-box', rangeField === 'end' ? 'on' : '']"
+						@click="switchRangeField('end')"
+					>
+						{{ formatCnDate(tempEnd) || '结束日期' }}
+					</view>
+				</view>
+				<picker-view class="pick-view" :value="datePickerValue" @change="onDatePickerChange">
+					<picker-view-column>
+						<view class="pick-item" v-for="y in yearList" :key="'dy' + y">{{ y }}年</view>
+					</picker-view-column>
+					<picker-view-column>
+						<view class="pick-item" v-for="m in 12" :key="'dm' + m">{{ m }}月</view>
+					</picker-view-column>
+					<picker-view-column>
+						<view class="pick-item" v-for="d in dayList" :key="'dd' + d">{{ d }}日</view>
+					</picker-view-column>
+				</picker-view>
+				<view class="pick-confirm" @click="confirmRangePicker">确定</view>
+			</view>
+		</view>
 	</view>
 </template>
 
@@ -74,18 +131,28 @@ import session from '../session.js';
 export default {
 	data() {
 		const now = new Date();
+		const year = now.getFullYear();
+		const yearList = [];
+		for (let y = year - 2; y <= year + 10; y++) yearList.push(y);
 		return {
 			fromMulti: false,
 			mode: 'fixed',
-			year: now.getFullYear(),
+			year,
 			month: now.getMonth() + 1,
 			weeks: ['日', '一', '二', '三', '四', '五', '六'],
 			selectedDates: [],
 			startDate: '',
 			endDate: '',
-			calendarShow: false,
-			calendarField: 'start',
-			submitting: false
+			submitting: false,
+			yearList,
+			monthPickerShow: false,
+			monthPickerValue: [2, now.getMonth()],
+			rangePickerShow: false,
+			rangeField: 'start',
+			tempStart: '',
+			tempEnd: '',
+			datePickerValue: [2, now.getMonth(), now.getDate() - 1],
+			pickerDay: now.getDate()
 		};
 	},
 	computed: {
@@ -102,6 +169,24 @@ export default {
 			}
 			return arr;
 		},
+		monthDateList() {
+			return this.days.filter(Boolean).map((d) => d.full);
+		},
+		isMonthAllSelected() {
+			const list = this.monthDateList;
+			if (!list.length) return false;
+			return list.every((d) => this.selectedDates.includes(d));
+		},
+		dayList() {
+			const y =
+				this.yearList[this.datePickerValue[0]] ||
+				this.year;
+			const m = (this.datePickerValue[1] || 0) + 1;
+			const total = new Date(y, m, 0).getDate();
+			const arr = [];
+			for (let d = 1; d <= total; d++) arr.push(d);
+			return arr;
+		},
 		canSubmit() {
 			if (this.mode === 'all') return true;
 			if (this.mode === 'fixed') return this.selectedDates.length > 0;
@@ -111,24 +196,32 @@ export default {
 	onLoad(query) {
 		this.fromMulti = query.from === 'multi';
 		if (this.fromMulti) this.mode = 'fixed';
+		this.syncMonthPickerValue();
 		this.hydrateFromEditExtra();
 	},
 	methods: {
-		/** 将日期统一为 YYYY-MM-DD,兼容接口带时分秒 */
 		normalizeDate(val) {
 			if (!val) return '';
 			const s = String(val).trim();
 			const m = s.match(/^(\d{4}-\d{2}-\d{2})/);
 			return m ? m[1] : s.slice(0, 10);
 		},
-		/** 编辑回显:优先根据 saleTimes 内容推断,不依赖顶层 saleDateType 一定有值 */
+		formatCnDate(val) {
+			const d = this.normalizeDate(val);
+			if (!d) return '';
+			const [y, m, day] = d.split('-');
+			return `${Number(y)}年${Number(m)}月${Number(day)}日`;
+		},
+		toYmd(y, m, d) {
+			return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
+		},
+		/** 编辑回显:优先根据 saleTimes 内容推断 */
 		hydrateFromEditExtra() {
 			const extra = session.getDishEditExtra() || {};
 			const saleTimes = Array.isArray(extra.saleTimes) ? extra.saleTimes : [];
 			const saleDateType = Number(extra.saleDateType);
 			if (!saleTimes.length && !saleDateType) return;
 
-			// 自定义区间:saleType=2 或带起止日期(接口可能不回顶层 saleDateType)
 			const range = saleTimes.find(
 				(t) => Number(t.saleType) === 2 || t.startDate || t.endDate
 			);
@@ -139,7 +232,6 @@ export default {
 				return;
 			}
 
-			// 固定日期:saleType=1 或带 saleDate
 			const fixedDates = saleTimes
 				.filter((t) => Number(t.saleType) === 1 || t.saleDate)
 				.map((t) => this.normalizeDate(t.saleDate))
@@ -152,50 +244,129 @@ export default {
 					const [y, m] = fixedDates[0].split('-').map(Number);
 					if (y) this.year = y;
 					if (m) this.month = m;
+					this.syncMonthPickerValue();
 				}
 				return;
 			}
 
-			// 无具体区间:全部日期售卖(仅多规格入口有该选项)
 			if ((saleDateType === 2 || !saleTimes.length) && this.fromMulti) {
 				this.mode = 'all';
 			}
 		},
+		syncMonthPickerValue() {
+			const yi = this.yearList.indexOf(this.year);
+			this.monthPickerValue = [yi >= 0 ? yi : 0, this.month - 1];
+		},
 		onModeChange() {
 			this.selectedDates = [];
 			this.startDate = '';
 			this.endDate = '';
 		},
-		changeMonth(delta) {
-			let m = this.month + delta;
-			let y = this.year;
-			if (m < 1) {
-				m = 12;
-				y -= 1;
-			}
-			if (m > 12) {
-				m = 1;
-				y += 1;
-			}
-			this.month = m;
-			this.year = y;
-		},
 		toggleDate(d) {
 			if (!d) return;
 			const i = this.selectedDates.indexOf(d.full);
 			if (i >= 0) this.selectedDates.splice(i, 1);
 			else this.selectedDates.push(d.full);
 		},
-		pickDate(field) {
-			this.calendarField = field;
-			this.calendarShow = true;
+		toggleSelectAll() {
+			const list = this.monthDateList;
+			if (this.isMonthAllSelected) {
+				this.selectedDates = this.selectedDates.filter((d) => !list.includes(d));
+			} else {
+				const set = new Set(this.selectedDates.concat(list));
+				this.selectedDates = Array.from(set).sort();
+			}
+		},
+		onMonthPickerChange(e) {
+			this.monthPickerValue = e.detail.value || [0, 0];
+		},
+		confirmMonthPicker() {
+			const [yi, mi] = this.monthPickerValue;
+			this.year = this.yearList[yi] || this.year;
+			this.month = (mi || 0) + 1;
+			this.monthPickerShow = false;
+		},
+		openRangePicker(field) {
+			this.rangeField = field || 'start';
+			this.tempStart = this.startDate;
+			this.tempEnd = this.endDate;
+			const base = this.normalizeDate(
+				field === 'end' ? this.endDate || this.startDate : this.startDate || this.endDate
+			);
+			this.applyDateToPicker(base);
+			// 首次打开且对应字段为空时,用当前滚轮日期预填
+			const now = new Date();
+			const fallback = this.toYmd(now.getFullYear(), now.getMonth() + 1, now.getDate());
+			const seed = base || fallback;
+			if (field === 'start' && !this.tempStart) this.tempStart = seed;
+			if (field === 'end' && !this.tempEnd) this.tempEnd = seed;
+			this.rangePickerShow = true;
+		},
+		switchRangeField(field) {
+			this.rangeField = field;
+			const base = this.normalizeDate(field === 'start' ? this.tempStart : this.tempEnd);
+			this.applyDateToPicker(base);
+		},
+		applyDateToPicker(ymd) {
+			const now = new Date();
+			let y = now.getFullYear();
+			let m = now.getMonth() + 1;
+			let d = now.getDate();
+			if (ymd) {
+				const parts = ymd.split('-').map(Number);
+				y = parts[0] || y;
+				m = parts[1] || m;
+				d = parts[2] || d;
+			}
+			let yi = this.yearList.indexOf(y);
+			if (yi < 0) yi = 0;
+			const total = new Date(y, m, 0).getDate();
+			if (d > total) d = total;
+			this.pickerDay = d;
+			this.datePickerValue = [yi, m - 1, d - 1];
+			this.$nextTick(() => {
+				this.datePickerValue = [yi, m - 1, d - 1];
+			});
+		},
+		onDatePickerChange(e) {
+			const val = e.detail.value || [0, 0, 0];
+			const y = this.yearList[val[0]] || this.year;
+			const m = (val[1] || 0) + 1;
+			const total = new Date(y, m, 0).getDate();
+			let d = (val[2] || 0) + 1;
+			if (d > total) d = total;
+			this.pickerDay = d;
+			this.datePickerValue = [val[0], val[1], d - 1];
+			const ymd = this.toYmd(y, m, d);
+			if (this.rangeField === 'start') this.tempStart = ymd;
+			else this.tempEnd = ymd;
 		},
-		onCalendar(e) {
-			const val = (e && e.result) || e;
-			if (!val || typeof val !== 'string') return;
-			if (this.calendarField === 'start') this.startDate = val;
-			else this.endDate = val;
-			this.calendarShow = false;
+		confirmRangePicker() {
+			// 滚轮停住后若未触发 change,用当前索引兜底写入当前字段
+			const [yi, mi, di] = this.datePickerValue;
+			const y = this.yearList[yi] || this.year;
+			const m = (mi || 0) + 1;
+			const total = new Date(y, m, 0).getDate();
+			let d = (di || 0) + 1;
+			if (d > total) d = total;
+			const ymd = this.toYmd(y, m, d);
+			if (this.rangeField === 'start') {
+				if (!this.tempStart) this.tempStart = ymd;
+			} else if (!this.tempEnd) {
+				this.tempEnd = ymd;
+			}
+
+			if (!this.tempStart || !this.tempEnd) {
+				uni.showToast({ title: '请选择开始和结束日期', icon: 'none' });
+				return;
+			}
+			if (this.tempStart > this.tempEnd) {
+				uni.showToast({ title: '开始日期不能晚于结束日期', icon: 'none' });
+				return;
+			}
+			this.startDate = this.tempStart;
+			this.endDate = this.tempEnd;
+			this.rangePickerShow = false;
 		},
 		goPrev() {
 			uni.navigateBack();
@@ -204,11 +375,14 @@ export default {
 			if (this.mode === 'fixed') {
 				return {
 					saleDateType: 1,
-					saleTimes: this.selectedDates.map((d, i) => ({
-						saleType: 1,
-						saleDate: d,
-						sort: i
-					}))
+					saleTimes: this.selectedDates
+						.slice()
+						.sort()
+						.map((d, i) => ({
+							saleType: 1,
+							saleDate: d,
+							sort: i
+						}))
 				};
 			}
 			if (this.mode === 'custom') {
@@ -224,7 +398,6 @@ export default {
 					]
 				};
 			}
-			// 全部日期:不传具体日期区间,交由后端默认处理
 			return {
 				saleDateType: 2,
 				saleTimes: []
@@ -249,7 +422,6 @@ export default {
 		buildSpecsAndSkus(draft) {
 			const specs = [];
 			(draft.specs || []).forEach((spec, si) => {
-				// 选中/未选中都提交,isSelected: 1 选中,0 未选中
 				(spec.values || []).forEach((v, oi) => {
 					const row = {
 						specName: spec.name,
@@ -258,14 +430,12 @@ export default {
 						optionSort: oi,
 						isSelected: v.selected ? 1 : 0
 					};
-					// 编辑时回传后端规格选项 id
 					if (v.persistId) row.id = v.persistId;
 					specs.push(row);
 				});
 			});
 			const skus = (draft.combos || []).map((c, i) => ({
 				specOptionNames: (c.label || '').replace(/\s*\|\s*/g, '/'),
-				// 售价 price,原价 sellingPrice
 				price: Number(c.price || 0),
 				sellingPrice: Number(c.sellingPrice != null ? c.sellingPrice : c.originPrice || 0),
 				stock: Number(c.stock || 0),
@@ -297,7 +467,6 @@ export default {
 				payload.id = draft.editId;
 			}
 			if (!isMulti) {
-				// 售价 price,原价 sellingPrice
 				payload.price = Number(draft.price || 0);
 				payload.sellingPrice = Number(draft.sellingPrice || 0);
 				payload.dailyStock = Number(draft.stock || 0);
@@ -308,7 +477,6 @@ export default {
 				const { specs, skus } = this.buildSpecsAndSkus(draft);
 				payload.specs = specs;
 				payload.skus = skus;
-				// 多规格时取第一档作为展示价/库存兜底
 				const first = (draft.combos || [])[0] || {};
 				payload.price = Number(first.price || 0);
 				payload.sellingPrice = Number(
@@ -379,6 +547,7 @@ export default {
 		&.required::before {
 			content: '*';
 			color: #ff4d4f;
+			margin-right: 4rpx;
 		}
 	}
 }
@@ -386,12 +555,40 @@ export default {
 	display: flex;
 	justify-content: space-between;
 	align-items: center;
+	margin-bottom: 24rpx;
+}
+.cal-month {
+	display: flex;
+	align-items: center;
+	gap: 8rpx;
 	font-size: 30rpx;
 	font-weight: 600;
-	margin-bottom: 20rpx;
+	color: #222;
+}
+.cal-all {
+	display: flex;
+	align-items: center;
+	gap: 12rpx;
+	font-size: 26rpx;
+	color: #666;
+	.check {
+		width: 32rpx;
+		height: 32rpx;
+		border-radius: 6rpx;
+		border: 2rpx solid #c7c6ca;
+		box-sizing: border-box;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		&.on {
+			background: #449aff;
+			border-color: #449aff;
+		}
+	}
 }
 .week {
 	display: flex;
+	margin-bottom: 8rpx;
 	text {
 		width: 14.28%;
 		text-align: center;
@@ -404,56 +601,153 @@ export default {
 	flex-wrap: wrap;
 	.day {
 		width: 14.28%;
-		height: 80rpx;
+		height: 88rpx;
 		display: flex;
 		align-items: center;
 		justify-content: center;
 		.n {
-			width: 56rpx;
-			height: 56rpx;
-			line-height: 56rpx;
+			width: 72rpx;
+			height: 72rpx;
+			line-height: 70rpx;
 			text-align: center;
-			border-radius: 50%;
-			font-size: 26rpx;
+			border-radius: 12rpx;
+			font-size: 28rpx;
+			color: #333;
+			border: 2rpx solid transparent;
+			box-sizing: border-box;
 		}
 		&.on .n {
-			background: #449aff;
-			color: #fff;
+			background: rgba(68, 154, 255, 0.12);
+			border-color: #449aff;
+			color: #449aff;
 		}
 		&.empty {
 			pointer-events: none;
 		}
 	}
 }
-.row {
+.range-row {
 	display: flex;
 	align-items: center;
-	min-height: 96rpx;
-	border-bottom: 1rpx solid #f0f0f0;
-	.label {
-		width: 180rpx;
-		margin-bottom: 0;
+	gap: 16rpx;
+}
+.range-box {
+	flex: 1;
+	height: 80rpx;
+	line-height: 80rpx;
+	text-align: center;
+	font-size: 28rpx;
+	color: #333;
+	background: #f7f8fc;
+	border-radius: 12rpx;
+	border: 1rpx solid #e8e8e8;
+}
+.range-sep {
+	color: #999;
+	font-size: 28rpx;
+}
+.footer {
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	display: flex;
+	gap: 24rpx;
+	padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
+	background: #fff;
+}
+.f-btn {
+	height: 88rpx;
+	line-height: 88rpx;
+	text-align: center;
+	font-size: 30rpx;
+	border-radius: 44rpx;
+	&.outline {
+		width: 240rpx;
+		flex-shrink: 0;
+		color: #449aff;
+		border: 2rpx solid #449aff;
+		box-sizing: border-box;
+		line-height: 84rpx;
+		background: #fff;
 	}
-	.value {
+	&.primary {
 		flex: 1;
-		text-align: right;
-		color: #666;
-		font-size: 28rpx;
+		color: #fff;
+		background: linear-gradient(90deg, #77b6ff 0%, #449aff 100%);
+	}
+	&.disabled {
+		opacity: 0.5;
 	}
 }
-.footer {
+.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;
+}
+.pick-title {
+	text-align: center;
+	font-size: 32rpx;
+	font-weight: 600;
+	color: #222;
+	padding: 36rpx 0 24rpx;
+}
+.range-preview {
 	display: flex;
 	gap: 20rpx;
-	padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
+	margin-bottom: 12rpx;
+}
+.preview-box {
+	flex: 1;
+	height: 72rpx;
+	line-height: 70rpx;
+	text-align: center;
+	font-size: 26rpx;
+	color: #666;
 	background: #fff;
-	::v-deep .u-btn {
-		flex: 1;
+	border: 2rpx solid #e5e5e5;
+	border-radius: 12rpx;
+	box-sizing: border-box;
+	&.on {
+		color: #449aff;
+		border-color: #449aff;
+		background: rgba(68, 154, 255, 0.06);
 	}
 }
+.pick-view {
+	height: 400rpx;
+	width: 100%;
+}
+.pick-item {
+	height: 80rpx;
+	line-height: 80rpx;
+	text-align: center;
+	font-size: 30rpx;
+	color: #333;
+}
+.pick-confirm {
+	margin-top: 12rpx;
+	height: 88rpx;
+	line-height: 88rpx;
+	text-align: center;
+	font-size: 30rpx;
+	color: #fff;
+	border-radius: 44rpx;
+	background: linear-gradient(90deg, #77b6ff 0%, #449aff 100%);
+}
 ::v-deep .u-radio {
 	margin-right: 24rpx;
 	margin-bottom: 12rpx;