Browse Source

酒店管理。菜品管理、房间管理部分代码

徐滕 3 weeks atrás
parent
commit
99267fb8d2

+ 8 - 0
scripts/generate-version.js

@@ -0,0 +1,8 @@
+const fs = require('fs');
+const path = require('path');
+
+const versionFile = path.join(__dirname, '../public/version.json');
+const version = Date.now().toString();
+
+fs.writeFileSync(versionFile, JSON.stringify({ version }));
+console.log('✅ Version generated: ' + version);

+ 66 - 0
src/api/hotel/HotelBusinessHoursService.js

@@ -0,0 +1,66 @@
+import httpRequest from "@/utils/httpRequest";
+import { HOTEL_ORDER_PATH as prefix } from "../AppPath";
+
+/**
+ * 营业时段管理API服务
+ */
+export default class HotelBusinessHoursService {
+	/** 分页查询营业时段列表 */
+	page(params) {
+		return httpRequest({
+			url: prefix + "/config/businessHours/list",
+			method: "get",
+			params,
+		});
+	}
+
+	/** 获取营业时段列表(全部) */
+	all() {
+		return httpRequest({
+			url: prefix + "/config/businessHours/all",
+			method: "get",
+		});
+	}
+
+	/** 获取启用的营业时段 */
+	enabled() {
+		return httpRequest({
+			url: prefix + "/config/businessHours/enabled",
+			method: "get",
+		});
+	}
+
+	/** 查询营业时段详情 */
+	queryById(id) {
+		return httpRequest({
+			url: prefix + "/config/businessHours/queryById",
+			method: "get",
+			params: { id },
+		});
+	}
+
+	/** 新增/编辑营业时段 */
+	save(data) {
+		return httpRequest({
+			url: prefix + "/config/businessHours/save",
+			method: "post",
+			data,
+		});
+	}
+
+	/** 切换状态 */
+	toggle(id) {
+		return httpRequest({
+			url: prefix + "/config/businessHours/toggle/" + id,
+			method: "post",
+		});
+	}
+
+	/** 删除营业时段 */
+	delete(id) {
+		return httpRequest({
+			url: prefix + "/config/businessHours/delete/" + id,
+			method: "post",
+		});
+	}
+}

+ 24 - 0
src/api/hotel/HotelConfigService.js

@@ -0,0 +1,24 @@
+import httpRequest from "@/utils/httpRequest";
+import { HOTEL_ORDER_PATH as prefix } from "../AppPath";
+
+/**
+ * 酒店配置管理API服务
+ */
+export default class HotelConfigService {
+	/** 获取酒店配置 */
+	get() {
+		return httpRequest({
+			url: prefix + "/config/get",
+			method: "get",
+		});
+	}
+
+	/** 保存/更新酒店配置 */
+	save(data) {
+		return httpRequest({
+			url: prefix + "/config/save",
+			method: "post",
+			data,
+		});
+	}
+}

+ 88 - 0
src/api/hotel/HotelDishSpecService.js

@@ -0,0 +1,88 @@
+import httpRequest from "@/utils/httpRequest";
+import { HOTEL_ORDER_PATH as prefix } from "../AppPath";
+
+/**
+ * 菜品规格管理API服务
+ */
+export default class HotelDishSpecService {
+	/** 分页查询规格组列表 */
+	groupPage(params) {
+		return httpRequest({
+			url: prefix + "/dishSpecGroup/page",
+			method: "get",
+			params,
+		});
+	}
+
+	/** 根据菜品ID查询所有规格组(含选项) */
+	findByDishId(dishId) {
+		return httpRequest({
+			url: prefix + "/dishSpecGroup/findByDishId",
+			method: "get",
+			params: { dishId },
+		});
+	}
+
+	/** 查询规格组详情 */
+	queryGroupById(id) {
+		return httpRequest({
+			url: prefix + "/dishSpecGroup/queryById",
+			method: "get",
+			params: { id },
+		});
+	}
+
+	/** 新增/编辑规格组 */
+	saveGroup(data) {
+		return httpRequest({
+			url: prefix + "/dishSpecGroup/save",
+			method: "post",
+			data,
+		});
+	}
+
+	/** 删除规格组 */
+	deleteGroup(id) {
+		return httpRequest({
+			url: prefix + "/dishSpecGroup/delete/" + id,
+			method: "post",
+		});
+	}
+
+	// ==================== 规格选项 ====================
+
+	/** 根据规格组ID查询所有选项 */
+	findOptionsByGroupId(groupId) {
+		return httpRequest({
+			url: prefix + "/dishSpecOption/findByGroupId",
+			method: "get",
+			params: { groupId },
+		});
+	}
+
+	/** 查询规格选项详情 */
+	queryOptionById(id) {
+		return httpRequest({
+			url: prefix + "/dishSpecOption/queryById",
+			method: "get",
+			params: { id },
+		});
+	}
+
+	/** 新增/编辑规格选项 */
+	saveOption(data) {
+		return httpRequest({
+			url: prefix + "/dishSpecOption/save",
+			method: "post",
+			data,
+		});
+	}
+
+	/** 删除规格选项 */
+	deleteOption(id) {
+		return httpRequest({
+			url: prefix + "/dishSpecOption/delete/" + id,
+			method: "post",
+		});
+	}
+}

