浏览代码

景聚庭-点菜订单页面调整

huangguoce 14 小时之前
父节点
当前提交
086fae0b0a

+ 655 - 0
src/views/psiManagement/dishManage/order/DishOrderAdd.vue

@@ -0,0 +1,655 @@
+<template>
+	<div class="page dish-order-add-page">
+		<div class="order-header">
+			<div>
+				<div class="order-room-name">{{ currentRoom.roomName || '-' }}</div>
+				<div class="order-room-sub">包房号:{{ currentRoom.roomNo || '-' }}</div>
+			</div>
+			<el-button @click="backOrderDetail()" type="primary" icon="el-icon-back">返回订单</el-button>
+		</div>
+
+		<div class="order-main">
+			<div class="dish-panel">
+				<div class="dish-search">
+					<el-input v-model="dishSearchName" placeholder="搜索菜品名称" clearable @keyup.enter.native="searchDish()"
+						@clear="searchDish()">
+						<template #append>
+							<el-button icon="el-icon-search" @click="searchDish()"></el-button>
+						</template>
+					</el-input>
+				</div>
+
+				<div class="dish-content">
+					<div class="category-list">
+						<div v-for="item in categoryList" :key="item.id" class="category-item"
+							:class="{ 'category-active': activeTypeId === item.id }" @click="changeType(item)">
+							{{ item.name }}
+						</div>
+					</div>
+
+					<div class="dish-list" v-loading="dishLoading">
+						<div v-if="dishList.length === 0" class="dish-empty">暂无菜品</div>
+						<div v-else class="dish-grid">
+							<div v-for="item in dishList" :key="item.id" class="dish-card">
+								<el-image v-if="item.previewImageUrl" class="dish-image" :src="item.previewImageUrl"
+									:preview-src-list="item.previewImageList" fit="cover" hide-on-click-modal
+									append-to-body :z-index="9999"></el-image>
+								<div v-else class="dish-image dish-image-empty">暂无图片</div>
+								<div class="dish-info">
+									<div class="dish-name">{{ item.dishName }}</div>
+									<div class="dish-desc">{{ item.spec || item.taste || item.remarks || '暂无说明' }}</div>
+									<div class="dish-bottom">
+										<span class="dish-price">¥{{ formatMoney(item.salePrice) }}</span>
+										<el-button type="primary" size="small" plain @click="addDish(item)">点菜</el-button>
+									</div>
+								</div>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+
+			<div class="ordered-panel" v-loading="orderLoading">
+				<div class="ordered-title">本次加菜</div>
+				<div v-if="orderedList.length === 0" class="ordered-empty">
+					<div class="ordered-empty-title">暂未加菜</div>
+					<div class="ordered-empty-sub">从左侧选择菜品后会显示在这里</div>
+				</div>
+				<div v-else class="ordered-list">
+					<div class="ordered-head">
+						<span style="text-align: center;">菜品</span>
+						<span style="text-align: center;">单价</span>
+						<span style="text-align: center;">总价</span>
+						<span style="text-align: center;">数量</span>
+					</div>
+					<div>
+						<div v-for="item in orderedList" :key="item.dishId" class="ordered-item">
+							<div class="ordered-dish">
+								<div class="ordered-dish-name">{{ item.dishName }}</div>
+							</div>
+							<div class="ordered-price">¥{{ formatMoney(item.salePrice) }}</div>
+							<div class="ordered-total">¥{{ formatMoney(itemTotal(item)) }}</div>
+							<div class="ordered-qty">
+								<el-button icon="el-icon-minus" size="mini" circle @click="reduceDish(item)"></el-button>
+								<span>{{ item.quantity }}</span>
+								<el-button icon="el-icon-plus" size="mini" circle @click="increaseOrderedDish(item)"></el-button>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div class="ordered-footer">
+					<div class="ordered-summary">
+						<span>共 {{ orderedList.length }} 项,本次加菜金额</span>
+						<strong>¥{{ formatMoney(orderTotal) }}</strong>
+					</div>
+					<el-button class="submit-button" type="primary" :loading="submitting" @click="confirmSubmitOrder()">下单</el-button>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+import DishRoomService from '@/api/psi/DishRoomService'
+import DishOrderService from '@/api/psi/DishOrderService'
+import OSSSerivce from '@/api/sys/OSSService'
+
+export default {
+	data() {
+		return {
+			currentRoom: {},
+			currentOrder: null,
+			categoryList: [],
+			activeTypeId: 'all',
+			dishSearchName: '',
+			dishList: [],
+			orderedList: [],
+			existDishSortMap: {},
+			maxDishSort: 0,
+			dishLoading: false,
+			orderLoading: false,
+			submitting: false
+		}
+	},
+	computed: {
+		orderTotal() {
+			return this.orderedList.reduce((total, item) => total + this.itemTotal(item), 0)
+		},
+		submitDetailList() {
+			let appendIndex = 0
+			return this.orderedList.map((item) => {
+				let dishSort = this.existDishSortMap[item.dishId]
+				if (!dishSort) {
+					appendIndex += 1
+					dishSort = this.maxDishSort + appendIndex
+				}
+				return {
+					dishId: item.dishId,
+					dishName: item.dishName,
+					quantity: item.quantity,
+					dishSort: dishSort
+				}
+			})
+		}
+	},
+	dishRoomService: null,
+	dishOrderService: null,
+	ossService: null,
+	created() {
+		this.dishRoomService = new DishRoomService()
+		this.dishOrderService = new DishOrderService()
+		this.ossService = new OSSSerivce()
+	},
+	mounted() {
+		this.initPage()
+	},
+	activated() {
+		this.initPage()
+	},
+	methods: {
+		initPage() {
+			const roomId = this.$route.query.roomId
+			if (!roomId) {
+				this.backRoomList()
+				return
+			}
+			this.orderedList = []
+			this.activeTypeId = 'all'
+			this.dishSearchName = ''
+			this.loadRoom(roomId)
+			this.loadCurrentOrder(roomId)
+			this.loadCategoryList()
+			this.loadDishList()
+		},
+		loadRoom(roomId) {
+			this.dishRoomService.findById(roomId).then((data) => {
+				this.currentRoom = data || {}
+			})
+		},
+		loadCurrentOrder(roomId) {
+			this.orderLoading = true
+			this.dishOrderService.currentOrder(roomId).then((data) => {
+				this.currentOrder = data || null
+				if (!this.currentOrder || !this.currentOrder.id) {
+					this.$message.warning('当前包房暂无未结账订单,请先下单')
+					this.backOrderDetail()
+					return
+				}
+				this.buildDishSortMap(this.currentOrder.detailList || [])
+			}).finally(() => {
+				this.orderLoading = false
+			})
+		},
+		buildDishSortMap(detailList) {
+			this.existDishSortMap = {}
+			this.maxDishSort = 0
+			detailList.forEach((item) => {
+				const sort = Number(item.dishSort || 0)
+				if (item.dishId && sort > 0) {
+					this.existDishSortMap[item.dishId] = sort
+				}
+				if (sort > this.maxDishSort) {
+					this.maxDishSort = sort
+				}
+			})
+		},
+		backRoomList() {
+			this.$router.push({
+				path: '/psiManagement/dishManage/order/DishOrder'
+			})
+		},
+		backOrderDetail() {
+			this.$router.push({
+				path: '/psiManagement/dishManage/order/DishOrderDetail',
+				query: {
+					roomId: this.$route.query.roomId,
+					refresh: Date.now()
+				}
+			})
+		},
+		loadCategoryList() {
+			this.dishOrderService.typeList().then((data) => {
+				this.categoryList = [
+					{ id: 'all', name: '所有菜品' },
+					...data.map(item => ({ id: item.id, name: item.name }))
+				]
+			})
+		},
+		changeType(item) {
+			this.activeTypeId = item.id
+			this.dishSearchName = ''
+			this.loadDishList()
+		},
+		searchDish() {
+			this.activeTypeId = 'all'
+			this.loadDishList()
+		},
+		loadDishList() {
+			this.dishLoading = true
+			this.dishOrderService.dishList({
+				typeId: this.activeTypeId === 'all' ? '' : this.activeTypeId,
+				dishName: this.dishSearchName
+			}).then((data) => {
+				this.dishList = data
+				this.loadDishImagePreview()
+			}).finally(() => {
+				this.dishLoading = false
+			})
+		},
+		addDish(dish) {
+			const row = this.orderedList.find(item => item.dishId === dish.id)
+			if (row) {
+				row.quantity += 1
+				return
+			}
+			this.orderedList.push(this.toOrderedItem(dish, 1))
+		},
+		increaseOrderedDish(item) {
+			item.quantity += 1
+		},
+		reduceDish(item) {
+			if (item.quantity <= 1) {
+				this.orderedList = this.orderedList.filter(row => row.dishId !== item.dishId)
+				return
+			}
+			item.quantity -= 1
+		},
+		confirmSubmitOrder() {
+			if (this.orderedList.length === 0) {
+				this.$message.warning('请先选择菜品')
+				return
+			}
+			this.$confirm('是否确认下单?', '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.submitOrder()
+			})
+		},
+		submitOrder() {
+			this.submitting = true
+			this.dishOrderService.submit({
+				id: this.currentOrder ? this.currentOrder.id : this.$route.query.orderId,
+				roomId: this.$route.query.roomId,
+				settleFlag: '0',
+				detailList: this.submitDetailList
+			}).then((data) => {
+				this.$message.success(data)
+				this.backOrderDetail()
+			}).finally(() => {
+				this.submitting = false
+			})
+		},
+		toOrderedItem(item, quantity) {
+			return {
+				dishId: item.dishId || item.id,
+				dishName: item.dishName,
+				salePrice: Number(item.salePrice || 0),
+				quantity: quantity
+			}
+		},
+		itemTotal(item) {
+			return Number(item.salePrice || 0) * Number(item.quantity || 0)
+		},
+		formatMoney(value) {
+			return Number(value || 0).toFixed(2)
+		},
+		loadDishImagePreview() {
+			this.dishList.forEach((row) => {
+				const urls = this.getImageUrls(row.imageUrl)
+				if (urls.length === 0) {
+					row.previewImageUrl = ''
+					row.previewImageList = []
+					return
+				}
+				row.previewImageList = urls
+				row.previewImageUrl = urls[0]
+				Promise.all(urls.map(url => this.getPreviewUrl(url))).then((list) => {
+					row.previewImageList = list
+					row.previewImageUrl = list[0]
+				})
+			})
+		},
+		getImageUrls(value) {
+			if (!value) {
+				return []
+			}
+			return value.split(',').map(item => item.trim()).filter(item => item)
+		},
+		getPreviewUrl(url) {
+			if (url.indexOf('http') === 0) {
+				return Promise.resolve(url)
+			}
+			return this.ossService.getTemporaryUrl(url).catch(() => url)
+		}
+	}
+}
+</script>
+
+<style scoped>
+.dish-order-add-page {
+	display: flex;
+	flex-direction: column;
+}
+
+.order-header {
+	height: 64px;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	border-bottom: 1px solid #ebeef5;
+	padding: 0 4px 12px;
+}
+
+.order-room-name {
+	font-size: 20px;
+	font-weight: 600;
+	color: #303133;
+	line-height: 28px;
+}
+
+.order-room-sub {
+	font-size: 13px;
+	color: #909399;
+	line-height: 20px;
+}
+
+.order-main {
+	flex: 1;
+	min-height: 0;
+	display: grid;
+	grid-template-columns: minmax(0, 1fr) 480px;
+	gap: 16px;
+	padding-top: 16px;
+}
+
+.dish-panel,
+.ordered-panel {
+	min-height: 0;
+	border: 1px solid #ebeef5;
+	border-radius: 8px;
+	background: #fff;
+	display: flex;
+	flex-direction: column;
+}
+
+.dish-search {
+	padding: 14px;
+	border-bottom: 1px solid #ebeef5;
+}
+
+.dish-content {
+	flex: 1;
+	min-height: 0;
+	display: grid;
+	grid-template-columns: 150px minmax(0, 1fr);
+}
+
+.category-list {
+	background: #f7f9fb;
+	border-right: 1px solid #ebeef5;
+	overflow: auto;
+}
+
+.category-item {
+	min-height: 46px;
+	display: flex;
+	align-items: center;
+	padding: 0 14px;
+	cursor: pointer;
+	color: #606266;
+	border-left: 3px solid transparent;
+}
+
+.category-item:hover {
+	color: #409eff;
+	background: #eef5ff;
+}
+
+.category-active {
+	color: #409eff;
+	background: #fff;
+	border-left-color: #409eff;
+	font-weight: 600;
+}
+
+.dish-list {
+	min-height: 0;
+	overflow: auto;
+	padding: 14px;
+}
+
+.dish-grid {
+	display: grid;
+	grid-template-columns: repeat(4, minmax(0, 1fr));
+	gap: 14px;
+}
+
+.dish-card {
+	min-width: 0;
+	border: 1px solid #ebeef5;
+	border-radius: 8px;
+	overflow: hidden;
+	background: #fff;
+}
+
+.dish-image {
+	width: 100%;
+	aspect-ratio: 4 / 3;
+	background: #f2f3f5;
+}
+
+.dish-image-empty {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	color: #909399;
+	font-size: 13px;
+}
+
+.dish-info {
+	padding: 10px;
+}
+
+.dish-name {
+	font-size: 15px;
+	font-weight: 600;
+	color: #303133;
+	line-height: 22px;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+
+.dish-desc {
+	height: 24px;
+	margin-top: 6px;
+	color: #606266;
+	font-size: 13px;
+	line-height: 18px;
+	overflow: hidden;
+	display: -webkit-box;
+	-webkit-line-clamp: 2;
+	-webkit-box-orient: vertical;
+}
+
+.dish-bottom {
+	margin-top: 10px;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	gap: 8px;
+}
+
+.dish-price {
+	color: #f56c6c;
+	font-size: 18px;
+	font-weight: 600;
+	line-height: 26px;
+}
+
+.dish-empty {
+	height: 100%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	color: #909399;
+}
+
+.ordered-title {
+	height: 54px;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 0 16px;
+	font-size: 16px;
+	font-weight: 600;
+	border-bottom: 1px solid #ebeef5;
+}
+
+.ordered-empty {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	color: #909399;
+	text-align: center;
+	padding: 24px;
+}
+
+.ordered-empty-title {
+	font-size: 16px;
+	color: #606266;
+	line-height: 24px;
+}
+
+.ordered-empty-sub {
+	margin-top: 6px;
+	font-size: 13px;
+	line-height: 20px;
+}
+
+.ordered-list {
+	flex: 1;
+	min-height: 0;
+	overflow: auto;
+	padding: 12px;
+}
+
+.ordered-head,
+.ordered-item {
+	display: grid;
+	grid-template-columns: minmax(0, 1.35fr) 72px 86px 104px;
+	gap: 8px;
+	align-items: center;
+}
+
+.ordered-head {
+	height: 32px;
+	padding: 0 8px;
+	font-size: 12px;
+	color: #909399;
+	background: #f7f9fb;
+	border-radius: 6px;
+}
+
+.ordered-item {
+	min-height: 64px;
+	padding: 10px 8px;
+	border-bottom: 1px solid #ebeef5;
+}
+
+.ordered-dish {
+	min-width: 0;
+}
+
+.ordered-dish-name {
+	font-size: 14px;
+	font-weight: 600;
+	color: #303133;
+	line-height: 20px;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+
+.ordered-price,
+.ordered-total {
+	font-size: 13px;
+	color: #606266;
+}
+
+.ordered-total {
+	color: #f56c6c;
+	font-weight: 600;
+}
+
+.ordered-qty {
+	display: flex;
+	align-items: center;
+	justify-content: flex-end;
+	gap: 8px;
+}
+
+.ordered-qty span {
+	min-width: 22px;
+	text-align: center;
+	font-weight: 600;
+	color: #303133;
+}
+
+.ordered-footer {
+	border-top: 1px solid #ebeef5;
+	padding: 12px;
+}
+
+.ordered-summary {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	margin-bottom: 12px;
+	font-size: 14px;
+	color: #606266;
+}
+
+.ordered-summary strong {
+	font-size: 22px;
+	color: #f56c6c;
+}
+
+.submit-button {
+	width: 100%;
+}
+
+@media (max-width: 1280px) {
+	.order-main {
+		grid-template-columns: minmax(0, 1fr) 380px;
+	}
+
+	.dish-grid {
+		grid-template-columns: repeat(3, minmax(0, 1fr));
+	}
+}
+
+@media (max-width: 960px) {
+	.order-main {
+		grid-template-columns: 1fr;
+	}
+
+	.dish-grid {
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+	}
+
+	.dish-content {
+		grid-template-columns: 120px minmax(0, 1fr);
+	}
+}
+
+@media (max-width: 640px) {
+	.dish-grid {
+		grid-template-columns: 1fr;
+	}
+}
+</style>

+ 40 - 3
src/views/psiManagement/dishManage/order/DishOrderApproval.vue

@@ -33,9 +33,13 @@
 								<template #default="scope">¥{{ formatMoney(scope.row.amount) }}</template>
 							</vxe-column>
 						</vxe-table>
+						<div class="dish-list-summary">
+							<span>共 {{ detailListCount(currentOrder.detailList) }} 项,总价</span>
+							<strong>¥{{ formatMoney(detailListAmount(currentOrder.detailList)) }}</strong>
+						</div>
 					</el-tab-pane>
 					<el-tab-pane label="加菜" name="add">
-						<vxe-table border="inner" auto-resize resizable :data="currentOrder.addDishList || []"
+						<vxe-table sort-by border="inner" auto-resize resizable :data="currentOrder.addDishList || []"
 							max-height="420">
 							<vxe-column type="seq" width="60" title="序号"></vxe-column>
 							<vxe-column min-width="180" title="菜品" field="dishName"></vxe-column>
@@ -50,8 +54,12 @@
 							</vxe-column>
 							<vxe-column min-width="170" title="加菜时间" field="createTime"></vxe-column>
 						</vxe-table>
+						<div class="dish-list-summary">
+							<span>共 {{ detailListCount(currentOrder.addDishList) }} 项,总价</span>
+							<strong>¥{{ formatMoney(detailListAmount(currentOrder.addDishList)) }}</strong>
+						</div>
 					</el-tab-pane>
-					<el-tab-pane label="减菜" name="reduce">
+					<el-tab-pane label="退菜" name="reduce">
 						<vxe-table border="inner" auto-resize resizable :data="currentOrder.reduceDishList || []"
 							max-height="420">
 							<vxe-column type="seq" width="60" title="序号"></vxe-column>
@@ -68,8 +76,13 @@
 								<template #default="scope">¥{{ formatMoney(Math.abs(scope.row.amount || 0))
 								}}</template>
 							</vxe-column>
