Ver código fonte

优化团餐添加商品

付晓文。 3 meses atrás
pai
commit
d484e35bb6

+ 3 - 10
common/router/modules/routes.js

@@ -611,17 +611,10 @@ const routes = [{
 		},
 	},
 	{
-		path: '/pages/groupMeal/goods/form-step1',
-		name: 'groupMealGoodsFormStep1',
+		path: '/pages/groupMeal/goods/form',
+		name: 'groupMealGoodsForm',
 		meta: {
-			title: '编辑商品1',
-		},
-	},
-	{
-		path: '/pages/groupMeal/goods/form-step2',
-		name: 'groupMealGoodsFormStep2',
-		meta: {
-			title: '编辑商品2',
+			title: '编辑商品',
 		},
 	},
 	{

+ 1 - 5
pages.json

@@ -478,11 +478,7 @@
 			"style": {}
 		},
 		{
-			"path": "pages/groupMeal/goods/form-step1",
-			"style": {}
-		},
-		{
-			"path": "pages/groupMeal/goods/form-step2",
+			"path": "pages/groupMeal/goods/form",
 			"style": {}
 		},
 		{

+ 0 - 436
pages/groupMeal/goods/form-step1.vue

@@ -1,436 +0,0 @@
-<template>
-	<view class="page-container">
-		<u-navbar title="新建团餐" :autoBack="true" :placeholder="true"></u-navbar>
-		
-		<view class="form-card">
-			<view class="section-title">基础信息</view>
-			<u-form :model="form" ref="uForm" :rules="rules" 
-				labelPosition="left" labelWidth="auto"
-				:error-type="['toast', 'border-bottom']"
-			> 
-				<u-form-item prop="category" label="商品分类" required borderBottom @click="show = true">
-					<u-input v-model="form.categoryName" class="form-input" disabled placeholder="去设置" height="41" inputAlign="right"></u-input>
-					<u-icon slot="right" name="arrow-right" color="#C7C6CA"></u-icon>
-				</u-form-item>
-				
-				<u-form-item prop="name" label="商品名称" required borderBottom>
-					<u-input v-model="form.name" placeholder="请输入商品名称" height="41" inputAlign="right"></u-input>
-				</u-form-item>
-
-				<u-form-item prop="productImage" label="商品封面图" required labelPosition="top">
-					<view class="upload-content">
-						<view class="upload-tip">支持jpg、png格式,建议尺寸为750px*750px,上传1-4张</view>
-						<u-upload
-							:action="uploadAction"
-							:file-list="fileList.cover"
-							:form-data="uploadFormData"
-							:max-count="4"
-							:multiple="true"
-							width="140"
-							height="140"
-							preview-full-image
-							@on-success="onCoverSuccess"
-							@on-remove="onCoverRemove"
-						/>
-					</view>
-				</u-form-item>
-
-				<u-form-item prop="detailImages" label="商品详情图" required borderBottom labelPosition="top">
-					<view class="upload-content">
-						<view class="upload-tip">支持jpg、png格式,最多20张</view>
-						<u-upload
-							:action="uploadAction"
-							:file-list="fileList.detail"
-							:form-data="uploadFormData"
-							:max-count="20"
-							:multiple="true"
-							width="140"
-							height="140"
-							preview-full-image
-							@on-success="onDetailSuccess"
-							@on-remove="onDetailRemove"
-						/>
-					</view>
-				</u-form-item>
-
-				<u-form-item prop="price" label="团餐价" required borderBottom>
-					<u-input type="digit" height="41" :clearable="false" 
-						inputAlign="right"
-						placeholder="请输入团餐价" 
-						v-model.number="form.price"
-						@input="onInput($event, 'price')"
-					></u-input>
-					<text slot="right" class="input-suffix">元</text>
-				</u-form-item>
-				
-				<u-form-item prop="sellingPrice" label="售价" required :borderBottom="true">
-					<u-input type="digit" height="41" :clearable="false" 
-					 	inputAlign="right" placeholder="请输入商品售价"
-						v-model.number="form.sellingPrice"
-						@input="onInput($event, 'sellingPrice')"
-					></u-input>
-					<text slot="right" class="input-suffix">元</text>
-				</u-form-item>
-			</u-form>
-		</view>
-
-		<view class="footer-bar">
-			<u-button type="primary" @click="goNextStep">下一步</u-button>
-		</view>
-		<!-- 商品分类选择 -->
-		<u-action-sheet :list="actionSheetList" v-model="show" @click="actionSheetCallback"></u-action-sheet>
-	</view>
-</template>
-
-<script>
-import configService from '@/common/service/config.service';
-export default {
-	data() {
-		return {
-			show: false,
-			uploadAction: configService.apiUrl + '/sys/common/upload',
-			imageHttp: configService.apiUrl + '/',
-			uploadFormData: { biz: 'temp' },
-			form: {
-				name: '',
-				category: '',
-				productImage: '',
-				detailImages: '',
-				price: '',
-				sellingPrice: '',
-			},
-			query: {},
-			fileList: {
-				cover: [],
-				detail: [],
-			},
-			actionSheetList: [
-				{
-					text: '早餐',
-					value: '1'
-				},
-				{
-					text: '午餐',
-					value: '2'
-				},
-				{
-					text: '晚餐',
-					value: '3'
-				}
-			],
-			rules: {
-				name: [
-					{
-						required: true,
-						message: '请输入名称',
-						trigger: ['blur', 'change']
-					}
-				],
-				category: [
-					{
-						required: true,
-						message: '请选择分类',
-						trigger: ['blur', 'change']
-					}
-				],
-				price: [
-					{
-						required: true,
-						type: 'number',
-						message: '请输入团餐价格',
-						trigger: ['blur', 'change']
-					},
-					{
-						asyncValidator: (rule, value, callback) => {
-							if (this.form.sellingPrice && value > this.form.sellingPrice) {
-								callback(new Error('团餐价格不能大于商品售价'));
-							} else {
-								callback();
-							}
-						},
-						trigger: ['blur', 'change']
-					},
-				],
-				sellingPrice: [
-					{
-						required: true,
-						type: 'number',
-						message: '请输入商品售价',
-						trigger: ['blur', 'change']
-					},
-					{
-						asyncValidator: (rule, value, callback) => {
-							if (this.form.price && value < this.form.price) {
-								callback(new Error('商品售价不能小于团餐价格'));
-							} else {
-								callback();
-							}
-						},
-						trigger: ['blur', 'change']
-					},
-				],
-				productImage: [
-					{
-						required: true,
-						message: '请上传商品封面图',
-						trigger: ['blur', 'change']
-					}
-				],
-				detailImages: [
-					{
-						required: true,
-						message: '请上传商品详情图片',
-						trigger: ['blur', 'change']
-					}
-				],
-			}
-		};
-	},
-	onLoad(query) {
-		if (query.id) {
-			this.query = query;
-			this.getData();
-		}
-		// 清除
-		uni.removeStorageSync('gm_goods');
-		// 获取餐品分类
-		this.getClassData();
-	},
-	onReady() {
-		this.$refs.uForm.setRules(this.rules);
-	},
-	methods: {
-		// 编辑数据
-		getData() {
-			this.$http.get('/groupmeal/gmCustomerPackage/queryById', { params: { id: this.query.id } }).then(res => {
-				if (res.data.code === 200) {
-					let data = res.data.result;
-					this.form = {
-						...data,
-						categoryName: this.actionSheetList.find(item => item.value == data.category).text
-					};
-					this.fileList = {
-						cover: data.productImage.split(',').map(item => {
-							return {
-								url: item
-							}
-						}),
-						detail: data.detailImages.split(',').map(item => {
-							return {
-								url: item
-							}
-						}),
-					};
-				}
-			});
-		},
-		// 餐品分类
-		getClassData() {
-			this.$http.get('/groupmeal/gmMerchantPackage/getGroupMealCategory', {}).then(res => {
-				if (res.data.code === 200) {
-					this.actionSheetList = res.data.result.map(item => {
-						return {
-							text: item.name,
-							value: item.code
-						}
-					})
-				}
-			});
-		},
-		// 分类选择,打开弹窗
-		actionSheetCallback(e) {
-			this.form.category = e.value;
-			this.form.categoryName = e.text;
-		},
-		// 价格数值处理
-		onInput(value, key) {
-			// 过滤所有非数字和非小数点的字符
-			let filtered = value.replace(/[^\d.]/g, '');
-			
-			// 处理多个小数点(只保留第一个)
-			filtered = filtered.replace(/^\./g, ''); // 移除开头的小数点(避免 ".123" 情况)
-			filtered = filtered.replace(/\.{2,}/g, '.'); // 多个小数点只保留第一个
-			
-			// 分割整数和小数部分,限制小数最多两位
-			const parts = filtered.split('.');
-			if (parts.length > 1) {
-				// 小数部分只保留前两位
-				filtered = parts[0] + '.' + (parts[1].slice(0, 2) || '');
-			}
-			
-			// 处理特殊情况(如 "000" 转为 "0",但价格允许 "0.00")
-			// 若需要严格限制整数部分首位不为0(非必须,根据业务决定):
-			// if (parts.length === 1 && filtered.length > 1 && filtered[0] === '0') {
-			//   filtered = filtered.replace(/^0+/, '0');
-			// }
-			
-			// 更新绑定值
-			this.$nextTick(() => {
-				this.form[key] = !filtered ? filtered : filtered - 0;
-			});
-		},
-		// 图片处理
-		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(',');
-		},
-		onCoverSuccess(res, index, lists) {
-			this.fileList.cover = lists;
-			this.processImageList(lists, 'productImage');
-		},
-		onCoverRemove(index, lists) {
-			this.fileList.cover = lists;
-			this.processImageList(lists, 'productImage');
-		},
-		onDetailSuccess(res, index, lists) {
-			this.fileList.detail = lists;
-			this.processImageList(lists, 'detailImages');
-		},
-		onDetailRemove(index, lists) {
-			this.fileList.detail = lists;
-			this.processImageList(lists, 'detailImages');
-		},
-		// 下一步
-		goNextStep() {
-			// 表单校验
-			this.$refs.uForm.validate(valid => {
-				if (valid) {
-					console.log('Form Data:', this.form, this.fileList);
-					const { categoryName, ...form } = this.form;
-					uni.setStorageSync('gm_goods', form);
-					uni.navigateTo({
-						url: '/pages/groupMeal/goods/form-step2'
-					});
-				} else {
-					console.log('验证失败');
-				}
-			});
-		}
-	}
-};
-</script>
-
-<style lang="scss" scoped>
-$uni-text-color: #303133;
-$uni-color-primary: #2979ff;
-
-.page-container {
-	min-height: 100vh;
-	background-color: #f5f5f5;
-	.section-title {
-		font-weight: 500;
-		font-size: 30rpx;
-		color: #333333;
-	}
-}
-
-.form-card {
-	margin: 24rpx;
-	padding: 20rpx 24rpx;
-	background-color: #fff;
-	border-radius: 16rpx;
-
-	::v-deep .u-form-item {
-		padding: 16rpx 0;
-		line-height: 1;
-		.u-form-item--left__content {
-			flex: none;
-			.u-form-item--left__content--required {
-				color: #fa3534; 
-				left: auto;
-				right: 0;
-			}
-		}
-	}
-
-	.form-input {
-		pointer-events: none;
-	}
-	::v-deep .uni-input-input:disabled {
-		pointer-events: none;
-	}
-
-	.input-suffix {
-		color: #232832;
-		font-size: 26rpx;
-		margin-left: 10rpx;
-	}
-
-	.upload-content {
-		width: 100%;
-
-		.upload-tip {
-			font-weight: 400;
-			font-size: 20rpx;
-			color: #999999;
-			margin-bottom: 20rpx;
-		}
-
-		::v-deep .u-add-tips {
-			margin-top: 10rpx;
-		}
-	}
-}
-
-// 底部按钮
-.footer-bar {
-	::v-deep .u-btn {
-		width: 100%;
-		height: 76rpx;
-		font-weight: 400;
-		font-size: 28rpx;
-		color: #449AFF;
-		border-radius: 54rpx;
-		border: 1rpx solid #449AFF;
-		background: #FFFFFF;
-	}
-}
-
-// 分类选择弹窗样式
-.category-popup {
-	padding: 30rpx;
-	
-	.popup-title {
-		text-align: center;
-		font-size: 34rpx;
-		font-weight: bold;
-		color: $uni-text-color;
-		margin-bottom: 30rpx;
-	}
-	
-	.category-list {
-		max-height: 600rpx; // 限制高度,可滚动
-	}
-	
-	.category-item {
-		padding: 20rpx 0;
-		font-size: 30rpx;
-		color: $uni-text-color;
-		border-bottom: 1rpx solid #eee;
-		display: flex;
-		justify-content: space-between;
-		align-items: center;
-		
-		&:last-child {
-			border-bottom: none;
-		}
-		
-		&.selected {
-			color: $uni-color-primary;
-			font-weight: bold;
-		}
-	}
-	
-	.popup-footer {
-		padding-top: 30rpx;
-	}
-}
-</style>

