ソースを参照

景聚庭-点餐代码提交

huangguoce 1 日 前
コミット
54f8e08a21

+ 40 - 0
src/api/psi/CreditCustomerService.js

@@ -0,0 +1,40 @@
+import request from "@/utils/httpRequest";
+import { PSI_MANAGEMANT as prefix } from "../AppPath";
+
+export default class CreditCustomerService {
+	list(param) {
+		return request({
+			url: prefix + "/psi/creditCustomer/list",
+			method: "get",
+			params: param,
+		});
+	}
+	save(param) {
+		return request({
+			url: prefix + "/psi/creditCustomer/save",
+			method: "post",
+			data: param,
+		});
+	}
+	findById(id) {
+		return request({
+			url: prefix + "/psi/creditCustomer/findById",
+			method: "get",
+			params: { id: id },
+		});
+	}
+	delete(ids) {
+		return request({
+			url: prefix + "/psi/creditCustomer/delete",
+			method: "delete",
+			params: { ids: ids },
+		});
+	}
+	changeStatus(id, creditStatus) {
+		return request({
+			url: prefix + "/psi/creditCustomer/changeStatus",
+			method: "get",
+			params: { id: id, creditStatus: creditStatus },
+		});
+	}
+}

+ 40 - 0
src/api/psi/DishLibraryService.js

@@ -0,0 +1,40 @@
+import request from "@/utils/httpRequest";
+import { PSI_MANAGEMANT as prefix } from "../AppPath";
+
+export default class DishLibraryService {
+	list(param) {
+		return request({
+			url: prefix + "/psi/dish/list",
+			method: "get",
+			params: param,
+		});
+	}
+	save(param) {
+		return request({
+			url: prefix + "/psi/dish/save",
+			method: "post",
+			data: param,
+		});
+	}
+	findById(id) {
+		return request({
+			url: prefix + "/psi/dish/findById",
+			method: "get",
+			params: { id: id },
+		});
+	}
+	delete(ids) {
+		return request({
+			url: prefix + "/psi/dish/delete",
+			method: "delete",
+			params: { ids: ids },
+		});
+	}
+	changeStatus(id, status) {
+		return request({
+			url: prefix + "/psi/dish/changeStatus",
+			method: "get",
+			params: { id: id, status: status },
+		});
+	}
+}

+ 78 - 0
src/api/psi/DishOrderService.js

@@ -0,0 +1,78 @@
+import request from "@/utils/httpRequest";
+import { PSI_MANAGEMANT as prefix } from "../AppPath";
+import DishLibraryService from "./DishLibraryService";
+import DishTypeService from "./DishTypeService";
+
+export default class DishOrderService {
+	constructor() {
+		this.dishLibraryService = new DishLibraryService();
+		this.dishTypeService = new DishTypeService();
+	}
+	typeList() {
+		return Promise.all([
+			this.dishTypeService.list({ status: "0" }),
+			this.getEnabledDishList({}),
+		]).then(([typeList, dishList]) => {
+			const typeIds = new Set(dishList.map((item) => item.typeId));
+			return (typeList || []).filter((item) => typeIds.has(item.id));
+		});
+	}
+	dishList(param) {
+		return this.getEnabledDishList(param);
+	}
+	currentOrder(roomId) {
+		return request({
+			url: prefix + "/psi/dishOrder/currentOrder",
+			method: "get",
+			params: { roomId: roomId },
+		});
+	}
+	submit(param) {
+		return request({
+			url: prefix + "/psi/dishOrder/submit",
+			method: "post",
+			data: param,
+		});
+	}
+	cancelOrder(id) {
+		return request({
+			url: prefix + "/psi/dishOrder/cancelOrder",
+			method: "get",
+			params: { id: id },
+		});
+	}
+	orderList(param) {
+		return request({
+			url: prefix + "/psi/dishOrder/orderList",
+			method: "get",
+			params: param,
+		});
+	}
+	exportFile(param) {
+		return request({
+			url: prefix + "/psi/dishOrder/exportFile",
+			method: "get",
+			params: param,
+			responseType: "blob",
+		});
+	}
+	findOrderById(id) {
+		return request({
+			url: prefix + "/psi/dishOrder/findOrderById",
+			method: "get",
+			params: { id: id },
+		});
+	}
+	getEnabledDishList(param) {
+		return this.dishLibraryService
+			.list({
+				current: 1,
+				size: 1000000,
+				status: "0",
+				...param,
+			})
+			.then((data) => {
+				return data && data.records ? data.records : [];
+			});
+	}
+}

+ 40 - 0
src/api/psi/DishRoomService.js

@@ -0,0 +1,40 @@
+import request from "@/utils/httpRequest";
+import { PSI_MANAGEMANT as prefix } from "../AppPath";
+
+export default class DishRoomService {
+	list(param) {
+		return request({
+			url: prefix + "/psi/dishRoom/list",
+			method: "get",
+			params: param,
+		});
+	}
+	save(param) {
+		return request({
+			url: prefix + "/psi/dishRoom/save",
+			method: "post",
+			data: param,
+		});
+	}
+	findById(id) {
+		return request({
+			url: prefix + "/psi/dishRoom/findById",
+			method: "get",
+			params: { id: id },
+		});
+	}
+	delete(ids) {
+		return request({
+			url: prefix + "/psi/dishRoom/delete",
+			method: "delete",
+			params: { ids: ids },
+		});
+	}
+	changeStatus(id, status) {
+		return request({
+			url: prefix + "/psi/dishRoom/changeStatus",
+			method: "get",
+			params: { id: id, status: status },
+		});
+	}
+}

+ 47 - 0
src/api/psi/DishTypeService.js

@@ -0,0 +1,47 @@
+import request from "@/utils/httpRequest";
+import { PSI_MANAGEMANT as prefix } from "../AppPath";
+
+export default class DishTypeService {
+	list(param) {
+		return request({
+			url: prefix + "/psi/dishType/list",
+			method: "get",
+			params: param,
+		});
+	}
+	treeData(param) {
+		return request({
+			url: prefix + "/psi/dishType/treeData",
+			method: "get",
+			params: param,
+		});
+	}
+	save(param) {
+		return request({
+			url: prefix + "/psi/dishType/save",
+			method: "post",
+			data: param,
+		});
+	}
+	findById(id) {
+		return request({
+			url: prefix + "/psi/dishType/findById",
+			method: "get",
+			params: { id: id },
+		});
+	}
+	remove(id) {
+		return request({
+			url: prefix + "/psi/dishType/deleteById",
+			method: "get",
+			params: { id: id },
+		});
+	}
+	changeStatus(id, status) {
+		return request({
+			url: prefix + "/psi/dishType/changeStatus",
+			method: "get",
+			params: { id: id, status: status },
+		});
+	}
+}

+ 5 - 2
src/views/flowable/task/TaskForm.vue