-							<vxe-column min-width="170" title="减菜时间" field="createTime"></vxe-column>
+							<vxe-column min-width="180" title="退菜原因" field="reason"></vxe-column>
+							<vxe-column min-width="170" title="退菜时间" field="createTime"></vxe-column>
 						</vxe-table>
+						<div class="dish-list-summary">
+							<span>共 {{ detailListCount(currentOrder.reduceDishList) }} 项,总价</span>
+							<strong>¥{{ formatMoney(detailListAmount(currentOrder.reduceDishList, true)) }}</strong>
+						</div>
 					</el-tab-pane>
 				</el-tabs>
 			</div>
@@ -194,6 +207,15 @@ export default {
 		},
 		formatMoney(value) {
 			return Number(value || 0).toFixed(2)
+		},
+		detailListCount(list) {
+			return (list || []).length
+		},
+		detailListAmount(list, absolute = false) {
+			return (list || []).reduce((total, item) => {
+				const amount = Number(item.amount || 0)
+				return total + (absolute ? Math.abs(amount) : amount)
+			}, 0)
 		}
 	}
 }
@@ -260,6 +282,21 @@ export default {
 	text-overflow: ellipsis;
 }
 
+.dish-list-summary {
+	display: flex;
+	align-items: center;
+	justify-content: flex-end;
+	gap: 12px;
+	margin-top: 10px;
+	font-size: 14px;
+	color: #606266;
+}
+
+.dish-list-summary strong {
+	font-size: 18px;
+	color: #f56c6c;
+}
+
 @media (max-width: 900px) {
 	.detail-summary {
 		grid-template-columns: repeat(2, minmax(0, 1fr));

+ 177 - 0
src/views/psiManagement/dishManage/order/DishOrderConfirmDialog.vue

@@ -0,0 +1,177 @@
+<template>
+	<el-dialog title="确认订单" :close-on-click-modal="false" top="22vh" draggable width="560px" :model-value="modelValue"
+		@update:model-value="updateVisible">
+		<div class="confirm-order-summary">
+			<div>
+				<span>包间名</span>
+				<strong>{{ currentRoom.roomName || '-' }}</strong>
+			</div>
+			<div>
+				<span>下单时间</span>
+				<strong>{{ confirmOrderTime || '-' }}</strong>
+			</div>
+		</div>
+		<el-form label-position="top" :model="inputForm">
+			<el-form-item label="备注">
+				<el-input v-model="inputForm.tasteRemark" type="textarea" :rows="3" maxlength="500" placeholder="请输入备注"
+					show-word-limit></el-input>
+			</el-form-item>
+			<el-form-item label="特殊要求">
+				<el-checkbox-group v-model="selectedSpecialRequirementValues" @change="handleSpecialRequirementChange">
+					<el-checkbox v-for="item in $dictUtils.getDictList('special_requirement')" :key="item.value"
+						:label="item.value">
+						{{ item.label || item.value }}
+					</el-checkbox>
+				</el-checkbox-group>
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<span class="dialog-footer">
+				<el-button @click="continueOrderDish()" icon="el-icon-circle-close">继续点菜</el-button>
+				<el-button type="primary" :loading="submitting" @click="confirmOrderSubmit()"
+					icon="el-icon-circle-check">确认下单</el-button>
+			</span>
+		</template>
+	</el-dialog>
+</template>
+
+<script>
+export default {
+	props: {
+		modelValue: {
+			type: Boolean,
+			default: false
+		},
+		currentRoom: {
+			type: Object,
+			default: () => ({})
+		},
+		submitting: {
+			type: Boolean,
+			default: false
+		}
+	},
+	emits: ['update:modelValue', 'confirm', 'continue'],
+	data() {
+		return {
+			confirmOrderTime: '',
+			inputForm: {
+				tasteRemark: ''
+			},
+			selectedSpecialRequirementValues: [],
+			lastSpecialRequirementValues: []
+		}
+	},
+	watch: {
+		modelValue(value) {
+			if (value) {
+				this.resetForm()
+			}
+		}
+	},
+	methods: {
+		resetForm() {
+			this.confirmOrderTime = this.formatDateTime(new Date())
+			this.inputForm = {
+				tasteRemark: ''
+			}
+			this.selectedSpecialRequirementValues = []
+			this.lastSpecialRequirementValues = []
+		},
+		updateVisible(value) {
+			this.$emit('update:modelValue', value)
+		},
+		continueOrderDish() {
+			this.updateVisible(false)
+			this.$emit('continue')
+		},
+		confirmOrderSubmit() {
+			this.$emit('confirm', {
+				tasteRemark: this.inputForm.tasteRemark,
+				specialRequirements: ''
+			})
+		},
+		handleSpecialRequirementChange(value) {
+			const current = Array.isArray(value) ? value : []
+			const previous = this.lastSpecialRequirementValues || []
+			current.filter(item => !previous.includes(item)).forEach(item => this.appendRemarkText(this.getSpecialRequirementLabel(item)))
+			previous.filter(item => !current.includes(item)).forEach(item => this.removeRemarkText(this.getSpecialRequirementLabel(item)))
+			this.lastSpecialRequirementValues = [...current]
+		},
+		getSpecialRequirementLabel(value) {
+			const list = this.$dictUtils.getDictList('special_requirement') || []
+			const row = list.find(item => String(item.value) === String(value))
+			return row ? (row.label || row.value) : value
+		},
+		appendRemarkText(text) {
+			if (!text) {
+				return
+			}
+			const items = this.getRemarkItems()
+			if (!items.includes(text)) {
+				items.push(text)
+			}
+			this.inputForm.tasteRemark = items.join(',')
+		},
+		removeRemarkText(text) {
+			if (!text) {
+				return
+			}
+			let value = this.inputForm.tasteRemark || ''
+			if (value.indexOf(`,${text}`) > -1) {
+				value = value.split(`,${text}`).join('')
+			} else if (value.indexOf(`${text},`) > -1) {
+				value = value.split(`${text},`).join('')
+			} else {
+				value = value.split(text).join('')
+			}
+			this.inputForm.tasteRemark = this.cleanupRemarkText(value)
+		},
+		cleanupRemarkText(value) {
+			return (value || '').split(',').map(item => item.trim()).filter(item => item).join(',')
+		},
+		getRemarkItems() {
+			return (this.inputForm.tasteRemark || '').split(',').map(item => item.trim()).filter(item => item)
+		},
+		formatDateTime(value) {
+			const date = value ? new Date(value) : new Date()
+			const pad = (num) => String(num).padStart(2, '0')
+			return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
+		}
+	}
+}
+</script>
+
+<style scoped>
+.confirm-order-summary {
+	display: grid;
+	grid-template-columns: repeat(2, minmax(0, 1fr));
+	gap: 10px;
+	margin-bottom: 16px;
+	padding: 12px;
+	border-radius: 6px;
+	background: #f7f8fa;
+}
+
+.confirm-order-summary div {
+	min-width: 0;
+}
+
+.confirm-order-summary span {
+	display: block;
+	font-size: 12px;
+	line-height: 18px;
+	color: #909399;
+}
+
+.confirm-order-summary strong {
+	display: block;
+	margin-top: 4px;
+	font-size: 15px;
+	line-height: 22px;
+	color: #303133;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+</style>

+ 147 - 191
src/views/psiManagement/dishManage/order/DishOrderDetail.vue

@@ -7,11 +7,16 @@
 				</div>
 			</div>
 
-			<el-button @click="backRoomList()" type="primary" icon="el-icon-back">返回包房</el-button>
+			<div class="order-header-actions">
+				<el-button v-if="hasCurrentOrder" @click="goAddDish()" type="success" icon="el-icon-plus">加菜</el-button>
+				<el-button v-if="hasCurrentOrder" @click="openReturnDishDialog()" type="warning"
+					icon="el-icon-minus">退菜</el-button>
+				<el-button @click="backRoomList()" type="primary" icon="el-icon-back">返回包房</el-button>
+			</div>
 		</div>
 
 		<div class="order-main">
-			<div class="dish-panel">
+			<div v-if="!hasCurrentOrder" class="dish-panel">
 				<div class="dish-search">
 					<el-input v-model="dishSearchName" placeholder="搜索菜品名称" clearable @keyup.enter.native="searchDish()"
 						@clear="searchDish()">
@@ -53,12 +58,13 @@
 				</div>
 			</div>
 
-			<div class="ordered-panel" v-loading="orderLoading">
+			<div class="ordered-panel" :class="{ 'ordered-panel-full': hasCurrentOrder }" v-loading="orderLoading">
 				<div class="ordered-title">
 					<span>已点菜</span>
 					<div v-if="hasCurrentOrder" class="ordered-title-actions">
 						<el-button type="primary" plain @click="viewCurrentOrder()">订单详情</el-button>
-						<el-button type="danger" plain :loading="submitting" @click="confirmCancelOrder()">取消订单</el-button>
+						<el-button type="danger" plain :loading="submitting"
+							@click="confirmCancelOrder()">取消订单</el-button>
 					</div>
 				</div>
 
@@ -87,10 +93,10 @@
 							<div class="ordered-price">¥{{ formatMoney(item.salePrice) }}</div>
 							<div class="ordered-total">¥{{ formatMoney(itemTotal(item)) }}</div>
 							<div class="ordered-qty">
-								<el-button icon="el-icon-minus" size="mini" circle
+								<el-button v-if="!hasCurrentOrder" icon="el-icon-minus" size="mini" circle
 									@click="reduceDish(item)"></el-button>
 								<span>{{ item.quantity }}</span>
-								<el-button icon="el-icon-plus" size="mini" circle
+								<el-button v-if="!hasCurrentOrder" icon="el-icon-plus" size="mini" circle
 									@click="increaseOrderedDish(item)"></el-button>
 							</div>
 						</div>
@@ -98,80 +104,27 @@
 				</div>
 				<div class="ordered-footer">
 					<div class="ordered-summary">
-						<span>总价</span>
+						<span>共 {{ orderedList.length }} 项</span>
 						<strong>¥{{ formatMoney(orderTotal) }}</strong>
 					</div>
-					<div class="ordered-actions" :class="{ 'ordered-actions-with-cancel': hasCurrentOrder }">
+					<div class="ordered-actions"
+						:class="{ 'ordered-actions-settle-only': hasCurrentOrder && !hasChangedDish }">
 
-						<el-button type="success" :loading="submitting" @click="openSettleDialog()">{{ settleButtonText
-						}}</el-button>
-						<el-button type="primary" :loading="submitting" @click="confirmSubmitOrder()">下单</el-button>
+						<el-button v-if="hasCurrentOrder && !hasChangedDish" style="width: 30%;" type="success"
+							:loading="submitting" @click="openSettleDialog()">结账</el-button>
+						<el-button v-if="!hasCurrentOrder || hasChangedDish" type="primary" :loading="submitting"
+							@click="confirmSubmitOrder()">下单</el-button>
 					</div>
 				</div>
 			</div>
 		</div>
-		<el-dialog title="订单结账" :close-on-click-modal="false" top="25vh" draggable width="520px"
-			v-model="settleDialogVisible">
-			<el-tabs v-model="settleType">
-				<el-tab-pane label="结账" name="0">
-					<div class="settle-normal">
-						<span>订单金额</span>
-						<strong>¥{{ formatMoney(orderTotal) }}</strong>
-					</div>
-				</el-tab-pane>
-				<el-tab-pane label="折扣结账" name="1">
-					<div class="settle-preview">
-						<div>
-							<span>账单原金额</span>
-							<strong>¥{{ formatMoney(orderTotal) }}</strong>
-						</div>
-						<div>
-							<span>折扣比例</span>
-							<strong>{{ formatMoney(discountRate) }}%</strong>
-						</div>
-						<div>
-							<span>折扣后金额</span>
-							<strong style="color: #f56c6c;">¥{{ formatMoney(settlePayableAmount) }}</strong>
-						</div>
-					</div>
-					<el-form label-width="100px">
-						<el-form-item label="折扣比例">
-							<el-input-number v-model="discountRate" :min="0" :max="100" :precision="2"
-								style="width: 100%"></el-input-number>
-						</el-form-item>
-					</el-form>
-				</el-tab-pane>
-				<el-tab-pane label="优惠结账" name="2">
-					<div class="settle-preview">
-						<div>
-							<span>账单原金额</span>
-							<strong>¥{{ formatMoney(orderTotal) }}</strong>
-						</div>
-						<div>
-							<span>优惠金额</span>
-							<strong>¥{{ formatMoney(settleDiscountAmount) }}</strong>
-						</div>
-						<div>
-							<span>优惠后金额</span>
-							<strong style="color: #f56c6c;">¥{{ formatMoney(settlePayableAmount) }}</strong>
-						</div>
-					</div>
-					<el-form label-width="100px">
-						<el-form-item label="优惠金额">
-							<el-input-number v-model="discountAmount" :min="0" :max="orderTotal" :precision="2"
-								style="width: 100%"></el-input-number>
-						</el-form-item>
-					</el-form>
-				</el-tab-pane>
-			</el-tabs>
-			<template #footer>
-				<span class="dialog-footer">
-					<el-button @click="settleDialogVisible = false" icon="el-icon-circle-close">取消</el-button>
-					<el-button type="primary" :loading="submitting" @click="confirmSettle()"
-						icon="el-icon-circle-check">确认结账</el-button>
-				</span>
-			</template>
-		</el-dialog>
+		<DishOrderSettleDialog v-model="settleDialogVisible" :order-total="orderTotal" :submitting="submitting"
+			@confirm="handleSettleConfirm"></DishOrderSettleDialog>
+		<DishOrderConfirmDialog v-model="confirmOrderDialogVisible" :current-room="currentRoom" :submitting="submitting"
+			@confirm="handleConfirmOrderSubmit" @continue="continueOrderDish"></DishOrderConfirmDialog>
+		<DishOrderReturnDialog v-model="returnDishDialogVisible" :current-room="currentRoom"
+			:current-order="currentOrder" :ordered-list="orderedList" :submitting="submitting"
+			@confirm="handleReturnDishConfirm"></DishOrderReturnDialog>
 		<DishOrderInfoDialog ref="dishOrderInfoDialog"></DishOrderInfoDialog>
 	</div>
 </template>
@@ -181,10 +134,16 @@ import DishRoomService from '@/api/psi/DishRoomService'
 import DishOrderService from '@/api/psi/DishOrderService'
 import OSSSerivce from '@/api/sys/OSSService'
 import DishOrderInfoDialog from './DishOrderInfoDialog'
+import DishOrderSettleDialog from './DishOrderSettleDialog'
+import DishOrderConfirmDialog from './DishOrderConfirmDialog'
+import DishOrderReturnDialog from './DishOrderReturnDialog'
 import Sortable from 'sortablejs'
 export default {
 	components: {
-		DishOrderInfoDialog
+		DishOrderInfoDialog,
+		DishOrderSettleDialog,
+		DishOrderConfirmDialog,
+		DishOrderReturnDialog
 	},
 	data() {
 		return {
@@ -199,9 +158,9 @@ export default {
 			orderLoading: false,
 			submitting: false,
 			settleDialogVisible: false,
-			settleType: '0',
-			discountRate: 100,
-			discountAmount: 0,
+			confirmOrderDialogVisible: false,
+			returnDishDialogVisible: false,
+			pendingOrderSubmit: null,
 			orderedSortable: null
 		}
 	},
@@ -220,9 +179,6 @@ export default {
 		hasCurrentOrder() {
 			return !!(this.currentOrder && this.currentOrder.id)
 		},
-		settleButtonText() {
-			return this.hasChangedDish ? '下单并结账' : '结账'
-		},
 		submitDetailList() {
 			return this.orderedList.map((item, index) => {
 				return {
@@ -232,25 +188,6 @@ export default {
 					dishSort: index + 1
 				}
 			})
-		},
-		settleDiscountAmount() {
-			if (this.settleType === '1') {
-				return this.orderTotal - this.settlePayableAmount
-			}
-			if (this.settleType === '2') {
-				return Math.min(Number(this.discountAmount || 0), this.orderTotal)
-			}
-			return 0
-		},
-		settlePayableAmount() {
-			if (this.settleType === '1') {
-				const rate = Math.min(Math.max(Number(this.discountRate || 0), 0), 100)
-				return this.orderTotal * rate / 100
-			}
-			if (this.settleType === '2') {
-				return Math.max(this.orderTotal - Number(this.discountAmount || 0), 0)
-			}
-			return this.orderTotal
 		}
 	},
 	dishRoomService: null,
@@ -320,6 +257,40 @@ export default {
 				path: '/psiManagement/dishManage/order/DishOrder'
 			})
 		},
+		goAddDish() {
+			if (!this.hasCurrentOrder) {
+				return
+			}
+			this.$router.push({
+				path: '/psiManagement/dishManage/order/DishOrderAdd',
+				query: {
+					roomId: this.$route.query.roomId,
+					orderId: this.currentOrder.id
+				}
+			})
+		},
+		openReturnDishDialog() {
+			if (!this.hasCurrentOrder) {
+				return
+			}
+			this.returnDishDialogVisible = true
+		},
+		handleReturnDishConfirm(detailList) {
+			this.submitting = true
+			this.dishOrderService.submit({
+				id: this.currentOrder.id,
+				roomId: this.$route.query.roomId,
+				settleFlag: '0',
+				detailList: detailList
+			}).then((data) => {
+				this.$message.success(data)
+				this.returnDishDialogVisible = false
+				this.loadRoom(this.$route.query.roomId)
+				this.loadCurrentOrder(this.$route.query.roomId)
+			}).finally(() => {
+				this.submitting = false
+			})
+		},
 		loadCategoryList() {
 			this.dishOrderService.typeList().then((data) => {
 				this.categoryList = [
@@ -392,17 +363,15 @@ export default {
 				this.$message.warning('请先选择菜品')
 				return
 			}
-			this.settleType = '0'
-			this.discountRate = 100
-			this.discountAmount = 0
 			this.settleDialogVisible = true
 		},
-		confirmSettle() {
-			this.submitOrder('1', {
-				settleType: this.settleType,
-				discountRate: this.settleType === '1' ? this.discountRate : 100,
-				discountAmount: this.settleType === '2' ? this.discountAmount : this.settleDiscountAmount
-			}, false)
+		handleSettleConfirm(settleInfo) {
+			if (!this.hasCurrentOrder) {
+				this.settleDialogVisible = false
+				this.openConfirmOrderDialog('1', settleInfo, false)
+				return
+			}
+			this.submitOrder('1', settleInfo, false)
 		},
 		confirmSubmitOrder() {
 			const detailList = this.changedDetailList
@@ -410,6 +379,10 @@ export default {
 				this.$message.warning('请先选择菜品')
 				return
 			}
+			if (!this.hasCurrentOrder) {
+				this.openConfirmOrderDialog('0', {}, true)
+				return
+			}
 			this.$confirm('是否确认下单?', '提示', {
 				confirmButtonText: '确定',
 				cancelButtonText: '取消',
@@ -418,6 +391,21 @@ export default {
 				this.submitOrder('0', {}, true)
 			})
 		},
+		openConfirmOrderDialog(settleFlag, settleInfo = {}, requireChanged = false) {
+			this.pendingOrderSubmit = {
+				settleFlag,
+				settleInfo,
+				requireChanged
+			}
+			this.confirmOrderDialogVisible = true
+		},
+		continueOrderDish() {
+			this.confirmOrderDialogVisible = false
+		},
+		handleConfirmOrderSubmit(orderInfo) {
+			const pending = this.pendingOrderSubmit || {}
+			this.submitOrder(pending.settleFlag || '0', pending.settleInfo || {}, pending.requireChanged, orderInfo)
+		},
 		confirmCancelOrder() {
 			if (!this.hasCurrentOrder) {
 				this.$message.warning('暂无可取消的订单')
@@ -445,7 +433,7 @@ export default {
 			}
 			this.$refs.dishOrderInfoDialog.init(this.currentOrder.id)
 		},
-		submitOrder(settleFlag, settleInfo = {}, requireChanged = false) {
+		submitOrder(settleFlag, settleInfo = {}, requireChanged = false, orderInfo = {}) {
 			const changedList = this.changedDetailList
 			const detailList = settleFlag === '1' && changedList.length === 0 ? [] : this.submitDetailList
 			if (requireChanged && changedList.length === 0) {
@@ -466,11 +454,13 @@ export default {
 				roomId: this.$route.query.roomId,
 				settleFlag: settleFlag,
 				...settleInfo,
+				...orderInfo,
 				detailList: detailList
 			}).then((data) => {
 				this.$message.success(data)
 				this.submitting = false
 				this.settleDialogVisible = false
+				this.confirmOrderDialogVisible = false
 				this.loadRoom(this.$route.query.roomId)
 				this.loadCurrentOrder(this.$route.query.roomId)
 			}).catch(() => {
@@ -495,31 +485,31 @@ export default {
 			}
 		},
 		initOrderedSortable() {
-			const el = this.$refs.orderedDragList
-			if (!el) {
-				this.destroyOrderedSortable()
-				return
-			}
-			if (this.orderedSortable) {
-				return
-			}
-			this.orderedSortable = Sortable.create(el, {
-				animation: 180,
-				filter: '.ordered-qty, .ordered-qty *',
-				preventOnFilter: false,
-				ghostClass: 'ordered-drag-ghost',
-				chosenClass: 'ordered-drag-chosen',
-				onEnd: (evt) => {
-					if (evt.oldIndex === evt.newIndex) {
-						return
-					}
-					const list = [...this.orderedList]
-					const moved = list.splice(evt.oldIndex, 1)[0]
-					list.splice(evt.newIndex, 0, moved)
-					this.orderedList = list
-					this.refreshDishSort()
-				}
-			})
+			// const el = this.$refs.orderedDragList
+			// if (!el) {
+			// 	this.destroyOrderedSortable()
+			// 	return
+			// }
+			// if (this.orderedSortable) {
+			// 	return
+			// }
+			// this.orderedSortable = Sortable.create(el, {
+			// 	animation: 180,
+			// 	filter: '.ordered-qty, .ordered-qty *',
+			// 	preventOnFilter: false,
+			// 	ghostClass: 'ordered-drag-ghost',
+			// 	chosenClass: 'ordered-drag-chosen',
+			// 	onEnd: (evt) => {
+			// 		if (evt.oldIndex === evt.newIndex) {
+			// 			return
+			// 		}
+			// 		const list = [...this.orderedList]
+			// 		const moved = list.splice(evt.oldIndex, 1)[0]
+			// 		list.splice(evt.newIndex, 0, moved)
+			// 		this.orderedList = list
+			// 		this.refreshDishSort()
+			// 	}
+			// })
 		},
 		destroyOrderedSortable() {
 			if (this.orderedSortable) {
@@ -598,6 +588,16 @@ export default {
 	line-height: 20px;
 }
 
+.order-header-actions {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+}
+
+.order-header-actions .el-button+.el-button {
+	margin-left: 0;
+}
+
 .order-main {
 	flex: 1;
 	min-height: 0;
@@ -607,6 +607,10 @@ export default {
 	padding-top: 16px;
 }
 
+.ordered-panel-full {
+	grid-column: 1 / -1;
+}
+
 .dish-panel,
 .ordered-panel {
 	min-height: 0;
@@ -768,7 +772,7 @@ export default {
 	gap: 8px;
 }
 
-.ordered-title-actions .el-button + .el-button {
+.ordered-title-actions .el-button+.el-button {
 	margin-left: 0;
 }
 
@@ -827,13 +831,13 @@ export default {
 
 .ordered-item {
 	min-height: 64px;
-	cursor: grab;
+	/* cursor: grab; */
 	padding: 10px 8px;
 	border-bottom: 1px solid #ebeef5;
 }
 
 .ordered-item-pending {
-	background: rgba(45, 140, 240, 0.3);
+	/* background: rgba(45, 140, 240, 0.3); */
 }
 
 .ordered-dish {
@@ -874,7 +878,7 @@ export default {
 .ordered-qty {
 	display: flex;
 	align-items: center;
-	justify-content: flex-end;
+	justify-content: center;
 	gap: 8px;
 }
 
@@ -905,64 +909,16 @@ export default {
 }
 
 .ordered-actions {
-	display: grid;
-	grid-template-columns: 1fr 1fr;
-	gap: 10px;
-}
-
-.ordered-actions-with-cancel {
-	grid-template-columns: 1fr 1fr;
-}
-
-.ordered-actions .el-button {
-	width: 100%;
-}
-
-.settle-normal {
-	height: 96px;
 	display: flex;
-	align-items: center;
-	justify-content: space-between;
-	padding: 0 4px;
-	font-size: 15px;
-	color: #606266;
-}
-
-.settle-normal strong {
-	font-size: 28px;
-	color: #f56c6c;
-}
-
-.settle-preview {
-	display: grid;
-	grid-template-columns: repeat(3, minmax(0, 1fr));
-	gap: 10px;
-	margin-bottom: 18px;
-	padding: 12px;
-	border-radius: 6px;
-	background: #f7f8fa;
-}
-
-.settle-preview div {
-	min-width: 0;
+	justify-content: flex-end;
 }
 
-.settle-preview span {
-	display: block;
-	font-size: 12px;
-	line-height: 18px;
-	color: #909399;
+.ordered-actions-settle-only {
+	justify-content: flex-end;
 }
 
-.settle-preview strong {
-	display: block;
-	margin-top: 4px;
-	font-size: 15px;
-	line-height: 22px;
-	color: #303133;
-	white-space: nowrap;
-	overflow: hidden;
-	text-overflow: ellipsis;
+.ordered-actions .el-button {
+	width: 50%;
 }
 
 @media (max-width: 1280px) {

+ 56 - 40
src/views/psiManagement/dishManage/order/DishOrderInfoDialog.vue

@@ -2,17 +2,25 @@
 	<div>
 		<el-dialog title="订单详情" :close-on-click-modal="false" draggable width="900px" v-model="visible">
 			<div v-if="currentOrder" v-loading="loading" class="order-detail">
-				<div class="detail-summary">
-					<div><span>订单编号</span><strong>{{ currentOrder.orderNo || '-' }}</strong></div>
-					<div><span>包房</span><strong>{{ currentOrder.roomName || '-' }}</strong></div>
-					<div><span>订单状态</span><strong>{{ orderStatusName(currentOrder.orderStatus) }}</strong></div>
-					<div><span>结账方式</span><strong>{{ settleTypeName(currentOrder.settleType) }}</strong></div>
-					<div><span>账单原金额</span><strong>¥{{ formatMoney(currentOrder.totalAmount) }}</strong></div>
-					<div><span>折扣比例</span><strong>{{ discountRateText(currentOrder) }}</strong></div>
-					<div><span>优惠金额</span><strong>¥{{ formatMoney(currentOrder.discountAmount) }}</strong></div>
-					<div><span>实收金额</span><strong>¥{{ formatMoney(currentOrder.payableAmount) }}</strong></div>
-					<div><span>结账时间/取消时间</span><strong>{{ currentOrder.settleTime || '-' }}</strong></div>
-				</div>
+				<el-descriptions :column="3" border class="detail-summary">
+					<el-descriptions-item label="订单编号">{{ currentOrder.orderNo || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="包房">{{ currentOrder.roomName || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="订单状态">{{ orderStatusName(currentOrder.orderStatus)
+					}}</el-descriptions-item>
+					<el-descriptions-item label="结账方式">{{ settleTypeName(currentOrder.settleType)
+					}}</el-descriptions-item>
+					<el-descriptions-item label="账单原金额">¥{{ formatMoney(currentOrder.totalAmount)
+					}}</el-descriptions-item>
+					<el-descriptions-item label="折扣比例">{{ discountRateText(currentOrder) }}</el-descriptions-item>
+					<el-descriptions-item label="优惠金额">¥{{ formatMoney(currentOrder.discountAmount)
+					}}</el-descriptions-item>
+					<el-descriptions-item label="实收金额">¥{{ formatMoney(currentOrder.payableAmount)
+					}}</el-descriptions-item>
+					<el-descriptions-item label="结账时间/取消时间">{{ currentOrder.settleTime || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="备注" :span="3">
+						<div class="description-text">{{ currentOrder.tasteRemark || '-' }}</div>
+					</el-descriptions-item>
+				</el-descriptions>
 				<el-tabs v-model="activeTab">
 					<el-tab-pane label="实际菜单" name="actual">
 						<vxe-table border="inner" auto-resize resizable :data="currentOrder.detailList || []"
@@ -30,6 +38,10 @@
 							</vxe-column>
 							<vxe-column min-width="170" title="下单时间" field="createTime"></vxe-column>
 						</vxe-table>
+						<div class="dish-list-summary">
+							<span>共 {{ detailListCount(currentOrder.detailList) }} 项,总价</span>
+							<strong>¥{{ formatMoney(detailListAmount(currentOrder.detailList)) }}</strong>
+						</div>
 					</el-tab-pane>
 					<el-tab-pane label="加菜" name="add">
 						<vxe-table border="inner" auto-resize resizable :data="currentOrder.addDishList || []"
@@ -47,8 +59,12 @@
 							</vxe-column>
 							<vxe-column min-width="170" title="加菜时间" field="createTime"></vxe-column>
 						</vxe-table>
+						<div class="dish-list-summary">
+							<span>共 {{ detailListCount(currentOrder.addDishList) }} 项,总价</span>
+							<strong>¥{{ formatMoney(detailListAmount(currentOrder.addDishList)) }}</strong>
+						</div>
 					</el-tab-pane>
-					<el-tab-pane label="减菜" name="reduce">
+					<el-tab-pane label="退菜" name="reduce">
 						<vxe-table border="inner" auto-resize resizable :data="currentOrder.reduceDishList || []"
 							max-height="420">
 							<vxe-column type="seq" width="60" title="序号"></vxe-column>
@@ -65,8 +81,13 @@
 								<template #default="scope">¥{{ formatMoney(Math.abs(scope.row.amount || 0))
 								}}</template>
 							</vxe-column>
-							<vxe-column min-width="170" title="减菜时间" field="createTime"></vxe-column>
+							<vxe-column min-width="180" title="退菜原因" field="reason"></vxe-column>
+							<vxe-column min-width="170" title="退菜时间" field="createTime"></vxe-column>
 						</vxe-table>
+						<div class="dish-list-summary">
+							<span>共 {{ detailListCount(currentOrder.reduceDishList) }} 项,总价</span>
+							<strong>¥{{ formatMoney(detailListAmount(currentOrder.reduceDishList, true)) }}</strong>
+						</div>
 					</el-tab-pane>
 				</el-tabs>
 			</div>
@@ -134,6 +155,15 @@ export default {
 		formatMoney(value) {
 			return Number(value || 0).toFixed(2)
 		},
+		detailListCount(list) {
+			return (list || []).length
+		},
+		detailListAmount(list, absolute = false) {
+			return (list || []).reduce((total, item) => {
+				const amount = Number(item.amount || 0)
+				return total + (absolute ? Math.abs(amount) : amount)
+			}, 0)
+		},
 		discountRateText(order) {
 			if (!order || order.settleType !== '1') {
 				return '-'
@@ -150,40 +180,26 @@ export default {
 }
 
 .detail-summary {
-	display: grid;
-	grid-template-columns: repeat(4, minmax(0, 1fr));
-	gap: 10px;
 	margin-bottom: 14px;
-	padding: 12px;
-	border-radius: 6px;
-	background: #f7f8fa;
 }
 
-.detail-summary div {
-	min-width: 0;
+.description-text {
+	white-space: pre-wrap;
+	word-break: break-word;
 }
 
-.detail-summary span {
-	display: block;
-	font-size: 12px;
-	line-height: 18px;
-	color: #909399;
-}
-
-.detail-summary strong {
-	display: block;
-	margin-top: 4px;
+.dish-list-summary {
+	display: flex;
+	align-items: center;
+	justify-content: flex-end;
+	gap: 12px;
+	margin-top: 10px;
 	font-size: 14px;
-	line-height: 22px;
-	color: #303133;
-	white-space: nowrap;
-	overflow: hidden;
-	text-overflow: ellipsis;
+	color: #606266;
 }
 
-@media (max-width: 900px) {
-	.detail-summary {
-		grid-template-columns: repeat(2, minmax(0, 1fr));
-	}
+.dish-list-summary strong {
+	font-size: 18px;
+	color: #f56c6c;
 }
 </style>

+ 214 - 0
src/views/psiManagement/dishManage/order/DishOrderReturnDialog.vue

@@ -0,0 +1,214 @@
+<template>
+	<el-dialog title="退菜" :close-on-click-modal="false" top="16vh" draggable width="760px" :model-value="modelValue"
+		@update:model-value="updateVisible">
+		<div class="return-dish-summary">
+			<div>
+				<span>包房名</span>
+				<strong>{{ currentRoom.roomName || '-' }}</strong>
+			</div>
+			<div>
+				<span>下单时间</span>
+				<strong>{{ currentOrder && currentOrder.createTime ? currentOrder.createTime : '-' }}</strong>
+			</div>
+		</div>
+		<div class="return-dish-list">
+			<div class="return-dish-head">
+				<span>序号</span>
+				<span>菜品名</span>
+				<span>现有数量</span>
+				<span>退菜数量</span>
+			</div>
+			<div v-if="returnDishList.length === 0" class="return-dish-empty">暂无可退菜品</div>
+			<div v-else class="return-dish-body">
+				<div v-for="(item, index) in returnDishList" :key="item.dishId" class="return-dish-item">
+					<span>{{ index + 1 }}</span>
+					<span class="return-dish-name">{{ item.dishName }}</span>
+					<span>{{ item.quantity }}</span>
+					<el-input-number v-model="item.returnQuantity" :min="0" :max="item.quantity" :precision="0"
+						:step="1" style="width: 120px"></el-input-number>
+				</div>
+			</div>
+		</div>
+		<el-form label-position="top" :model="inputForm" class="return-reason-form">
+			<el-form-item label="退菜原因">
+				<el-input v-model="inputForm.reason" type="textarea" :rows="3" maxlength="500" placeholder="请输入退菜原因"
+					show-word-limit></el-input>
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<span class="dialog-footer">
+				<el-button @click="updateVisible(false)" icon="el-icon-circle-close">取消</el-button>
+				<el-button type="primary" :loading="submitting" @click="confirmReturnDish()"
+					icon="el-icon-circle-check">确认退菜</el-button>
+			</span>
+		</template>
+	</el-dialog>
+</template>
+
+<script>
+export default {
+	props: {
+		modelValue: {
+			type: Boolean,
+			default: false
+		},
+		currentRoom: {
+			type: Object,
+			default: () => ({})
+		},
+		currentOrder: {
+			type: Object,
+			default: null
+		},
+		orderedList: {
+			type: Array,
+			default: () => []
+		},
+		submitting: {
+			type: Boolean,
+			default: false
+		}
+	},
+	emits: ['update:modelValue', 'confirm'],
+	data() {
+		return {
+			inputForm: {
+				reason: ''
+			},
+			returnDishList: []
+		}
+	},
+	watch: {
+		modelValue(value) {
+			if (value) {
+				this.resetForm()
+			}
+		}
+	},
+	methods: {
+		resetForm() {
+			this.inputForm = {
+				reason: ''
+			}
+			this.returnDishList = this.orderedList.map(item => {
+				return {
+					dishId: item.dishId,
+					dishName: item.dishName,
+					quantity: item.quantity,
+					dishSort: item.dishSort,
+					returnQuantity: 0
+				}
+			})
+		},
+		updateVisible(value) {
+			this.$emit('update:modelValue', value)
+		},
+		confirmReturnDish() {
+			const detailList = this.returnDishList
+				.filter(item => Number(item.returnQuantity || 0) > 0)
+				.map(item => {
+					return {
+						dishId: item.dishId,
+						dishName: item.dishName,
+						quantity: -Number(item.returnQuantity || 0),
+						dishSort: item.dishSort,
+						reason: this.inputForm.reason
+					}
+				})
+			if (detailList.length === 0) {
+				this.$message.warning('请填写退菜数量')
+				return
+			}
+			this.$emit('confirm', detailList)
+		}
+	}
+}
+</script>
+
+<style scoped>
+.return-dish-summary {
+	display: grid;
+	grid-template-columns: repeat(2, minmax(0, 1fr));
+	gap: 10px;
+	margin-bottom: 14px;
+	padding: 12px;
+	border-radius: 6px;
+	background: #f7f8fa;
+}
+
+.return-dish-summary div {
+	min-width: 0;
+}
+
+.return-dish-summary span {
+	display: block;
+	font-size: 12px;
+	line-height: 18px;
+	color: #909399;
+}
+
+.return-dish-summary strong {
+	display: block;
+	margin-top: 4px;
+	font-size: 15px;
+	line-height: 22px;
+	color: #303133;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.return-dish-list {
+	border: 1px solid #ebeef5;
+	border-radius: 6px;
+	overflow: hidden;
+}
+
+.return-dish-head,
+.return-dish-item {
+	display: grid;
+	grid-template-columns: 70px minmax(0, 1fr) 100px 140px;
+	gap: 8px;
+	align-items: center;
+}
+
+.return-dish-head {
+	height: 38px;
+	padding: 0 12px;
+	font-size: 12px;
+	color: #909399;
+	background: #f7f9fb;
+}
+
+.return-dish-body {
+	max-height: 320px;
+	overflow: auto;
+}
+
+.return-dish-item {
+	min-height: 58px;
+	padding: 10px 12px;
+	border-top: 1px solid #ebeef5;
+}
+
+.return-dish-name {
+	min-width: 0;
+	font-weight: 600;
+	color: #303133;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.return-dish-empty {
+	height: 120px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	color: #909399;
+}
+
+.return-reason-form {
+	margin-top: 14px;
+}
+</style>

+ 189 - 0
src/views/psiManagement/dishManage/order/DishOrderSettleDialog.vue

@@ -0,0 +1,189 @@
+<template>
+	<el-dialog title="订单结账" :close-on-click-modal="false" top="25vh" draggable width="520px"
+		:model-value="modelValue" @update:model-value="updateVisible">
+		<el-tabs v-model="settleType">
+			<el-tab-pane label="结账" name="0">
+				<div class="settle-normal">
+					<span>订单金额</span>
+					<strong>¥{{ formatMoney(orderTotal) }}</strong>
+				</div>
+			</el-tab-pane>
+			<el-tab-pane label="折扣结账" name="1">
+				<div class="settle-preview">
+					<div>
+						<span>账单原金额</span>
+						<strong>¥{{ formatMoney(orderTotal) }}</strong>
+					</div>
+					<div>
+						<span>折扣比例</span>
+						<strong>{{ formatMoney(discountRate) }}%</strong>
+					</div>
+					<div>
+						<span>折扣后金额</span>
+						<strong style="color: #f56c6c;">¥{{ formatMoney(settlePayableAmount) }}</strong>
+					</div>
+				</div>
+				<el-form label-width="100px">
+					<el-form-item label="折扣比例">
+						<el-input-number v-model="discountRate" :min="0" :max="100" :precision="2"
+							style="width: 100%"></el-input-number>
+					</el-form-item>
+				</el-form>
+			</el-tab-pane>
+			<el-tab-pane label="优惠结账" name="2">
+				<div class="settle-preview">
+					<div>
+						<span>账单原金额</span>
+						<strong>¥{{ formatMoney(orderTotal) }}</strong>
+					</div>
+					<div>
+						<span>优惠金额</span>
+						<strong>¥{{ formatMoney(settleDiscountAmount) }}</strong>
+					</div>
+					<div>
+						<span>优惠后金额</span>
+						<strong style="color: #f56c6c;">¥{{ formatMoney(settlePayableAmount) }}</strong>
+					</div>
+				</div>
+				<el-form label-width="100px">
+					<el-form-item label="优惠金额">
+						<el-input-number v-model="discountAmount" :min="0" :max="orderTotal" :precision="2"
+							style="width: 100%"></el-input-number>
+					</el-form-item>
+				</el-form>
+			</el-tab-pane>
+		</el-tabs>
+		<template #footer>
+			<span class="dialog-footer">
+				<el-button @click="updateVisible(false)" icon="el-icon-circle-close">取消</el-button>
+				<el-button type="primary" :loading="submitting" @click="confirmSettle()"
+					icon="el-icon-circle-check">确认结账</el-button>
+			</span>
+		</template>
+	</el-dialog>
+</template>
+
+<script>
+export default {
+	props: {
+		modelValue: {
+			type: Boolean,
+			default: false
+		},
+		orderTotal: {
+			type: Number,
+			default: 0
+		},
+		submitting: {
+			type: Boolean,
+			default: false
+		}
+	},
+	emits: ['update:modelValue', 'confirm'],
+	data() {
+		return {
+			settleType: '0',
+			discountRate: 100,
+			discountAmount: 0
+		}
+	},
+	computed: {
+		settleDiscountAmount() {
+			if (this.settleType === '1') {
+				return this.orderTotal - this.settlePayableAmount
+			}
+			if (this.settleType === '2') {
+				return Math.min(Number(this.discountAmount || 0), this.orderTotal)
+			}
+			return 0
+		},
+		settlePayableAmount() {
+			if (this.settleType === '1') {
+				const rate = Math.min(Math.max(Number(this.discountRate || 0), 0), 100)
+				return this.orderTotal * rate / 100
+			}
+			if (this.settleType === '2') {
+				return Math.max(this.orderTotal - Number(this.discountAmount || 0), 0)
+			}
+			return this.orderTotal
+		}
+	},
+	watch: {
+		modelValue(value) {
+			if (value) {
+				this.resetForm()
+			}
+		}
+	},
+	methods: {
+		resetForm() {
+			this.settleType = '0'
+			this.discountRate = 100
+			this.discountAmount = 0
+		},
+		updateVisible(value) {
+			this.$emit('update:modelValue', value)
+		},
+		confirmSettle() {
+			this.$emit('confirm', {
+				settleType: this.settleType,
+				discountRate: this.settleType === '1' ? this.discountRate : 100,
+				discountAmount: this.settleType === '2' ? this.discountAmount : this.settleDiscountAmount
+			})
+		},
+		formatMoney(value) {
+			return Number(value || 0).toFixed(2)
+		}
+	}
+}
+</script>
+
+<style scoped>
+.settle-normal {
+	height: 96px;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 0 4px;
+	font-size: 15px;
+	color: #606266;
+}
+
+.settle-normal strong {
+	font-size: 28px;
+	color: #f56c6c;
+}
+
+.settle-preview {
+	display: grid;
+	grid-template-columns: repeat(3, minmax(0, 1fr));
+	gap: 10px;
+	margin-bottom: 18px;
+	padding: 12px;
+	border-radius: 6px;
+	background: #f7f8fa;
+}
+
+.settle-preview div {
+	min-width: 0;
+}
+
+.settle-preview span {
+	display: block;
+	font-size: 12px;
+	line-height: 18px;
+	color: #909399;
+}
+
+.settle-preview strong {
+	display: block;
+	margin-top: 4px;
+	font-size: 15px;
+	line-height: 22px;
+	color: #303133;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+</style>