+ 0 - 429
pages/groupMeal/goods/form-step2.vue

@@ -1,429 +0,0 @@
-<template>
-	<view class="page-container">
-		<u-navbar title="新建团餐" :autoBack="true" :placeholder="true"></u-navbar>
-		
-		<u-form :model="form" :rules="rules" ref="formRef" :error-type="['toast', 'border-bottom']">
-			<u-collapse ref="collapseRef"
-				:value="activeNames" 
-				@change="onCollapseChange">
-				<u-collapse-item
-					:class="{
-						active: activeNames == category.id
-					}"
-					v-for="(category, catIndex) in form.dishCategories"
-					:key="category.id"
-					:name="category.id"
-					@click="onClick"
-				>
-					<view slot="title" class="collapse-title">
-						<text>{{ category.name }}</text>
-						<text class="num" v-if="isEditedShow(category)">({{ category.items.length }})</text>
-						<u-switch 
-							v-if="false"
-							size="20"
-							activeColor="#3c9cff"
-							:value="category.isActive" 
-							@change="onSwitchChange($event, category.id)" 
-						></u-switch>
-					</view>
-					
-					<view class="collapse-content">
-						<view class="dish-item" v-for="(dish, dishIndex) in category.items" :key="dish.id">
-							<view class="dish-header">
-								<view class="dish-title">
-									<u-image width="32rpx" height="32rpx" src="/static/icon/icon_edit.png"></u-image>
-									<text>{{ category.name }}{{ dishIndex + 1 }}</text>
-								</view>
-								<view class="dish-actions">
-									<u-button v-if="dishIndex === category.items.length - 1" 
-										type="primary" size="mini"
-										hover-class="none"
-										:custom-style="{
-											color: '#2D88F4',
-											border: '1rpx solid #2D88F4',
-											background: 'rgba(45,136,244,0.1)',
-										}"		
-										@click="addDish(category.id)"
-									>
-										<view class="text">
-											<u-image width="32rpx" height="32rpx" src="/static/icon/icon_plus.png"></u-image>
-											<text>添加</text>
-										</view>
-									</u-button>
-									<u-button v-if="category.items.length > 1" 
-										type="error" size="mini"
-										hover-class="none"
-										:custom-style="{
-											color: '#FF6067',
-											border: '1rpx solid #FF6067',
-											background: 'rgba(255,96,103,0.1)',
-										}"	
-										@click="removeDish(category.id, dish.id)"
-									>
-										<view class="text">
-											<u-image width="32rpx" height="32rpx" src="/static/icon/icon_del.png"></u-image>
-											<text>删除</text>
-										</view>
-									</u-button>
-								</view>
-							</view>
-							<view class="dish-form">
-								<u-form-item label="名称" :prop="`dishCategories.${catIndex}.items.${dishIndex}.name`" :required="isRequired(dish, dishIndex)">
-									<u-input v-model="dish.name" height="41" placeholder="请输入" inputAlign="right"></u-input>
-								</u-form-item>
-								<u-form-item label="价格" :prop="`dishCategories.${catIndex}.items.${dishIndex}.price`" :required="isRequired(dish, dishIndex)">
-									<u-input v-model.number="dish.price" type="digit" height="41" placeholder="输入金额" :clearable="false" inputAlign="right"></u-input>
-									<text slot="right" style="color: #333333;">元</text>
-								</u-form-item>
-								<u-form-item label="描述" :prop="`dishCategories.${catIndex}.items.${dishIndex}.description`" :required="isRequired(dish, dishIndex)">
-									<u-input v-model="dish.description" height="41" placeholder="请描述菜品的使用素材" inputAlign="right"></u-input>
-								</u-form-item>
-							</view>
-						</view>
-					</view>
-				</u-collapse-item>
-			</u-collapse>
-		</u-form>
-
-		<view class="footer-bar">
-			<u-button 
-				:custom-style="{
-					width: '205rpx',
-					fontWeight: '400',
-					fontSize: '28rpx',
-					color: '#449AFF',
-					border: '1rpx solid #449AFF',
-					background: '#fff',
-					marginRight: '24rpx'
-				}"
-				@click="goBack"
-			>上一步</u-button>
-			<u-button
-				:custom-style="{
-					fontWeight: '400',
-					fontSize: '28rpx',
-					color: '#FFFFFF',
-					background: 'linear-gradient(90deg, #77B6FF 0%, #449AFF 100%)',
-				}"
-				@click="submit"
-			>提交审核</u-button>
-		</view>
-	</view>
-</template>
-
-<script>
-let dishIdCounter = 100; // 简单的唯一ID生成器
-export default {
-	data() {
-		return {
-			// 数据
-			form: {
-				dishCategories: [
-					{
-						id: 1,
-						name: '荤菜',
-						isActive: false,
-						items: [{ id: dishIdCounter++, name: '', price: '', description: '' }]
-					},
-					{
-						id: 2,
-						name: '素菜',
-						isActive: false,
-						items: [{ id: dishIdCounter++, name: '', price: '', description: '' }]
-					},
-					{
-						id: 3,
-						name: '主食',
-						isActive: false,
-						items: [{ id: dishIdCounter++, name: '', price: '', description: '' }]
-					},
-					{
-						id: 4,
-						name: '汤',
-						isActive: false,
-						items: [{ id: dishIdCounter++, name: '', price: '', description: '' }]
-					},
-					{
-						id: 5,
-						name: '其他',
-						isActive: false,
-						items: [{ id: dishIdCounter++, name: '', price: '', description: '' }]
-					}
-				],
-			},
-			rules: {},
-			activeNames: [1], // 控制折叠面板展开的name数组
-		};
-	},
-	watch: {
-		'form.dishCategories': {
-			handler() {
-				this.$nextTick(() => {
-					this.updateValidationRules();
-				});
-			},
-			deep: true
-		}
-	},
-	onLoad() {
-		const goods = uni.getStorageSync('gm_goods');
-		if (goods.gmMerchantPackageDetails && goods.gmMerchantPackageDetails.length > 0) {
-			this.form.dishCategories.map(a => {
-				let items = goods.gmMerchantPackageDetails.filter(b => b.category == a.id);
-				if (items.length > 0) a.items = items;
-			});
-		}
-	},
-	mounted() {
-		this.$nextTick(() => {
-			this.updateValidationRules();
-		});
-	},
-	methods: {
-		// 判断一个菜品是否被编辑过
-		isEdited(dish) {
-			return !!(dish.name || dish.price || dish.description);
-		},
-		// 首个菜品始终展示必填标识;填写任一项后该菜品全部字段必填
-		isRequired(dish, dishIndex) {
-			return dishIndex === 0 || this.isEdited(dish);
-		},
-		shouldValidate(dish) {
-			return this.isEdited(dish);
-		},
-		isComplete(dish) {
-			const name = (dish.name || '').trim();
-			const description = (dish.description || '').trim();
-			const price = dish.price;
-			return !!(name && description && (price === 0 || price));
-		},
-		isEditedShow(e) {
-			const goods = uni.getStorageSync('gm_goods');
-			return goods.gmMerchantPackageDetails && goods.gmMerchantPackageDetails.length > 0 && goods.gmMerchantPackageDetails.some(b => b.category == e.id);;
-		},
-		updateValidationRules() {
-			if (!this.$refs.formRef) return;
-			
-			const newRules = {};
-			this.form.dishCategories.forEach((category, catIndex) => {
-				category.items.forEach((dish, dishIndex) => {
-					if (this.shouldValidate(dish)) {
-						// 任一字段被填写后,该菜品全部字段必填
-						const nameProp = `dishCategories.${catIndex}.items.${dishIndex}.name`;
-						const priceProp = `dishCategories.${catIndex}.items.${dishIndex}.price`;
-						const descProp = `dishCategories.${catIndex}.items.${dishIndex}.description`;
-						
-						newRules[nameProp] = [{ required: true, message: '请输入名称', trigger: ['blur', 'change'] }];
-						newRules[priceProp] = [{ required: true, type: 'number', message: '请输入价格', trigger: ['blur', 'change'] }];
-						newRules[descProp] = [{ required: true, message: '请输入描述', trigger: ['blur', 'change'] }];
-					}
-				});
-			});
-			
-			this.rules = newRules;
-			this.$refs.formRef.setRules(this.rules);
-		},
-		// Switch开关状态变化
-		onSwitchChange(isActive, categoryId) {
-			const index = this.activeNames.indexOf(categoryId);
-			if (isActive) {
-				if (index === -1) {
-					this.activeNames.push(categoryId);
-				}
-			} else {
-				if (index > -1) {
-					this.activeNames.splice(index, 1);
-				}
-			}
-		},
-		// 折叠面板状态变化
-		onCollapseChange(names) {
-			console.log(names)
-			this.activeNames = names;
-		},
-		// 添加菜品
-		addDish(categoryId) {
-			const category = this.form.dishCategories.find(c => c.id === categoryId);
-			if (category) {
-				category.items.push({
-					id: dishIdCounter++,
-					name: '',
-					price: '',
-					description: ''
-				});
-			}
-		},
-		// 删除菜品
-		removeDish(categoryId, dishId) {
-			const category = this.form.dishCategories.find(c => c.id === categoryId);
-			if (category && category.items.length > 1) {
-				const index = category.items.findIndex(d => d.id === dishId);
-				if (index > -1) {
-					category.items.splice(index, 1);
-				}
-			}
-		},
-		// 上一步
-		goBack() {
-			uni.navigateBack({
-				delta: 1
-			});
-		},
-		goBackToList() {
-			setTimeout(() => {
-				// 返回两层(step2 + step1),回到进入表单前的列表页,避免页面栈残留 step1
-				uni.navigateBack({ delta: 2 });
-			}, 500);
-		},
-		buildDetails() {
-			const details = [];
-			this.form.dishCategories.forEach(category => {
-				if (!category.items || !Array.isArray(category.items)) return;
-				category.items.forEach((item) => {
-					if (this.isEdited(item)) {
-						details.push({
-							category: category.id,
-							name: item.name,
-							price: item.price,
-							description: item.description
-						});
-					}
-				});
-			});
-			return details;
-		},
-		// 提交审核
-		submit() {
-			this.updateValidationRules();
-			const hasCompleteDish = this.form.dishCategories.some(category =>
-				category.items.some(item => this.isComplete(item))
-			);
-			if (!hasCompleteDish) {
-				this.$tip.error('请至少添加一个完整的菜品信息');
-				return;
-			}
-			this.$refs.formRef.validate().then(res => {
-				const goods = uni.getStorageSync('gm_goods');
-				const details = this.buildDetails();
-				if (details.length === 0) {
-					this.$tip.error('请至少添加一个完整的菜品信息');
-					return;
-				}
-				let params = {
-					...goods,
-					gmMerchantPackageDetails: [...details]
-				};
-				console.log('表单校验成功, 提交的数据:', details, params);
-				this.$http.post('/groupmeal/gmMerchantPackage/submit', params).then(res => {
-					if (res.data.code === 200) {
-						uni.removeStorageSync('gm_goods');
-						uni.showToast({
-							title: res.data.message || '提交成功',
-							icon: 'none'
-						});
-						this.goBackToList();
-					} else {
-						this.$tip.error(res.data.message);
-					}
-				});
-			}).catch(errors => {
-				console.log('校验失败', errors);
-			});
-		}
-	}
-};
-</script>
-
-<style lang="scss" scoped>
-.page-container {
-	min-height: 100vh;
-	background-color: #f5f5f5;
-	padding-bottom: 160rpx;
-
-	::v-deep .u-collapse-item {
-		margin-bottom: 16rpx;
-		.u-collapse-head {
-			padding: 20rpx 32rpx;
-			background-color: #fff;
-		}
-		&.active {
-			.u-collapse-body {
-				height: auto !important;
-			}
-		}
-	}
-
-	.collapse-title {
-		font-weight: 500;
-		font-size: 32rpx;
-		color: #333333;
-		display: flex;
-		align-items: center;
-		justify-content: space-between;
-		.num {
-			padding: 0 10rpx;
-		}
-	}
-	.collapse-content {
-		.dish-item {
-			margin-top: 16rpx;
-			padding: 20rpx 32rpx;
-			background: #FFFFFF;
-			box-shadow: 0 2rpx 8rpx 0 rgba(214,225,237,0.1);
-			border: 1rpx solid #F0F3F5;
-			border-radius: 16rpx;
-			.dish-header {
-				margin-bottom: 20rpx;
-				display: flex;
-				align-items: center;
-				justify-content: space-between;
-				.dish-title {
-					font-size: 28rpx;
-					color: #333333;
-					display: flex;
-					align-items: center;
-				}
-				.dish-actions {
-					display: flex;
-					gap: 16rpx;
-					::v-deep .u-btn {
-						font-weight: 400;
-						font-size: 22rpx;
-					}
-					.text {
-						display: flex;
-						align-items: center;
-						text {
-							line-height: 1;
-							margin-left: 4rpx;
-						}
-					}
-				}
-			}
-			.dish-form {
-				padding: 6rpx 16rpx;
-				border-radius: 8rpx;
-				background: #F7F8FC;
-			}
-			::v-deep .u-form-item {
-				padding: 16rpx 0;
-				line-height: 1;
-				.u-form-item--left__content {
-					flex: none;
-				}
-			}
-		}
-	}
-}
-
-.footer-bar {
-	::v-deep .u-btn {
-		width: 100%;
-		height: 76rpx;
-		border-radius: 54rpx;
-		&::after {
-			display: none;
-		}
-	}
-}
-</style>