@@ -772,6 +772,7 @@ export default {
 		// Process_1775013611447 进销存-领用退回申请
 		// Process_1775549295075 进销存-报损申请
 		// Process_1776049799724 进销存-食材采购统计
+		// Process_1779092642832 景聚庭-折扣结账
 		// 驳回
 		reject(vars) {
 			if (this.procDefId.includes('Process_1667978088459') ||
@@ -875,7 +876,8 @@ export default {
 				this.procDefId.includes('Process_1775013293806') ||
 				this.procDefId.includes('Process_1775013611447') ||
 				this.procDefId.includes('Process_1775549295075') ||
-				this.procDefId.includes('Process_1776049799724')
+				this.procDefId.includes('Process_1776049799724') ||
+				this.procDefId.includes('Process_1779092642832')
 
 			) {
 				console.log('进入新版驳回')
@@ -1036,7 +1038,8 @@ export default {
 				this.procDefId.includes('Process_1775013293806') ||
 				this.procDefId.includes('Process_1775013611447') ||
 				this.procDefId.includes('Process_1775549295075') ||
-				this.procDefId.includes('Process_1776049799724')
+				this.procDefId.includes('Process_1776049799724') ||
+				this.procDefId.includes('Process_1779092642832')
 
 			) {
 				if (this.formType === '2') {

+ 217 - 0
src/views/psiManagement/dishManage/creditCustomer/CreditCustomer.vue

@@ -0,0 +1,217 @@
+<template>
+	<div class="page">
+		<el-form :inline="true" v-if="searchVisible" class="query-form m-b-10" ref="searchForm" :model="searchForm"
+			@keyup.enter.native="refreshList()" @submit.native.prevent>
+			<el-form-item label="姓名" prop="customerName">
+				<el-input v-model="searchForm.customerName" placeholder="请输入姓名" clearable></el-input>
+			</el-form-item>
+			<el-form-item label="手机号" prop="mobile">
+				<el-input v-model="searchForm.mobile" placeholder="请输入手机号" clearable></el-input>
+			</el-form-item>
+			<el-form-item label="公司名称" prop="companyName">
+				<el-input v-model="searchForm.companyName" placeholder="请输入公司名称" clearable></el-input>
+			</el-form-item>
+			<el-form-item label="挂账状态" prop="creditStatus">
+				<el-select v-model="searchForm.creditStatus" placeholder="请选择挂账状态" clearable style="width: 140px">
+					<el-option v-for="item in $dictUtils.getDictList('credit_status')" :key="item.value"
+						:label="item.label" :value="item.value"></el-option>
+				</el-select>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="refreshList()" icon="el-icon-search">查询</el-button>
+				<el-button @click="resetSearch()" icon="el-icon-refresh-right">重置</el-button>
+			</el-form-item>
+		</el-form>
+
+		<div class="jp-table top">
+			<vxe-toolbar :refresh="{ query: refreshList }" custom>
+				<template #buttons>
+					<el-button v-if="hasPermission('psi:creditCustomer:add')" type="primary" icon="el-icon-plus"
+						@click="add()">新建</el-button>
+					<el-button v-if="hasPermission('psi:creditCustomer:del')" type="danger" icon="el-icon-delete" @click="del()"
+						:disabled="$refs.creditCustomerTable && $refs.creditCustomerTable.getCheckboxRecords().length === 0"
+						plain>删除</el-button>
+				</template>
+				<template #tools>
+					<vxe-button text type="primary" :title="searchVisible ? '收起检索' : '展开检索'" icon="vxe-icon-search"
+						class="tool-btn" @click="searchVisible = !searchVisible"></vxe-button>
+				</template>
+			</vxe-toolbar>
+			<div style="height: calc(100% - 90px)">
+				<vxe-table border="inner" auto-resize resizable height="auto" :loading="loading" ref="creditCustomerTable"
+					show-header-overflow show-overflow highlight-hover-row :menu-config="{}" :data="dataList"
+					:checkbox-config="{}">
+					<vxe-column type="seq" width="60" title="序号"></vxe-column>
+					<vxe-column type="checkbox" width="60"></vxe-column>
+					<vxe-column min-width="130" align="center" title="姓名" field="customerName">
+						<template #default="scope">
+							<el-link type="primary" :underline="false" v-if="hasPermission('psi:creditCustomer:view')"
+								@click="view(scope.row.id)">{{ scope.row.customerName }}</el-link>
+							<span v-else>{{ scope.row.customerName }}</span>
+						</template>
+					</vxe-column>
+					<vxe-column min-width="130" align="center" title="手机号" field="mobile"></vxe-column>
+					<vxe-column min-width="180" align="center" title="公司名称" field="companyName"></vxe-column>
+					<vxe-column width="110" align="center" title="挂账状态" field="creditStatus">
+						<template #default="scope">
+							<el-tag :type="getStatusTagType(scope.row.creditStatus)">{{ getStatusText(scope.row.creditStatus) }}</el-tag>
+						</template>
+					</vxe-column>
+					<vxe-column width="130" align="right" title="累计欠款金额" field="totalArrearsAmount">
+						<template #default="scope">{{ formatMoney(scope.row.totalArrearsAmount) }}</template>
+					</vxe-column>
+					<vxe-column width="120" align="center" title="未结账单数" field="unsettledBillCount"></vxe-column>
+					<vxe-column min-width="170" align="center" title="首次挂账时间" field="firstCreditTime"></vxe-column>
+					<vxe-column min-width="170" align="center" title="最后消费时间" field="lastConsumeTime"></vxe-column>
+					<vxe-column min-width="120" align="center" title="录入人" field="createName"></vxe-column>
+					<vxe-column min-width="120" align="center" title="审核人" field="auditorName"></vxe-column>
+					<vxe-column title="操作" width="250px" fixed="right" align="center">
+						<template #default="scope">
+							<el-button v-if="hasPermission('psi:creditCustomer:edit')" text type="primary" size="small"
+								@click="edit(scope.row.id)">修改</el-button>
+							<el-dropdown v-if="hasPermission('psi:creditCustomer:edit')" trigger="click" @command="handleStatusCommand(scope.row, $event)">
+								<el-button text type="primary" size="small">状态<i class="el-icon-arrow-down el-icon--right"></i></el-button>
+								<template #dropdown>
+									<el-dropdown-menu>
+										<el-dropdown-item v-for="item in $dictUtils.getDictList('credit_status')" :key="item.value"
+											:command="item.value" :disabled="scope.row.creditStatus === item.value">{{ item.label }}</el-dropdown-item>
+									</el-dropdown-menu>
+								</template>
+							</el-dropdown>
+							<el-button v-if="hasPermission('psi:creditCustomer:del')" text type="primary" size="small"
+								@click="del(scope.row.id)">删除</el-button>
+						</template>
+					</vxe-column>
+				</vxe-table>
+				<vxe-pager background :current-page="tablePage.currentPage" :page-size="tablePage.pageSize"
+					:total="tablePage.total" :page-sizes="[10, 20, 100, 1000, { label: '全量数据', value: 1000000 }]"
+					:layouts="['PrevPage', 'JumpNumber', 'NextPage', 'FullJump', 'Sizes', 'Total']"
+					@page-change="currentChangeHandle">
+				</vxe-pager>
+			</div>
+		</div>
+		<CreditCustomerForm ref="creditCustomerForm" @refreshList="refreshList"></CreditCustomerForm>
+	</div>
+</template>
+
+<script>
+import CreditCustomerService from '@/api/psi/CreditCustomerService'
+import CreditCustomerForm from './CreditCustomerForm'
+export default {
+	data() {
+		return {
+			searchVisible: true,
+			searchForm: {
+				customerName: '',
+				mobile: '',
+				companyName: '',
+				creditStatus: ''
+			},
+			dataList: [],
+			tablePage: {
+				total: 0,
+				currentPage: 1,
+				pageSize: 10,
+				orders: []
+			},
+			loading: false
+		}
+	},
+	creditCustomerService: null,
+	created() {
+		this.creditCustomerService = new CreditCustomerService()
+	},
+	components: {
+		CreditCustomerForm
+	},
+	mounted() {
+		this.refreshList()
+	},
+	activated() {
+		this.refreshList()
+	},
+	methods: {
+		add() {
+			this.$refs.creditCustomerForm.init('add', '')
+		},
+		edit(id) {
+			this.$refs.creditCustomerForm.init('edit', id)
+		},
+		view(id) {
+			this.$refs.creditCustomerForm.init('view', id)
+		},
+		refreshList() {
+			this.loading = true
+			this.creditCustomerService.list({
+				'current': this.tablePage.currentPage,
+				'size': this.tablePage.pageSize,
+				'orders': this.tablePage.orders,
+				...this.searchForm
+			}).then((data) => {
+				this.dataList = data.records
+				this.tablePage.total = data.total
+				this.loading = false
+			}).catch(() => {
+				this.loading = false
+			})
+		},
+		currentChangeHandle({ currentPage, pageSize }) {
+			this.tablePage.currentPage = currentPage
+			this.tablePage.pageSize = pageSize
+			this.refreshList()
+		},
+		handleStatusCommand(row, creditStatus) {
+			if (row.creditStatus === creditStatus) {
+				return
+			}
+			this.$confirm(`确定将挂账状态修改为【${this.getStatusText(creditStatus)}】吗?`, '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.creditCustomerService.changeStatus(row.id, creditStatus).then((data) => {
+					this.$message.success(data)
+					this.refreshList()
+				})
+			})
+		},
+		del(id) {
+			let ids = id || this.$refs.creditCustomerTable.getCheckboxRecords().map(item => {
+				return item.id
+			}).join(',')
+			this.$confirm(`确定删除所选项吗?`, '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.loading = true
+				this.creditCustomerService.delete(ids).then((data) => {
+					this.$message.success(data)
+					this.refreshList()
+					this.loading = false
+				}).catch(() => {
+					this.loading = false
+				})
+			})
+		},
+		resetSearch() {
+			this.$refs.searchForm.resetFields()
+			this.tablePage.currentPage = 1
+			this.refreshList()
+		},
+		getStatusText(status) {
+			return this.$dictUtils.getDictLabel('credit_status', status, "正常挂账")
+		},
+		getStatusTagType(status) {
+			return {
+				'1': 'success',
+				'2': 'warning',
+				'3': 'danger'
+			}[status] || 'success'
+		},
+		formatMoney(value) {
+			return Number(value || 0).toFixed(2)
+		}
+	}
+}
+</script>

+ 287 - 0
src/views/psiManagement/dishManage/creditCustomer/CreditCustomerForm.vue

@@ -0,0 +1,287 @@
+<template>
+	<div>
+		<el-dialog :title="title" :close-on-click-modal="false" draggable width="980px" @close="close"
+			@keyup.enter.native="doSubmit" v-model="visible">
+			<el-form :model="inputForm" ref="inputForm" v-loading="loading"
+				:class="method === 'view' ? 'readonly' : ''" :disabled="method === 'view'" label-width="130px"
+				@submit.native.prevent>
+				<div class="form-section-title">基本信息</div>
+				<el-row :gutter="20">
+					<el-col :span="8">
+						<el-form-item label="姓名" prop="customerName"
+							:rules="[{ required: true, message: '姓名不能为空', trigger: 'blur' }]">
+							<el-input v-model="inputForm.customerName" maxlength="50" placeholder="请输入姓名"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="手机号" prop="mobile"
+							:rules="[{ required: true, message: '手机号不能为空', trigger: 'blur' }]">
+							<el-input v-model="inputForm.mobile" maxlength="30" placeholder="请输入手机号"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="挂账状态" prop="creditStatus">
+							<el-select v-model="inputForm.creditStatus" placeholder="请选择挂账状态" clearable style="width: 100%">
+								<el-option v-for="item in $dictUtils.getDictList('credit_status')" :key="item.value"
+									:label="item.label" :value="item.value"></el-option>
+							</el-select>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="8">
+						<el-form-item label="生日" prop="birthday">
+							<el-date-picker v-model="inputForm.birthday" type="date" value-format="YYYY-MM-DD"
+								placeholder="请选择生日" style="width: 100%"></el-date-picker>
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="身份证号" prop="idCard">
+							<el-input v-model="inputForm.idCard" maxlength="30" placeholder="请输入身份证号"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="年龄" prop="age">
+							<el-input-number v-model="inputForm.age" :min="0" :max="150" :precision="0"
+								style="width: 100%"></el-input-number>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="8">
+						<el-form-item label="职业" prop="occupation">
+							<el-input v-model="inputForm.occupation" maxlength="100" placeholder="请输入职业"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="公司名称" prop="companyName">
+							<el-input v-model="inputForm.companyName" maxlength="200" placeholder="请输入公司名称"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="紧急联系人" prop="emergencyContact">
+							<el-input v-model="inputForm.emergencyContact" maxlength="50" placeholder="请输入紧急联系人"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="8">
+						<el-form-item label="紧急联系人电话" prop="emergencyContactPhone">
+							<el-input v-model="inputForm.emergencyContactPhone" maxlength="30" placeholder="请输入紧急联系人电话"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="16">
+						<el-form-item label="住址" prop="address">
+							<el-input v-model="inputForm.address" maxlength="500" placeholder="请输入住址"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+
+				<div class="form-section-title">开票信息</div>
+				<el-row :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="抬头" prop="invoiceTitle">
+							<el-input v-model="inputForm.invoiceTitle" maxlength="200" placeholder="请输入开票抬头"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="税号" prop="taxNo">
+							<el-input v-model="inputForm.taxNo" maxlength="100" placeholder="请输入税号"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="开户行" prop="bankName">
+							<el-input v-model="inputForm.bankName" maxlength="200" placeholder="请输入开户行"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="账号" prop="bankAccount">
+							<el-input v-model="inputForm.bankAccount" maxlength="100" placeholder="请输入账号"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="24">
+						<el-form-item label="开票地址电话" prop="invoiceAddressPhone">
+							<el-input v-model="inputForm.invoiceAddressPhone" maxlength="500" placeholder="请输入开票地址电话"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+
+				<div class="form-section-title">挂账信息</div>
+				<el-row :gutter="20">
+					<el-col :span="8">
+						<el-form-item label="累计欠款金额" prop="totalArrearsAmount">
+							<el-input-number v-model="inputForm.totalArrearsAmount" :min="0" :precision="2"
+								style="width: 100%"></el-input-number>
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="未结账单数" prop="unsettledBillCount">
+							<el-input-number v-model="inputForm.unsettledBillCount" :min="0" :precision="0"
+								style="width: 100%"></el-input-number>
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="首次挂账时间" prop="firstCreditTime">
+							<el-date-picker v-model="inputForm.firstCreditTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss"
+								placeholder="请选择首次挂账时间" style="width: 100%"></el-date-picker>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="8">
+						<el-form-item label="最后消费时间" prop="lastConsumeTime">
+							<el-date-picker v-model="inputForm.lastConsumeTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss"
+								placeholder="请选择最后消费时间" style="width: 100%"></el-date-picker>
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="录入人" prop="createName">
+							<el-input v-model="inputForm.createName" disabled placeholder="保存后自动记录"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="审核人" prop="auditorName">
+							<el-input v-model="inputForm.auditorName" disabled placeholder="暂未审核"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="24">
+						<el-form-item label="备注" prop="remarks">
+							<el-input v-model="inputForm.remarks" type="textarea" :rows="4" maxlength="500"
+								placeholder="请输入备注" show-word-limit></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="close()" icon="el-icon-circle-close">关闭</el-button>
+					<el-button type="primary" v-if="method !== 'view'" @click="doSubmit()"
+						icon="el-icon-circle-check" v-noMoreClick>确定</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script>
+import CreditCustomerService from '@/api/psi/CreditCustomerService'
+export default {
+	data() {
+		return {
+			title: '',
+			method: '',
+			visible: false,
+			loading: false,
+			inputForm: this.defaultForm()
+		}
+	},
+	creditCustomerService: null,
+	created() {
+		this.creditCustomerService = new CreditCustomerService()
+	},
+	methods: {
+		defaultForm() {
+			return {
+				customerName: '',
+				mobile: '',
+				birthday: '',
+				idCard: '',
+				age: undefined,
+				occupation: '',
+				companyName: '',
+				address: '',
+				emergencyContact: '',
+				emergencyContactPhone: '',
+				invoiceTitle: '',
+				taxNo: '',
+				bankName: '',
+				bankAccount: '',
+				invoiceAddressPhone: '',
+				creditStatus: '1',
+				totalArrearsAmount: 0,
+				unsettledBillCount: 0,
+				firstCreditTime: '',
+				lastConsumeTime: '',
+				auditorId: '',
+				auditorName: '',
+				createName: '',
+				remarks: ''
+			}
+		},
+		init(method, id) {
+			this.method = method
+			this.inputForm = this.defaultForm()
+			if (method === 'add') {
+				this.title = '新建挂账人'
+			} else if (method === 'edit') {
+				this.title = '修改挂账人'
+				this.inputForm.id = id
+			} else if (method === 'view') {
+				this.title = '查看挂账人'
+				this.inputForm.id = id
+			}
+			this.visible = true
+			this.loading = false
+			this.$nextTick(() => {
+				this.$refs.inputForm && this.$refs.inputForm.clearValidate()
+				if (method === 'edit' || method === 'view') {
+					this.loading = true
+					this.creditCustomerService.findById(this.inputForm.id).then((data) => {
+						this.inputForm = this.recover(this.inputForm, data)
+						this.inputForm.totalArrearsAmount = Number(this.inputForm.totalArrearsAmount || 0)
+						this.inputForm.unsettledBillCount = Number(this.inputForm.unsettledBillCount || 0)
+						this.inputForm = JSON.parse(JSON.stringify(this.inputForm))
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		doSubmit() {
+			this.$refs.inputForm.validate((valid) => {
+				if (valid) {
+					this.loading = true
+					this.creditCustomerService.save(this.inputForm).then((data) => {
+						if (data && data.indexOf('重复') > -1) {
+							this.$message.error(data)
+						} else {
+							this.$message.success(data)
+							this.close()
+							this.$emit('refreshList')
+						}
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		close() {
+			this.$refs.inputForm && this.$refs.inputForm.resetFields()
+			this.visible = false
+		}
+	}
+}
+</script>
+
+<style scoped>
+.form-section-title {
+	font-size: 14px;
+	font-weight: 600;
+	color: #303133;
+	padding: 4px 0 12px;
+}
+
+.form-section-title:not(:first-child) {
+	margin-top: 8px;
+	border-top: 1px solid #ebeef5;
+	padding-top: 16px;
+}
+</style>

+ 343 - 0
src/views/psiManagement/dishManage/library/DishImageUpload.vue

@@ -0,0 +1,343 @@
+<template>
+	<div :key="uploadKey" class="dish-image-upload">
+		<el-upload ref="upload" action="" :limit="limit" :http-request="httpRequest" :multiple="true"
+			:disabled="currentAuth === 'view'" :on-exceed="onExceed" :show-file-list="false" :on-change="changes"
+			:on-progress="uploadProcess" :file-list="fileList" accept=".jpg,.jpeg,.png,.gif,.bmp">
+			<template v-if="currentAuth === 'view'" #tip>
+				<el-button :loading="loading" type="primary" size="default" :disabled="true">点击上传</el-button>
+			</template>
+			<template v-else #trigger>
+				<el-button :loading="loading" type="primary" size="default">点击上传</el-button>
+			</template>
+			<template #tip>
+				<div class="el-upload__tip">
+					支持上传多张图片,格式限 JPG、JPEG、PNG、GIF、BMP,单张不超过 {{ maxValue }}MB
+				</div>
+			</template>
+		</el-upload>
+
+		<el-progress v-if="progressFlag" :percentage="loadProgress" style="margin-top: 10px"></el-progress>
+
+		<div class="image-list">
+			<div class="image-item" v-for="(item, index) in dataListNew" :key="item.url || item.uid || index">
+				<el-image class="image-thumb" :src="item.lsUrl || item.url" :preview-src-list="previewList"
+					:initial-index="index" fit="cover" hide-on-click-modal :preview-teleported="true" :z-index="9999">
+					<template #error>
+						<div class="image-error">图片</div>
+					</template>
+				</el-image>
+				<div v-if="currentAuth !== 'view'" class="image-actions">
+					<el-tooltip effect="dark" content="删除" placement="top">
+						<el-icon style="cursor: pointer;">
+							<Delete @click="deleteById(item, index)" />
+						</el-icon>
+					</el-tooltip>
+				</div>
+				<el-tooltip effect="dark" :content="item.name" placement="top">
+					<div class="image-name">{{ item.name }}</div>
+				</el-tooltip>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+import OSSSerivce, {
+	httpRequest,
+	fileName,
+	beforeAvatarUpload
+} from '@/api/sys/OSSService'
+import moment from 'moment'
+export default {
+	props: {
+		modelValue: {
+			type: String,
+			default: ''
+		},
+		auth: {
+			type: String,
+			default: ''
+		},
+		limit: {
+			type: Number,
+			default: 9
+		},
+		directory: {
+			type: String,
+			default: 'dish'
+		},
+		maxSize: {
+			type: Number,
+			default: 10
+		}
+	},
+	data() {
+		return {
+			uploadKey: '',
+			progressFlag: false,
+			loadProgress: 0,
+			fileList: [],
+			dataList: [],
+			dataListNew: [],
+			ossService: null,
+			localAuth: '',
+			uploadDirectory: 'dish',
+			maxValue: 10,
+			loading: false,
+			fileLoading: true,
+			innerValue: '',
+			setValueKey: 0
+		}
+	},
+	computed: {
+		previewList() {
+			return this.dataListNew.map(item => item.lsUrl || item.url).filter(item => item)
+		},
+		currentAuth() {
+			return this.localAuth || this.auth
+		}
+	},
+	watch: {
+		modelValue(val) {
+			if (val !== this.innerValue) {
+				this.setValue(val)
+			}
+		}
+	},
+	created() {
+		this.ossService = new OSSSerivce()
+	},
+	mounted() {
+		this.uploadDirectory = this.directory || 'dish'
+		this.maxValue = this.maxSize || 10
+		this.setValue(this.modelValue)
+	},
+	methods: {
+		async newUpload(auth, value, directory, maxValue) {
+			this.localAuth = auth
+			this.uploadDirectory = directory || 'dish'
+			this.maxValue = maxValue || 10
+			await this.setValue(value)
+		},
+		async setValue(value) {
+			const key = ++this.setValueKey
+			const urls = this.parseValue(value)
+			this.innerValue = urls.join(',')
+			this.dataList = []
+			this.dataListNew = []
+			this.fileList = []
+			this.fileLoading = false
+			for (const url of urls) {
+				const item = {
+					name: this.getFileNameByUrl(url),
+					url: url,
+					status: 'success'
+				}
+				await this.fillPreviewUrl(item)
+				if (key !== this.setValueKey) {
+					return
+				}
+				this.dataList.push(item)
+				this.dataListNew.push(item)
+				this.fileList.push(item)
+			}
+			this.fileLoading = true
+		},
+		parseValue(value) {
+			if (value === undefined || value === null || value === '') {
+				return []
+			}
+			if (Array.isArray(value)) {
+				return value.map(item => item.url || item).filter(item => item)
+			}
+			return value.split(',').map(item => item.trim()).filter(item => item)
+		},
+		async httpRequest(file) {
+			await httpRequest(file, fileName(file), this.uploadDirectory, this.maxValue)
+		},
+		uploadProcess(event) {
+			this.progressFlag = true
+			this.loadProgress = parseInt(event.percent)
+			if (this.loadProgress >= 100) {
+				this.loadProgress = 100
+				setTimeout(() => {
+					this.progressFlag = false
+				}, 1000)
+			}
+		},
+		async changes(file) {
+			if (file.status === 'ready') {
+				return
+			}
+			if (this.dataListNew.length >= this.limit) {
+				this.$message.warning(`当前限制上传 ${this.limit} 张图片`)
+				return
+			}
+			if (!this.isImage(file.name)) {
+				this.$message.warning('仅支持 JPG、JPEG、PNG、GIF、BMP 图片')
+				return
+			}
+			if (!beforeAvatarUpload(file.raw || file, [], this.maxValue)) {
+				this.$message.error('图片大小不能超过 ' + this.maxValue + ' MB')
+				return
+			}
+			const url = file.raw && file.raw.url ? file.raw.url : file.url
+			if (!url) {
+				return
+			}
+			const exists = this.dataListNew.some(item => item.url === url || item.name === file.name)
+			if (exists) {
+				this.$message.error(`${file.name}已存在,无法重复上传`)
+				return
+			}
+			const item = {
+				name: file.name,
+				url: url,
+				status: 'success',
+				createTime: moment(new Date()).format('YYYY-MM-DD HH:mm:ss'),
+				createBy: {
+					id: this.$store.state.user.id,
+					name: this.$store.state.user.name
+				}
+			}
+			await this.fillPreviewUrl(item)
+			this.fileList.push(item)
+			this.dataList.push(item)
+			this.dataListNew.push(item)
+			this.emitValue()
+		},
+		async fillPreviewUrl(item) {
+			if (!item.url) {
+				return
+			}
+			if (item.url.indexOf('http') === 0) {
+				item.lsUrl = item.url
+				return
+			}
+			try {
+				item.lsUrl = await this.ossService.getTemporaryUrl(item.url)
+			} catch (e) {
+				item.lsUrl = item.url
+			}
+		},
+		deleteById(row, index) {
+			this.dataListNew.splice(index, 1)
+			this.dataList = this.dataList.filter(item => item.url !== row.url)
+			this.fileList = this.fileList.filter(item => item.url !== row.url)
+			this.emitValue()
+		},
+		clearUpload() {
+			this.$refs.upload && this.$refs.upload.clearFiles()
+			this.dataList = []
+			this.dataListNew = []
+			this.fileList = []
+			this.innerValue = ''
+			this.$emit('update:modelValue', '')
+		},
+		getDataList() {
+			return this.dataListNew
+		},
+		checkProgress() {
+			if (this.progressFlag === true) {
+				this.$message.warning('请等待图片上传完成后再进行操作')
+				return true
+			}
+			if (this.fileLoading === false) {
+				this.$message.warning('请等待图片加载完成后再进行操作')
+				return true
+			}
+			const invalidFile = this.dataListNew.find(file => !file.url)
+			if (invalidFile) {
+				this.$message.warning(`${invalidFile.name}的URL为空,请检查后重新上传`)
+				return true
+			}
+			return false
+		},
+		emitValue() {
+			const value = this.dataListNew.map(item => item.url).filter(item => item).join(',')
+			this.innerValue = value
+			this.$emit('update:modelValue', value)
+		},
+		isImage(name) {
+			const suffix = (name || '').substring((name || '').lastIndexOf('.') + 1).toLowerCase()
+			return ['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(suffix)
+		},
+		getFileNameByUrl(url) {
+			if (!url) {
+				return ''
+			}
+			const arr = url.split('/')
+			return arr[arr.length - 1]
+		},
+		onExceed() {
+			this.$message.warning(`当前限制上传 ${this.limit} 张图片`)
+		}
+	}
+}
+</script>
+
+<style scoped>
+.dish-image-upload {
+	width: 100%;
+}
+
+.image-list {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 10px;
+	margin-top: 10px;
+}
+
+.image-item {
+	width: 104px;
+	position: relative;
+	border-radius: 4px;
+	background-color: #f7f9fa;
+	padding: 6px;
+}
+
+.image-thumb {
+	width: 92px;
+	height: 92px;
+	border-radius: 4px;
+	background-color: #f0f2f5;
+}
+
+.image-actions {
+	position: absolute;
+	top: 6px;
+	right: 6px;
+	width: 24px;
+	height: 24px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	border-radius: 0 4px 0 4px;
+	background: rgba(245, 108, 108, 0.9);
+	color: #fff;
+}
+
+.image-name {
+	margin-top: 4px;
+	font-size: 12px;
+	line-height: 18px;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+
+.image-error {
+	width: 100%;
+	height: 100%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	color: #909399;
+	font-size: 12px;
+}
+
+.el-upload__tip {
+	color: #909399;
+	line-height: 20px;
+}
+</style>

+ 247 - 0
src/views/psiManagement/dishManage/library/DishLibrary.vue

@@ -0,0 +1,247 @@
+<template>
+	<div class="page">
+		<el-form :inline="true" v-if="searchVisible" class="query-form m-b-10" ref="searchForm" :model="searchForm"
+			@keyup.enter.native="refreshList()" @submit.native.prevent>
+			<el-form-item label="菜品名称" prop="dishName">
+				<el-input v-model="searchForm.dishName" placeholder="请输入菜品名称" clearable></el-input>
+			</el-form-item>
+			<el-form-item label="菜品编码" prop="dishCode">
+				<el-input v-model="searchForm.dishCode" placeholder="请输入菜品编码" clearable></el-input>
+			</el-form-item>
+			<el-form-item label="菜品分类" prop="typeId">
+				<SelectTree ref="searchDishTypeTree" :props="{
+					value: 'id',
+					label: 'name',
+					children: 'children'
+				}" url="/psi-management-server/psi/dishType/treeData" :value="searchForm.typeId" :clearable="true"
+					:accordion="true" size="default" style="width: 180px"
+					@getValue="(value) => { searchForm.typeId = value }" />
+			</el-form-item>
+			<el-form-item label="状态" prop="status">
+				<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 120px">
+					<el-option label="启用" value="0"></el-option>
+					<el-option label="停用" value="1"></el-option>
+				</el-select>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="refreshList()" icon="el-icon-search">查询</el-button>
+				<el-button @click="resetSearch()" icon="el-icon-refresh-right">重置</el-button>
+			</el-form-item>
+		</el-form>
+
+		<div class="jp-table top">
+			<vxe-toolbar :refresh="{ query: refreshList }" custom>
+				<template #buttons>
+					<el-button v-if="hasPermission('psi:dish:add')" type="primary" icon="el-icon-plus"
+						@click="add()">新建</el-button>
+					<el-button v-if="hasPermission('psi:dish:del')" type="danger" icon="el-icon-delete" @click="del()"
+						:disabled="$refs.dishTable && $refs.dishTable.getCheckboxRecords().length === 0"
+						plain>删除</el-button>
+				</template>
+				<template #tools>
+					<vxe-button text type="primary" :title="searchVisible ? '收起检索' : '展开检索'" icon="vxe-icon-search"
+						class="tool-btn" @click="searchVisible = !searchVisible"></vxe-button>
+				</template>
+			</vxe-toolbar>
+			<div style="height: calc(100% - 90px)">
+				<vxe-table border="inner" auto-resize resizable height="auto" :loading="loading" ref="dishTable"
+					show-header-overflow show-overflow highlight-hover-row :menu-config="{}" :data="dataList"
+					:checkbox-config="{}">
+					<vxe-column type="seq" width="60" title="序号"></vxe-column>
+					<vxe-column type="checkbox" width="60"></vxe-column>
+					<vxe-column width="80" align="center" title="菜品图片" field="imageUrl">
+						<template #default="scope">
+							<el-image v-if="scope.row.previewImageUrl" :src="scope.row.previewImageUrl"
+								:preview-src-list="scope.row.previewImageList" fit="cover"
+								style="width: 44px; height: 44px; border-radius: 4px;" hide-on-click-modal
+								append-to-body :z-index="9999"></el-image>
+							<span v-else>暂无图片</span>
+						</template>
+					</vxe-column>
+					<vxe-column min-width="150" align="center" title="菜品编码" field="dishCode"></vxe-column>
+					<vxe-column min-width="180" align="center" title="菜品名称" field="dishName">
+						<template #default="scope">
+							<el-link type="primary" :underline="false" v-if="hasPermission('psi:dish:view')"
+								@click="view(scope.row.id)">{{ scope.row.dishName }}</el-link>
+							<span v-else>{{ scope.row.dishName }}</span>
+						</template>
+					</vxe-column>
+					<vxe-column min-width="150" align="center" title="菜品分类" field="typeName"></vxe-column>
+					<vxe-column width="90" align="center" title="单位" field="unit"></vxe-column>
+					<vxe-column min-width="120" align="center" title="规格" field="spec"></vxe-column>
+					<vxe-column width="110" align="center" title="销售价" field="salePrice"></vxe-column>
+					<vxe-column width="120" align="center" title="参考成本价" field="costPrice"></vxe-column>
+					<vxe-column width="100" align="center" title="状态" field="status">
+						<template #default="scope">
+							<el-tag :type="scope.row.status === '0' ? 'success' : 'info'">
+								{{ scope.row.status === '0' ? '启用' : '停用' }}
+							</el-tag>
+						</template>
+					</vxe-column>
+					<vxe-column min-width="130" align="center" title="创建人" field="createName"></vxe-column>
+					<vxe-column min-width="180" align="center" title="创建时间" field="createTime"></vxe-column>
+					<vxe-column title="操作" width="220px" fixed="right" align="center">
+						<template #default="scope">
+							<el-button v-if="hasPermission('psi:dish:edit')" text type="primary" size="small"
+								@click="edit(scope.row.id)">修改</el-button>
+							<el-button v-if="hasPermission('psi:dish:edit')" text type="primary" size="small"
+								@click="changeStatus(scope.row)">{{ scope.row.status === '0' ? '停用' : '启用'
+								}}</el-button>
+							<el-button v-if="hasPermission('psi:dish:del')" text type="primary" size="small"
+								@click="del(scope.row.id)">删除</el-button>
+						</template>
+					</vxe-column>
+				</vxe-table>
+				<vxe-pager background :current-page="tablePage.currentPage" :page-size="tablePage.pageSize"
+					:total="tablePage.total" :page-sizes="[10, 20, 100, 1000, { label: '全量数据', value: 1000000 }]"
+					:layouts="['PrevPage', 'JumpNumber', 'NextPage', 'FullJump', 'Sizes', 'Total']"
+					@page-change="currentChangeHandle">
+				</vxe-pager>
+			</div>
+		</div>
+		<DishLibraryForm ref="dishLibraryForm" @refreshList="refreshList"></DishLibraryForm>
+	</div>
+</template>
+
+<script>
+import DishLibraryService from '@/api/psi/DishLibraryService'
+import DishLibraryForm from './DishLibraryForm'
+import SelectTree from '@/components/treeSelect/treeSelect.vue'
+import OSSSerivce from '@/api/sys/OSSService'
+export default {
+	data() {
+		return {
+			searchVisible: true,
+			searchForm: {
+				dishName: '',
+				dishCode: '',
+				typeId: '',
+				status: ''
+			},
+			dataList: [],
+			tablePage: {
+				total: 0,
+				currentPage: 1,
+				pageSize: 10,
+				orders: []
+			},
+			loading: false
+		}
+	},
+	dishLibraryService: null,
+	ossService: null,
+	created() {
+		this.dishLibraryService = new DishLibraryService()
+		this.ossService = new OSSSerivce()
+	},
+	components: {
+		DishLibraryForm,
+		SelectTree
+	},
+	mounted() {
+		this.refreshList()
+	},
+	activated() {
+		this.refreshList()
+	},
+	methods: {
+		add() {
+			this.$refs.dishLibraryForm.init('add', '')
+		},
+		edit(id) {
+			this.$refs.dishLibraryForm.init('edit', id)
+		},
+		view(id) {
+			this.$refs.dishLibraryForm.init('view', id)
+		},
+		refreshList() {
+			this.loading = true
+			this.dishLibraryService.list({
+				'current': this.tablePage.currentPage,
+				'size': this.tablePage.pageSize,
+				'orders': this.tablePage.orders,
+				...this.searchForm
+			}).then((data) => {
+				this.dataList = data.records
+				this.tablePage.total = data.total
+				this.loading = false
+				this.loadImagePreview()
+			}).catch(() => {
+				this.loading = false
+			})
+		},
+		loadImagePreview() {
+			this.dataList.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)
+		},
+		currentChangeHandle({ currentPage, pageSize }) {
+			this.tablePage.currentPage = currentPage
+			this.tablePage.pageSize = pageSize
+			this.refreshList()
+		},
+		changeStatus(row) {
+			const status = row.status === '0' ? '1' : '0'
+			const message = status === '0' ? '确定启用该菜品吗?' : '确定停用该菜品吗?'
+			this.$confirm(message, '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.dishLibraryService.changeStatus(row.id, status).then((data) => {
+					this.$message.success(data)
+					this.refreshList()
+				})
+			})
+		},
+		del(id) {
+			let ids = id || this.$refs.dishTable.getCheckboxRecords().map(item => {
+				return item.id
+			}).join(',')
+			this.$confirm(`确定删除所选项吗?`, '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.loading = true
+				this.dishLibraryService.delete(ids).then((data) => {
+					this.$message.success(data)
+					this.refreshList()
+					this.loading = false
+				}).catch(() => {
+					this.loading = false
+				})
+			})
+		},
+		resetSearch() {
+			this.$refs.searchForm.resetFields()
+			this.searchForm.typeId = ''
+			this.tablePage.currentPage = 1
+			this.refreshList()
+		}
+	}
+}
+</script>

+ 230 - 0
src/views/psiManagement/dishManage/library/DishLibraryForm.vue

@@ -0,0 +1,230 @@
+<template>
+	<div>
+		<el-dialog :title="title" :close-on-click-modal="false" draggable width="900px" @close="close"
+			@keyup.enter.native="doSubmit" v-model="visible">
+			<el-form :model="inputForm" ref="inputForm" v-loading="loading" :class="method === 'view' ? 'readonly' : ''"
+				:disabled="method === 'view'" label-width="110px" @submit.native.prevent>
+				<el-row :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="菜品编码" prop="dishCode">
+							<el-input v-model="inputForm.dishCode" placeholder="不填写则自动生成"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="菜品名称" prop="dishName"
+							:rules="[{ required: true, message: '菜品名称不能为空', trigger: 'blur' }]">
+							<el-input v-model="inputForm.dishName" placeholder="请填写菜品名称"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="菜品分类" prop="typeId"
+							:rules="[{ required: true, message: '请选择菜品分类', trigger: 'change' }]">
+							<SelectTree ref="dishTypeTree" v-if="visible" :props="{
+								value: 'id',
+								label: 'name',
+								children: 'children'
+							}" url="/psi-management-server/psi/dishType/treeData" :value="inputForm.typeId" :clearable="true"
+								:accordion="true" :is-only-select-leaf="true" size="default"
+								@getValue="(value) => { inputForm.typeId = value }" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="单位" prop="unit">
+							<el-input v-model="inputForm.unit" placeholder="如:份、盘、碗"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="规格" prop="spec">
+							<el-input v-model="inputForm.spec" placeholder="如:300g/份"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="口味" prop="taste">
+							<el-input v-model="inputForm.taste" placeholder="如:微辣、清淡"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="销售价(元)" prop="salePrice">
+							<el-input-number v-model="inputForm.salePrice" :min="0" :precision="2"
+								style="width: 100%"></el-input-number>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="参考成本价(元)" prop="costPrice">
+							<el-input-number v-model="inputForm.costPrice" :min="0" :precision="2"
+								style="width: 100%"></el-input-number>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="排序" prop="sort">
+							<el-input-number v-model="inputForm.sort" :min="0" style="width: 100%"></el-input-number>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="状态" prop="status">
+							<el-radio-group v-model="inputForm.status">
+								<el-radio label="0">启用</el-radio>
+								<el-radio label="1">停用</el-radio>
+							</el-radio-group>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+
+					<el-col :span="24">
+						<el-form-item label="备注" prop="remarks">
+							<el-input v-model="inputForm.remarks" type="textarea" :rows="6" maxlength="500"
+								placeholder="请输入备注" show-word-limit></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="菜品图片" prop="imageUrl">
+							<DishImageUpload ref="dishImageUpload" v-model="inputForm.imageUrl" :auth="method"
+								directory="dish" :max-size="10" :limit="9">
+							</DishImageUpload>
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="close()" icon="el-icon-circle-close">关闭</el-button>
+					<el-button type="primary" v-if="method !== 'view'" @click="doSubmit()" icon="el-icon-circle-check"
+						v-noMoreClick>确定</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script>
+import DishLibraryService from '@/api/psi/DishLibraryService'
+import SelectTree from './DishTypeSelectTree.vue'
+import DishImageUpload from './DishImageUpload'
+export default {
+	data() {
+		return {
+			title: '',
+			method: '',
+			visible: false,
+			loading: false,
+			inputForm: {
+				dishCode: '',
+				dishName: '',
+				typeId: '',
+				imageUrl: '',
+				unit: '',
+				spec: '',
+				salePrice: 0,
+				costPrice: 0,
+				taste: '',
+				status: '0',
+				sort: 0,
+				remarks: ''
+			}
+		}
+	},
+	dishLibraryService: null,
+	created() {
+		this.dishLibraryService = new DishLibraryService()
+	},
+	components: {
+		SelectTree,
+		DishImageUpload
+	},
+	methods: {
+		init(method, id) {
+			this.method = method
+			this.inputForm = {
+				dishCode: '',
+				dishName: '',
+				typeId: '',
+				imageUrl: '',
+				unit: '',
+				spec: '',
+				salePrice: 0,
+				costPrice: 0,
+				taste: '',
+				status: '0',
+				sort: 0,
+				remarks: ''
+			}
+			if (method === 'add') {
+				this.title = '新建菜品'
+			} else if (method === 'edit') {
+				this.title = '修改菜品'
+				this.inputForm.id = id
+			} else if (method === 'view') {
+				this.title = '查看菜品'
+				this.inputForm.id = id
+			}
+			this.visible = true
+			this.loading = false
+			this.$nextTick(() => {
+				if (method === 'edit' || method === 'view') {
+					this.loading = true
+					this.$refs.inputForm.resetFields()
+					this.dishLibraryService.findById(this.inputForm.id).then((data) => {
+						this.inputForm = this.recover(this.inputForm, data)
+						this.inputForm = JSON.parse(JSON.stringify(this.inputForm))
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		doSubmit() {
+			this.$refs.inputForm.validate((valid) => {
+				if (valid) {
+					if (!this.validateDishTypeLeaf()) {
+						return
+					}
+					this.loading = true
+					if (this.$refs.dishImageUpload && this.$refs.dishImageUpload.checkProgress()) {
+						this.loading = false
+						return
+					}
+					this.dishLibraryService.save(this.inputForm).then((data) => {
+						if (data && (data.indexOf('重复') > -1 || data.indexOf('请选择') > -1)) {
+							this.$message.error(data)
+						} else {
+							this.$message.success(data)
+							this.close()
+							this.$emit('refreshList')
+						}
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		validateDishTypeLeaf() {
+			const dishTypeTree = this.$refs.dishTypeTree
+			if (!dishTypeTree || !this.inputForm.typeId) {
+				return true
+			}
+			const node = dishTypeTree.getNode(this.inputForm.typeId)
+			if (node && node.childNodes && node.childNodes.length > 0) {
+				this.$message.warning('菜品分类只能选择最子级分类')
+				return false
+			}
+			return true
+		},
+		close() {
+			this.$refs.inputForm && this.$refs.inputForm.resetFields()
+			this.$refs.dishImageUpload && this.$refs.dishImageUpload.clearUpload()
+			this.visible = false
+		}
+	}
+}
+</script>

+ 305 - 0
src/views/psiManagement/dishManage/library/DishTypeSelectTree.vue

@@ -0,0 +1,305 @@
+<template>
+  <el-select v-model="valueTitle" :size="size"  :disabled="disabled" :clearable="clearable" :placeholder="placeholderText" @clear="clearHandle">
+    <el-option :value="valueTitle"  :label="valueTitle" class="options">
+      <el-tree  id="tree-option"
+        ref="selectTree"
+        :accordion="accordion"
+        :data="optionData"
+        :show-checkbox="showCheckbox"
+        :props="props"
+        highlight-current
+        :check-strictly="checkStrictly"
+        :check-on-click-node="checkOnClickNode"
+        :node-key="props.value"
+        :default-expanded-keys="defaultExpandedKey"
+        @check-change="handleCheckChange"
+        @node-click="handleNodeClick">
+      </el-tree>
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+export default {
+  name: 'el-tree-select',
+  props: {
+    /* 配置项 */
+    props: {
+      type: Object,
+      default: () => {
+        return {
+          value: 'id',             // ID字段名
+          label: 'label',         // 显示名称
+          children: 'children'    // 子级字段名
+        }
+      }
+    },
+    /* 选项列表数据(树形结构的对象数组) */
+    data: {
+      type: Array,
+      default: () => { return [] }
+    },
+     /* 选项列表数据(树形结构的对象数组) */
+    list: {
+      type: Array,
+      default: () => { return null }
+    },
+    /* 初始值 */
+    value: {
+      type: String,
+      default: () => { return '' }
+    },
+        /* 初始值 */
+    url: {
+      type: String,
+      default: () => { return null }
+    },
+    disabled: {
+      type: Boolean,
+      dafault: () => { return false }
+    },
+    showCheckbox: {
+      type: Boolean,
+      dafault: () => { return false }
+    },
+    /* 初始值 */
+    label: {
+      type: String,
+      default: () => { return '' }
+    },
+    /* 可清空选项 */
+    clearable: {
+      type: Boolean,
+      default: () => { return true }
+    },
+    /* 自动收起 */
+    accordion: {
+      type: Boolean,
+      default: () => { return false }
+    },
+    size: {
+      type: String,
+      default: () => { return 'small' }
+    },
+    placeholder: {
+      type: String,
+      default: () => { return '请选择' }
+    },
+    isOnlySelectLeaf: {
+      type: Boolean,
+      default: () => {
+        return false
+      }
+    },
+    // 在显示复选框的情况下,是否严格的遵循父子不互相关联的做法,默认为 false
+    checkStrictly: {
+      type: Boolean,
+      default: () => {
+        return false
+      }
+    },
+    // 是否在点击节点的时候选中节点,默认值为 false,即只有在点击复选框时才会选中节点。
+    checkOnClickNode: {
+      type: Boolean,
+      default: () => {
+        return false
+      }
+    }
+  },
+  data () {
+    return {
+      valueId: this.value,    // 初始值
+      valueTitle: this.label,
+      defaultExpandedKey: [],
+      placeholderText: this.placeholder,
+      treeList: [],
+      valueData: this.data
+    }
+  },
+  created () {
+    if (this.url !== null) {
+      this.placeholderText = '加载数据中...'
+      let interval = setInterval(() => {
+        this.placeholderText = this.placeholderText + '.'
+      }, 500)
+      this.$http({
+        url: this.url,
+        method: 'get'
+      }).then((data) => {
+        this.valueData = data
+        this.setTreeList(this.valueData)
+        this.$nextTick(() => {
+          this.initHandle()
+          this.placeholderText = this.placeholder
+          clearInterval(interval)
+        })
+      })
+    } else {
+      this.valueData = this.data
+      this.setTreeList(this.valueData)
+    }
+  },
+  methods: {
+    setTreeList (datas) { // 遍历树  获取id数组
+      for (var i in datas) {
+        this.treeList.push(datas[i])
+        if (datas[i].children) {
+          this.setTreeList(datas[i].children)
+        }
+      }
+    },
+    // 初始化值
+    initHandle () {
+      if (this.valueId) {
+        if (this.showCheckbox) {
+          let ids = this.valueId.split(',')
+          this.$refs.selectTree.setCheckedKeys(ids)
+          let titles = []
+          ids.forEach((id) => {
+            this.treeList.forEach((d) => {
+              if (id === d[this.props.value]) {
+                titles.push(d[this.props.label])
+              }
+            })
+          })
+
+          this.valueTitle = titles.join(',')
+        } else if (this.$refs.selectTree.getNode(this.valueId)) {
+          this.valueTitle = this.$refs.selectTree.getNode(this.valueId).data[this.props.label]     // 初始化显示
+          this.$refs.selectTree.setCurrentKey(this.valueId)       // 设置默认选中
+          this.defaultExpandedKey = [this.valueId]      // 设置默认展开
+        }
+      }
+      this.initScroll()
+    },
+    getNode (id) {
+      return this.$refs.selectTree.getNode(id)
+    },
+    // 初始化滚动条
+    initScroll () {
+      this.$nextTick(() => {
+        let scrollWrap = document.querySelectorAll('.el-scrollbar .el-select-dropdown__wrap')[0]
+        let scrollBar = document.querySelectorAll('.el-scrollbar .el-scrollbar__bar')
+        if (scrollWrap) { scrollWrap.style.cssText = 'margin: 0px; max-height: none; overflow: hidden;' }
+        if (scrollBar) {
+          scrollBar.forEach(ele => {
+          // eslint-disable-next-line no-return-assign
+            return ele.style.width = 0
+          })
+        }
+      })
+    },
+    // 切换选项
+    handleNodeClick (node) {
+      if (this.showCheckbox) {
+        return
+      }
+      if (node['disabled']) {
+        // this.$message.warning('节点(' + node[this.props.label] + ')被禁止选择,请重新选择。')
+        return
+      }
+      const children = node[this.props.children] || []
+      if (this.isOnlySelectLeaf && children.length > 0) {
+        this.$message.warning('菜品分类只能选择最子级分类')
+        return
+      }
+      this.valueTitle = node[this.props.label]
+      this.valueId = node[this.props.value]
+      this.$emit('getValue', this.valueId, this.valueTitle, node)
+    },
+    handleCheckChange (data, checked, indeterminate) {
+      let nodes = this.$refs.selectTree.getCheckedNodes()
+      this.valueTitle = nodes.map((node) => {
+        return node[this.props.label]
+      }).join(',')
+      this.valueId = nodes.map((node) => {
+        return node[this.props.value]
+      }).join(',')
+      this.$emit('getValue', this.valueId, this.valueTitle)
+    },
+    // 清除选中
+    clearHandle () {
+      this.valueTitle = ''
+      this.valueId = null
+      this.defaultExpandedKey = []
+      this.clearSelected()
+      this.$emit('getValue', null, null, null)
+    },
+    /* 清空选中样式 */
+    clearSelected () {
+      let allNode = document.querySelectorAll('#tree-option .el-tree-node')
+      allNode.forEach((element) => element.classList.remove('is-current'))
+    }
+  },
+  watch: {
+    value () {
+      this.valueId = this.value
+      if (this.value === '' || this.value === null || this.value === undefined) {
+        this.clearHandle()
+      } else {
+        this.initHandle()
+      }
+    },
+    data () {
+      this.valueData = this.data
+    }
+  },
+  computed: {
+    optionData () {
+      if (this.list) {
+        let cloneData = JSON.parse(JSON.stringify(this.list))      // 对源数据深度克隆
+        return cloneData.filter(father => {                      // 循环所有项,并添加children属性
+          let branchArr = cloneData.filter(child => father.id === child.parentId)       // 返回每一项的子级数组
+            // eslint-disable-next-line no-unused-expressions
+          branchArr.length > 0 ? father.children = branchArr : ''   // 给父级添加一个children属性,并赋值
+          return father.parentId === '0'      // 返回第一层
+        })
+      } else {
+        return this.valueData
+      }
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+  .el-select{
+    width: 100%;
+  }
+  .el-scrollbar .el-scrollbar__view .el-select-dropdown__item{
+    height: auto;
+    max-height: 274px;
+    padding: 0;
+    overflow: hidden;
+    overflow-y: auto;
+  }
+  .el-select-dropdown__item.selected{
+    font-weight: normal;
+  }
+  ul li >>>.el-tree .el-tree-node__content{
+    height:auto;
+    padding: 0 20px;
+  }
+  .el-tree-node__label{
+    font-weight: normal;
+  }
+  .el-tree >>>.is-current .el-tree-node__label{
+    color: #409EFF;
+    font-weight: 700;
+  }
+  .el-tree >>>.is-current .el-tree-node__children .el-tree-node__label{
+    color:#606266;
+    font-weight: normal;
+  }
+  /* 开发禁用 */
+  /* .el-tree-node:focus>.el-tree-node__content{
+    background-color:transparent;
+    background-color: #f5f7fa;
+    color: #c0c4cc;
+    cursor: not-allowed;
+  }
+  .el-tree-node__content:hover{
+    background-color: #f5f7fa;
+  } */
+</style>

+ 207 - 0
src/views/psiManagement/dishManage/order/DishOrder.vue

@@ -0,0 +1,207 @@
+<template>
+	<div class="page dish-order-page">
+		<el-form :inline="true" class="query-form m-b-10" ref="searchForm" :model="searchForm"
+			@keyup.enter.native="refreshRoomList()" @submit.native.prevent>
+			<el-form-item label="包房名称" prop="roomName">
+				<el-input v-model="searchForm.roomName" placeholder="请输入包房名称" clearable></el-input>
+			</el-form-item>
+			<el-form-item label="包房编号" prop="roomNo">
+				<el-input v-model="searchForm.roomNo" placeholder="请输入包房编号" clearable></el-input>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="refreshRoomList()" icon="el-icon-search">查询</el-button>
+				<el-button @click="resetSearch()" icon="el-icon-refresh-right">重置</el-button>
+			</el-form-item>
+		</el-form>
+
+		<div class="room-board" v-loading="roomLoading">
+			<div class="room-grid">
+				<div v-for="item in roomList" :key="item.id" class="room-card"
+					:class="{ 'room-card-disabled': item.status === '1' }" @click="enterOrder(item)">
+					<div class="room-card-top">
+						<div>
+							<div class="room-name">{{ item.roomName }}</div>
+							<div class="room-no">{{ '包房号:' + (item.roomNo || '-') }}</div>
+						</div>
+						<el-tag :type="item.useStatus === '0' ? 'success' : 'primary'">
+							{{ item.useStatus === '0' ? '空闲中' : '使用中' }}
+						</el-tag>
+					</div>
+					<div class="room-meta">
+						<div>
+							<span class="meta-label">容纳人数</span>
+							<span class="meta-value">{{ item.capacity || 0 }} 人</span>
+						</div>
+					</div>
+					<div class="room-remarks">{{ item.remarks || '暂无备注' }}</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+import DishRoomService from '@/api/psi/DishRoomService'
+export default {
+	data() {
+		return {
+			searchForm: {
+				roomName: '',
+				roomNo: '',
+				status: '0'
+			},
+			roomList: [],
+			roomLoading: false
+		}
+	},
+	dishRoomService: null,
+	created() {
+		this.dishRoomService = new DishRoomService()
+	},
+	mounted() {
+		this.refreshRoomList()
+	},
+	activated() {
+		this.refreshRoomList()
+	},
+	methods: {
+		refreshRoomList() {
+			this.roomLoading = true
+			this.dishRoomService.list({ ...this.searchForm }).then((data) => {
+				this.roomList = data
+				this.roomLoading = false
+			}).catch(() => {
+				this.roomLoading = false
+			})
+		},
+		resetSearch() {
+			this.$refs.searchForm.resetFields()
+			this.refreshRoomList()
+		},
+		enterOrder(room) {
+			if (room.status === '1') {
+				this.$message.warning('该包房已停用')
+				return
+			}
+			this.$router.push({
+				path: '/psiManagement/dishManage/order/DishOrderDetail',
+				query: {
+					roomId: room.id
+				}
+			})
+		}
+	}
+}
+</script>
+
+<style scoped>
+.dish-order-page {
+	display: flex;
+	flex-direction: column;
+}
+
+.room-board {
+	flex: 1;
+	min-height: 0;
+	overflow: auto;
+	padding: 4px 2px 16px;
+}
+
+.room-grid {
+	display: grid;
+	grid-template-columns: repeat(3, minmax(0, 1fr));
+	gap: 16px;
+}
+
+.room-card {
+	min-height: 180px;
+	border: 3px solid #e5e7eb;
+	border-radius: 8px;
+	background: #fff;
+	padding: 18px;
+	cursor: pointer;
+	transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
+}
+
+.room-card:hover {
+	border-color: #409eff;
+	box-shadow: 0 8px 22px rgba(31, 45, 61, 0.08);
+	transform: translateY(-1px);
+}
+
+.room-card-disabled {
+	background: #f7f8fa;
+	cursor: not-allowed;
+}
+
+.room-card-top {
+	display: flex;
+	justify-content: space-between;
+	gap: 12px;
+}
+
+.room-name {
+	font-size: 20px;
+	font-weight: 600;
+	line-height: 28px;
+	color: #1f2937;
+}
+
+.room-no {
+	margin-top: 4px;
+	font-size: 13px;
+	color: #909399;
+}
+
+.room-meta {
+	display: grid;
+	grid-template-columns: repeat(2, minmax(0, 1fr));
+	gap: 10px;
+	margin-top: 18px;
+}
+
+.room-meta > div {
+	border-radius: 6px;
+	background: #f7f9fb;
+	padding: 10px;
+}
+
+.meta-label {
+	display: block;
+	font-size: 12px;
+	color: #909399;
+	line-height: 18px;
+}
+
+.meta-value {
+	display: block;
+	margin-top: 4px;
+	font-size: 16px;
+	font-weight: 600;
+	color: #303133;
+	line-height: 22px;
+}
+
+.room-remarks {
+	margin-top: 14px;
+	color: #606266;
+	font-size: 13px;
+	line-height: 20px;
+	overflow: hidden;
+	display: -webkit-box;
+	-webkit-line-clamp: 2;
+	-webkit-box-orient: vertical;
+}
+
+@media (max-width: 1200px) {
+	.room-grid {
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+	}
+}
+
+@media (max-width: 760px) {
+	.room-grid {
+		grid-template-columns: 1fr;
+	}
+}
+</style>

+ 268 - 0
src/views/psiManagement/dishManage/order/DishOrderApproval.vue

@@ -0,0 +1,268 @@
+<template>
+	<div class="dish-order-approval-page" v-loading="loading">
+		<!-- <div class="approval-title">折扣优惠结账信息</div> -->
+		<el-card shadow="never" class="approval-card">
+			<template #header>
+				<div class="card-header">订单信息</div>
+			</template>
+			<div v-if="currentOrder" 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-tabs v-model="activeTab">
+					<el-tab-pane label="实际菜单" name="actual">
+						<vxe-table border="inner" auto-resize resizable :data="currentOrder.detailList || []"
+							max-height="420">
+							<vxe-column type="seq" width="60" title="序号"></vxe-column>
+							<vxe-column min-width="180" title="菜品" field="dishName"></vxe-column>
+							<vxe-column min-width="130" title="分类" field="typeName"></vxe-column>
+							<vxe-column width="100" title="规格" field="spec"></vxe-column>
+							<vxe-column width="90" title="单价" field="salePrice">
+								<template #default="scope">¥{{ formatMoney(scope.row.salePrice) }}</template>
+							</vxe-column>
+							<vxe-column width="80" title="数量" field="quantity"></vxe-column>
+							<vxe-column width="100" title="金额" field="amount">
+								<template #default="scope">¥{{ formatMoney(scope.row.amount) }}</template>
+							</vxe-column>
+						</vxe-table>
+					</el-tab-pane>
+					<el-tab-pane label="加菜" name="add">
+						<vxe-table 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>
+							<vxe-column min-width="130" title="分类" field="typeName"></vxe-column>
+							<vxe-column width="100" title="规格" field="spec"></vxe-column>
+							<vxe-column width="90" title="单价" field="salePrice">
+								<template #default="scope">¥{{ formatMoney(scope.row.salePrice) }}</template>
+							</vxe-column>
+							<vxe-column width="80" title="数量" field="quantity"></vxe-column>
+							<vxe-column width="100" title="金额" field="amount">
+								<template #default="scope">¥{{ formatMoney(scope.row.amount) }}</template>
+							</vxe-column>
+							<vxe-column min-width="170" title="加菜时间" field="createTime"></vxe-column>
+						</vxe-table>
+					</el-tab-pane>
+					<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>
+							<vxe-column min-width="180" title="菜品" field="dishName"></vxe-column>
+							<vxe-column min-width="130" title="分类" field="typeName"></vxe-column>
+							<vxe-column width="100" title="规格" field="spec"></vxe-column>
+							<vxe-column width="90" title="单价" field="salePrice">
+								<template #default="scope">¥{{ formatMoney(scope.row.salePrice) }}</template>
+							</vxe-column>
+							<vxe-column width="80" title="数量" field="quantity">
+								<template #default="scope">{{ Math.abs(scope.row.quantity || 0) }}</template>
+							</vxe-column>
+							<vxe-column width="100" title="金额" field="amount">
+								<template #default="scope">¥{{ formatMoney(Math.abs(scope.row.amount || 0))
+								}}</template>
+							</vxe-column>
+							<vxe-column min-width="170" title="减菜时间" field="createTime"></vxe-column>
+						</vxe-table>
+					</el-tab-pane>
+				</el-tabs>
+			</div>
+		</el-card>
+	</div>
+</template>
+
+<script>
+import DishOrderService from '@/api/psi/DishOrderService'
+export default {
+	props: {
+		businessId: {
+			type: String,
+			default: ''
+		},
+		formReadOnly: {
+			type: Boolean,
+			default: true
+		},
+		status: {
+			type: String,
+			default: ''
+		}
+	},
+	data() {
+		return {
+			loading: false,
+			activeTab: 'actual',
+			currentOrder: null
+		}
+	},
+	dishOrderService: null,
+	created() {
+		this.dishOrderService = new DishOrderService()
+		this.init()
+	},
+	activated() {
+		this.init()
+	},
+	watch: {
+		businessId() {
+			this.init()
+		}
+	},
+	methods: {
+		init() {
+			const id = this.getBusinessId()
+			this.activeTab = 'actual'
+			this.currentOrder = null
+			if (!id || id === 'false') {
+				return
+			}
+			this.loading = true
+			this.$emit('changeLoading', true)
+			this.dishOrderService.findOrderById(id).then((data) => {
+				this.currentOrder = data || {}
+				this.loading = false
+				this.$emit('changeLoading', false)
+			}).catch(() => {
+				this.loading = false
+				this.$emit('changeLoading', false)
+			})
+		},
+		getBusinessId() {
+			return this.businessId || this.$route.query.businessId || this.$route.query.id || ''
+		},
+		getKeyWatch() {
+			this.init()
+		},
+		close() {
+			this.currentOrder = null
+		},
+		saveForm(callback) {
+			callback && callback()
+		},
+		agreeForm(callback) {
+			callback && callback()
+		},
+		startForm(callback) {
+			const order = this.currentOrder || {}
+			callback && callback('psi_dish_order', order.id || this.getBusinessId(), order)
+		},
+		reapplyForm(callback) {
+			const order = this.currentOrder || {}
+			callback && callback('psi_dish_order', order.id || this.getBusinessId(), order)
+		},
+		updateStatusById(type, callback) {
+			const order = this.currentOrder || {}
+			if (callback) {
+				callback('psi_dish_order', order.id || this.getBusinessId(), order)
+			}
+		},
+		orderStatusName(value) {
+			if (value === '0') {
+				return '用餐中'
+			}
+			if (value === '1') {
+				return '已结账'
+			}
+			if (value === '2') {
+				return '已取消'
+			}
+			return '-'
+		},
+		settleTypeName(value) {
+			if (value === '1') {
+				return '折扣结账'
+			}
+			if (value === '2') {
+				return '优惠结账'
+			}
+			if (value === '0') {
+				return '结账'
+			}
+			return '-'
+		},
+		discountRateText(order) {
+			if (!order || order.settleType !== '1') {
+				return '-'
+			}
+			return this.formatMoney(order.discountRate) + '%'
+		},
+		formatMoney(value) {
+			return Number(value || 0).toFixed(2)
+		}
+	}
+}
+</script>
+
+<style scoped>
+.dish-order-approval-page {
+	padding: 10px 0 66px;
+	min-height: 100px;
+}
+
+.approval-title {
+	text-align: center;
+	font-size: 22px;
+	font-weight: 600;
+	line-height: 32px;
+	margin-bottom: 14px;
+	color: #303133;
+}
+
+.approval-card {
+	border-radius: 6px;
+}
+
+.card-header {
+	font-size: 15px;
+	font-weight: 600;
+	color: #303133;
+}
+
+.order-detail {
+	min-height: 220px;
+}
+
+.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;
+}
+
+.detail-summary span {
+	display: block;
+	font-size: 12px;
+	line-height: 18px;
+	color: #909399;
+}
+
+.detail-summary strong {
+	display: block;
+	margin-top: 4px;
+	font-size: 14px;
+	line-height: 22px;
+	color: #303133;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+@media (max-width: 900px) {
+	.detail-summary {
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+	}
+}
+</style>

+ 974 - 0
src/views/psiManagement/dishManage/order/DishOrderDetail.vue

@@ -0,0 +1,974 @@
+<template>
+	<div class="page dish-order-detail-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="backRoomList()" 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-type">{{ item.typeName || '-' }}</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">
+					<span>已点菜</span>
+					<!-- <span class="ordered-count">{{ orderedList.length }} 项</span> -->
+					<el-button v-if="hasCurrentOrder" type="danger" plain :loading="submitting"
+						@click="confirmCancelOrder()">取消订单</el-button>
+				</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></span>
+						<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 ref="orderedDragList" class="ordered-drag-list">
+						<div v-for="item in orderedList" :key="item.dishId" class="ordered-item"
+							:class="{ 'ordered-item-pending': item.pendingQuantity !== 0 }" :data-dish-id="item.dishId">
+							<div class="ordered-drag-handle" title="拖拽排序">
+								<i class="el-icon-rank"></i>
+							</div>
+							<div class="ordered-dish">
+								<div class="ordered-dish-name">{{ item.dishName }}</div>
+								<!-- <div class="ordered-dish-spec">{{ item.spec || item.unit || '-' }}</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>总价</span>
+						<strong>¥{{ formatMoney(orderTotal) }}</strong>
+					</div>
+					<div class="ordered-actions" :class="{ 'ordered-actions-with-cancel': hasCurrentOrder }">
+
+						<el-button type="success" :loading="submitting" @click="openSettleDialog()">{{ settleButtonText
+						}}</el-button>
+						<el-button 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>
+	</div>
+</template>
+
+<script>
+import DishRoomService from '@/api/psi/DishRoomService'
+import DishOrderService from '@/api/psi/DishOrderService'
+import OSSSerivce from '@/api/sys/OSSService'
+import Sortable from 'sortablejs'
+export default {
+	data() {
+		return {
+			currentRoom: {},
+			currentOrder: null,
+			categoryList: [],
+			activeTypeId: 'all',
+			dishSearchName: '',
+			dishList: [],
+			orderedList: [],
+			dishLoading: false,
+			orderLoading: false,
+			submitting: false,
+			settleDialogVisible: false,
+			settleType: '0',
+			discountRate: 100,
+			discountAmount: 0,
+			orderedSortable: null
+		}
+	},
+	computed: {
+		orderTotal() {
+			return this.orderedList.reduce((total, item) => {
+				return total + this.itemTotal(item)
+			}, 0)
+		},
+		changedDetailList() {
+			return this.orderedList.filter(item => item.pendingQuantity !== 0)
+		},
+		hasChangedDish() {
+			return this.changedDetailList.length > 0
+		},
+		hasCurrentOrder() {
+			return !!(this.currentOrder && this.currentOrder.id)
+		},
+		settleButtonText() {
+			return this.hasChangedDish ? '下单并结账' : '结账'
+		},
+		submitDetailList() {
+			return this.orderedList.map((item, index) => {
+				return {
+					dishId: item.dishId,
+					dishName: item.dishName,
+					quantity: item.pendingQuantity,
+					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,
+	dishOrderService: null,
+	ossService: null,
+	created() {
+		this.dishRoomService = new DishRoomService()
+		this.dishOrderService = new DishOrderService()
+		this.ossService = new OSSSerivce()
+	},
+	mounted() {
+		this.initPage()
+		this.$nextTick(() => {
+			this.initOrderedSortable()
+		})
+	},
+	activated() {
+		this.initPage()
+		this.$nextTick(() => {
+			this.initOrderedSortable()
+		})
+	},
+	beforeUnmount() {
+		this.destroyOrderedSortable()
+	},
+	methods: {
+		initPage() {
+			const roomId = this.$route.query.roomId
+			if (!roomId) {
+				this.backRoomList()
+				return
+			}
+			this.loadRoom(roomId)
+			this.loadCurrentOrder(roomId)
+			this.activeTypeId = 'all'
+			this.dishSearchName = ''
+			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
+				const detailList = data && data.detailList ? data.detailList : []
+				const orderedDetailList = [...detailList].sort((a, b) => {
+					const sortA = Number(a.dishSort || 999999)
+					const sortB = Number(b.dishSort || 999999)
+					return sortA - sortB
+				})
+				this.orderedList = orderedDetailList.map(item => this.toOrderedItem(item, item.quantity || 0, 0))
+				this.refreshDishSort()
+				this.$nextTick(() => {
+					this.initOrderedSortable()
+				})
+				this.orderLoading = false
+			}).catch(() => {
+				this.orderLoading = false
+			})
+		},
+		backRoomList() {
+			this.$router.push({
+				path: '/psiManagement/dishManage/order/DishOrder'
+			})
+		},
+		loadCategoryList() {
+			this.dishOrderService.typeList().then((data) => {
+				this.categoryList = [
+					{ id: 'all', name: '所有菜品' },
+					...data.map(item => {
+						return {
+							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.dishLoading = false
+				this.loadDishImagePreview()
+			}).catch(() => {
+				this.dishLoading = false
+			})
+		},
+		addDish(dish) {
+			const row = this.orderedList.find(item => item.dishId === dish.id)
+			if (row) {
+				row.quantity += 1
+				row.pendingQuantity += 1
+				return
+			}
+			this.orderedList.push(this.toOrderedItem(dish, 1, 1))
+			this.refreshDishSort()
+			this.$nextTick(() => {
+				this.initOrderedSortable()
+			})
+		},
+		increaseOrderedDish(item) {
+			item.quantity += 1
+			item.pendingQuantity += 1
+		},
+		reduceDish(item) {
+			if (item.quantity <= 0) {
+				return
+			}
+			item.quantity -= 1
+			item.pendingQuantity -= 1
+			if (item.quantity === 0 && item.baseQuantity === 0) {
+				this.orderedList = this.orderedList.filter(row => row.dishId !== item.dishId)
+				this.refreshDishSort()
+				this.$nextTick(() => {
+					this.initOrderedSortable()
+				})
+			}
+		},
+		openSettleDialog() {
+			const changedList = this.changedDetailList
+			if (!this.hasCurrentOrder && changedList.length === 0) {
+				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)
+		},
+		confirmSubmitOrder() {
+			const detailList = this.changedDetailList
+			if (detailList.length === 0) {
+				this.$message.warning('请先选择菜品')
+				return
+			}
+			this.$confirm('是否确认下单?', '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.submitOrder('0', {}, true)
+			})
+		},
+		confirmCancelOrder() {
+			if (!this.hasCurrentOrder) {
+				this.$message.warning('暂无可取消的订单')
+				return
+			}
+			this.$confirm('确定取消当前订单吗?取消后包房将释放,未结账菜品不再计入订单。', '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.submitting = true
+				this.dishOrderService.cancelOrder(this.currentOrder.id).then((data) => {
+					this.$message.success(data)
+					this.submitting = false
+					this.backRoomList()
+				}).catch(() => {
+					this.submitting = false
+				})
+			})
+		},
+		submitOrder(settleFlag, settleInfo = {}, requireChanged = false) {
+			const changedList = this.changedDetailList
+			const detailList = settleFlag === '1' && changedList.length === 0 ? [] : this.submitDetailList
+			if (requireChanged && changedList.length === 0) {
+				this.$message.warning('请先选择菜品')
+				return
+			}
+			if (settleFlag === '1' && !this.hasCurrentOrder && changedList.length === 0) {
+				this.$message.warning('请先选择菜品')
+				return
+			}
+			if (settleFlag !== '1' && detailList.length === 0) {
+				this.$message.warning('请先选择菜品')
+				return
+			}
+			this.submitting = true
+			this.dishOrderService.submit({
+				id: this.hasCurrentOrder ? this.currentOrder.id : '',
+				roomId: this.$route.query.roomId,
+				settleFlag: settleFlag,
+				...settleInfo,
+				detailList: detailList
+			}).then((data) => {
+				this.$message.success(data)
+				this.submitting = false
+				this.settleDialogVisible = false
+				this.loadRoom(this.$route.query.roomId)
+				this.loadCurrentOrder(this.$route.query.roomId)
+			}).catch(() => {
+				this.submitting = false
+			})
+		},
+		toOrderedItem(item, quantity, pendingQuantity) {
+			const salePrice = Number(item.salePrice || 0)
+			return {
+				dishId: item.dishId || item.id,
+				dishCode: item.dishCode,
+				dishName: item.dishName,
+				typeId: item.typeId,
+				typeName: item.typeName,
+				unit: item.unit,
+				spec: item.spec,
+				salePrice: salePrice,
+				quantity: quantity,
+				baseQuantity: quantity - pendingQuantity,
+				pendingQuantity: pendingQuantity,
+				dishSort: item.dishSort || 0
+			}
+		},
+		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()
+				}
+			})
+		},
+		destroyOrderedSortable() {
+			if (this.orderedSortable) {
+				this.orderedSortable.destroy()
+				this.orderedSortable = null
+			}
+		},
+		refreshDishSort() {
+			this.orderedList.forEach((item, index) => {
+				item.dishSort = index + 1
+			})
+		},
+		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-detail-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;
+}
+
+.dish-panel,
+.ordered-panel {
+	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-type {
+	margin-top: 2px;
+	color: #909399;
+	font-size: 12px;
+	line-height: 18px;
+}
+
+.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-count {
+	font-size: 12px;
+	font-weight: 400;
+	color: #909399;
+}
+
+.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: 24px 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;
+	cursor: grab;
+	padding: 10px 8px;
+	border-bottom: 1px solid #ebeef5;
+}
+
+.ordered-item-pending {
+	background: rgba(45, 140, 240, 0.3);
+}
+
+.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-dish-spec {
+	margin-top: 2px;
+	font-size: 12px;
+	color: #909399;
+	line-height: 18px;
+	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;
+}
+
+.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;
+}
+
+.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;
+}
+
+@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) {
+	.dish-grid {
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+	}
+
+	.order-main {
+		grid-template-columns: 1fr;
+	}
+
+	.dish-content {
+		grid-template-columns: 120px minmax(0, 1fr);
+	}
+}
+
+@media (max-width: 640px) {
+	.dish-grid {
+		grid-template-columns: 1fr;
+	}
+}
+</style>

+ 188 - 0
src/views/psiManagement/dishManage/order/DishOrderInfoDialog.vue

@@ -0,0 +1,188 @@
+<template>
+	<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-tabs v-model="activeTab">
+					<el-tab-pane label="实际菜单" name="actual">
+						<vxe-table border="inner" auto-resize resizable :data="currentOrder.detailList || []"
+							max-height="420">
+							<vxe-column type="seq" width="60" title="序号"></vxe-column>
+							<vxe-column min-width="180" title="菜品" field="dishName"></vxe-column>
+							<vxe-column min-width="130" title="分类" field="typeName"></vxe-column>
+							<vxe-column width="100" title="规格" field="spec"></vxe-column>
+							<vxe-column width="90" title="单价" field="salePrice">
+								<template #default="scope">¥{{ formatMoney(scope.row.salePrice) }}</template>
+							</vxe-column>
+							<vxe-column width="80" title="数量" field="quantity"></vxe-column>
+							<vxe-column width="100" title="金额" field="amount">
+								<template #default="scope">¥{{ formatMoney(scope.row.amount) }}</template>
+							</vxe-column>
+						</vxe-table>
+					</el-tab-pane>
+					<el-tab-pane label="加菜" name="add">
+						<vxe-table 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>
+							<vxe-column min-width="130" title="分类" field="typeName"></vxe-column>
+							<vxe-column width="100" title="规格" field="spec"></vxe-column>
+							<vxe-column width="90" title="单价" field="salePrice">
+								<template #default="scope">¥{{ formatMoney(scope.row.salePrice) }}</template>
+							</vxe-column>
+							<vxe-column width="80" title="数量" field="quantity"></vxe-column>
+							<vxe-column width="100" title="金额" field="amount">
+								<template #default="scope">¥{{ formatMoney(scope.row.amount) }}</template>
+							</vxe-column>
+							<vxe-column min-width="170" title="加菜时间" field="createTime"></vxe-column>
+						</vxe-table>
+					</el-tab-pane>
+					<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>
+							<vxe-column min-width="180" title="菜品" field="dishName"></vxe-column>
+							<vxe-column min-width="130" title="分类" field="typeName"></vxe-column>
+							<vxe-column width="100" title="规格" field="spec"></vxe-column>
+							<vxe-column width="90" title="单价" field="salePrice">
+								<template #default="scope">¥{{ formatMoney(scope.row.salePrice) }}</template>
+							</vxe-column>
+							<vxe-column width="80" title="数量" field="quantity">
+								<template #default="scope">{{ Math.abs(scope.row.quantity || 0) }}</template>
+							</vxe-column>
+							<vxe-column width="100" title="金额" field="amount">
+								<template #default="scope">¥{{ formatMoney(Math.abs(scope.row.amount || 0))
+								}}</template>
+							</vxe-column>
+							<vxe-column min-width="170" title="减菜时间" field="createTime"></vxe-column>
+						</vxe-table>
+					</el-tab-pane>
+				</el-tabs>
+			</div>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="visible = false" icon="el-icon-circle-close">关闭</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script>
+import DishOrderService from '@/api/psi/DishOrderService'
+export default {
+	data() {
+		return {
+			visible: false,
+			loading: false,
+			activeTab: 'actual',
+			currentOrder: null
+		}
+	},
+	dishOrderService: null,
+	created() {
+		this.dishOrderService = new DishOrderService()
+	},
+	methods: {
+		init(id) {
+			this.visible = true
+			this.loading = true
+			this.activeTab = 'actual'
+			this.currentOrder = null
+			this.dishOrderService.findOrderById(id).then((data) => {
+				this.currentOrder = data || {}
+				this.loading = false
+			}).catch(() => {
+				this.loading = false
+			})
+		},
+		orderStatusName(value) {
+			if (value === '0') {
+				return '用餐中'
+			}
+			if (value === '1') {
+				return '已结账'
+			}
+			if (value === '2') {
+				return '已取消'
+			}
+			return '-'
+		},
+		settleTypeName(value) {
+			if (value === '1') {
+				return '折扣结账'
+			}
+			if (value === '2') {
+				return '优惠结账'
+			}
+			if (value === '0') {
+				return '结账'
+			}
+			return '-'
+		},
+		formatMoney(value) {
+			return Number(value || 0).toFixed(2)
+		},
+		discountRateText(order) {
+			if (!order || order.settleType !== '1') {
+				return '-'
+			}
+			return this.formatMoney(order.discountRate) + '%'
+		},
+	}
+}
+</script>
+
+<style scoped>
+.order-detail {
+	min-height: 220px;
+}
+
+.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;
+}
+
+.detail-summary span {
+	display: block;
+	font-size: 12px;
+	line-height: 18px;
+	color: #909399;
+}
+
+.detail-summary strong {
+	display: block;
+	margin-top: 4px;
+	font-size: 14px;
+	line-height: 22px;
+	color: #303133;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+@media (max-width: 900px) {
+	.detail-summary {
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+	}
+}
+</style>

+ 305 - 0
src/views/psiManagement/dishManage/order/DishOrderList.vue

@@ -0,0 +1,305 @@
+<template>
+	<div class="page dish-order-list-page">
+		<el-form :inline="true" v-if="searchVisible" class="query-form m-b-10" ref="searchForm" :model="searchForm"
+			@keyup.enter.native="refreshList()" @submit.native.prevent>
+			<el-form-item label="订单编号" prop="orderNo">
+				<el-input v-model="searchForm.orderNo" placeholder="请输入订单编号" clearable></el-input>
+			</el-form-item>
+			<el-form-item label="包房名称" prop="roomName">
+				<el-input v-model="searchForm.roomName" placeholder="请输入包房名称" clearable></el-input>
+			</el-form-item>
+			<el-form-item label="订单状态" prop="orderStatus">
+				<el-select v-model="searchForm.orderStatus" placeholder="请选择状态" clearable style="width: 130px">
+					<el-option label="使用中" value="0"></el-option>
+					<el-option label="已结账" value="1"></el-option>
+					<el-option label="已取消" value="2"></el-option>
+				</el-select>
+			</el-form-item>
+			<el-form-item label="结账方式" prop="settleType">
+				<el-select v-model="searchForm.settleType" placeholder="请选择方式" clearable style="width: 150px">
+					<el-option label="结账" value="0"></el-option>
+					<el-option label="折扣结账" value="1"></el-option>
+					<el-option label="优惠结账" value="2"></el-option>
+				</el-select>
+			</el-form-item>
+			<el-form-item label="下单时间" prop="createTimes">
+				<el-date-picker v-model="searchForm.createTimes" type="datetimerange" format="YYYY-MM-DD HH:mm:ss"
+					value-format="YYYY-MM-DD HH:mm:ss" range-separator="至" start-placeholder="开始时间"
+					end-placeholder="结束时间" placement="bottom-start" style="width: 360px">
+				</el-date-picker>
+			</el-form-item>
+			<el-form-item label="结账/取消时间" prop="settleTimes">
+				<el-date-picker v-model="searchForm.settleTimes" type="datetimerange" format="YYYY-MM-DD HH:mm:ss"
+					value-format="YYYY-MM-DD HH:mm:ss" range-separator="至" start-placeholder="开始时间"
+					end-placeholder="结束时间" placement="bottom-start" style="width: 360px">
+				</el-date-picker>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="refreshList()" icon="el-icon-search">查询</el-button>
+				<el-button @click="resetSearch()" icon="el-icon-refresh-right">重置</el-button>
+			</el-form-item>
+		</el-form>
+
+		<div class="jp-table top">
+			<vxe-toolbar ref="toolbarRef" :refresh="{ query: refreshList }" export custom>
+				<template #buttons>
+					<!-- <el-button type="primary" v-if="hasPermission('psi:order:export')" icon="el-icon-download"
+						@click="exportFile()">导出</el-button> -->
+				</template>
+				<template #tools>
+					<vxe-button text type="primary" :title="searchVisible ? '收起检索' : '展开检索'" icon="vxe-icon-search"
+						class="tool-btn" @click="searchVisible = !searchVisible"></vxe-button>
+				</template>
+			</vxe-toolbar>
+			<div style="height: calc(100% - 90px)">
+				<vxe-table :export-config="{
+					remote: true,
+					filename: `${moment(new Date()).format('YYYY-MM-DD')}订单数据导出`,
+					sheetName: `${moment(new Date()).format('YYYY-MM-DD')}订单数据导出`,
+					exportMethod: exportMethod,
+					types: ['xls'],
+					modes: ['current', 'selected', 'all']
+				}" :menu-config="{}" border="inner" auto-resize resizable height="auto" :loading="loading" ref="orderTable"
+					show-header-overflow show-overflow highlight-hover-row :data="dataList">
+					<!-- 多选 -->
+					<vxe-column type="checkbox" width="60"></vxe-column>
+					<vxe-column type="seq" width="60" title="序号" field="rowNo"></vxe-column>
+					<vxe-column min-width="170" align="center" title="订单编号" field="orderNo">
+						<template #default="scope">
+							<el-link v-if="hasPermission('psi:order:detail')" type="primary" :underline="false"
+								@click="view(scope.row.id)">{{ scope.row.orderNo
+								}}</el-link>
+							<span v-else>{{ scope.row.orderNo }}</span>
+						</template>
+					</vxe-column>
+					<vxe-column min-width="130" align="center" title="包房编号" field="roomNo"></vxe-column>
+					<vxe-column min-width="150" align="center" title="包房名称" field="roomName"></vxe-column>
+					<vxe-column min-width="180" align="center" title="下单时间" field="createTime"></vxe-column>
+					<vxe-column min-width="180" align="center" title="结账时间/取消时间" field="settleTime"></vxe-column>
+					<vxe-column width="100" align="center" title="订单状态" field="orderStatus">
+						<template #default="scope">
+							<el-tag :type="orderStatusTagType(scope.row.orderStatus)">
+								{{ orderStatusName(scope.row.orderStatus) }}
+							</el-tag>
+						</template>
+					</vxe-column>
+					<vxe-column width="120" align="center" title="结账方式" field="settleType">
+						<template #default="scope">
+							<span>{{ settleTypeName(scope.row.settleType) }}</span>
+						</template>
+					</vxe-column>
+					<vxe-column width="110" align="center" title="账单原金额" field="totalAmount">
+						<template #default="scope">¥{{ formatMoney(scope.row.totalAmount) }}</template>
+					</vxe-column>
+					<vxe-column width="110" align="center" title="折扣比例" field="discountRate">
+						<template #default="scope">{{ scope.row.settleType === '1' ? formatMoney(scope.row.discountRate)
+							+ '%' : '-' }}</template>
+					</vxe-column>
+					<vxe-column width="110" align="center" title="优惠金额" field="discountAmount">
+						<template #default="scope">¥{{ formatMoney(scope.row.discountAmount) }}</template>
+					</vxe-column>
+					<vxe-column width="110" align="center" title="实收金额" field="payableAmount">
+						<template #default="scope">¥{{ formatMoney(scope.row.payableAmount) }}</template>
+					</vxe-column>
+					<vxe-column min-width="150" align="center" title="创建人" field="createName"></vxe-column>
+
+					<vxe-column title="操作" width="100" fixed="right" align="center">
+						<template #default="scope">
+							<el-button text type="primary" size="small" @click="view(scope.row.id)"
+								v-if="hasPermission('psi:order:detail')">详情</el-button>
+						</template>
+					</vxe-column>
+				</vxe-table>
+				<vxe-pager background :current-page="tablePage.currentPage" :page-size="tablePage.pageSize"
+					:total="tablePage.total" :page-sizes="[10, 20, 100, 1000]"
+					:layouts="['PrevPage', 'JumpNumber', 'NextPage', 'FullJump', 'Sizes', 'Total']"
+					@page-change="currentChangeHandle">
+				</vxe-pager>
+			</div>
+		</div>
+
+		<DishOrderInfoDialog ref="dishOrderInfoDialog"></DishOrderInfoDialog>
+	</div>
+</template>
+
+<script>
+import DishOrderService from '@/api/psi/DishOrderService'
+import DishOrderInfoDialog from './DishOrderInfoDialog'
+import moment from 'moment'
+export default {
+	components: {
+		DishOrderInfoDialog
+	},
+	data() {
+		return {
+			searchVisible: true,
+			searchForm: {
+				orderNo: '',
+				roomName: '',
+				orderStatus: '',
+				settleType: '',
+				createTimes: [],
+				settleTimes: []
+			},
+			dataList: [],
+			tablePage: {
+				total: 0,
+				currentPage: 1,
+				pageSize: 10,
+				orders: []
+			},
+			loading: false,
+			tooltipConfig: {
+				showAll: false,
+				enterable: true,
+			},
+		}
+	},
+	dishOrderService: null,
+	created() {
+		this.dishOrderService = new DishOrderService()
+	},
+	mounted() {
+		this.$nextTick(() => {
+			// 将表格和工具栏进行关联
+			const $table = this.$refs.orderTable;
+			const $toolbar = this.$refs.toolbarRef;
+			$table.connect($toolbar);
+		});
+		this.refreshList()
+
+	},
+	activated() {
+		this.refreshList()
+	},
+	methods: {
+
+		refreshList() {
+			this.loading = true
+			this.dishOrderService.orderList({
+				current: this.tablePage.currentPage,
+				size: this.tablePage.pageSize,
+				orders: this.tablePage.orders,
+				...this.searchForm
+			}).then((data) => {
+				this.dataList = data.records
+				this.tablePage.total = data.total
+				this.loading = false
+			}).catch(() => {
+				this.loading = false
+			})
+		},
+		// 自定义服务端导出
+		exportMethod({ options }) {
+			// 传给服务端的参数
+			const params = {
+				'current': this.tablePage.currentPage,
+				'size': this.tablePage.pageSize,
+				'orders': this.tablePage.orders,
+				...this.searchForm,
+				filename: options.filename,
+				sheetName: options.sheetName,
+				isHeader: options.isHeader,
+				original: options.original,
+				mode: options.mode,
+				selectIds: options.mode === 'selected' ? options.data.map(item => item.id) : [],
+				exportFields: this.getExportFields(options.columns)
+			}
+			return this.dishOrderService.exportFile(params).then((res) => {
+				// 将二进制流文件写入excel表,以下为重要步骤
+				this.$utils.downloadExcel(res, options.filename + ".xls")
+			}).catch(function (err) {
+				if (err.response) {
+					console.log(err.response)
+				}
+			})
+		},
+		view(id) {
+			this.$refs.dishOrderInfoDialog.init(id)
+		},
+		currentChangeHandle({ currentPage, pageSize }) {
+			this.tablePage.currentPage = currentPage
+			this.tablePage.pageSize = pageSize
+			this.refreshList()
+		},
+		resetSearch() {
+			this.$refs.searchForm.resetFields()
+			this.tablePage.currentPage = 1
+			this.refreshList()
+		},
+		exportFile() {
+			const options = {
+				filename: `${moment(new Date()).format('YYYY-MM-DD')}订单数据导出`,
+				sheetName: '订单数据导出',
+				mode: 'all'
+			}
+			const $table = this.$refs.orderTable
+			const tableColumn = $table && $table.getTableColumn ? $table.getTableColumn() : null
+			const columns = tableColumn ? tableColumn.visibleColumn : ($table && $table.getColumns ? $table.getColumns() : [])
+			this.loading = true
+			this.dishOrderService.exportFile({
+				current: this.tablePage.currentPage,
+				size: this.tablePage.pageSize,
+				orders: this.tablePage.orders,
+				exportFields: this.getExportFields(columns),
+				...options,
+				...this.searchForm
+			}).then((res) => {
+				this.$utils.downloadExcel(res, options.filename)
+				this.loading = false
+			}).catch(() => {
+				this.loading = false
+			})
+		},
+		orderStatusName(value) {
+			if (value === '0') {
+				return '使用中'
+			}
+			if (value === '1') {
+				return '已结账'
+			}
+			if (value === '2') {
+				return '已取消'
+			}
+			return '-'
+		},
+		orderStatusTagType(value) {
+			if (value === '0') {
+				return 'warning'
+			}
+			if (value === '1') {
+				return 'success'
+			}
+			if (value === '2') {
+				return 'info'
+			}
+			return 'info'
+		},
+		settleTypeName(value) {
+			if (value === '1') {
+				return '折扣结账'
+			}
+			if (value === '2') {
+				return '优惠结账'
+			}
+			if (value === '0') {
+				return '结账'
+			}
+			return '-'
+		},
+		formatMoney(value) {
+			return Number(value || 0).toFixed(2)
+		},
+		getExportFields(columns) {
+			return (columns || []).map(column => column.property).filter(Boolean)
+		}
+	}
+}
+</script>
+
+<style scoped>
+.dish-order-list-page {
+	display: flex;
+	flex-direction: column;
+}
+</style>

+ 156 - 0
src/views/psiManagement/dishManage/room/DishRoomForm.vue

@@ -0,0 +1,156 @@
+<template>
+	<div>
+		<el-dialog :title="title" :close-on-click-modal="false" draggable width="650px" @close="close"
+			@keyup.enter.native="doSubmit" v-model="visible">
+			<el-form :model="inputForm" ref="inputForm" v-loading="loading"
+				:class="method === 'view' ? 'readonly' : ''" :disabled="method === 'view'" label-width="110px"
+				@submit.native.prevent>
+				<el-row :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="包房编号" prop="roomNo">
+							<el-input v-model="inputForm.roomNo" placeholder="不填写则自动生成"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="包房名称" prop="roomName"
+							:rules="[{ required: true, message: '包房名称不能为空', trigger: 'blur' }]">
+							<el-input v-model="inputForm.roomName" placeholder="请填写包房名称"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="容纳人数" prop="capacity">
+							<el-input-number v-model="inputForm.capacity" :min="0" :precision="0"
+								style="width: 100%"></el-input-number>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="最低消费" prop="minConsumption">
+							<el-input-number v-model="inputForm.minConsumption" :min="0" :precision="2"
+								style="width: 100%"></el-input-number>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="排序" prop="sort">
+							<el-input-number v-model="inputForm.sort" :min="0" style="width: 100%"></el-input-number>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="状态" prop="status">
+							<el-radio-group v-model="inputForm.status">
+								<el-radio label="0">启用</el-radio>
+								<el-radio label="1">停用</el-radio>
+							</el-radio-group>
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="24">
+						<el-form-item label="备注" prop="remarks">
+							<el-input v-model="inputForm.remarks" type="textarea" :rows="4" maxlength="500"
+								placeholder="请输入备注" show-word-limit></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="close()" icon="el-icon-circle-close">关闭</el-button>
+					<el-button type="primary" v-if="method !== 'view'" @click="doSubmit()"
+						icon="el-icon-circle-check" v-noMoreClick>确定</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script>
+import DishRoomService from '@/api/psi/DishRoomService'
+export default {
+	data() {
+		return {
+			title: '',
+			method: '',
+			visible: false,
+			loading: false,
+			inputForm: {
+				roomNo: '',
+				roomName: '',
+				capacity: 0,
+				minConsumption: 0,
+				status: '0',
+				sort: 0,
+				remarks: ''
+			}
+		}
+	},
+	dishRoomService: null,
+	created() {
+		this.dishRoomService = new DishRoomService()
+	},
+	methods: {
+		init(method, id) {
+			this.method = method
+			this.inputForm = {
+				roomNo: '',
+				roomName: '',
+				capacity: 0,
+				minConsumption: 0,
+				status: '0',
+				sort: 0,
+				remarks: ''
+			}
+			if (method === 'add') {
+				this.title = '新建包房'
+			} else if (method === 'edit') {
+				this.title = '修改包房'
+				this.inputForm.id = id
+			} else if (method === 'view') {
+				this.title = '查看包房'
+				this.inputForm.id = id
+			}
+			this.visible = true
+			this.loading = false
+			this.$nextTick(() => {
+				if (method === 'edit' || method === 'view') {
+					this.loading = true
+					this.$refs.inputForm.resetFields()
+					this.dishRoomService.findById(this.inputForm.id).then((data) => {
+						this.inputForm = this.recover(this.inputForm, data)
+						this.inputForm = JSON.parse(JSON.stringify(this.inputForm))
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		doSubmit() {
+			this.$refs.inputForm.validate((valid) => {
+				if (valid) {
+					this.loading = true
+					this.dishRoomService.save(this.inputForm).then((data) => {
+						if (data && data.indexOf('重复') > -1) {
+							this.$message.error(data)
+						} else {
+							this.$message.success(data)
+							this.close()
+							this.$emit('refreshList')
+						}
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		close() {
+			this.$refs.inputForm && this.$refs.inputForm.resetFields()
+			this.visible = false
+		}
+	}
+}
+</script>

+ 329 - 0
src/views/psiManagement/dishManage/room/DishRoomList.vue

@@ -0,0 +1,329 @@
+<template>
+	<div class="page dish-room-page">
+		<el-form :inline="true" class="query-form m-b-10" ref="searchForm" :model="searchForm"
+			@keyup.enter.native="refreshList()" @submit.native.prevent>
+			<el-form-item label="包房名称" prop="roomName">
+				<el-input v-model="searchForm.roomName" placeholder="请输入包房名称" clearable></el-input>
+			</el-form-item>
+			<el-form-item label="包房编号" prop="roomNo">
+				<el-input v-model="searchForm.roomNo" placeholder="请输入包房编号" clearable></el-input>
+			</el-form-item>
+			<el-form-item label="状态" prop="status">
+				<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 120px">
+					<el-option label="启用" value="0"></el-option>
+					<el-option label="停用" value="1"></el-option>
+				</el-select>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="refreshList()" icon="el-icon-search">查询</el-button>
+				<el-button @click="resetSearch()" icon="el-icon-refresh-right">重置</el-button>
+			</el-form-item>
+		</el-form>
+
+		<div class="room-board" v-loading="loading">
+			<div class="room-grid">
+				<div v-for="item in dataList" :key="item.id" class="room-card"
+					:class="{ 'room-card-disabled': item.status === '1' }">
+					<div class="room-card-top">
+						<div>
+							<div class="room-name">{{ item.roomName }}</div>
+							<div class="room-no">{{ '包房号:' + (item.roomNo || '-') }}</div>
+						</div>
+						<el-dropdown trigger="click" @command="(command) => handleCommand(command, item)"
+							v-if="hasPermission('psi:dishRoom:edit') || hasPermission('psi:dishRoom:del')">
+							<span class="room-menu">•••</span>
+							<template #dropdown>
+								<el-dropdown-menu>
+									<el-dropdown-item command="edit"
+										v-if="hasPermission('psi:dishRoom:edit')">修改</el-dropdown-item>
+									<el-dropdown-item command="status">{{ item.status === '0' ? '停用' : '启用'
+									}}</el-dropdown-item>
+									<el-dropdown-item command="delete" v-if="hasPermission('psi:dishRoom:del')"
+										divided>删除</el-dropdown-item>
+								</el-dropdown-menu>
+							</template>
+						</el-dropdown>
+					</div>
+					<div class="room-status-row">
+						<el-tag :type="item.status === '0' ? 'success' : 'error'">
+							{{ item.status === '0' ? '启用' : '停用' }}
+						</el-tag>
+					</div>
+					<div class="room-meta">
+						<div>
+							<span class="meta-label">容纳人数</span>
+							<span class="meta-value">{{ item.capacity || 0 }} 人</span>
+						</div>
+						<div>
+							<span class="meta-label">最低消费</span>
+							<span class="meta-value">{{ item.minConsumption || 0 }} 元</span>
+						</div>
+					</div>
+					<div class="room-remarks">{{ item.remarks || '暂无备注' }}</div>
+				</div>
+
+				<div class="room-card room-add-card" @click="add" v-if="hasPermission('psi:dishRoom:add')">
+					<div class="add-icon">+</div>
+					<div class="add-text">新增包房</div>
+				</div>
+			</div>
+		</div>
+
+		<DishRoomForm ref="dishRoomForm" @refreshList="refreshList"></DishRoomForm>
+	</div>
+</template>
+
+<script>
+import DishRoomService from '@/api/psi/DishRoomService'
+import DishRoomForm from './DishRoomForm'
+export default {
+	data() {
+		return {
+			searchForm: {
+				roomName: '',
+				roomNo: '',
+				status: ''
+			},
+			dataList: [],
+			loading: false
+		}
+	},
+	dishRoomService: null,
+	created() {
+		this.dishRoomService = new DishRoomService()
+	},
+	components: {
+		DishRoomForm
+	},
+	mounted() {
+		this.refreshList()
+	},
+	activated() {
+		this.refreshList()
+	},
+	methods: {
+		add() {
+			this.$refs.dishRoomForm.init('add', '')
+		},
+		edit(id) {
+			this.$refs.dishRoomForm.init('edit', id)
+		},
+		refreshList() {
+			this.loading = true
+			this.dishRoomService.list({ ...this.searchForm }).then((data) => {
+				this.dataList = data
+				this.loading = false
+			}).catch(() => {
+				this.loading = false
+			})
+		},
+		handleCommand(command, row) {
+			if (command === 'edit') {
+				this.edit(row.id)
+			} else if (command === 'delete') {
+				this.del(row.id)
+			} else if (command === 'status') {
+				this.changeStatus(row)
+			}
+		},
+		changeStatus(row) {
+			if (row.useStatus === '1') {
+				this.$message.warning('该包房正在使用中,无法修改状态')
+				return
+			}
+			const status = row.status === '0' ? '1' : '0'
+			const message = status === '0' ? '确定启用该包房吗?' : '确定停用该包房吗?'
+			this.$confirm(message, '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.dishRoomService.changeStatus(row.id, status).then((data) => {
+					this.$message.success(data)
+					this.refreshList()
+				})
+			})
+		},
+		del(id) {
+			this.$confirm(`确定删除该包房吗?`, '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.loading = true
+				this.dishRoomService.delete(id).then((data) => {
+					this.$message.success(data)
+					this.refreshList()
+					this.loading = false
+				}).catch(() => {
+					this.loading = false
+				})
+			})
+		},
+		resetSearch() {
+			this.$refs.searchForm.resetFields()
+			this.refreshList()
+		}
+	}
+}
+</script>
+
+<style scoped>
+.dish-room-page {
+	display: flex;
+	flex-direction: column;
+}
+
+.room-board {
+	flex: 1;
+	min-height: 0;
+	overflow: auto;
+	padding: 4px 2px 16px;
+}
+
+.room-grid {
+	display: grid;
+	grid-template-columns: repeat(3, minmax(0, 1fr));
+	gap: 16px;
+}
+
+.room-card {
+	position: relative;
+	min-height: 180px;
+	border: 3px solid #e5e7eb;
+	border-radius: 8px;
+	background: #fff;
+	padding: 18px;
+	transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
+}
+
+.room-card:hover {
+	border-color: #409eff;
+	box-shadow: 0 8px 22px rgba(31, 45, 61, 0.08);
+	transform: translateY(-1px);
+}
+
+.room-card-disabled {
+	background: #f7f8fa;
+}
+
+.room-card-top {
+	display: flex;
+	justify-content: space-between;
+	gap: 12px;
+}
+
+.room-name {
+	font-size: 20px;
+	font-weight: 600;
+	line-height: 28px;
+	color: #1f2937;
+}
+
+.room-no {
+	margin-top: 4px;
+	font-size: 13px;
+	color: #909399;
+}
+
+.room-menu {
+	display: inline-flex;
+	align-items: center;
+	justify-content: center;
+	width: 30px;
+	height: 30px;
+	border-radius: 4px;
+	cursor: pointer;
+	color: #606266;
+	font-size: 18px;
+	line-height: 1;
+}
+
+.room-menu:hover {
+	background: #f2f6fc;
+	color: #409eff;
+}
+
+.room-status-row {
+	margin-top: 12px;
+}
+
+.room-meta {
+	display: grid;
+	grid-template-columns: repeat(2, minmax(0, 1fr));
+	gap: 10px;
+	margin-top: 18px;
+}
+
+.room-meta>div {
+	border-radius: 6px;
+	background: #f7f9fb;
+	padding: 10px;
+}
+
+.meta-label {
+	display: block;
+	font-size: 12px;
+	color: #909399;
+	line-height: 18px;
+}
+
+.meta-value {
+	display: block;
+	margin-top: 4px;
+	font-size: 16px;
+	font-weight: 600;
+	color: #303133;
+	line-height: 22px;
+}
+
+.room-remarks {
+	margin-top: 14px;
+	color: #606266;
+	font-size: 13px;
+	line-height: 20px;
+	overflow: hidden;
+	display: -webkit-box;
+	-webkit-line-clamp: 2;
+	-webkit-box-orient: vertical;
+}
+
+.room-add-card {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	cursor: pointer;
+	border-style: dashed;
+	color: #409eff;
+}
+
+.add-icon {
+	width: 42px;
+	height: 42px;
+	border-radius: 50%;
+	border: 1px dashed #409eff;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 28px;
+	line-height: 1;
+}
+
+.add-text {
+	margin-top: 12px;
+	font-size: 15px;
+}
+
+@media (max-width: 1200px) {
+	.room-grid {
+		grid-template-columns: repeat(2, minmax(0, 1fr));
+	}
+}
+
+@media (max-width: 760px) {
+	.room-grid {
+		grid-template-columns: 1fr;
+	}
+}
+</style>

+ 155 - 0
src/views/psiManagement/dishManage/type/DishType.vue

@@ -0,0 +1,155 @@
+<template>
+	<div class="page">
+		<el-form :inline="true" class="query-form" ref="searchForm" :model="searchForm"
+			@keyup.enter.native="refreshList()" @submit.native.prevent>
+			<el-form-item label="分类名称" prop="name">
+				<el-input v-model="searchForm.name" placeholder="请输入分类名称" clearable></el-input>
+			</el-form-item>
+			<el-form-item label="状态" prop="status">
+				<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 140px">
+					<el-option label="启用" value="0"></el-option>
+					<el-option label="停用" value="1"></el-option>
+				</el-select>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="refreshList()" icon="el-icon-search">查询</el-button>
+				<el-button @click="resetSearch()" icon="el-icon-refresh-right">重置</el-button>
+			</el-form-item>
+		</el-form>
+
+		<div class="jp-table top">
+			<vxe-toolbar :refresh="{ query: refreshList }" custom>
+				<template #buttons>
+					<el-button v-if="hasPermission('psi:dishType:add')" type="primary" icon="el-icon-plus"
+						@click="add()">新建</el-button>
+				</template>
+			</vxe-toolbar>
+			<div style="height: calc(100% - 50px)">
+				<vxe-table border="inner" auto-resize resizable height="auto" :loading="loading" ref="typeTable"
+					show-header-overflow show-overflow highlight-hover-row :menu-config="{}" :data="dataList"
+					:tree-config="{ transform: true, rowField: 'id', parentField: 'parentId', expandAll: true }"
+					:checkbox-config="{}">
+					<vxe-column type="seq" width="60" title="序号"></vxe-column>
+					<vxe-column type="checkbox" width="60"></vxe-column>
+					<vxe-column title="分类名称" field="name" align="left" tree-node></vxe-column>
+					<vxe-column width="100" title="排序" field="sort"></vxe-column>
+					<vxe-column width="100" title="状态" field="status" align="center">
+						<template #default="scope">
+							<el-tag :type="scope.row.status === '0' ? 'success' : 'info'">
+								{{ scope.row.status === '0' ? '启用' : '停用' }}
+							</el-tag>
+						</template>
+					</vxe-column>
+					<vxe-column min-width="180" title="备注" field="remarks"></vxe-column>
+					<vxe-column title="操作" width="280px" fixed="right" align="center">
+						<template #default="scope">
+							<el-button v-if="hasPermission('psi:dishType:add')" text type="primary"
+								@click="addChild(scope.row.id)">添加下级</el-button>
+							<el-button v-if="hasPermission('psi:dishType:edit')" text type="primary"
+								@click="edit(scope.row.id)">修改</el-button>
+							<el-button v-if="hasPermission('psi:dishType:edit')" text type="primary"
+								@click="changeStatus(scope.row)">{{ scope.row.status === '0' ? '停用' : '启用' }}</el-button>
+							<el-button v-if="hasPermission('psi:dishType:del')" text type="primary"
+								@click="del(scope.row.id)">删除</el-button>
+						</template>
+					</vxe-column>
+				</vxe-table>
+			</div>
+		</div>
+		<DishTypeForm ref="dishTypeForm" @refreshList="refreshList"></DishTypeForm>
+	</div>
+</template>
+
+<script>
+import DishTypeService from '@/api/psi/DishTypeService'
+import DishTypeForm from './DishTypeForm'
+export default {
+	data() {
+		return {
+			searchForm: {
+				name: '',
+				status: ''
+			},
+			dataList: [],
+			loading: false
+		}
+	},
+	dishTypeService: null,
+	created() {
+		this.dishTypeService = new DishTypeService()
+	},
+	components: {
+		DishTypeForm
+	},
+	mounted() {
+		this.refreshList()
+	},
+	activated() {
+		this.refreshList()
+	},
+	methods: {
+		add() {
+			this.$refs.dishTypeForm.init('add', '')
+		},
+		addChild(id) {
+			this.$refs.dishTypeForm.init('addChild', id)
+		},
+		edit(id) {
+			id = id || this.$refs.typeTable.getCheckboxRecords().map(item => {
+				return item.id
+			})[0]
+			this.$refs.dishTypeForm.init('edit', id)
+		},
+		refreshList() {
+			this.loading = true
+			this.dishTypeService.list({ ...this.searchForm }).then((data) => {
+				this.dataList = data
+				this.loading = false
+				this.$nextTick(() => {
+					this.$refs.typeTable.setAllTreeExpand(true)
+				})
+			}).catch(() => {
+				this.loading = false
+			})
+		},
+		changeStatus(row) {
+			const status = row.status === '0' ? '1' : '0'
+			const message = status === '0' ? '确定启用该分类吗?' : '确定停用该分类吗?'
+			this.$confirm(message, '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.dishTypeService.changeStatus(row.id, status).then((data) => {
+					this.$message.success(data)
+					this.refreshList()
+				})
+			})
+		},
+		del(id) {
+			this.$confirm(`确定删除所选项吗?`, '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.loading = true
+				this.dishTypeService.remove(id).then((data) => {
+					if (data && (data.indexOf('不能删除') > -1 || data.indexOf('请选择') > -1)) {
+						this.$message.error(data)
+					} else {
+						this.$message.success(data)
+					}
+					this.refreshList()
+					this.loading = false
+				}).catch(() => {
+					this.loading = false
+				})
+			})
+		},
+		resetSearch() {
+			this.$refs.searchForm.resetFields()
+			this.refreshList()
+		}
+	}
+}
+</script>

+ 147 - 0
src/views/psiManagement/dishManage/type/DishTypeForm.vue

@@ -0,0 +1,147 @@
+<template>
+	<div>
+		<el-dialog :title="title" :close-on-click-modal="false" draggable width="560px" @close="close"
+			@keyup.enter.native="doSubmit" v-model="visible">
+			<el-form :model="inputForm" ref="inputForm" v-loading="loading"
+				:class="method === 'view' ? 'readonly' : ''" :disabled="method === 'view'" label-width="100px"
+				@submit.native.prevent>
+				<el-row :gutter="15">
+					<el-col :span="22">
+						<el-form-item label="上级分类" prop="parentId">
+							<SelectTree ref="dishTypeTree" v-if="visible" :props="{
+								value: 'id',
+								label: 'name',
+								children: 'children'
+							}" url="/psi-management-server/psi/dishType/treeData" :value="inputForm.parentId"
+								:clearable="true" :accordion="true" size="default"
+								@getValue="(value) => { inputForm.parentId = value }" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="22">
+						<el-form-item label="分类名称" prop="name"
+							:rules="[{ required: true, message: '分类名称不能为空', trigger: 'blur' }]">
+							<el-input v-model="inputForm.name" placeholder="请填写分类名称"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="22">
+						<el-form-item label="排序" prop="sort">
+							<el-input-number v-model="inputForm.sort" :min="0" style="width: 100%"></el-input-number>
+						</el-form-item>
+					</el-col>
+					<el-col :span="22">
+						<el-form-item label="状态" prop="status">
+							<el-radio-group v-model="inputForm.status">
+								<el-radio label="0">启用</el-radio>
+								<el-radio label="1">停用</el-radio>
+							</el-radio-group>
+						</el-form-item>
+					</el-col>
+					<el-col :span="22">
+						<el-form-item label="备注" prop="remarks">
+							<el-input v-model="inputForm.remarks" type="textarea" :rows="3" maxlength="500"
+								placeholder="请输入备注" show-word-limit></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="close()" icon="el-icon-circle-close">关闭</el-button>
+					<el-button type="primary" v-if="method !== 'view'" @click="doSubmit()"
+						icon="el-icon-circle-check" v-noMoreClick>确定</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script>
+import DishTypeService from '@/api/psi/DishTypeService'
+import SelectTree from '@/components/treeSelect/treeSelect.vue'
+export default {
+	data() {
+		return {
+			title: '',
+			method: '',
+			visible: false,
+			loading: false,
+			inputForm: {
+				name: '',
+				parentId: '',
+				sort: 0,
+				status: '0',
+				remarks: ''
+			}
+		}
+	},
+	dishTypeService: null,
+	created() {
+		this.dishTypeService = new DishTypeService()
+	},
+	components: {
+		SelectTree
+	},
+	methods: {
+		init(method, id) {
+			this.method = method
+			this.inputForm = {
+				name: '',
+				parentId: '',
+				sort: 0,
+				status: '0',
+				remarks: ''
+			}
+			if (method === 'add') {
+				this.title = '新建菜品分类'
+			} else if (method === 'addChild') {
+				this.title = '添加下级分类'
+				this.inputForm.parentId = id
+			} else if (method === 'edit') {
+				this.title = '修改菜品分类'
+				this.inputForm.id = id
+			} else if (method === 'view') {
+				this.title = '查看菜品分类'
+				this.inputForm.id = id
+			}
+			this.visible = true
+			this.loading = false
+			this.$nextTick(() => {
+				if (method === 'edit' || method === 'view') {
+					this.loading = true
+					this.$refs.inputForm.resetFields()
+					this.dishTypeService.findById(this.inputForm.id).then((data) => {
+						this.inputForm = this.recover(this.inputForm, data)
+						this.inputForm = JSON.parse(JSON.stringify(this.inputForm))
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		doSubmit() {
+			this.$refs.inputForm.validate((valid) => {
+				if (valid) {
+					this.loading = true
+					this.dishTypeService.save(this.inputForm).then((data) => {
+						if (data && data.indexOf('重复') > -1) {
+							this.$message.error(data)
+						} else {
+							this.$message.success(data)
+							this.close()
+							this.$emit('refreshList')
+						}
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		close() {
+			this.$refs.inputForm && this.$refs.inputForm.resetFields()
+			this.visible = false
+		}
+	}
+}
+</script>