|
|
@@ -0,0 +1,310 @@
|
|
|
+<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="roomNo">
|
|
|
+ <el-input v-model="searchForm.roomNo" placeholder="请输入房间号" clearable></el-input>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="房间类型" prop="roomTypeId">
|
|
|
+ <el-select v-model="searchForm.roomTypeId" placeholder="请选择类型" clearable style="width: 140px">
|
|
|
+ <el-option v-for="item in roomTypeList" :key="item.id" :label="item.name" :value="item.id"></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="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>
|
|
|
+ <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:room:add')" type="primary" icon="el-icon-plus"
|
|
|
+ @click="add()">新增房间</el-button>
|
|
|
+ <el-button v-if="hasPermission('hotel:admin:room:list')" type="success" icon="el-icon-download"
|
|
|
+ :disabled="selectedRows.length === 0"
|
|
|
+ @click="batchDownloadQr()" 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="dataTable"
|
|
|
+ show-header-overflow show-overflow highlight-hover-row :menu-config="{}" :data="dataList"
|
|
|
+ :checkbox-config="{}"
|
|
|
+ @checkbox-change="handleCheckboxChange"
|
|
|
+ @checkbox-all="handleCheckboxChange">
|
|
|
+ <vxe-column type="seq" width="60" title="序号"></vxe-column>
|
|
|
+ <vxe-column type="checkbox" width="60"></vxe-column>
|
|
|
+ <vxe-column title="房间号" field="roomNo" min-width="120" align="center">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button text type="primary" @click="viewDetail(scope.row.id)">{{ scope.row.roomNo }}</el-button>
|
|
|
+ </template>
|
|
|
+ </vxe-column>
|
|
|
+ <vxe-column title="房间类型" field="roomTypeName" min-width="120" align="center"></vxe-column>
|
|
|
+ <vxe-column title="楼层" field="floor" min-width="80" align="center"></vxe-column>
|
|
|
+ <vxe-column title="状态" field="status" min-width="100" align="center">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-tag :type="statusTagType(scope.row.status)" effect="plain">
|
|
|
+ {{ statusLabel(scope.row.status) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </vxe-column>
|
|
|
+ <vxe-column title="二维码" field="qrCodeImage" min-width="100" align="center">
|
|
|
+ <template #default="scope">
|
|
|
+ <div v-if="scope.row.qrCodeImage" class="qr-thumb" @click="viewQr(scope.row)">
|
|
|
+ <img :src="'data:image/png;base64,' + scope.row.qrCodeImage" alt="二维码" />
|
|
|
+ </div>
|
|
|
+ <span v-else class="qr-empty">未生成</span>
|
|
|
+ </template>
|
|
|
+ </vxe-column>
|
|
|
+ <vxe-column title="生成时间" field="qrGeneratedTime" min-width="170" align="center"></vxe-column>
|
|
|
+ <vxe-column title="操作" width="280" fixed="right" align="center">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button v-if="hasPermission('hotel:admin:room:edit')" text type="primary" size="small"
|
|
|
+ @click="edit(scope.row.id)">编辑</el-button>
|
|
|
+ <el-button v-if="hasPermission('hotel:admin:room:edit')" text type="primary" size="small"
|
|
|
+ @click="doToggleStatus(scope.row)">{{ statusAction(scope.row.status) }}</el-button>
|
|
|
+ <el-button v-if="hasPermission('hotel:admin:room:list') && scope.row.qrCodeImage" text
|
|
|
+ type="primary" size="small" @click="viewQr(scope.row)">查看</el-button>
|
|
|
+ <el-button v-if="hasPermission('hotel:admin:room:edit') && scope.row.qrCodeImage" text
|
|
|
+ type="primary" size="small" @click="doRegenerateQr(scope.row)">重新生成</el-button>
|
|
|
+ <el-button v-if="hasPermission('hotel:admin:room: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>
|
|
|
+ <RoomForm ref="roomForm" @refreshList="refreshList"></RoomForm>
|
|
|
+ <QrCodeDialog ref="qrCodeDialog"></QrCodeDialog>
|
|
|
+ <RoomDetail ref="roomDetail"></RoomDetail>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import HotelRoomService from '@/api/hotel/HotelRoomService'
|
|
|
+import HotelRoomTypeService from '@/api/hotel/HotelRoomTypeService'
|
|
|
+import JSZip from 'jszip'
|
|
|
+import RoomForm from './RoomForm'
|
|
|
+import QrCodeDialog from './QrCodeDialog'
|
|
|
+import RoomDetail from './RoomDetail'
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ searchVisible: true,
|
|
|
+ searchForm: {
|
|
|
+ roomNo: '',
|
|
|
+ roomTypeId: '',
|
|
|
+ status: ''
|
|
|
+ },
|
|
|
+ roomTypeList: [],
|
|
|
+ dataList: [],
|
|
|
+ selectedRows: [],
|
|
|
+ tablePage: {
|
|
|
+ total: 0,
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ orders: []
|
|
|
+ },
|
|
|
+ loading: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ hotelRoomService: null,
|
|
|
+ hotelRoomTypeService: null,
|
|
|
+ created() {
|
|
|
+ this.hotelRoomService = new HotelRoomService()
|
|
|
+ this.hotelRoomTypeService = new HotelRoomTypeService()
|
|
|
+ this.loadRoomTypes()
|
|
|
+ },
|
|
|
+ components: {
|
|
|
+ RoomForm,
|
|
|
+ QrCodeDialog,
|
|
|
+ RoomDetail
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.refreshList()
|
|
|
+ },
|
|
|
+ activated() {
|
|
|
+ this.refreshList()
|
|
|
+ this.loadRoomTypes()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ add() {
|
|
|
+ this.$refs.roomForm.init('add', '')
|
|
|
+ },
|
|
|
+ edit(id) {
|
|
|
+ this.$refs.roomForm.init('edit', id)
|
|
|
+ },
|
|
|
+ viewDetail(id) {
|
|
|
+ this.$refs.roomDetail.init(id)
|
|
|
+ },
|
|
|
+ refreshList() {
|
|
|
+ this.loading = true
|
|
|
+ this.hotelRoomService.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
|
|
|
+ this.selectedRows = []
|
|
|
+ }).catch(() => {
|
|
|
+ this.loading = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+ currentChangeHandle({ currentPage, pageSize }) {
|
|
|
+ this.tablePage.currentPage = currentPage
|
|
|
+ this.tablePage.pageSize = pageSize
|
|
|
+ this.refreshList()
|
|
|
+ },
|
|
|
+ resetSearch() {
|
|
|
+ this.$refs.searchForm.resetFields()
|
|
|
+ this.searchForm.roomTypeId = ''
|
|
|
+ this.tablePage.currentPage = 1
|
|
|
+ this.refreshList()
|
|
|
+ },
|
|
|
+ handleCheckboxChange() {
|
|
|
+ this.selectedRows = this.$refs.dataTable.getCheckboxRecords()
|
|
|
+ },
|
|
|
+ doToggleStatus(row) {
|
|
|
+ const statusMap = { '0': '1', '1': '0', '2': '0' }
|
|
|
+ const targetStatus = statusMap[row.status] || '0'
|
|
|
+ const labelMap = { '0': '暂停', '1': '恢复', '2': '恢复' }
|
|
|
+ this.$confirm(`确定${labelMap[row.status]}房间【${row.roomNo}】吗?`, '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ this.loading = true
|
|
|
+ this.hotelRoomService.toggleStatus(row.id, targetStatus).then(() => {
|
|
|
+ this.$message.success('操作成功')
|
|
|
+ this.refreshList()
|
|
|
+ this.loading = false
|
|
|
+ }).catch(() => {
|
|
|
+ this.loading = false
|
|
|
+ })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ viewQr(row) {
|
|
|
+ this.$refs.qrCodeDialog.init(row.roomNo, row.qrCodeUrl, row.qrCodeImage)
|
|
|
+ },
|
|
|
+ doRegenerateQr(row) {
|
|
|
+ this.$confirm(`确定重新生成房间【${row.roomNo}】的二维码吗?旧二维码将失效。`, '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ this.loading = true
|
|
|
+ this.hotelRoomService.regenerateQr(row.id).then((data) => {
|
|
|
+ this.$message.success('重新生成成功')
|
|
|
+ this.$refs.qrCodeDialog.init(row.roomNo, data.qrCodeUrl || row.qrCodeUrl, data.qrCodeImage)
|
|
|
+ this.refreshList()
|
|
|
+ this.loading = false
|
|
|
+ }).catch(() => {
|
|
|
+ this.loading = false
|
|
|
+ })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ doDelete(id) {
|
|
|
+ this.$confirm('确定删除该房间吗?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ this.loading = true
|
|
|
+ this.hotelRoomService.delete(id).then(() => {
|
|
|
+ this.$message.success('删除成功')
|
|
|
+ this.refreshList()
|
|
|
+ this.loading = false
|
|
|
+ }).catch(() => {
|
|
|
+ this.loading = false
|
|
|
+ })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ async batchDownloadQr() {
|
|
|
+ const rows = this.selectedRows.filter(r => r.qrCodeImage)
|
|
|
+ if (rows.length === 0) {
|
|
|
+ this.$message.warning('所选房间中没有已生成二维码的房间')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const zip = new JSZip()
|
|
|
+ const folder = zip.folder('房间二维码')
|
|
|
+ rows.forEach((row) => {
|
|
|
+ folder.file(`房间${row.roomNo}.png`, row.qrCodeImage, { base64: true })
|
|
|
+ })
|
|
|
+ const content = await zip.generateAsync({ type: 'blob' })
|
|
|
+ const link = document.createElement('a')
|
|
|
+ link.href = URL.createObjectURL(content)
|
|
|
+ link.download = `房间二维码_${new Date().toLocaleDateString()}.zip`
|
|
|
+ link.click()
|
|
|
+ URL.revokeObjectURL(link.href)
|
|
|
+ this.$message.success(`已打包下载 ${rows.length} 个二维码`)
|
|
|
+ } catch (e) {
|
|
|
+ this.$message.error('打包失败:' + e.message)
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ statusLabel(status) {
|
|
|
+ const map = { '0': '正常', '1': '暂停', '2': '停用' }
|
|
|
+ return map[status] || '未知'
|
|
|
+ },
|
|
|
+ statusTagType(status) {
|
|
|
+ const map = { '0': 'success', '1': 'warning', '2': 'info' }
|
|
|
+ return map[status] || 'info'
|
|
|
+ },
|
|
|
+ statusAction(status) {
|
|
|
+ const map = { '0': '暂停', '1': '恢复', '2': '恢复' }
|
|
|
+ return map[status] || '恢复'
|
|
|
+ },
|
|
|
+ loadRoomTypes() {
|
|
|
+ this.hotelRoomTypeService.all().then((data) => {
|
|
|
+ this.roomTypeList = Array.isArray(data) ? data : (data.list || [])
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.qr-thumb {
|
|
|
+ display: inline-block;
|
|
|
+ cursor: pointer;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ transition: box-shadow 0.2s;
|
|
|
+}
|
|
|
+.qr-thumb:hover {
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+.qr-thumb img {
|
|
|
+ display: block;
|
|
|
+ width: 48px;
|
|
|
+ height: 48px;
|
|
|
+}
|
|
|
+.qr-empty {
|
|
|
+ color: #c0c4cc;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+</style>
|