想快速掌握一个框架,就是要不停的写项目,看别人的项目,让自己学习到的编程知识学以致用。今天就给大家分享我最近使用springboot2.7 开发的一个前端后分离项目:酒店管理系统,来练习自己的编程技术。
java的版本是:21
springboot版本是:2.7
数据库操作:mybatis-plus
前端使用的是 vue2 + element-ui
mysql:8
写这个项目主要是练习从0到1自己搭建一个项目并完成需求开发。因为是练习项目,功能做的也不是很多,主要做了: 首页统计 酒店管理 楼宇管理 房间管理 会员管理 开房登记 登记管理 设备维修
安全检查 管理员管理。
接下来跟大家分享一些页面效果:
首页:
后端代码:
bash
package com.jsonll.base.controller;
import com.jsonll.base.core.R;
import com.jsonll.base.mapper.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 首页控制器
*/
@RestController
@RequestMapping("/home")
public class HomeController extends BaseController {
@Autowired
private HotelMapper hotelMapper;
@Autowired
private HotelBuildingMapper hotelBuildingMapper;
@Autowired
private RoomMapper roomMapper;
@Autowired
private MemberMapper memberMapper;
@Autowired
private RoomRegistrationMapper roomRegistrationMapper;
@Autowired
private DeviceRepairMapper deviceRepairMapper;
@Autowired
private SafetyInspectionMapper safetyInspectionMapper;
/**
* 获取首页统计数据
* @return 统计数据
*/
@GetMapping("data")
public R data(){
Map<String, Object> result = new HashMap<>();
// 酒店数量
long hotelCount = hotelMapper.selectCount(null);
// 楼宇数量
long buildingCount = hotelBuildingMapper.selectCount(null);
// 房间数量
long roomCount = roomMapper.selectCount(null);
// 会员数量
long memberCount = memberMapper.selectCount(null);
// 房间状态统计
List<Map<String, Object>> roomStatusStats = roomMapper.getRoomStatusStats();
// 入住登记统计(按月)
List<Map<String, Object>> checkInMonthlyStats = roomRegistrationMapper.getCheckInMonthlyStats();
// 设备维修统计
List<Map<String, Object>> repairStatusStats = deviceRepairMapper.getRepairStatusStats();
// 安全检查统计(按月)
List<Map<String, Object>> safetyMonthlyStats = safetyInspectionMapper.getSafetyMonthlyStats();
result.put("hotelCount", hotelCount);
result.put("buildingCount", buildingCount);
result.put("roomCount", roomCount);
result.put("memberCount", memberCount);
result.put("roomStatusStats", roomStatusStats);
result.put("checkInMonthlyStats", checkInMonthlyStats);
result.put("repairStatusStats", repairStatusStats);
result.put("safetyMonthlyStats", safetyMonthlyStats);
return R.successData(result);
}
}
前端代码:
bash
<template>
<div class="home-container">
<!-- 数量统计卡片 -->
<div class="count-cards">
<div class="count-card">
<div class="card-icon">
<i class="el-icon-office-building"></i>
</div>
<div class="card-content">
<div class="card-value">{{ statsData.hotelCount }}</div>
<div class="card-title">酒店数量</div>
</div>
</div>
<div class="count-card">
<div class="card-icon">
<i class="el-icon-school"></i>
</div>
<div class="card-content">
<div class="card-value">{{ statsData.buildingCount }}</div>
<div class="card-title">楼宇数量</div>
</div>
</div>
<div class="count-card">
<div class="card-icon">
<i class="el-icon-house"></i>
</div>
<div class="card-content">
<div class="card-value">{{ statsData.roomCount }}</div>
<div class="card-title">房间数量</div>
</div>
</div>
<div class="count-card">
<div class="card-icon">
<i class="el-icon-user"></i>
</div>
<div class="card-content">
<div class="card-value">{{ statsData.memberCount }}</div>
<div class="card-title">会员数量</div>
</div>
</div>
</div>
<div class="chart-container">
<!-- 左侧图表 -->
<div class="chart-left">
<!-- 房间状态统计图表 -->
<div class="chart-item">
<div class="chart-title">房间状态统计</div>
<div ref="roomStatusChart" class="chart"></div>
</div>
<!-- 设备维修状态统计图表 -->
<div class="chart-item">
<div class="chart-title">设备维修状态统计</div>
<div ref="repairStatusChart" class="chart"></div>
</div>
</div>
<!-- 右侧图表 -->
<div class="chart-right">
<!-- 入住登记月度统计图表(近6个月) -->
<div class="chart-item">
<div class="chart-title">入住登记月度统计(近6个月)</div>
<div ref="checkInMonthlyChart" class="chart"></div>
</div>
<!-- 安全检查月度统计图表(近6个月) -->
<div class="chart-item">
<div class="chart-title">安全检查月度统计(近6个月)</div>
<div ref="safetyMonthlyChart" class="chart"></div>
</div>
</div>
</div>
</div>
</template>
<script>
// 引入echarts
import * as echarts from 'echarts'
import { getHomeData } from '@/api/home'
export default {
name: 'Home',
data() {
return {
// 图表实例
roomStatusChartInstance: null,
checkInMonthlyChartInstance: null,
repairStatusChartInstance: null,
safetyMonthlyChartInstance: null,
// 统计数据
statsData: {
hotelCount: 0,
buildingCount: 0,
roomCount: 0,
memberCount: 0,
roomStatusStats: [],
checkInMonthlyStats: [],
repairStatusStats: [],
safetyMonthlyStats: []
}
}
},
mounted() {
// 初始化图表
this.initCharts()
// 获取数据
this.fetchData()
},
methods: {
// 初始化所有图表
initCharts() {
// 初始化房间状态图表
this.roomStatusChartInstance = echarts.init(this.$refs.roomStatusChart)
// 初始化入住登记月度图表
this.checkInMonthlyChartInstance = echarts.init(this.$refs.checkInMonthlyChart)
// 初始化设备维修状态图表
this.repairStatusChartInstance = echarts.init(this.$refs.repairStatusChart)
// 初始化安全检查月度图表
this.safetyMonthlyChartInstance = echarts.init(this.$refs.safetyMonthlyChart)
// 监听窗口大小变化,调整图表大小
window.addEventListener('resize', this.resizeCharts)
},
// 调整所有图表大小
resizeCharts() {
this.roomStatusChartInstance && this.roomStatusChartInstance.resize()
this.checkInMonthlyChartInstance && this.checkInMonthlyChartInstance.resize()
this.repairStatusChartInstance && this.repairStatusChartInstance.resize()
this.safetyMonthlyChartInstance && this.safetyMonthlyChartInstance.resize()
},
// 获取统计数据
async fetchData() {
try {
const res = await getHomeData()
if (res.code === 1000 && res.data) {
this.statsData = res.data
// 更新图表
this.updateCharts()
}
} catch (error) {
console.error('获取首页数据失败', error)
}
},
// 更新所有图表
updateCharts() {
this.updateRoomStatusChart()
this.updateCheckInMonthlyChart()
this.updateRepairStatusChart()
this.updateSafetyMonthlyChart()
},
// 更新房间状态图表
updateRoomStatusChart() {
// 房间状态映射
const statusMap = {
1: '空闲',
2: '入住中',
3: '维修中'
}
// 处理数据
const data = this.statsData.roomStatusStats.map(item => {
return {
name: statusMap[item.status] || `状态${item.status}`,
value: item.count
}
})
// 设置图表配置
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 10,
data: data.map(item => item.name)
},
series: [
{
name: '房间状态',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data
}
]
}
// 更新图表
this.roomStatusChartInstance.setOption(option)
},
// 更新入住登记月度图表
updateCheckInMonthlyChart() {
// 处理数据
const months = this.statsData.checkInMonthlyStats.map(item => item.month)
const counts = this.statsData.checkInMonthlyStats.map(item => item.count)
// 设置图表配置
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: months,
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value'
},
series: [
{
name: '入住登记数',
type: 'bar',
barWidth: '60%',
data: counts,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
}
}
]
}
// 更新图表
this.checkInMonthlyChartInstance.setOption(option)
},
// 更新设备维修状态图表
updateRepairStatusChart() {
// 维修状态映射
const statusMap = {
1: '正在维修',
2: '已维修',
3: '放弃维修'
}
// 处理数据
const data = this.statsData.repairStatusStats.map(item => {
return {
name: statusMap[item.status] || `状态${item.status}`,
value: item.count
}
})
// 设置图表配置
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 10,
data: data.map(item => item.name)
},
series: [
{
name: '维修状态',
type: 'pie',
radius: '50%',
data: data,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
// 更新图表
this.repairStatusChartInstance.setOption(option)
},
// 更新安全检查月度图表
updateSafetyMonthlyChart() {
// 处理数据
const months = this.statsData.safetyMonthlyStats.map(item => item.month)
const counts = this.statsData.safetyMonthlyStats.map(item => item.count)
// 设置图表配置
const option = {
tooltip: {
trigger: 'axis'
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: months
},
yAxis: {
type: 'value'
},
series: [
{
name: '安全检查数',
type: 'line',
stack: '总量',
areaStyle: {},
emphasis: {
focus: 'series'
},
data: counts
}
]
}
// 更新图表
this.safetyMonthlyChartInstance.setOption(option)
}
},
beforeDestroy() {
// 移除窗口大小变化监听
window.removeEventListener('resize', this.resizeCharts)
// 销毁图表实例
this.roomStatusChartInstance && this.roomStatusChartInstance.dispose()
this.checkInMonthlyChartInstance && this.checkInMonthlyChartInstance.dispose()
this.repairStatusChartInstance && this.repairStatusChartInstance.dispose()
this.safetyMonthlyChartInstance && this.safetyMonthlyChartInstance.dispose()
}
}
</script>
<style lang="scss" scoped>
.home-container {
padding: 20px;
// 数量统计卡片样式
.count-cards {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 20px;
.count-card {
width: 23%;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 20px;
display: flex;
align-items: center;
margin-bottom: 15px;
.card-icon {
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #f0f9eb;
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
i {
font-size: 30px;
color: #67c23a;
}
}
&:nth-child(2) .card-icon {
background-color: #f2f6fc;
i {
color: #409eff;
}
}
&:nth-child(3) .card-icon {
background-color: #fdf6ec;
i {
color: #e6a23c;
}
}
&:nth-child(4) .card-icon {
background-color: #fef0f0;
i {
color: #f56c6c;
}
}
.card-content {
flex: 1;
.card-value {
font-size: 24px;
font-weight: bold;
color: #333;
line-height: 1.2;
}
.card-title {
font-size: 14px;
color: #999;
margin-top: 5px;
}
}
}
}
.chart-container {
display: flex;
justify-content: space-between;
.chart-left {
width: 38%;
.chart-item {
height: 400px;
margin-bottom: 20px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 20px;
.chart-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.chart {
width: 100%;
height: calc(100% - 35px);
}
}
}
.chart-right {
width: 60%;
.chart-item {
height: 400px;
margin-bottom: 20px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 20px;
.chart-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.chart {
width: 100%;
height: calc(100% - 35px);
}
}
}
}
}
@media screen and (max-width: 1200px) {
.home-container .chart-container .chart-item {
width: 100%;
}
.home-container .count-cards .count-card {
width: 48%;
}
}
@media screen and (max-width: 768px) {
.home-container .count-cards .count-card {
width: 100%;
}
}
</style>
登记入住页面效果:
bash
package com.jsonll.base.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jsonll.base.core.R;
import com.jsonll.base.entity.RoomRegistration;
import com.jsonll.base.request.RegistrationRequest;
import com.jsonll.base.service.IRoomRegistrationService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
/**
* 房间登记 控制器
*/
@RestController
@RequestMapping("/registration")
public class RoomRegistrationController {
@Resource
IRoomRegistrationService roomRegistrationService;
/**
* 分页查询房间登记列表
*/
@PostMapping("/page")
public R page(@RequestBody RegistrationRequest request) {
Page<RoomRegistration> page = roomRegistrationService.pageList(request);
return R.successData(page);
}
/**
* 登记入住
*/
@PostMapping("/register")
public R register(@RequestBody RegistrationRequest request) {
boolean result = roomRegistrationService.register(request);
return result ? R.success() : R.error("登记入住失败");
}
/**
* 获取房间当前有效的登记信息
*/
@GetMapping("/getCurrentRegistration/{roomId}")
public R getCurrentRegistration(@PathVariable Integer roomId) {
RoomRegistration registration = roomRegistrationService.getCurrentRegistration(roomId);
return R.successData(registration);
}
/**
* 续期入住
*/
@PostMapping("/renew")
public R renew(@RequestBody RegistrationRequest request) {
boolean result = roomRegistrationService.renew(request);
return result ? R.success() : R.error("续期入住失败");
}
/**
* 退房
*/
@PostMapping("/checkout/{roomId}")
public R checkout(@PathVariable Integer roomId) {
boolean result = roomRegistrationService.checkout(roomId);
return result ? R.success() : R.error("退房失败");
}
/**
* 获取房间登记详情(包含子表数据)
*/
@GetMapping("/detail/{id}")
public R getDetail(@PathVariable Integer id) {
Map<String, Object> detailMap = roomRegistrationService.getRegistrationDetail(id);
return R.successData(detailMap);
}
}
前端代码:
bash
<template>
<div class="checkin-container">
<!-- 上部分:搜索条件 -->
<div class="search-container">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="酒店">
<el-select v-model="searchForm.hotelId" placeholder="请选择酒店" @change="handleHotelChange">
<el-option
v-for="item in hotelOptions"
:key="item.id"
:label="item.hotelName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="楼宇">
<el-select v-model="searchForm.buildingId" placeholder="请选择楼宇" @change="handleBuildingChange" :disabled="!searchForm.hotelId">
<el-option
v-for="item in buildingOptions"
:key="item.id"
:label="item.buildingName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="楼层">
<el-select v-model="searchForm.floorId" placeholder="请选择楼层" @change="handleSearch" :disabled="!searchForm.buildingId">
<el-option
v-for="item in floorOptions"
:key="item.id"
:label="item.floorName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 下部分:房间列表 -->
<div class="room-container">
<div class="room-list">
<div
v-for="room in roomList"
:key="room.id"
class="room-item"
:class="getRoomStatusClass(room.roomStatus)"
>
<div class="room-icon">
<i class="el-icon-house"></i>
</div>
<div class="room-info">
<div class="room-number">{{ room.roomNumber }}</div>
<div class="room-name">{{ room.roomName }}</div>
<div class="room-status">{{ getRoomStatusText(room.roomStatus) }}</div>
<div class="room-features">
<span class="feature-item">
<i class="el-icon-sunny"></i>
{{ room.isSouth==1?'朝南' : '非朝南' }}
</span>
<span class="feature-item">
<i class="el-icon-view"></i>
{{ room.hasWindow === 1 ? '有窗' : '无窗' }}
</span>
</div>
</div>
<div class="room-actions">
<el-button
v-if="room.roomStatus == 1"
type="primary"
size="mini"
@click="handleRegister(room)"
>登记</el-button>
<template v-if="room.roomStatus == 2">
<el-button type="warning" size="mini" @click="handleRenew(room)">续期</el-button>
<el-button type="danger" size="mini" @click="handleCheckout(room)">退房</el-button>
</template>
</div>
</div>
<div v-if="roomList.length === 0" class="no-data">
<span>暂无房间数据</span>
</div>
</div>
</div>
<!-- 登记弹窗 -->
<el-dialog title="房间登记" :visible.sync="registerDialogVisible" width="900px">
<el-form :model="registerForm" :rules="registerRules" ref="registerForm" label-width="100px" class="register-form">
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="登记类型" prop="registrationType">
<el-radio-group v-model="registerForm.registrationType">
<el-radio :label="1">临时入驻</el-radio>
<el-radio :label="2">会员入驻</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" v-if="registerForm.registrationType === 2">
<el-col :span="12">
<el-form-item label="会员" prop="memberId">
<el-select v-model="registerForm.memberId" placeholder="请选择会员" @change="handleMemberChange" filterable>
<el-option
v-for="item in memberOptions"
:key="item.id"
:label="item.memberName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="入住人姓名" prop="guestName">
<el-input v-model="registerForm.guestName" placeholder="请输入入住人姓名"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入住人电话" prop="guestPhone">
<el-input v-model="registerForm.guestPhone" placeholder="请输入入住人电话"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="身份证" prop="idCard">
<el-input v-model="registerForm.idCard" placeholder="请输入入住人身份证"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否早餐" prop="needBreakfast">
<el-switch
v-model="registerForm.needBreakfast"
:active-value="1"
:inactive-value="0"
></el-switch>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="入住开始时间" prop="checkInTime">
<el-date-picker
v-model="registerForm.checkInTime"
type="datetime"
placeholder="选择入住开始时间"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入住到期时间" prop="checkOutTime">
<el-date-picker
v-model="registerForm.checkOutTime"
type="datetime"
placeholder="选择入住到期时间"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="随行人员">
<div v-for="(companion, index) in registerForm.companions" :key="index" class="companion-item">
<el-input v-model="companion.name" placeholder="姓名" style="width: 200px; margin-right: 10px;"></el-input>
<el-input v-model="companion.idCard" placeholder="身份证" style="width: 300px; margin-right: 10px;"></el-input>
<el-button type="danger" icon="el-icon-delete" circle @click="removeCompanion(index)"></el-button>
</div>
<el-button type="primary" icon="el-icon-plus" @click="addCompanion">添加随行人员</el-button>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注">
<el-input type="textarea" v-model="registerForm.remarks" placeholder="请输入备注信息"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="订单金额">
<el-input-number v-model="registerForm.orderAmount" :precision="2" :step="10" :min="0"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="支付金额">
<el-input-number v-model="registerForm.paymentAmount" :precision="2" :step="10" :min="0"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="优惠金额">
<el-input-number v-model="registerForm.discountAmount" :precision="2" :step="10" :min="0"></el-input-number>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="registerDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitRegister">确 定</el-button>
</div>
</el-dialog>
<!-- 续期弹窗 -->
<el-dialog title="房间续期" :visible.sync="renewDialogVisible" width="1200px">
<el-form :model="renewForm" :rules="renewRules" ref="renewForm" label-width="120px" class="renew-form">
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="入住人姓名">
<el-input v-model="renewForm.guestName" disabled></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="入住人联系电话">
<el-input v-model="renewForm.guestPhone" disabled></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="入住开始时间">
<el-date-picker
v-model="renewForm.checkInTime"
type="datetime"
placeholder="选择入住开始时间"
style="width: 100%"
disabled
></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="入住到期时间" prop="checkOutTime">
<el-date-picker
v-model="renewForm.checkOutTime"
type="datetime"
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="订单金额">
<el-input-number v-model="renewForm.orderAmount" :precision="2" :step="10" :min="0"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="支付金额">
<el-input-number v-model="renewForm.paymentAmount" :precision="2" :step="10" :min="0"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="优惠金额">
<el-input-number v-model="renewForm.discountAmount" :precision="2" :step="10" :min="0"></el-input-number>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="renewDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitRenew">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getRoomList, registerRoom, getMemberList, getHotelList, getBuildingList, getFloorList, getCurrentRegistration, renewRegistration, checkoutRoom } from '@/api/registration'
import { parseTime } from '@/utils'
export default {
name: 'Checkin',
data() {
return {
// 搜索表单
searchForm: {
hotelId: '',
buildingId: '',
floorId: ''
},
// 下拉选项
hotelOptions: [],
buildingOptions: [],
floorOptions: [],
memberOptions: [],
// 房间列表
roomList: [],
// 登记弹窗
registerDialogVisible: false,
// 续期弹窗
renewDialogVisible: false,
// 登记表单
registerForm: {
registrationType: 1, // 1临时入驻 2会员入驻
memberId: null,
guestName: '',
guestPhone: '',
roomId: null,
checkInTime: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
checkOutTime: '',
idCard: '',
companions: [],
remarks: '',
needBreakfast: 0,
orderAmount: 0,
paymentAmount: 0,
discountAmount: 0
},
// 续期表单
renewForm: {
id: '', // 当前登记记录ID
guestName: '',
guestPhone: '',
checkInTime: '',
checkOutTime: '',
orderAmount: 0,
paymentAmount: 0,
discountAmount: 0
},
// 表单验证规则
registerRules: {
guestName: [
{ required: true, message: '请输入入住人姓名', trigger: 'blur' }
],
guestPhone: [
{ required: true, message: '请输入入住人电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
idCard: [
{ required: true, message: '请输入入住人身份证', trigger: 'blur' },
],
checkInTime: [
{ required: true, message: '请选择入住时间', trigger: 'change' }
],
checkOutTime: [
{ required: true, message: '请选择到期时间', trigger: 'change' }
],
memberId: [
{ required: true, message: '请选择会员', trigger: 'change' }
]
},
// 续期表单验证规则
renewRules: {
checkOutTime: [
{ required: true, message: '请选择入住到期时间', trigger: 'change' }
]
}
}
},
created() {
this.fetchHotelList();
this.handleSearch();
},
methods: {
// 获取酒店列表
fetchHotelList() {
getHotelList().then(response => {
if (response.code === 1000) {
this.hotelOptions = response.data.records || []
}
})
},
// 获取楼宇列表
fetchBuildingList(hotelId) {
getBuildingList({ hotelId }).then(response => {
if (response.code === 1000) {
this.buildingOptions = response.data.records || []
}
})
},
// 获取楼层列表
fetchFloorList(buildingId) {
getFloorList({ buildingId }).then(response => {
if (response.code === 1000) {
this.floorOptions = response.data.records || []
}
})
},
// 获取房间列表
fetchRoomList() {
const params = { ...this.searchForm }
getRoomList(params).then(response => {
if (response.code === 1000) {
this.roomList = response.data.records || []
}
})
},
// 获取会员列表
fetchMemberList() {
getMemberList().then(response => {
if (response.code === 1000) {
this.memberOptions = response.data.records || []
}
})
},
// 酒店选择变化
handleHotelChange(val) {
this.searchForm.buildingId = ''
this.searchForm.floorId = ''
this.buildingOptions = []
this.floorOptions = []
if (val) {
this.fetchBuildingList(val)
// 选择酒店后自动触发房间查询
this.fetchRoomList()
}
},
// 楼宇选择变化
handleBuildingChange(val) {
this.searchForm.floorId = ''
this.floorOptions = []
if (val) {
this.fetchFloorList(val)
// 选择楼宇后自动触发房间查询
this.fetchRoomList()
}
},
// 搜索
handleSearch() {
this.fetchRoomList()
},
// 重置搜索
resetSearch() {
this.searchForm = {
hotelId: '',
buildingId: '',
floorId: ''
}
this.buildingOptions = []
this.floorOptions = []
this.roomList = []
},
// 获取房间状态样式类
getRoomStatusClass(status) {
// 将字符串类型的状态转换为数字
const statusNum = parseInt(status)
switch (statusNum) {
case 1: return 'room-free'
case 2: return 'room-occupied'
case 3: return 'room-maintenance'
default: return ''
}
},
// 获取房间状态文本
getRoomStatusText(status) {
// 将字符串类型的状态转换为数字
const statusNum = parseInt(status)
switch (statusNum) {
case 1: return '空闲'
case 2: return '入住中'
case 3: return '维修中'
default: return '未知'
}
},
// 处理登记
handleRegister(room) {
this.registerForm = {
registrationType: 1,
memberId: null,
guestName: '',
guestPhone: '',
roomId: room.id,
checkInTime: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
checkOutTime: '',
idCard: '',
companions: [],
remarks: '',
needBreakfast: 0
}
this.fetchMemberList()
this.registerDialogVisible = true
},
// 处理续期
handleRenew(room) {
console.log('续期按钮被点击', room)
this.currentRoom = room
// 获取当前房间的登记信息
getCurrentRegistration(room.id).then(response => {
if (response.code === 1000) {
const registration = response.data
if (registration) {
// 填充续期表单
this.renewForm = {
id: registration.id,
guestName: registration.guestName,
guestPhone: registration.guestPhone,
roomId: registration.roomId,
checkInTime: registration.checkInTime,
checkOutTime: new Date(registration.checkOutTime),
orderAmount: 0,
paymentAmount: 0,
discountAmount: 0
}
// 显示续期弹窗
this.renewDialogVisible = true
} else {
this.$message.warning('该房间没有有效的登记信息')
}
} else {
this.$message.error(response.msg || '获取登记信息失败')
}
}).catch(err => {
console.error('获取登记信息失败', err)
this.$message.error('获取登记信息失败')
})
},
// 提交续期表单
submitRenew() {
this.$refs.renewForm.validate(valid => {
if (valid) {
// 检查续期时间是否大于当前时间
const now = new Date()
if (new Date(this.renewForm.checkOutTime) <= now) {
this.$message.warning('续期时间必须大于当前时间')
return
}
// 构建续期请求参数
const renewRequest = {
id: this.renewForm.id,
roomId: this.currentRoom.id,
checkOutTime: parseTime(this.renewForm.checkOutTime, '{y}-{m}-{d} {h}:{i}:{s}'),
orderAmount: this.renewForm.orderAmount,
paymentAmount: this.renewForm.paymentAmount,
discountAmount: this.renewForm.discountAmount
}
// 调用续期API
renewRegistration(renewRequest).then(response => {
if (response.code === 1000) {
this.$message.success('房间续期成功')
this.renewDialogVisible = false
// 刷新房间列表
this.fetchRoomList()
} else {
this.$message.error(response.msg || '房间续期失败')
}
}).catch(err => {
console.error('房间续期失败', err)
this.$message.error('房间续期失败')
})
} else {
return false
}
})
},
// 处理退房
handleCheckout(room) {
// 显示确认对话框
this.$confirm(`确定要为${room.roomNumber}房间办理退房吗?`, '退房确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 用户确认退房,调用退房接口
checkoutRoom(room.id).then(response => {
if (response.code === 1000) {
this.$message.success('退房成功')
// 刷新房间列表
this.fetchRoomList()
} else {
this.$message.error(response.msg || '退房失败')
}
}).catch(err => {
console.error('退房失败', err)
this.$message.error('退房失败')
})
}).catch(() => {
// 用户取消退房
this.$message.info('已取消退房操作')
})
},
// 会员选择变化
handleMemberChange(memberId) {
if (memberId) {
const member = this.memberOptions.find(item => item.id === memberId)
if (member) {
this.registerForm.guestName = member.memberName
this.registerForm.guestPhone = member.contact
}
}
},
// 添加随行人员
addCompanion() {
this.registerForm.companions.push({ name: '', idCard: '' })
},
// 移除随行人员
removeCompanion(index) {
this.registerForm.companions.splice(index, 1)
},
// 提交登记
submitRegister() {
this.$refs.registerForm.validate(valid => {
if (valid) {
// 处理随行人员数据
const companions = this.registerForm.companions.filter(item => item.name && item.idCard)
const params = {
...this.registerForm,
companions: JSON.stringify(companions)
}
params.checkOutTime=parseTime(params.checkOutTime, '{y}-{m}-{d} {h}:{i}:{s}'),
registerRoom(params).then(response => {
if (response.code === 1000) {
this.$message.success('登记成功')
this.registerDialogVisible = false
this.fetchRoomList() // 刷新房间列表
} else {
this.$message.error(response.msg || '登记失败')
}
})
}
})
}
}
}
</script>
<style scoped>
.checkin-container {
height: 100%;
display: flex;
flex-direction: column;
}
.search-container {
padding: 15px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 15px;
}
.room-container {
flex: 1;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
padding: 15px;
overflow-y: auto;
}
.room-list {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.room-item {
width: 220px;
height: 220px;
border-radius: 8px;
padding: 15px;
display: flex;
flex-direction: column;
justify-content: space-between;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.room-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.room-free {
background-color: #f0f9eb;
border: 1px solid #e1f3d8;
}
.room-occupied {
background-color: #fef0f0;
border: 1px solid #fde2e2;
}
.room-maintenance {
background-color: #f4f4f5;
border: 1px solid #e9e9eb;
}
.room-icon {
text-align: center;
margin-bottom: 10px;
}
.room-icon i {
font-size: 28px;
color: #409EFF;
}
.room-info {
text-align: center;
}
.room-number {
font-size: 18px;
font-weight: bold;
margin-bottom: 5px;
}
.room-name {
font-size: 14px;
color: #606266;
margin-bottom: 5px;
}
.room-status {
display: inline-block;
padding: 2px 8px;
font-size: 12px;
border-radius: 10px;
background-color: #f0f0f0;
margin-bottom: 8px;
}
.room-features {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 5px;
font-size: 12px;
}
.feature-item {
display: flex;
align-items: center;
color: #606266;
}
.feature-item i {
margin-right: 3px;
color: #409EFF;
}
.room-free .room-status {
background-color: #67c23a;
color: #fff;
}
.room-occupied .room-status {
background-color: #f56c6c;
color: #fff;
}
.room-maintenance .room-status {
background-color: #909399;
color: #fff;
}
.room-actions {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 10px;
}
.no-data {
width: 100%;
height: 200px;
display: flex;
justify-content: center;
align-items: center;
color: #909399;
}
.companion-item {
display: flex;
margin-bottom: 10px;
align-items: center;
}
.companion-input {
margin-right: 10px;
}
</style>
如果你是刚开始学习 Java,可以从零基础开始尝试搭建一个系统。你也可以参考这个系统,并结合自己的想法,开发出一个更完善的管理系统。希望对你有所帮助。
为了更好的帮助到学习编程,但是没有想法的小伙伴,我把我写的这个项目搭建了一个预览地址,方便大家预览参考~
https://test.wwwoop.com/?s=jiu-dian-guan-li&no=Hotel-001&rand=0.4133917792569839