+ 168 - 0
src/views/hotel/admin/BusinessHoursForm.vue

@@ -0,0 +1,168 @@
+<template>
+	<div>
+		<el-dialog :title="title" :close-on-click-modal="false" draggable width="500px" @close="close"
+			@keyup.enter.native="doSubmit" v-model="visible">
+			<el-form :model="inputForm" ref="inputForm" v-loading="loading" :rules="rules" label-width="100px"
+				@submit.native.prevent>
+				<!-- 餐段类型下拉 -->
+				<el-form-item label="餐段类型" prop="mealType">
+					<el-select v-model="inputForm.mealType" placeholder="请选择餐段类型" clearable style="width: 100%"
+						@change="onMealTypeChange">
+						<el-option label="早餐" value="breakfast"></el-option>
+						<el-option label="午餐" value="lunch"></el-option>
+						<el-option label="晚餐" value="dinner"></el-option>
+						<el-option label="夜宵" value="night"></el-option>
+					</el-select>
+				</el-form-item>
+				<!-- 餐段名称(可手动修改) -->
+				<el-form-item label="餐段名称" prop="mealName">
+					<el-input v-model="inputForm.mealName" placeholder="如:早餐、午餐" clearable></el-input>
+				</el-form-item>
+				<!-- 星期选择 -->
+				<el-form-item label="适用星期" prop="dayOfWeek">
+					<el-select v-model="inputForm.dayOfWeek" placeholder="每天(不限制星期)" clearable style="width: 100%">
+						<el-option label="每天" :value="null"></el-option>
+						<el-option v-for="(day, index) in weekDays" :key="index" :label="day" :value="index"></el-option>
+					</el-select>
+				</el-form-item>
+				<!-- 开始时间 -->
+				<el-form-item label="开始时间" prop="startTime">
+					<el-time-select v-model="inputForm.startTime" :picker-options="{ start: '00:00', step: '00:30', end: '23:30' }"
+						placeholder="请选择开始时间" style="width: 100%"></el-time-select>
+				</el-form-item>
+				<!-- 结束时间 -->
+				<el-form-item label="结束时间" prop="endTime">
+					<el-time-select v-model="inputForm.endTime" :picker-options="{ start: '00:00', step: '00:30', end: '23:59', min: inputForm.startTime }"
+						placeholder="请选择结束时间" style="width: 100%"></el-time-select>
+				</el-form-item>
+				<!-- 排序号 -->
+				<el-form-item label="排序号" prop="sortOrder">
+					<el-input-number v-model="inputForm.sortOrder" :min="0" :max="999" style="width: 100%"></el-input-number>
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="close()" icon="el-icon-circle-close">关闭</el-button>
+					<el-button type="primary" @click="doSubmit()" icon="el-icon-circle-check"
+						v-noMoreClick>确定</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script>
+import HotelBusinessHoursService from '@/api/hotel/HotelBusinessHoursService'
+
+/** 餐段类型与默认名称映射 */
+const MEAL_TYPE_NAMES = {
+	breakfast: '早餐',
+	lunch: '午餐',
+	dinner: '晚餐',
+	night: '夜宵'
+}
+
+export default {
+	data() {
+		return {
+			title: '',
+			method: '',
+			visible: false,
+			loading: false,
+			/** 星期映射:索引对应Calendar常量(0=周日) */
+			weekDays: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
+			rules: {
+				mealType: [{ required: true, message: '请选择餐段类型', trigger: 'change' }],
+				mealName: [{ required: true, message: '请输入餐段名称', trigger: 'blur' }],
+				startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
+				endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }]
+			},
+			inputForm: {
+				mealType: '',
+				mealName: '',
+				dayOfWeek: null,
+				startTime: '',
+				endTime: '',
+				sortOrder: 0
+			}
+		}
+	},
+	hotelBusinessHoursService: null,
+	created() {
+		this.hotelBusinessHoursService = new HotelBusinessHoursService()
+	},
+	methods: {
+		/**
+		 * 初始化弹窗
+		 * @param {string} method - add或edit
+		 * @param {string} id - 编辑时传入ID
+		 */
+		init(method, id) {
+			this.method = method
+			this.inputForm = {
+				mealType: '',
+				mealName: '',
+				dayOfWeek: null,
+				startTime: '',
+				endTime: '',
+				sortOrder: 0
+			}
+			if (method === 'add') {
+				this.title = '新增营业时段'
+			} else if (method === 'edit') {
+				this.title = '编辑营业时段'
+				this.inputForm.id = id
+			}
+			this.visible = true
+			this.loading = false
+			this.$nextTick(() => {
+				this.$refs.inputForm.resetFields()
+				if (method === 'edit') {
+					this.loading = true
+					this.hotelBusinessHoursService.queryById(id).then((data) => {
+						this.inputForm = {
+							id: data.id,
+							mealType: data.mealType || '',
+							mealName: data.mealName || '',
+							dayOfWeek: data.dayOfWeek !== null && data.dayOfWeek !== undefined ? data.dayOfWeek : null,
+							startTime: data.startTime || '',
+							endTime: data.endTime || '',
+							sortOrder: data.sortOrder || 0
+						}
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		/** 餐段类型变化时自动填充名称 */
+		onMealTypeChange(val) {
+			if (val && MEAL_TYPE_NAMES[val]) {
+				this.inputForm.mealName = MEAL_TYPE_NAMES[val]
+			}
+		},
+		/** 提交表单 */
+		doSubmit() {
+			this.$refs.inputForm.validate((valid) => {
+				if (valid) {
+					this.loading = true
+					this.hotelBusinessHoursService.save(this.inputForm).then(() => {
+						this.$message.success('操作成功')
+						this.close()
+						this.$emit('refreshList')
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		/** 关闭弹窗 */
+		close() {
+			this.$refs.inputForm && this.$refs.inputForm.resetFields()
+			this.visible = false
+		}
+	}
+}
+</script>

+ 180 - 0
src/views/hotel/admin/BusinessHoursManagement.vue

@@ -0,0 +1,180 @@
+<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="mealType">
+				<el-select v-model="searchForm.mealType" placeholder="请选择餐段" clearable style="width: 120px">
+					<el-option label="早餐" value="breakfast"></el-option>
+					<el-option label="午餐" value="lunch"></el-option>
+					<el-option label="晚餐" value="dinner"></el-option>
+					<el-option label="夜宵" value="night"></el-option>
+				</el-select>
+			</el-form-item>
+			<el-form-item label="状态" prop="status">
+				<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 120px">
+					<el-option label="启用" value="ENABLED"></el-option>
+					<el-option label="禁用" value="DISABLED"></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('hotel:admin:businessHours:add')" type="primary" icon="el-icon-plus"
+						@click="add()">新增时段</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="dataTable"
+					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 title="餐段名称" field="mealName" min-width="120" align="center"></vxe-column>
+					<vxe-column title="星期" width="120" align="center">
+						<template #default="scope">
+							{{ scope.row.dayOfWeek !== null && scope.row.dayOfWeek !== undefined ? weekDays[scope.row.dayOfWeek] : '每天' }}
+						</template>
+					</vxe-column>
+					<vxe-column title="开始时间" field="startTime" width="120" align="center"></vxe-column>
+					<vxe-column title="结束时间" field="endTime" width="120" align="center"></vxe-column>
+					<vxe-column title="状态" field="status" width="100" align="center">
+						<template #default="scope">
+							<el-tag :type="scope.row.status === 'ENABLED' ? 'success' : 'info'" effect="plain">
+								{{ scope.row.status === 'ENABLED' ? '启用' : '禁用' }}
+							</el-tag>
+						</template>
+					</vxe-column>
+					<vxe-column title="排序号" field="sortOrder" width="100" align="center"></vxe-column>
+					<vxe-column title="操作" width="200" fixed="right" align="center">
+						<template #default="scope">
+							<el-button v-if="hasPermission('hotel:admin:businessHours:edit')" text type="primary" size="small"
+								@click="edit(scope.row.id)">编辑</el-button>
+							<el-button v-if="hasPermission('hotel:admin:businessHours:edit')" text type="primary" size="small"
+								@click="doToggle(scope.row)">{{ scope.row.status === 'ENABLED' ? '禁用' : '启用' }}</el-button>
+							<el-button v-if="hasPermission('hotel:admin:businessHours:delete')" text type="danger" size="small"
+								@click="doDelete(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>
+		<BusinessHoursForm ref="businessHoursForm" @refreshList="refreshList"></BusinessHoursForm>
+	</div>
+</template>
+
+<script>
+import HotelBusinessHoursService from '@/api/hotel/HotelBusinessHoursService'
+import BusinessHoursForm from './BusinessHoursForm'
+
+export default {
+	data() {
+		return {
+			searchVisible: true,
+			searchForm: {
+				mealType: '',
+				status: ''
+			},
+			dataList: [],
+			tablePage: {
+				total: 0,
+				currentPage: 1,
+				pageSize: 10,
+				orders: []
+			},
+			loading: false,
+			weekDays: ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
+		}
+	},
+	hotelBusinessHoursService: null,
+	created() {
+		this.hotelBusinessHoursService = new HotelBusinessHoursService()
+	},
+	components: {
+		BusinessHoursForm
+	},
+	mounted() {
+		this.refreshList()
+	},
+	activated() {
+		this.refreshList()
+	},
+	methods: {
+		add() {
+			this.$refs.businessHoursForm.init('add', '')
+		},
+		edit(id) {
+			this.$refs.businessHoursForm.init('edit', id)
+		},
+		refreshList() {
+			this.loading = true
+			this.hotelBusinessHoursService.page({
+				'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 || 0
+				this.tablePage.currentPage = data.current || 1
+				this.loading = false
+			}).catch(() => {
+				this.loading = false
+			})
+		},
+		currentChangeHandle({ currentPage, pageSize }) {
+			this.tablePage.currentPage = currentPage
+			this.tablePage.pageSize = pageSize
+			this.refreshList()
+		},
+		resetSearch() {
+			this.$refs.searchForm.resetFields()
+			this.tablePage.currentPage = 1
+			this.refreshList()
+		},
+		doToggle(row) {
+			const action = row.status === 'ENABLED' ? '禁用' : '启用'
+			this.$confirm(`确定${action}该营业时段吗?`, '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.hotelBusinessHoursService.toggle(row.id).then(() => {
+					this.$message.success('操作成功')
+					this.refreshList()
+				})
+			})
+		},
+		doDelete(id) {
+			this.$confirm('确定删除该营业时段吗?', '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.loading = true
+				this.hotelBusinessHoursService.delete(id).then(() => {
+					this.$message.success('删除成功')
+					this.refreshList()
+					this.loading = false
+				}).catch(() => {
+					this.loading = false
+				})
+			})
+		}
+	}
+}
+</script>

+ 22 - 0
src/views/hotel/admin/DishManagement.vue

@@ -0,0 +1,22 @@
+<template>
+  <div class="admin-dish-page">
+    <!-- 后台管理 - 菜品管理页 -->
+    <h2>后台管理 - 菜品管理</h2>
+    <p>待实现:参考HTML原型 admin-dish.html</p>
+    <p>关键功能:菜品CRUD、分类管理、上下架</p>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+
+onMounted(() => {
+  console.log('AdminDishManagement mounted')
+})
+</script>
+
+<style scoped>
+.admin-dish-page {
+  padding: 20px;
+}
+</style>

+ 130 - 0
src/views/hotel/admin/DishSpecForm.vue

@@ -0,0 +1,130 @@
+<template>
+	<div>
+		<el-dialog :title="title" :close-on-click-modal="false" draggable width="500px" @close="close"
+			@keyup.enter.native="doSubmit" v-model="visible">
+			<el-form :model="inputForm" ref="inputForm" v-loading="loading" :rules="rules" label-width="100px"
+				@submit.native.prevent>
+				<el-form-item label="关联菜品" prop="dishId">
+					<el-select v-model="inputForm.dishId" placeholder="请选择菜品" clearable filterable style="width: 100%">
+						<el-option v-for="item in dishList" :key="item.id" :label="item.name" :value="item.id"></el-option>
+					</el-select>
+				</el-form-item>
+				<el-form-item label="规格组名称" prop="groupName">
+					<el-input v-model="inputForm.groupName" placeholder="如:熟度、酱汁、份量" clearable></el-input>
+				</el-form-item>
+				<el-form-item label="是否必选" prop="required">
+					<el-radio-group v-model="inputForm.required">
+						<el-radio :label="1">必选</el-radio>
+						<el-radio :label="0">可选</el-radio>
+					</el-radio-group>
+				</el-form-item>
+				<el-form-item label="排序号" prop="sortOrder">
+					<el-input-number v-model="inputForm.sortOrder" :min="0" :max="999" style="width: 100%"></el-input-number>
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="close()" icon="el-icon-circle-close">关闭</el-button>
+					<el-button type="primary" @click="doSubmit()" icon="el-icon-circle-check"
+						v-noMoreClick>确定</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script>
+import HotelDishSpecService from '@/api/hotel/HotelDishSpecService'
+import HotelDishService from '@/api/hotel/HotelDishService'
+
+export default {
+	data() {
+		return {
+			title: '',
+			method: '',
+			visible: false,
+			loading: false,
+			dishList: [],
+			rules: {
+				dishId: [{ required: true, message: '请选择关联菜品', trigger: 'change' }],
+				groupName: [{ required: true, message: '请输入规格组名称', trigger: 'blur' }]
+			},
+			inputForm: {
+				dishId: '',
+				groupName: '',
+				required: 0,
+				sortOrder: 0
+			}
+		}
+	},
+	hotelDishSpecService: null,
+	hotelDishService: null,
+	created() {
+		this.hotelDishSpecService = new HotelDishSpecService()
+		this.hotelDishService = new HotelDishService()
+	},
+	methods: {
+		/** 加载菜品列表(下拉框用) */
+		loadDishList() {
+			this.hotelDishService.page({ 'current': 1, 'size': 10000 }).then((data) => {
+				this.dishList = (data.records || []).filter(d => d.status === 'ON_SALE')
+			})
+		},
+		init(method, id) {
+			this.method = method
+			this.inputForm = {
+				dishId: '',
+				groupName: '',
+				required: 0,
+				sortOrder: 0
+			}
+			if (method === 'add') {
+				this.title = '新增规格组'
+			} else if (method === 'edit') {
+				this.title = '编辑规格组'
+				this.inputForm.id = id
+			}
+			this.visible = true
+			this.loading = false
+			this.loadDishList()
+			this.$nextTick(() => {
+				this.$refs.inputForm.resetFields()
+				if (method === 'edit') {
+					this.loading = true
+					this.hotelDishSpecService.queryGroupById(id).then((data) => {
+						this.inputForm = {
+							id: data.id,
+							dishId: data.dishId,
+							groupName: data.groupName,
+							required: data.required || 0,
+							sortOrder: data.sortOrder || 0
+						}
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		doSubmit() {
+			this.$refs.inputForm.validate((valid) => {
+				if (valid) {
+					this.loading = true
+					this.hotelDishSpecService.saveGroup(this.inputForm).then(() => {
+						this.$message.success('操作成功')
+						this.close()
+						this.$emit('refreshList')
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		close() {
+			this.$refs.inputForm && this.$refs.inputForm.resetFields()
+			this.visible = false
+		}
+	}
+}
+</script>

+ 237 - 0
src/views/hotel/admin/DishSpecManagement.vue

@@ -0,0 +1,237 @@
+<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="groupName">
+				<el-input v-model="searchForm.groupName" placeholder="请输入规格组名称" clearable></el-input>
+			</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('hotel:admin:dishSpec:add')" type="primary" icon="el-icon-plus"
+						@click="addGroup()">新增规格组</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="dataTable"
+					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 title="菜品名称" field="dishName" min-width="150" align="center"></vxe-column>
+					<vxe-column title="规格组名称" field="groupName" min-width="150" align="center"></vxe-column>
+					<vxe-column title="是否必选" field="required" width="100" align="center">
+						<template #default="scope">
+							<el-tag :type="scope.row.required === 1 ? 'danger' : 'info'" effect="plain" size="small">
+								{{ scope.row.required === 1 ? '必选' : '可选' }}
+							</el-tag>
+						</template>
+					</vxe-column>
+					<vxe-column title="选项数量" width="100" align="center">
+						<template #default="scope">
+							<el-button text type="primary" size="small" @click="showOptions(scope.row)">
+								{{ (scope.row.options || []).length }} 个选项
+							</el-button>
+						</template>
+					</vxe-column>
+					<vxe-column title="排序号" field="sortOrder" width="100" align="center"></vxe-column>
+					<vxe-column title="操作" width="200" fixed="right" align="center">
+						<template #default="scope">
+							<el-button v-if="hasPermission('hotel:admin:dishSpec:edit')" text type="primary" size="small"
+								@click="editGroup(scope.row.id)">编辑</el-button>
+							<el-button text type="primary" size="small" @click="showOptions(scope.row)">管理选项</el-button>
+							<el-button v-if="hasPermission('hotel:admin:dishSpec:delete')" text type="danger" size="small"
+								@click="doDeleteGroup(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>
+
+		<!-- 规格组表单弹窗 -->
+		<DishSpecForm ref="dishSpecForm" @refreshList="refreshList"></DishSpecForm>
+
+		<!-- 规格选项管理弹窗 -->
+		<el-dialog title="规格选项管理" :close-on-click-modal="false" draggable width="600px" v-model="optionsVisible">
+			<div v-if="currentGroup">
+				<p style="margin-bottom: 12px; color: #606266;">
+					规格组:<strong>{{ currentGroup.groupName }}</strong>
+					({{ currentGroup.required === 1 ? '必选' : '可选' }})
+				</p>
+				<el-button type="primary" size="small" icon="el-icon-plus" @click="addOption()" style="margin-bottom: 12px;">
+					新增选项
+				</el-button>
+				<vxe-table border="inner" auto-resize resizable height="300" :data="optionList"
+					show-header-overflow show-overflow>
+					<vxe-column type="seq" width="60" title="序号"></vxe-column>
+					<vxe-column title="选项名称" field="optionName" min-width="150" align="center"></vxe-column>
+					<vxe-column title="加价金额" field="priceExtra" width="120" align="center">
+						<template #default="scope">
+							{{ scope.row.priceExtra > 0 ? '+¥' + scope.row.priceExtra : '-' }}
+						</template>
+					</vxe-column>
+					<vxe-column title="排序号" field="sortOrder" width="100" align="center"></vxe-column>
+					<vxe-column title="操作" width="150" fixed="right" align="center">
+						<template #default="scope">
+							<el-button text type="primary" size="small" @click="editOption(scope.row.id)">编辑</el-button>
+							<el-button text type="danger" size="small" @click="doDeleteOption(scope.row.id)">删除</el-button>
+						</template>
+					</vxe-column>
+				</vxe-table>
+			</div>
+		</el-dialog>
+
+		<!-- 规格选项表单弹窗 -->
+		<DishSpecOptionForm ref="dishSpecOptionForm" @refreshOptions="loadOptions"></DishSpecOptionForm>
+	</div>
+</template>
+
+<script>
+import HotelDishSpecService from '@/api/hotel/HotelDishSpecService'
+import DishSpecForm from './DishSpecForm'
+import DishSpecOptionForm from './DishSpecOptionForm'
+
+export default {
+	data() {
+		return {
+			searchVisible: true,
+			searchForm: {
+				dishName: '',
+				groupName: ''
+			},
+			dataList: [],
+			tablePage: {
+				total: 0,
+				currentPage: 1,
+				pageSize: 10,
+				orders: []
+			},
+			loading: false,
+			// 选项管理
+			optionsVisible: false,
+			currentGroup: null,
+			optionList: []
+		}
+	},
+	hotelDishSpecService: null,
+	created() {
+		this.hotelDishSpecService = new HotelDishSpecService()
+	},
+	components: {
+		DishSpecForm,
+		DishSpecOptionForm
+	},
+	mounted() {
+		this.refreshList()
+	},
+	activated() {
+		this.refreshList()
+	},
+	methods: {
+		/** 刷新规格组列表 */
+		refreshList() {
+			this.loading = true
+			this.hotelDishSpecService.groupPage({
+				'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 || 0
+				this.tablePage.currentPage = data.current || 1
+				this.loading = false
+			}).catch(() => {
+				this.loading = false
+			})
+		},
+		currentChangeHandle({ currentPage, pageSize }) {
+			this.tablePage.currentPage = currentPage
+			this.tablePage.pageSize = pageSize
+			this.refreshList()
+		},
+		resetSearch() {
+			this.$refs.searchForm.resetFields()
+			this.tablePage.currentPage = 1
+			this.refreshList()
+		},
+		/** 新增规格组 */
+		addGroup() {
+			this.$refs.dishSpecForm.init('add', '')
+		},
+		/** 编辑规格组 */
+		editGroup(id) {
+			this.$refs.dishSpecForm.init('edit', id)
+		},
+		/** 删除规格组 */
+		doDeleteGroup(id) {
+			this.$confirm('确定删除该规格组吗?(将同时删除其下所有选项)', '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.loading = true
+				this.hotelDishSpecService.deleteGroup(id).then(() => {
+					this.$message.success('删除成功')
+					this.refreshList()
+					this.loading = false
+				}).catch(() => {
+					this.loading = false
+				})
+			})
+		},
+		/** 显示规格选项管理弹窗 */
+		showOptions(group) {
+			this.currentGroup = group
+			this.optionsVisible = true
+			this.loadOptions()
+		},
+		/** 加载规格选项列表 */
+		loadOptions() {
+			if (!this.currentGroup) return
+			this.hotelDishSpecService.findOptionsByGroupId(this.currentGroup.id).then((data) => {
+				this.optionList = Array.isArray(data) ? data : []
+			})
+		},
+		/** 新增选项 */
+		addOption() {
+			this.$refs.dishSpecOptionForm.init('add', '', this.currentGroup.id)
+		},
+		/** 编辑选项 */
+		editOption(id) {
+			this.$refs.dishSpecOptionForm.init('edit', id, this.currentGroup.id)
+		},
+		/** 删除选项 */
+		doDeleteOption(id) {
+			this.$confirm('确定删除该规格选项吗?', '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			}).then(() => {
+				this.hotelDishSpecService.deleteOption(id).then(() => {
+					this.$message.success('删除成功')
+					this.loadOptions()
+				})
+			})
+		}
+	}
+}
+</script>

+ 112 - 0
src/views/hotel/admin/DishSpecOptionForm.vue

@@ -0,0 +1,112 @@
+<template>
+	<div>
+		<el-dialog :title="title" :close-on-click-modal="false" draggable width="460px" @close="close"
+			@keyup.enter.native="doSubmit" v-model="visible">
+			<el-form :model="inputForm" ref="inputForm" v-loading="loading" :rules="rules" label-width="80px"
+				@submit.native.prevent>
+				<el-form-item label="选项名称" prop="optionName">
+					<el-input v-model="inputForm.optionName" placeholder="如:三分熟、黑椒汁" clearable></el-input>
+				</el-form-item>
+				<el-form-item label="加价金额" prop="priceExtra">
+					<el-input-number v-model="inputForm.priceExtra" :min="0" :max="9999" :precision="2"
+						style="width: 100%"></el-input-number>
+				</el-form-item>
+				<el-form-item label="排序号" prop="sortOrder">
+					<el-input-number v-model="inputForm.sortOrder" :min="0" :max="999" style="width: 100%"></el-input-number>
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="close()" icon="el-icon-circle-close">关闭</el-button>
+					<el-button type="primary" @click="doSubmit()" icon="el-icon-circle-check"
+						v-noMoreClick>确定</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script>
+import HotelDishSpecService from '@/api/hotel/HotelDishSpecService'
+
+export default {
+	data() {
+		return {
+			title: '',
+			method: '',
+			visible: false,
+			loading: false,
+			groupId: '',
+			rules: {
+				optionName: [{ required: true, message: '请输入选项名称', trigger: 'blur' }]
+			},
+			inputForm: {
+				optionName: '',
+				priceExtra: 0,
+				sortOrder: 0
+			}
+		}
+	},
+	hotelDishSpecService: null,
+	created() {
+		this.hotelDishSpecService = new HotelDishSpecService()
+	},
+	methods: {
+		init(method, id, groupId) {
+			this.method = method
+			this.groupId = groupId
+			this.inputForm = {
+				optionName: '',
+				priceExtra: 0,
+				sortOrder: 0
+			}
+			if (method === 'add') {
+				this.title = '新增规格选项'
+				this.inputForm.groupId = groupId
+			} else if (method === 'edit') {
+				this.title = '编辑规格选项'
+				this.inputForm.id = id
+			}
+			this.visible = true
+			this.loading = false
+			this.$nextTick(() => {
+				this.$refs.inputForm.resetFields()
+				if (method === 'edit') {
+					this.loading = true
+					this.hotelDishSpecService.queryOptionById(id).then((data) => {
+						this.inputForm = {
+							id: data.id,
+							groupId: data.groupId,
+							optionName: data.optionName,
+							priceExtra: data.priceExtra || 0,
+							sortOrder: data.sortOrder || 0
+						}
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		doSubmit() {
+			this.$refs.inputForm.validate((valid) => {
+				if (valid) {
+					this.loading = true
+					this.hotelDishSpecService.saveOption(this.inputForm).then(() => {
+						this.$message.success('操作成功')
+						this.close()
+						this.$emit('refreshOptions')
+						this.loading = false
+					}).catch(() => {
+						this.loading = false
+					})
+				}
+			})
+		},
+		close() {
+			this.$refs.inputForm && this.$refs.inputForm.resetFields()
+			this.visible = false
+		}
+	}
+}
+</script>

+ 102 - 0
src/views/hotel/admin/HotelConfigManagement.vue

@@ -0,0 +1,102 @@
+<template>
+	<div class="page">
+		<div class="jp-table top">
+			<vxe-toolbar :refresh="{ query: loadConfig }" custom>
+				<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="padding: 20px;" v-loading="loading">
+				<el-form :model="configForm" ref="configForm" label-width="140px" style="max-width: 600px; margin: 0 auto;">
+					<el-form-item label="酒店名称" prop="hotelName">
+						<el-input v-model="configForm.hotelName" placeholder="请输入酒店名称" clearable></el-input>
+					</el-form-item>
+					<el-form-item label="主题配色" prop="theme">
+						<el-select v-model="configForm.theme" placeholder="请选择主题配色" style="width: 100%">
+							<el-option label="商务深蓝" value="blue"></el-option>
+							<el-option label="暖金雅致" value="gold"></el-option>
+							<el-option label="墨绿清新" value="green"></el-option>
+						</el-select>
+					</el-form-item>
+					<el-form-item label="配送费(元)" prop="deliveryFee">
+						<el-input-number v-model="configForm.deliveryFee" :min="0" :max="999" :precision="2"
+							style="width: 100%"></el-input-number>
+					</el-form-item>
+					<el-form-item label="预计配送时长" prop="estimatedDeliveryMinutes">
+						<el-input-number v-model="configForm.estimatedDeliveryMinutes" :min="1" :max="120"
+							style="width: 100%"></el-input-number>
+						<span style="color: #909399; font-size: 12px; margin-left: 8px;">分钟</span>
+					</el-form-item>
+					<el-form-item>
+						<el-button type="primary" @click="doSave()" icon="el-icon-circle-check" v-noMoreClick>
+							保存配置
+						</el-button>
+						<el-button @click="loadConfig()" icon="el-icon-refresh-right">重置</el-button>
+					</el-form-item>
+				</el-form>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+import HotelConfigService from '@/api/hotel/HotelConfigService'
+
+/**
+ * 酒店配置管理页面
+ * 管理酒店基本信息:名称、主题、配送费、预计配送时长
+ */
+export default {
+	data() {
+		return {
+			searchVisible: true,
+			loading: false,
+			configForm: {
+				hotelName: '',
+				theme: 'blue',
+				deliveryFee: 0,
+				estimatedDeliveryMinutes: 30
+			}
+		}
+	},
+	hotelConfigService: null,
+	created() {
+		this.hotelConfigService = new HotelConfigService()
+	},
+	mounted() {
+		this.loadConfig()
+	},
+	activated() {
+		this.loadConfig()
+	},
+	methods: {
+		/** 加载酒店配置 */
+		loadConfig() {
+			this.loading = true
+			this.hotelConfigService.get().then((data) => {
+				const config = data.config || data || {}
+				this.configForm = {
+					hotelName: config.hotelName || '',
+					theme: config.theme || 'blue',
+					deliveryFee: config.deliveryFee || 0,
+					estimatedDeliveryMinutes: config.estimatedDeliveryMinutes || 30
+				}
+				this.loading = false
+			}).catch(() => {
+				this.loading = false
+			})
+		},
+		/** 保存酒店配置 */
+		doSave() {
+			this.loading = true
+			this.hotelConfigService.save(this.configForm).then(() => {
+				this.$message.success('配置保存成功')
+				this.loading = false
+			}).catch(() => {
+				this.loading = false
+			})
+		}
+	}
+}
+</script>

+ 21 - 0
src/views/hotel/customer/CustomerMenu.vue

@@ -0,0 +1,21 @@
+<template>
+  <div class="customer-menu-page">
+    <!-- 顾客端 - 点餐菜单页 -->
+    <h2>顾客点餐菜单</h2>
+    <p>待实现:参考HTML原型 customer-menu.html</p>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+
+onMounted(() => {
+  console.log('CustomerMenu mounted')
+})
+</script>
+
+<style scoped>
+.customer-menu-page {
+  padding: 20px;
+}
+</style>

+ 0 - 7
src/views/hotel/dish/CategoryForm.vue

@@ -12,11 +12,6 @@
 						</el-form-item>
 					</el-col>
 					<el-col :span="22">
-						<el-form-item label="图标" prop="icon">
-							<el-input v-model="inputForm.icon" placeholder="输入emoji图标,如 🍳"></el-input>
-						</el-form-item>
-					</el-col>
-					<el-col :span="22">
 						<el-form-item label="排序号" prop="sortOrder">
 							<el-input-number v-model="inputForm.sortOrder" :min="0" style="width: 100%"></el-input-number>
 						</el-form-item>
@@ -45,7 +40,6 @@ export default {
 			inputForm: {
 				id: '',
 				name: '',
-				icon: '',
 				sortOrder: 0
 			}
 		}
@@ -60,7 +54,6 @@ export default {
 			this.inputForm = {
 				id: '',
 				name: '',
-				icon: '',
 				sortOrder: 0
 			}
 			if (method === 'add') {

+ 0 - 5
src/views/hotel/dish/CategoryManagement.vue

@@ -13,11 +13,6 @@
 					:data="dataList" :checkbox-config="{}">
 					<vxe-column type="seq" width="60" title="序号"></vxe-column>
 					<vxe-column title="分类名称" field="name" align="left" min-width="150"></vxe-column>
-					<vxe-column title="图标" field="icon" width="80" align="center">
-						<template #default="scope">
-							<span style="font-size: 20px;">{{ scope.row.icon || '-' }}</span>
-						</template>
-					</vxe-column>
 					<vxe-column title="排序号" field="sortOrder" width="100" align="center"></vxe-column>
 					<vxe-column title="菜品数量" field="dishCount" width="100" align="center"></vxe-column>
 					<vxe-column title="状态" field="status" width="100" align="center">

+ 22 - 0
src/views/hotel/hotelfront/HotelFrontOrders.vue

@@ -0,0 +1,22 @@
+<template>
+  <div class="hotelfront-orders-page">
+    <!-- 宾馆前台 - 订单总览页 -->
+    <h2>宾馆前台订单总览</h2>
+    <p>待实现:参考HTML原型 hotelfront-orders.html</p>
+    <p>关键功能:所有订单查看、退单管理</p>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+
+onMounted(() => {
+  console.log('HotelFrontOrders mounted')
+})
+</script>
+
+<style scoped>
+.hotelfront-orders-page {
+  padding: 20px;
+}
+</style>

+ 39 - 0
src/views/hotel/kitchen/KitchenOrders.vue

@@ -0,0 +1,39 @@
+<template>
+  <div class="kitchen-orders-page">
+    <!-- 厨房端 - 订单接收页(需WebSocket + 语音播报) -->
+    <h2>厨房订单接收</h2>
+    <p>待实现:参考HTML原型 kitchen-orders.html</p>
+    <p>关键功能:WebSocket实时接收、Web Speech API语音播报</p>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue'
+import hotelWS from '@/utils/websocket'
+
+onMounted(() => {
+  console.log('KitchenOrders mounted')
+  // TODO: 连接WebSocket
+  // hotelWS.connect(token, handleMessage, handleError)
+})
+
+onUnmounted(() => {
+  // TODO: 断开WebSocket
+  // hotelWS.disconnect()
+})
+
+function handleMessage(event) {
+  const message = JSON.parse(event.data)
+  console.log('收到消息:', message)
+}
+
+function handleError(error) {
+  console.error('WebSocket错误:', error)
+}
+</script>
+
+<style scoped>
+.kitchen-orders-page {
+  padding: 20px;
+}
+</style>

+ 21 - 0
src/views/hotel/restaurant/RestaurantOrders.vue

@@ -0,0 +1,21 @@
+<template>
+  <div class="restaurant-orders-page">
+    <!-- 餐厅前台 - 订单管理页 -->
+    <h2>餐厅前台订单管理</h2>
+    <p>待实现:参考HTML原型 restaurant-orders.html</p>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+
+onMounted(() => {
+  console.log('RestaurantOrders mounted')
+})
+</script>
+
+<style scoped>
+.restaurant-orders-page {
+  padding: 20px;
+}
+</style>