+ 667 - 0
pages/groupMeal/goods/form.vue

@@ -0,0 +1,667 @@
+<template>
+	<view class="page-container" :class="{ 'step2-page': currentStep === 2 }">
+		<u-navbar title="新建团餐" :autoBack="true" :placeholder="true"></u-navbar>
+
+		<!-- 第一步:基础信息 -->
+		<view v-show="currentStep === 1" class="form-card">
+			<view class="section-title">基础信息</view>
+			<u-form :model="baseForm" ref="baseFormRef" :rules="baseRules"
+				labelPosition="left" labelWidth="auto"
+				:error-type="['toast', 'border-bottom']"
+			>
+				<u-form-item prop="category" label="商品分类" required borderBottom @click="show = true">
+					<u-input v-model="baseForm.categoryName" class="form-input" disabled placeholder="去设置" height="41" inputAlign="right"></u-input>
+					<u-icon slot="right" name="arrow-right" color="#C7C6CA"></u-icon>
+				</u-form-item>
+
+				<u-form-item prop="name" label="商品名称" required borderBottom>
+					<u-input v-model="baseForm.name" placeholder="请输入商品名称" height="41" inputAlign="right"></u-input>
+				</u-form-item>
+
+				<u-form-item prop="productImage" label="商品封面图" required labelPosition="top">
+					<view class="upload-content">
+						<view class="upload-tip">支持jpg、png格式,建议尺寸为750px*750px,上传1-4张</view>
+						<u-upload
+							:action="uploadAction"
+							:file-list="fileList.cover"
+							:form-data="uploadFormData"
+							:max-count="4"
+							:multiple="true"
+							width="140"
+							height="140"
+							preview-full-image
+							@on-success="onCoverSuccess"
+							@on-remove="onCoverRemove"
+						/>
+					</view>
+				</u-form-item>
+
+				<u-form-item prop="detailImages" label="商品详情图" required borderBottom labelPosition="top">
+					<view class="upload-content">
+						<view class="upload-tip">支持jpg、png格式,最多20张</view>
+						<u-upload
+							:action="uploadAction"
+							:file-list="fileList.detail"
+							:form-data="uploadFormData"
+							:max-count="20"
+							:multiple="true"
+							width="140"
+							height="140"
+							preview-full-image
+							@on-success="onDetailSuccess"
+							@on-remove="onDetailRemove"
+						/>
+					</view>
+				</u-form-item>
+
+				<u-form-item prop="price" label="团餐价" required borderBottom>
+					<u-input type="digit" height="41" :clearable="false"
+						inputAlign="right"
+						placeholder="请输入团餐价"
+						v-model.number="baseForm.price"
+						@input="onInput($event, 'price')"
+					></u-input>
+					<text slot="right" class="input-suffix">元</text>
+				</u-form-item>
+
+				<u-form-item prop="sellingPrice" label="售价" required :borderBottom="true">
+					<u-input type="digit" height="41" :clearable="false"
+						inputAlign="right" placeholder="请输入商品售价"
+						v-model.number="baseForm.sellingPrice"
+						@input="onInput($event, 'sellingPrice')"
+					></u-input>
+					<text slot="right" class="input-suffix">元</text>
+				</u-form-item>
+			</u-form>
+		</view>
+
+		<!-- 第二步:菜品信息 -->
+		<u-form v-show="currentStep === 2" :model="dishForm" :rules="dishRules" ref="dishFormRef" :error-type="['toast', 'border-bottom']">
+			<u-collapse ref="collapseRef"
+				:value="activeNames"
+				@change="onCollapseChange">
+				<u-collapse-item
+					:class="{ active: activeNames == category.id }"
+					v-for="(category, catIndex) in dishForm.dishCategories"
+					:key="category.id"
+					:name="category.id"
+				>
+					<view slot="title" class="collapse-title">
+						<text>{{ category.name }}</text>
+						<text class="num" v-if="isEditedShow(category)">({{ category.items.length }})</text>
+					</view>
+
+					<view class="collapse-content">
+						<view class="dish-item" v-for="(dish, dishIndex) in category.items" :key="dish.id">
+							<view class="dish-header">
+								<view class="dish-title">
+									<u-image width="32rpx" height="32rpx" src="/static/icon/icon_edit.png"></u-image>
+									<text>{{ category.name }}{{ dishIndex + 1 }}</text>
+								</view>
+								<view class="dish-actions">
+									<u-button v-if="dishIndex === category.items.length - 1"
+										type="primary" size="mini"
+										hover-class="none"
+										:custom-style="{
+											color: '#2D88F4',
+											border: '1rpx solid #2D88F4',
+											background: 'rgba(45,136,244,0.1)',
+										}"
+										@click="addDish(category.id)"
+									>
+										<view class="text">
+											<u-image width="32rpx" height="32rpx" src="/static/icon/icon_plus.png"></u-image>
+											<text>添加</text>
+										</view>
+									</u-button>
+									<u-button v-if="category.items.length > 1"
+										type="error" size="mini"
+										hover-class="none"
+										:custom-style="{
+											color: '#FF6067',
+											border: '1rpx solid #FF6067',
+											background: 'rgba(255,96,103,0.1)',
+										}"
+										@click="removeDish(category.id, dish.id)"
+									>
+										<view class="text">
+											<u-image width="32rpx" height="32rpx" src="/static/icon/icon_del.png"></u-image>
+											<text>删除</text>
+										</view>
+									</u-button>
+								</view>
+							</view>
+							<view class="dish-form">
+								<u-form-item label="名称" :prop="`dishCategories.${catIndex}.items.${dishIndex}.name`" :required="isRequired(dish, dishIndex)">
+									<u-input v-model="dish.name" height="41" placeholder="请输入" inputAlign="right"></u-input>
+								</u-form-item>
+								<u-form-item label="价格" :prop="`dishCategories.${catIndex}.items.${dishIndex}.price`" :required="isRequired(dish, dishIndex)">
+									<u-input v-model.number="dish.price" type="digit" height="41" placeholder="输入金额" :clearable="false" inputAlign="right"></u-input>
+									<text slot="right" style="color: #333333;">元</text>
+								</u-form-item>
+								<u-form-item label="描述" :prop="`dishCategories.${catIndex}.items.${dishIndex}.description`" :required="isRequired(dish, dishIndex)">
+									<u-input v-model="dish.description" height="41" placeholder="请描述菜品的使用素材" inputAlign="right"></u-input>
+								</u-form-item>
+							</view>
+						</view>
+					</view>
+				</u-collapse-item>
+			</u-collapse>
+		</u-form>
+
+		<view class="footer-bar" :class="{ 'footer-bar--dual': currentStep === 2 }">
+			<u-button v-if="currentStep === 2"
+				:custom-style="{
+					width: '205rpx',
+					fontWeight: '400',
+					fontSize: '28rpx',
+					color: '#449AFF',
+					border: '1rpx solid #449AFF',
+					background: '#fff',
+					marginRight: '24rpx'
+				}"
+				@click="goPrevStep"
+			>上一步</u-button>
+			<u-button v-if="currentStep === 1" type="primary" @click="goNextStep">下一步</u-button>
+			<u-button v-if="currentStep === 2"
+				:custom-style="{
+					fontWeight: '400',
+					fontSize: '28rpx',
+					color: '#FFFFFF',
+					background: 'linear-gradient(90deg, #77B6FF 0%, #449AFF 100%)',
+				}"
+				@click="submit"
+			>提交审核</u-button>
+		</view>
+
+		<u-action-sheet :list="actionSheetList" v-model="show" @click="actionSheetCallback"></u-action-sheet>
+	</view>
+</template>
+
+<script>
+import configService from '@/common/service/config.service';
+
+let dishIdCounter = 100;
+
+export default {
+	data() {
+		return {
+			currentStep: 1,
+			show: false,
+			uploadAction: configService.apiUrl + '/sys/common/upload',
+			imageHttp: configService.apiUrl + '/',
+			uploadFormData: { biz: 'temp' },
+			baseForm: {
+				name: '',
+				category: '',
+				categoryName: '',
+				productImage: '',
+				detailImages: '',
+				price: '',
+				sellingPrice: '',
+			},
+			dishForm: {
+				dishCategories: [
+					{ id: 1, name: '荤菜', isActive: false, items: [{ id: dishIdCounter++, name: '', price: '', description: '' }] },
+					{ id: 2, name: '素菜', isActive: false, items: [{ id: dishIdCounter++, name: '', price: '', description: '' }] },
+					{ id: 3, name: '主食', isActive: false, items: [{ id: dishIdCounter++, name: '', price: '', description: '' }] },
+					{ id: 4, name: '汤', isActive: false, items: [{ id: dishIdCounter++, name: '', price: '', description: '' }] },
+					{ id: 5, name: '其他', isActive: false, items: [{ id: dishIdCounter++, name: '', price: '', description: '' }] },
+				],
+			},
+			query: {},
+			fileList: {
+				cover: [],
+				detail: [],
+			},
+			actionSheetList: [],
+			activeNames: [1],
+			dishRules: {},
+			baseRules: {
+				name: [{ required: true, message: '请输入名称', trigger: ['blur', 'change'] }],
+				category: [{ required: true, message: '请选择分类', trigger: ['blur', 'change'] }],
+				price: [
+					{ required: true, type: 'number', message: '请输入团餐价格', trigger: ['blur', 'change'] },
+					{
+						asyncValidator: (rule, value, callback) => {
+							if (this.baseForm.sellingPrice && value > this.baseForm.sellingPrice) {
+								callback(new Error('团餐价格不能大于商品售价'));
+							} else {
+								callback();
+							}
+						},
+						trigger: ['blur', 'change']
+					},
+				],
+				sellingPrice: [
+					{ required: true, type: 'number', message: '请输入商品售价', trigger: ['blur', 'change'] },
+					{
+						asyncValidator: (rule, value, callback) => {
+							if (this.baseForm.price && value < this.baseForm.price) {
+								callback(new Error('商品售价不能小于团餐价格'));
+							} else {
+								callback();
+							}
+						},
+						trigger: ['blur', 'change']
+					},
+				],
+				productImage: [{ required: true, message: '请上传商品封面图', trigger: ['blur', 'change'] }],
+				detailImages: [{ required: true, message: '请上传商品详情图片', trigger: ['blur', 'change'] }],
+			},
+		};
+	},
+	watch: {
+		'dishForm.dishCategories': {
+			handler() {
+				if (this.currentStep === 2) {
+					this.$nextTick(() => {
+						this.updateValidationRules();
+					});
+				}
+			},
+			deep: true
+		}
+	},
+	onLoad(query) {
+		if (query.id) {
+			this.query = query;
+			this.getData();
+		}
+		this.getClassData();
+	},
+	onReady() {
+		this.$refs.baseFormRef.setRules(this.baseRules);
+	},
+	methods: {
+		getData() {
+			this.$http.get('/groupmeal/gmCustomerPackage/queryById', { params: { id: this.query.id } }).then(res => {
+				if (res.data.code === 200) {
+					const data = res.data.result;
+					this.baseForm = {
+						...data,
+						categoryName: ''
+					};
+					this.fileList = {
+						cover: data.productImage.split(',').map(item => ({ url: item })),
+						detail: data.detailImages.split(',').map(item => ({ url: item })),
+					};
+					if (data.gmMerchantPackageDetails && data.gmMerchantPackageDetails.length > 0) {
+						this.dishForm.dishCategories.forEach(category => {
+							const items = data.gmMerchantPackageDetails.filter(b => b.category == category.id);
+							if (items.length > 0) {
+								category.items = items;
+							}
+						});
+					}
+					this.$nextTick(() => {
+						const matched = this.actionSheetList.find(item => item.value == data.category);
+						if (matched) {
+							this.baseForm.categoryName = matched.text;
+						}
+					});
+				}
+			});
+		},
+		getClassData() {
+			this.$http.get('/groupmeal/gmMerchantPackage/getGroupMealCategory', {}).then(res => {
+				if (res.data.code === 200) {
+					this.actionSheetList = res.data.result.map(item => ({
+						text: item.name,
+						value: item.code
+					}));
+					if (this.baseForm.category) {
+						const matched = this.actionSheetList.find(item => item.value == this.baseForm.category);
+						if (matched) {
+							this.baseForm.categoryName = matched.text;
+						}
+					}
+				}
+			});
+		},
+		actionSheetCallback(e) {
+			this.baseForm.category = e.value;
+			this.baseForm.categoryName = e.text;
+		},
+		onInput(value, key) {
+			let filtered = value.replace(/[^\d.]/g, '');
+			filtered = filtered.replace(/^\./g, '');
+			filtered = filtered.replace(/\.{2,}/g, '.');
+			const parts = filtered.split('.');
+			if (parts.length > 1) {
+				filtered = parts[0] + '.' + (parts[1].slice(0, 2) || '');
+			}
+			this.$nextTick(() => {
+				this.baseForm[key] = !filtered ? filtered : filtered - 0;
+			});
+		},
+		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.baseForm[formKey] = urls.join(',');
+		},
+		onCoverSuccess(res, index, lists) {
+			this.fileList.cover = lists;
+			this.processImageList(lists, 'productImage');
+		},
+		onCoverRemove(index, lists) {
+			this.fileList.cover = lists;
+			this.processImageList(lists, 'productImage');
+		},
+		onDetailSuccess(res, index, lists) {
+			this.fileList.detail = lists;
+			this.processImageList(lists, 'detailImages');
+		},
+		onDetailRemove(index, lists) {
+			this.fileList.detail = lists;
+			this.processImageList(lists, 'detailImages');
+		},
+		goNextStep() {
+			this.$refs.baseFormRef.validate(valid => {
+				if (valid) {
+					this.currentStep = 2;
+					this.$nextTick(() => {
+						this.updateValidationRules();
+						uni.pageScrollTo({ scrollTop: 0, duration: 0 });
+					});
+				}
+			});
+		},
+		goPrevStep() {
+			this.currentStep = 1;
+			this.$nextTick(() => {
+				uni.pageScrollTo({ scrollTop: 0, duration: 0 });
+			});
+		},
+		isEdited(dish) {
+			return !!(dish.name || dish.price || dish.description);
+		},
+		isRequired(dish, dishIndex) {
+			return dishIndex === 0 || this.isEdited(dish);
+		},
+		shouldValidate(dish) {
+			return this.isEdited(dish);
+		},
+		isComplete(dish) {
+			const name = (dish.name || '').trim();
+			const description = (dish.description || '').trim();
+			const price = dish.price;
+			return !!(name && description && (price === 0 || price));
+		},
+		isEditedShow(category) {
+			if (!this.query.id) return false;
+			return category.items.some(item => this.isEdited(item));
+		},
+		updateValidationRules() {
+			if (!this.$refs.dishFormRef) return;
+
+			const newRules = {};
+			this.dishForm.dishCategories.forEach((category, catIndex) => {
+				category.items.forEach((dish, dishIndex) => {
+					if (this.shouldValidate(dish)) {
+						const nameProp = `dishCategories.${catIndex}.items.${dishIndex}.name`;
+						const priceProp = `dishCategories.${catIndex}.items.${dishIndex}.price`;
+						const descProp = `dishCategories.${catIndex}.items.${dishIndex}.description`;
+
+						newRules[nameProp] = [{ required: true, message: '请输入名称', trigger: ['blur', 'change'] }];
+						newRules[priceProp] = [{ required: true, type: 'number', message: '请输入价格', trigger: ['blur', 'change'] }];
+						newRules[descProp] = [{ required: true, message: '请输入描述', trigger: ['blur', 'change'] }];
+					}
+				});
+			});
+
+			this.dishRules = newRules;
+			this.$refs.dishFormRef.setRules(this.dishRules);
+		},
+		onCollapseChange(names) {
+			this.activeNames = names;
+		},
+		addDish(categoryId) {
+			const category = this.dishForm.dishCategories.find(c => c.id === categoryId);
+			if (category) {
+				category.items.push({
+					id: dishIdCounter++,
+					name: '',
+					price: '',
+					description: ''
+				});
+			}
+		},
+		removeDish(categoryId, dishId) {
+			const category = this.dishForm.dishCategories.find(c => c.id === categoryId);
+			if (category && category.items.length > 1) {
+				const index = category.items.findIndex(d => d.id === dishId);
+				if (index > -1) {
+					category.items.splice(index, 1);
+				}
+			}
+		},
+		buildDetails() {
+			const details = [];
+			this.dishForm.dishCategories.forEach(category => {
+				if (!category.items || !Array.isArray(category.items)) return;
+				category.items.forEach((item) => {
+					if (this.isEdited(item)) {
+						details.push({
+							category: category.id,
+							name: item.name,
+							price: item.price,
+							description: item.description
+						});
+					}
+				});
+			});
+			return details;
+		},
+		submit() {
+			this.updateValidationRules();
+			const hasCompleteDish = this.dishForm.dishCategories.some(category =>
+				category.items.some(item => this.isComplete(item))
+			);
+			if (!hasCompleteDish) {
+				this.$tip.error('请至少添加一个完整的菜品信息');
+				return;
+			}
+			this.$refs.dishFormRef.validate().then(() => {
+				const { categoryName, ...baseData } = this.baseForm;
+				const details = this.buildDetails();
+				if (details.length === 0) {
+					this.$tip.error('请至少添加一个完整的菜品信息');
+					return;
+				}
+				const params = {
+					...baseData,
+					gmMerchantPackageDetails: [...details]
+				};
+				this.$http.post('/groupmeal/gmMerchantPackage/submit', params).then(res => {
+					if (res.data.code === 200) {
+						uni.showToast({
+							title: res.data.message || '提交成功',
+							icon: 'none'
+						});
+						setTimeout(() => {
+							uni.navigateBack({ delta: 1 });
+						}, 500);
+					} else {
+						this.$tip.error(res.data.message);
+					}
+				});
+			}).catch(errors => {
+				console.log('校验失败', errors);
+			});
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+$uni-text-color: #303133;
+$uni-color-primary: #2979ff;
+
+.page-container {
+	min-height: 100vh;
+	background-color: #f5f5f5;
+
+	&.step2-page {
+		padding-bottom: 160rpx;
+	}
+
+	.section-title {
+		font-weight: 500;
+		font-size: 30rpx;
+		color: #333333;
+	}
+
+	::v-deep .u-collapse-item {
+		margin-bottom: 16rpx;
+		.u-collapse-head {
+			padding: 20rpx 32rpx;
+			background-color: #fff;
+		}
+		&.active {
+			.u-collapse-body {
+				height: auto !important;
+			}
+		}
+	}
+
+	.collapse-title {
+		font-weight: 500;
+		font-size: 32rpx;
+		color: #333333;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		.num {
+			padding: 0 10rpx;
+		}
+	}
+
+	.collapse-content {
+		.dish-item {
+			margin-top: 16rpx;
+			padding: 20rpx 32rpx;
+			background: #FFFFFF;
+			box-shadow: 0 2rpx 8rpx 0 rgba(214,225,237,0.1);
+			border: 1rpx solid #F0F3F5;
+			border-radius: 16rpx;
+			.dish-header {
+				margin-bottom: 20rpx;
+				display: flex;
+				align-items: center;
+				justify-content: space-between;
+				.dish-title {
+					font-size: 28rpx;
+					color: #333333;
+					display: flex;
+					align-items: center;
+				}
+				.dish-actions {
+					display: flex;
+					gap: 16rpx;
+					::v-deep .u-btn {
+						font-weight: 400;
+						font-size: 22rpx;
+					}
+					.text {
+						display: flex;
+						align-items: center;
+						text {
+							line-height: 1;
+							margin-left: 4rpx;
+						}
+					}
+				}
+			}
+			.dish-form {
+				padding: 6rpx 16rpx;
+				border-radius: 8rpx;
+				background: #F7F8FC;
+			}
+			::v-deep .u-form-item {
+				padding: 16rpx 0;
+				line-height: 1;
+				.u-form-item--left__content {
+					flex: none;
+				}
+			}
+		}
+	}
+}
+
+.form-card {
+	margin: 24rpx;
+	padding: 20rpx 24rpx;
+	background-color: #fff;
+	border-radius: 16rpx;
+
+	::v-deep .u-form-item {
+		padding: 16rpx 0;
+		line-height: 1;
+		.u-form-item--left__content {
+			flex: none;
+			.u-form-item--left__content--required {
+				color: #fa3534;
+				left: auto;
+				right: 0;
+			}
+		}
+	}
+
+	.form-input {
+		pointer-events: none;
+	}
+	::v-deep .uni-input-input:disabled {
+		pointer-events: none;
+	}
+
+	.input-suffix {
+		color: #232832;
+		font-size: 26rpx;
+		margin-left: 10rpx;
+	}
+
+	.upload-content {
+		width: 100%;
+
+		.upload-tip {
+			font-weight: 400;
+			font-size: 20rpx;
+			color: #999999;
+			margin-bottom: 20rpx;
+		}
+
+		::v-deep .u-add-tips {
+			margin-top: 10rpx;
+		}
+	}
+}
+
+.footer-bar {
+	::v-deep .u-btn {
+		width: 100%;
+		height: 76rpx;
+		font-weight: 400;
+		font-size: 28rpx;
+		color: #449AFF;
+		border-radius: 54rpx;
+		border: 1rpx solid #449AFF;
+		background: #FFFFFF;
+		&::after {
+			display: none;
+		}
+	}
+
+	&--dual {
+		display: flex;
+		align-items: center;
+	}
+}
+</style>

+ 2 - 2
pages/groupMeal/goods/list.vue

@@ -328,14 +328,14 @@ export default {
 		addGoods() {
 			this.isManaging = false;
 			uni.navigateTo({
-				url: '/pages/groupMeal/goods/form-step1'
+				url: '/pages/groupMeal/goods/form'
 			});
 		},
 		// 编辑商品
 		editGoods(goods) {
 			this.isManaging = false;
 			uni.navigateTo({
-				url: '/pages/groupMeal/goods/form-step1?id=' + goods.id
+				url: '/pages/groupMeal/goods/form?id=' + goods.id
 			});
 		},
 		// 单选