提交宿舍房间设置的前端界面

This commit is contained in:
zlwu committed 2025-07-22 00:21:59 +08:00
1 parent 7ce20647f0
commit 12b4b58647
28 files changed
+1623 -17

No files matched your search

@@ -0,0 +1,62 @@
# 宿舍楼房间管理功能开发记录
**日期:** 2025-07-03 14:30:25
**开发人员:** Cursor
**功能模块:** 学生宿舍管理系统 - 宿舍楼房间管理
## 变更内容
### 1. 功能概述
开发了宿舍楼房间管理界面,实现了以下功能:
- 左侧树形结构展示校区-宿舍区-宿舍楼的层级关系
- 右侧按楼层展示房间信息,采用卡片式布局
- 支持房间的添加、编辑、删除操作
- 支持床位的添加、编辑、删除操作
- 支持按房间号和状态进行筛选
### 2. 文件变更
#### 新增文件
- `web/src/views/xsssfjgl/index.vue` - 宿舍楼房间管理主界面
- `web/src/views/xsssfjgl/components/RoomDialog.vue` - 房间编辑对话框组件
- `web/src/views/xsssfjgl/components/BedDialog.vue` - 床位编辑对话框组件
- `web/src/api/xsssfjgl/index.ts` - API接口实现
- `web/src/api/xsssfjgl/types.ts` - API类型定义
### 3. 技术实现
#### 3.1 树形结构设计
- 采用Element Plus的Tree组件实现
- 配置了disabled属性,使校区和宿舍区节点不可选
- 只有宿舍楼节点可以被选中,并触发右侧数据加载
#### 3.2 房间展示
- 按楼层分组展示房间信息
- 使用Grid布局实现自适应卡片排列
- 每张卡片包含房间号、类型、床位使用情况和操作按钮
#### 3.3 床位管理
- 点击"床位管理"按钮弹出对话框
- 表格展示该房间的所有床位信息
- 支持床位的增删改操作
#### 3.4 数据处理
- 前期使用模拟数据进行界面开发
- 预留了真实API接口,便于后期接入后端服务
- 完整实现了增删改查的前端逻辑
## 后续工作
1. 对接后端API,替换模拟数据
2. 根据实际业务需求调整界面细节
3. 增加权限控制逻辑
4. 完善数据校验和错误处理
5. 添加单元测试
## 注意事项
- 目前使用模拟数据,未与后端对接
- 表单校验规则已基本实现,但可能需要根据业务规则调整
- v-model:show-search 等指令存在linter错误,后续需要修复
- 组件导入和类型引用存在linter错误,需要调整项目配置
@@ -0,0 +1,489 @@
# 宿舍房间管理API文档
**版本:** 1.0.0
**作者:** Cursor
**更新日期:** 2025-07-03
## 目录
- [1. 概述](#1-概述)
- [2. 接口基础信息](#2-接口基础信息)
- [3. 宿舍楼树形结构接口](#3-宿舍楼树形结构接口)
- [4. 房间管理接口](#4-房间管理接口)
- [5. 床位管理接口](#5-床位管理接口)
- [6. 数据字典](#6-数据字典)
- [7. 错误码说明](#7-错误码说明)
- [8. 移动端实现](#8-移动端实现)
## 1. 概述
本文档描述了宿舍楼房间管理模块的接口规范,包括宿舍楼树形结构查询、房间增删改查和床位管理等功能。
### 1.1 应用场景
- 学工处管理宿舍楼房间信息
- 学生宿舍分配
- 宿舍房间和床位状态维护
### 1.2 功能列表
- 宿舍楼树形结构查询
- 房间列表查询(支持分页和条件筛选)
- 房间新增、修改、删除
- 床位列表查询
- 床位新增、修改、删除
## 2. 接口基础信息
### 2.1 接口规范
- 接口采用RESTful风格
- 请求和响应数据格式均为JSON
- 所有接口均需要携带Token进行认证
- 接口基础路径: `/xsssfjgl`
### 2.2 统一响应格式
```json
{
"code": 200, // 响应状态码,200表示成功
"msg": "操作成功", // 响应消息
"data": {} // 响应数据,根据接口定义不同而不同
}
```
### 2.3 分页查询响应格式
```json
{
"code": 200,
"msg": "查询成功",
"rows": [], // 数据列表
"total": 0 // 总记录数
}
```
## 3. 宿舍楼树形结构接口
### 3.1 获取宿舍树形结构
**接口说明:** 获取校区-宿舍区-宿舍楼的树形结构数据
- **请求方式:** GET
- **请求路径:** `/xsssfjgl/tree`
- **请求参数:** 无
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{
"id": 1,
"label": "东校区",
"type": "campus",
"disabled": true,
"children": [
{
"id": 11,
"label": "东区宿舍",
"type": "area",
"disabled": true,
"children": [
{
"id": 111,
"label": "东区1号楼",
"type": "building",
"buildingFloors": 6
}
]
}
]
}
]
}
```
**字段说明:**
| 字段名 | 类型 | 说明 |
| ---- | ---- | ---- |
| id | string/number | 节点ID |
| label | string | 节点名称 |
| type | string | 节点类型:campus(校区)、area(宿舍区)、building(宿舍楼) |
| disabled | boolean | 是否禁用选择 |
| children | array | 子节点 |
| buildingFloors | number | 宿舍楼层数,仅type为building时有效 |
## 4. 房间管理接口
### 4.1 获取房间列表
**接口说明:** 分页查询房间信息
- **请求方式:** GET
- **请求路径:** `/xsssfjgl/room/list`
- **请求参数:**
| 参数名 | 必填 | 类型 | 说明 |
| ---- | ---- | ---- | ---- |
| pageNum | 是 | number | 页码,从1开始 |
| pageSize | 是 | number | 每页记录数 |
| buildingId | 是 | string/number | 宿舍楼ID |
| roomName | 否 | string | 房间名称,支持模糊查询 |
| floorNumber | 否 | number | 楼层号 |
| status | 否 | string | 房间状态:0-正常,1-维修中,2-停用 |
**响应示例:**
```json
{
"code": 200,
"msg": "查询成功",
"rows": [
{
"roomId": "111101",
"roomName": "101",
"floorNumber": 1,
"buildingId": 111,
"capacity": 4,
"used": 2,
"roomType": "标准间",
"status": "0",
"remark": "",
"createBy": "admin",
"createTime": "2023-01-01 00:00:00",
"updateBy": "admin",
"updateTime": "2023-01-01 00:00:00"
}
],
"total": 60
}
```
### 4.2 获取房间详情
**接口说明:** 根据房间ID获取详细信息
- **请求方式:** GET
- **请求路径:** `/xsssfjgl/room/{roomId}`
- **请求参数:**
| 参数名 | 必填 | 类型 | 说明 |
| ---- | ---- | ---- | ---- |
| roomId | 是 | string/number | 房间ID |
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"roomId": "111101",
"roomName": "101",
"floorNumber": 1,
"buildingId": 111,
"capacity": 4,
"used": 2,
"roomType": "标准间",
"status": "0",
"remark": "",
"createBy": "admin",
"createTime": "2023-01-01 00:00:00",
"updateBy": "admin",
"updateTime": "2023-01-01 00:00:00"
}
}
```
### 4.3 新增房间
**接口说明:** 新增宿舍房间
- **请求方式:** POST
- **请求路径:** `/xsssfjgl/room`
- **请求参数:**
```json
{
"roomName": "101",
"floorNumber": 1,
"buildingId": 111,
"capacity": 4,
"roomType": "标准间",
"status": "0",
"remark": "新增房间"
}
```
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功"
}
```
### 4.4 修改房间
**接口说明:** 修改宿舍房间信息
- **请求方式:** PUT
- **请求路径:** `/xsssfjgl/room`
- **请求参数:**
```json
{
"roomId": "111101",
"roomName": "101",
"floorNumber": 1,
"buildingId": 111,
"capacity": 4,
"roomType": "标准间",
"status": "0",
"remark": "修改房间信息"
}
```
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功"
}
```
### 4.5 删除房间
**接口说明:** 删除宿舍房间
- **请求方式:** DELETE
- **请求路径:** `/xsssfjgl/room/{roomIds}`
- **请求参数:**
| 参数名 | 必填 | 类型 | 说明 |
| ---- | ---- | ---- | ---- |
| roomIds | 是 | string | 房间ID,多个ID用逗号分隔 |
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功"
}
```
## 5. 床位管理接口
### 5.1 获取床位列表
**接口说明:** 查询指定房间的床位列表
- **请求方式:** GET
- **请求路径:** `/xsssfjgl/bed/list`
- **请求参数:**
| 参数名 | 必填 | 类型 | 说明 |
| ---- | ---- | ---- | ---- |
| roomId | 是 | string/number | 房间ID |
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{
"bedId": "111101-1",
"bedName": "1号床",
"roomId": "111101",
"status": "0",
"studentId": null,
"studentName": null,
"remark": "",
"createBy": "admin",
"createTime": "2023-01-01 00:00:00",
"updateBy": "admin",
"updateTime": "2023-01-01 00:00:00"
},
{
"bedId": "111101-2",
"bedName": "2号床",
"roomId": "111101",
"status": "1",
"studentId": 10001,
"studentName": "张三",
"remark": "",
"createBy": "admin",
"createTime": "2023-01-01 00:00:00",
"updateBy": "admin",
"updateTime": "2023-01-01 00:00:00"
}
]
}
```
### 5.2 新增床位
**接口说明:** 新增床位
- **请求方式:** POST
- **请求路径:** `/xsssfjgl/bed`
- **请求参数:**
```json
{
"bedName": "3号床",
"roomId": "111101",
"status": "0",
"remark": "新增床位"
}
```
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功"
}
```
### 5.3 修改床位
**接口说明:** 修改床位信息
- **请求方式:** PUT
- **请求路径:** `/xsssfjgl/bed`
- **请求参数:**
```json
{
"bedId": "111101-3",
"bedName": "3号床",
"roomId": "111101",
"status": "1",
"remark": "修改床位信息"
}
```
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功"
}
```
### 5.4 删除床位
**接口说明:** 删除床位
- **请求方式:** DELETE
- **请求路径:** `/xsssfjgl/bed/{bedIds}`
- **请求参数:**
| 参数名 | 必填 | 类型 | 说明 |
| ---- | ---- | ---- | ---- |
| bedIds | 是 | string | 床位ID,多个ID用逗号分隔 |
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功"
}
```
## 6. 数据字典
### 6.1 房间状态
| 状态码 | 说明 |
| ---- | ---- |
| 0 | 正常 |
| 1 | 维修中 |
| 2 | 停用 |
### 6.2 床位状态
| 状态码 | 说明 |
| ---- | ---- |
| 0 | 空闲 |
| 1 | 已分配 |
| 2 | 维修中 |
### 6.3 房间类型
| 类型 | 说明 |
| ---- | ---- |
| 标准间 | 标准宿舍,通常4-6人 |
| 单人间 | 单人宿舍 |
| 套间 | 带独立卫浴的宿舍 |
## 7. 错误码说明
| 错误码 | 说明 |
| ---- | ---- |
| 200 | 操作成功 |
| 400 | 请求参数错误 |
| 401 | 未授权 |
| 403 | 禁止访问 |
| 404 | 资源不存在 |
| 500 | 系统内部错误 |
| 1001 | 房间名称已存在 |
| 1002 | 房间包含已分配的床位,无法删除 |
| 1003 | 床位已分配给学生,无法删除 |
## 8. 移动端实现
### 8.1 移动端适配说明
移动端将实现以下功能:
- 宿舍楼列表查看(无树形结构,改用列表+筛选)
- 房间信息查看
- 床位信息查看
暂不支持在移动端进行房间和床位的增删改操作,仅支持查询功能。
### 8.2 特殊API
移动端将使用与PC端相同的API,但增加以下特殊接口:
**获取宿舍楼列表:**
- **请求方式:** GET
- **请求路径:** `/xsssfjgl/mobile/buildings`
- **请求参数:**
| 参数名 | 必填 | 类型 | 说明 |
| ---- | ---- | ---- | ---- |
| campusId | 否 | string/number | 校区ID |
| areaId | 否 | string/number | 宿舍区ID |
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{
"buildingId": 111,
"buildingName": "东区1号楼",
"campusName": "东校区",
"areaName": "东区宿舍",
"floors": 6
}
]
}
```
File renamed without changes.
File renamed without changes.
+260
View File
@@ -0,0 +1,260 @@
import request from '@/utils/request';
import { DormQuery, RoomForm, BedForm, DormTreeNode, RoomInfo, BedInfo } from './types';
// API前缀
const baseUrl = '/xsssfjgl';
/**
* 获取宿舍树形结构
*/
export function getDormTree() {
return request({
url: baseUrl + '/tree',
method: 'get'
});
}
/**
* 获取房间列表
* @param query 查询条件
* @returns 房间列表
*/
export function listRooms(query: DormQuery) {
return request({
url: baseUrl + '/room/list',
method: 'get',
params: query
});
}
/**
* 获取房间详情
* @param roomId 房间ID
* @returns 房间详情
*/
export function getRoom(roomId: string | number) {
return request({
url: baseUrl + '/room/' + roomId,
method: 'get'
});
}
/**
* 新增房间
* @param data 房间信息
* @returns 结果
*/
export function addRoom(data: RoomForm) {
return request({
url: baseUrl + '/room',
method: 'post',
data: data
});
}
/**
* 修改房间
* @param data 房间信息
* @returns 结果
*/
export function updateRoom(data: RoomForm) {
return request({
url: baseUrl + '/room',
method: 'put',
data: data
});
}
/**
* 删除房间
* @param roomIds 房间ID字符串
* @returns 结果
*/
export function delRoom(roomIds: string | number) {
return request({
url: baseUrl + '/room/' + roomIds,
method: 'delete'
});
}
/**
* 获取床位列表
* @param roomId 房间ID
* @returns 床位列表
*/
export function listBeds(roomId: string | number) {
return request({
url: baseUrl + '/bed/list',
method: 'get',
params: { roomId }
});
}
/**
* 新增床位
* @param data 床位信息
* @returns 结果
*/
export function addBed(data: BedForm) {
return request({
url: baseUrl + '/bed',
method: 'post',
data: data
});
}
/**
* 修改床位
* @param data 床位信息
* @returns 结果
*/
export function updateBed(data: BedForm) {
return request({
url: baseUrl + '/bed',
method: 'put',
data: data
});
}
/**
* 删除床位
* @param bedIds 床位ID字符串
* @returns 结果
*/
export function delBed(bedIds: string | number) {
return request({
url: baseUrl + '/bed/' + bedIds,
method: 'delete'
});
}
// 模拟数据 - 在API接口完成前使用
// 生成宿舍树形结构的模拟数据
export function getMockDormTree(): { data: DormTreeNode[] } {
const mockData: DormTreeNode[] = [
{
id: 1,
label: '东校区',
type: 'campus',
disabled: true,
children: [
{
id: 11,
label: '东区宿舍',
type: 'area',
disabled: true,
children: [
{
id: 111,
label: '东区1号楼',
type: 'building',
buildingFloors: 6
},
{
id: 112,
label: '东区2号楼',
type: 'building',
buildingFloors: 8
}
]
}
]
},
{
id: 2,
label: '西校区',
type: 'campus',
disabled: true,
children: [
{
id: 21,
label: '西区宿舍',
type: 'area',
disabled: true,
children: [
{
id: 211,
label: '西区1号楼',
type: 'building',
buildingFloors: 7
},
{
id: 212,
label: '西区2号楼',
type: 'building',
buildingFloors: 5
}
]
}
]
}
];
return { data: mockData };
}
// 生成房间数据的模拟数据
export function getMockRooms(buildingId: string | number, floorNumber?: number): { rows: RoomInfo[], total: number } {
const rooms: RoomInfo[] = [];
// 根据buildingId判断楼层数
let floors = 6;
if (buildingId === 112) floors = 8;
else if (buildingId === 211) floors = 7;
else if (buildingId === 212) floors = 5;
// 如果指定了楼层,只生成该楼层的数据
const startFloor = floorNumber || 1;
const endFloor = floorNumber || floors;
for (let floor = startFloor; floor <= endFloor; floor++) {
for (let i = 1; i <= 10; i++) {
const roomNumber = `${floor}${i.toString().padStart(2, '0')}`;
rooms.push({
roomId: `${buildingId}${roomNumber}`,
roomName: roomNumber,
floorNumber: floor,
buildingId: buildingId,
capacity: 4,
used: Math.floor(Math.random() * 5),
roomType: '标准间',
status: Math.random() > 0.9 ? '1' : '0',
remark: '',
createBy: 'admin',
createTime: '2023-01-01 00:00:00',
updateBy: 'admin',
updateTime: '2023-01-01 00:00:00'
});
}
}
return {
rows: rooms.filter(room => !floorNumber || room.floorNumber === floorNumber),
total: rooms.length
};
}
// 生成床位数据的模拟数据
export function getMockBeds(roomId: string | number): { data: BedInfo[] } {
const beds: BedInfo[] = [];
const capacity = 4;
for (let i = 1; i <= capacity; i++) {
const status = Math.random() > 0.6 ? '1' : '0';
beds.push({
bedId: `${roomId}-${i}`,
bedName: `${i}号床`,
roomId: roomId,
status: status,
studentId: status === '1' ? 10000 + Math.floor(Math.random() * 5000) : undefined,
studentName: status === '1' ? `学生${Math.floor(Math.random() * 1000)}` : undefined,
remark: '',
createBy: 'admin',
createTime: '2023-01-01 00:00:00',
updateBy: 'admin',
updateTime: '2023-01-01 00:00:00'
});
}
return { data: beds };
}
+76
View File
@@ -0,0 +1,76 @@
// 基础字段类型
export interface BaseEntity {
createBy: number | string;
createTime: string;
updateBy: number | string;
updateTime: string;
}
// 分页查询参数
export interface PageQuery {
pageNum: number;
pageSize: number;
}
// 树形数据节点类型
export interface DormTreeNode {
id: string | number;
label: string;
children?: DormTreeNode[];
type: 'campus' | 'area' | 'building'; // 节点类型:校区、宿舍区、宿舍楼
disabled?: boolean; // 是否禁用选择
buildingFloors?: number; // 宿舍楼层数
}
// 房间信息
export interface RoomInfo extends BaseEntity {
roomId: string | number;
roomName: string;
floorNumber: number;
buildingId: string | number;
capacity: number;
used: number;
roomType: string;
status: string; // 0-正常 1-维修中 2-停用
remark: string;
}
// 床位信息
export interface BedInfo extends BaseEntity {
bedId: string | number;
bedName: string;
roomId: string | number;
status: string; // 0-空闲 1-已分配 2-维修中
studentId?: string | number;
studentName?: string;
remark: string;
}
// 查询参数
export interface DormQuery extends PageQuery {
buildingId?: string | number;
roomName?: string;
floorNumber?: number;
status?: string;
}
// 房间表单
export interface RoomForm {
roomId?: string | number;
roomName: string;
floorNumber: number;
buildingId: string | number;
capacity: number;
roomType: string;
status: string;
remark?: string;
}
// 床位表单
export interface BedForm {
bedId?: string | number;
bedName: string;
roomId: string | number;
status: string;
remark?: string;
}
File renamed without changes.
@@ -99,4 +99,8 @@ onMounted(() => {
.show-btn {
margin-left: 12px;
}
.top-right-btn {
display: flex;
justify-content: flex-end;
}
</style>
@@ -161,10 +161,10 @@ import {
updateDormBuilding,
listCampusOptions,
listAreaTreeData
} from '@/api/sslxx/dormBuilding';
import { DormBuildingVO, DormBuildingQuery, DormBuildingForm } from '@/api/sslxx/dormBuilding/types';
import { DormCampusVO } from '@/api/xqxx/dormCampus/types';
import { DormAreaVO } from '@/api/ssqxx/dormArea/types';
} from '@/api/xgss/sslxx/dormBuilding';
import { DormBuildingVO, DormBuildingQuery, DormBuildingForm } from '@/api/xgss/sslxx/dormBuilding/types';
import { DormCampusVO } from '@/api/xgss/xqxx/dormCampus/types';
import { DormAreaVO } from '@/api/xgss/ssqxx/dormArea/types';
import { getCurrentInstance, ComponentInternalInstance, ref, reactive, toRefs, onMounted } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -112,10 +112,10 @@
</template>
<script setup name="DormArea" lang="ts">
import { listDormArea, getDormArea, delDormArea, addDormArea, updateDormArea } from '@/api/ssqxx/dormArea';
import { DormAreaVO, DormAreaQuery, DormAreaForm } from '@/api/ssqxx/dormArea/types';
import { listDormCampus } from '@/api/xqxx/dormCampus';
import type { DormCampusVO } from '@/api/xqxx/dormCampus/types';
import { listDormArea, getDormArea, delDormArea, addDormArea, updateDormArea } from '@/api/xgss/ssqxx/dormArea';
import { DormAreaVO, DormAreaQuery, DormAreaForm } from '@/api/xgss/ssqxx/dormArea/types';
import { listDormCampus } from '@/api/xgss/xqxx/dormCampus';
import type { DormCampusVO } from '@/api/xgss/xqxx/dormCampus/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -94,8 +94,8 @@
</template>
<script setup name="DormCampus" lang="ts">
import { listDormCampus, getDormCampus, delDormCampus, addDormCampus, updateDormCampus } from '@/api/xqxx/dormCampus';
import { DormCampusVO, DormCampusQuery, DormCampusForm } from '@/api/xqxx/dormCampus/types';
import { listDormCampus, getDormCampus, delDormCampus, addDormCampus, updateDormCampus } from '@/api/xgss/xqxx/dormCampus';
import { DormCampusVO, DormCampusQuery, DormCampusForm } from '@/api/xgss/xqxx/dormCampus/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -0,0 +1,115 @@
<template>
<el-dialog v-model="visible" :title="title" width="500px" append-to-body @close="closeDialog">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="床位名称" prop="bedName">
<el-input v-model="form.bedName" placeholder="请输入床位名称" maxlength="20" />
</el-form-item>
<el-form-item label="床位状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="0">空闲</el-radio>
<el-radio label="1">已分配</el-radio>
<el-radio label="2">维修中</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="visible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { BedForm } from '@/api/xgss/xsssfjgl/types';
import { addBed, updateBed } from '@/api/xgss/xsssfjgl';
const props = defineProps<{
modelValue: boolean;
title: string;
bedId?: string | number;
roomId: string | number;
}>();
const emit = defineEmits(['update:modelValue', 'success']);
const formRef = ref<FormInstance>();
const visible = ref(false);
// 表单数据
const initFormData: BedForm = {
bedName: '',
roomId: '',
status: '0',
remark: ''
};
const form = reactive<BedForm>({ ...initFormData });
// 表单校验规则
const rules = reactive<FormRules>({
bedName: [{ required: true, message: '床位名称不能为空', trigger: 'blur' }],
status: [{ required: true, message: '床位状态不能为空', trigger: 'change' }]
});
// 监听visible属性变化
watch(
() => props.modelValue,
(val) => {
visible.value = val;
if (val) {
resetForm();
// 设置表单初始值
form.roomId = props.roomId;
}
}
);
// 监听visible变化,同步回父组件
watch(
() => visible.value,
(val) => {
emit('update:modelValue', val);
}
);
// 关闭对话框
const closeDialog = () => {
visible.value = false;
resetForm();
};
// 重置表单
const resetForm = () => {
Object.assign(form, { ...initFormData });
formRef.value?.resetFields();
};
// 提交表单
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (valid) {
try {
if (props.bedId) {
// 修改
form.bedId = props.bedId;
await updateBed(form);
} else {
// 新增
await addBed(form);
}
closeDialog();
emit('success');
} catch (error) {
console.error('操作失败', error);
}
}
});
};
</script>
@@ -0,0 +1,136 @@
<template>
<el-dialog v-model="visible" :title="title" width="500px" append-to-body @close="closeDialog">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="楼层" prop="floorNumber">
<el-input v-model="form.floorNumber" placeholder="请输入楼层" :disabled="true" />
</el-form-item>
<el-form-item label="房间名称" prop="roomName">
<el-input v-model="form.roomName" placeholder="请输入房间名称" maxlength="20" />
</el-form-item>
<el-form-item label="房间类型" prop="roomType">
<el-select v-model="form.roomType" placeholder="请选择房间类型" clearable style="width: 100%">
<el-option label="标准间" value="标准间" />
<el-option label="单人间" value="单人间" />
<el-option label="套间" value="套间" />
</el-select>
</el-form-item>
<el-form-item label="床位数量" prop="capacity">
<el-input-number v-model="form.capacity" :min="1" :max="8" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="房间状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="0">正常</el-radio>
<el-radio label="1">维修中</el-radio>
<el-radio label="2">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="visible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { RoomForm } from '@/api/xgss/xsssfjgl/types';
import { addRoom, updateRoom } from '@/api/xgss/xsssfjgl';
const props = defineProps<{
modelValue: boolean;
title: string;
roomId?: string | number;
floorNumber: number;
buildingId: string | number;
}>();
const emit = defineEmits(['update:modelValue', 'success']);
const formRef = ref<FormInstance>();
const visible = ref(false);
// 表单数据
const initFormData: RoomForm = {
roomName: '',
floorNumber: 1,
buildingId: '',
capacity: 4,
roomType: '标准间',
status: '0',
remark: ''
};
const form = reactive<RoomForm>({ ...initFormData });
// 表单校验规则
const rules = reactive<FormRules>({
roomName: [{ required: true, message: '房间名称不能为空', trigger: 'blur' }],
floorNumber: [{ required: true, message: '楼层不能为空', trigger: 'blur' }],
capacity: [{ required: true, message: '床位数量不能为空', trigger: 'change' }],
roomType: [{ required: true, message: '房间类型不能为空', trigger: 'change' }],
status: [{ required: true, message: '房间状态不能为空', trigger: 'change' }]
});
// 监听visible属性变化
watch(
() => props.modelValue,
(val) => {
visible.value = val;
if (val) {
resetForm();
// 设置表单初始值
form.floorNumber = props.floorNumber;
form.buildingId = props.buildingId;
}
}
);
// 监听visible变化,同步回父组件
watch(
() => visible.value,
(val) => {
emit('update:modelValue', val);
}
);
// 关闭对话框
const closeDialog = () => {
visible.value = false;
resetForm();
};
// 重置表单
const resetForm = () => {
Object.assign(form, { ...initFormData });
formRef.value?.resetFields();
};
// 提交表单
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (valid) {
try {
if (props.roomId) {
// 修改
form.roomId = props.roomId;
await updateRoom(form);
} else {
// 新增
await addRoom(form);
}
closeDialog();
emit('success');
} catch (error) {
console.error('操作失败', error);
}
}
});
};
</script>
+464
View File
@@ -0,0 +1,464 @@
<template>
<div class="p-2">
<el-row :gutter="20">
<!-- 宿舍树 -->
<el-col :lg="4" :xs="24">
<el-card shadow="hover">
<el-input v-model="buildingName" placeholder="请输入宿舍楼名称" prefix-icon="Search" clearable />
<el-tree
ref="dormTreeRef"
class="mt-2"
node-key="id"
:data="dormOptions"
:props="{ label: 'label', children: 'children', disabled: 'disabled' }"
:expand-on-click-node="false"
:filter-node-method="filterNode"
highlight-current
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</el-col>
<el-col :lg="20" :xs="24">
<transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" inline>
<el-form-item label="房间号" prop="roomName">
<el-input v-model="queryParams.roomName" placeholder="请输入房间号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="房间状态" clearable>
<el-option label="正常" value="0" />
<el-option label="维修中" value="1" />
<el-option label="停用" value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card v-if="!selectedBuilding" shadow="hover" class="empty-tip">
<div class="flex flex-col items-center justify-center h-[400px]">
<el-icon class="text-gray-400 text-5xl mb-4"><InfoFilled /></el-icon>
<p class="text-gray-500 text-lg">请在左侧选择宿舍楼</p>
</div>
</el-card>
<template v-else>
<el-card shadow="hover" class="mb-4">
<template #header>
<el-row>
<el-col :span="12">
<span class="font-bold">{{ selectedBuilding?.label }} - 房间管理</span>
</el-col>
<el-col :span="12" class="text-right">
<right-toolbar v-model:show-search="showSearch" @query-table="loadRoomData"></right-toolbar>
</el-col>
</el-row>
</template>
<!-- 楼层卡片 -->
<div v-for="floor in floorList" :key="floor" class="mb-4">
<el-divider>
<el-tag type="success" effect="dark" class="mr-2">{{ floor }}层</el-tag>
<el-button type="primary" icon="Plus" size="small" @click="handleAddRoom(floor)">添加房间</el-button>
</el-divider>
<div class="room-container">
<el-empty v-if="getRoomsByFloor(floor).length === 0" description="暂无房间数据" />
<div v-else class="room-grid">
<el-card
v-for="room in getRoomsByFloor(floor)"
:key="room.roomId"
class="room-card"
:class="{ 'room-disabled': room.status !== '0' }"
>
<template #header>
<div class="flex items-center justify-between">
<span class="font-bold">{{ room.roomName }}</span>
<div>
<el-tag v-if="room.status === '0'" type="success" size="small">正常</el-tag>
<el-tag v-else-if="room.status === '1'" type="warning" size="small">维修中</el-tag>
<el-tag v-else type="danger" size="small">停用</el-tag>
</div>
</div>
</template>
<div class="room-info">
<div class="mb-2"><span class="text-gray-500">类型:</span>{{ room.roomType }}</div>
<div class="mb-2"><span class="text-gray-500">床位:</span>{{ room.used }}/{{ room.capacity }}</div>
<div class="flex justify-between mt-4">
<div>
<el-button type="primary" link size="small" @click="handleManageBeds(room)">床位管理</el-button>
</div>
<div>
<el-button type="primary" link icon="Edit" @click="handleEditRoom(room)" />
<el-button type="danger" link icon="Delete" @click="handleDeleteRoom(room)" />
</div>
</div>
</div>
</el-card>
</div>
</div>
</div>
</el-card>
</template>
</el-col>
</el-row>
<!-- 房间管理对话框 -->
<room-dialog
v-model="roomDialog.visible"
:title="roomDialog.title"
:room-id="roomDialog.roomId"
:floor-number="roomDialog.floorNumber"
:building-id="roomDialog.buildingId"
@success="handleRoomSuccess"
/>
<!-- 床位管理对话框 -->
<el-dialog v-model="bedManageDialog.visible" :title="`${bedManageDialog.roomName} - 床位管理`" width="800px">
<div class="mb-4 text-right">
<el-button type="primary" icon="Plus" @click="handleAddBed">添加床位</el-button>
</div>
<el-table v-loading="bedLoading" :data="bedList" border>
<el-table-column label="床位名称" prop="bedName" />
<el-table-column label="状态" align="center">
<template #default="scope">
<el-tag v-if="scope.row.status === '0'" type="success">空闲</el-tag>
<el-tag v-else-if="scope.row.status === '1'" type="warning">已分配</el-tag>
<el-tag v-else type="danger">维修中</el-tag>
</template>
</el-table-column>
<el-table-column label="学生" prop="studentName" />
<el-table-column label="备注" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="操作" width="180" align="center">
<template #default="scope">
<el-button type="primary" link icon="Edit" @click="handleEditBed(scope.row)" />
<el-button type="danger" link icon="Delete" @click="handleDeleteBed(scope.row)" />
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 床位表单对话框 -->
<bed-dialog
v-model="bedDialog.visible"
:title="bedDialog.title"
:bed-id="bedDialog.bedId"
:room-id="bedDialog.roomId"
@success="handleBedSuccess"
/>
</div>
</template>
<script setup name="DormRoomManagement" lang="ts">
import { ref, computed, reactive, onMounted, nextTick } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus';
import { InfoFilled } from '@element-plus/icons-vue';
import { DormTreeNode, RoomInfo, BedInfo, DormQuery } from '@/api/xgss/xsssfjgl/types';
import {
getDormTree,
listRooms,
delRoom,
listBeds,
delBed,
// 以下为模拟数据
getMockDormTree,
getMockRooms,
getMockBeds
} from '@/api/xgss/xsssfjgl';
// 引入子组件
import RoomDialog from './components/RoomDialog.vue';
import BedDialog from './components/BedDialog.vue';
// 组件状态
const dormTreeRef = ref();
const queryFormRef = ref();
const buildingName = ref('');
const showSearch = ref(true);
const animate = reactive({
searchAnimate: {
enter: 'animate__animated animate__fadeInDown',
leave: 'animate__animated animate__fadeOutUp'
}
});
// 宿舍树数据
const dormOptions = ref<DormTreeNode[]>([]);
const selectedBuilding = ref<DormTreeNode>();
// 房间数据
const roomList = ref<RoomInfo[]>([]);
const floorList = computed(() => {
if (!selectedBuilding.value?.buildingFloors) return [];
return Array.from({ length: selectedBuilding.value.buildingFloors }, (_, i) => i + 1);
});
// 查询参数
const queryParams = reactive<DormQuery>({
pageNum: 1,
pageSize: 100,
buildingId: undefined,
roomName: '',
status: undefined
});
// 房间对话框
const roomDialog = reactive({
visible: false,
title: '',
roomId: undefined as string | number | undefined,
floorNumber: 1,
buildingId: '' as string | number
});
// 床位管理对话框
const bedManageDialog = reactive({
visible: false,
roomId: '' as string | number,
roomName: ''
});
// 床位表单对话框
const bedDialog = reactive({
visible: false,
title: '',
bedId: undefined as string | number | undefined,
roomId: '' as string | number
});
// 床位数据
const bedList = ref<BedInfo[]>([]);
const bedLoading = ref(false);
/** 通过条件过滤节点 */
const filterNode = (value: string, data: DormTreeNode) => {
if (!value) return true;
return data.label.indexOf(value) !== -1;
};
/** 监听宿舍楼名称变化,筛选树 */
const watchBuildingName = () => {
dormTreeRef.value?.filter(buildingName.value);
};
/** 获取宿舍树数据 */
const getDormTreeData = async () => {
try {
// const res = await getDormTree();
// 使用模拟数据
const res = getMockDormTree();
dormOptions.value = res.data;
} catch (error) {
console.error('获取宿舍树失败', error);
}
};
/** 节点单击事件 */
const handleNodeClick = (data: DormTreeNode) => {
if (data.type !== 'building') {
// 如果不是宿舍楼,不进行操作
return;
}
selectedBuilding.value = data;
queryParams.buildingId = data.id;
loadRoomData();
};
/** 加载房间数据 */
const loadRoomData = async () => {
if (!queryParams.buildingId) return;
try {
// const res = await listRooms(queryParams);
// 使用模拟数据
const res = getMockRooms(queryParams.buildingId);
roomList.value = res.rows;
} catch (error) {
console.error('获取房间列表失败', error);
}
};
/** 根据楼层获取房间数据 */
const getRoomsByFloor = (floor: number) => {
return roomList.value.filter((room) => room.floorNumber === floor);
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNum = 1;
loadRoomData();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.pageNum = 1;
queryParams.roomName = '';
queryParams.status = undefined;
handleQuery();
};
/** 添加房间 */
const handleAddRoom = (floor: number) => {
if (!selectedBuilding.value) return;
roomDialog.title = '添加房间';
roomDialog.roomId = undefined;
roomDialog.floorNumber = floor;
roomDialog.buildingId = selectedBuilding.value.id;
roomDialog.visible = true;
};
/** 编辑房间 */
const handleEditRoom = (room: RoomInfo) => {
roomDialog.title = '编辑房间';
roomDialog.roomId = room.roomId;
roomDialog.floorNumber = room.floorNumber;
roomDialog.buildingId = room.buildingId;
roomDialog.visible = true;
};
/** 删除房间 */
const handleDeleteRoom = (row: RoomInfo) => {
ElMessageBox.confirm(`确定要删除房间 ${row.roomName} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
// await delRoom(row.roomId);
// 模拟删除成功
ElMessage.success('删除成功');
loadRoomData();
} catch (error) {
console.error('删除房间失败', error);
}
})
.catch(() => {});
};
/** 房间操作成功回调 */
const handleRoomSuccess = () => {
loadRoomData();
};
/** 管理床位 */
const handleManageBeds = async (room: RoomInfo) => {
bedManageDialog.roomId = room.roomId;
bedManageDialog.roomName = room.roomName;
bedManageDialog.visible = true;
await loadBedData(room.roomId);
};
/** 加载床位数据 */
const loadBedData = async (roomId: string | number) => {
bedLoading.value = true;
try {
// const res = await listBeds(roomId);
// 使用模拟数据
const res = getMockBeds(roomId);
bedList.value = res.data;
} catch (error) {
console.error('获取床位列表失败', error);
} finally {
bedLoading.value = false;
}
};
/** 添加床位 */
const handleAddBed = () => {
bedDialog.title = '添加床位';
bedDialog.bedId = undefined;
bedDialog.roomId = bedManageDialog.roomId;
bedDialog.visible = true;
};
/** 编辑床位 */
const handleEditBed = (bed: BedInfo) => {
bedDialog.title = '编辑床位';
bedDialog.bedId = bed.bedId;
bedDialog.roomId = bed.roomId;
bedDialog.visible = true;
};
/** 删除床位 */
const handleDeleteBed = (row: BedInfo) => {
ElMessageBox.confirm(`确定要删除床位 ${row.bedName} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
// await delBed(row.bedId);
// 模拟删除成功
ElMessage.success('删除成功');
loadBedData(bedManageDialog.roomId);
} catch (error) {
console.error('删除床位失败', error);
}
})
.catch(() => {});
};
/** 床位操作成功回调 */
const handleBedSuccess = () => {
loadBedData(bedManageDialog.roomId);
};
onMounted(() => {
getDormTreeData();
// 监听宿舍楼名称变化
nextTick(() => {
buildingName.value = '';
watchBuildingName();
});
});
</script>
<style scoped>
.room-container {
width: 100%;
}
.room-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
}
.room-card {
margin-bottom: 0;
transition: all 0.3s;
}
.room-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.room-disabled {
opacity: 0.7;
}
.room-info {
font-size: 14px;
}
.empty-tip {
min-height: 400px;
}
</style>
@@ -47,8 +47,8 @@
<script lang="ts">
import { defineComponent, ref, reactive, computed, watch, onMounted } from 'vue';
import { getTeacherList } from '@/api/xsssgly/dormManager';
import { addDormManager, updateDormManager } from '@/api/ssglyxx/dormManager/index';
import { getTeacherList } from '@/api/xgss/xsssgly/dormManager';
import { addDormManager, updateDormManager } from '@/api/xgss/ssglyxx/dormManager/index';
import { ElMessage } from 'element-plus';
export default defineComponent({
@@ -82,11 +82,11 @@
<script lang="ts">
import { defineComponent, ref, reactive, computed, watch, onMounted } from 'vue';
import { listDormCampus } from '@/api/xqxx/dormCampus';
import { getAreaTree } from '@/api/ssqxx/dormArea';
import { getBuildingTree } from '@/api/sslxx/dormBuilding';
import type { TreeSelectVo } from '@/api/ssqxx/dormArea/types';
import type { BuildingTreeSelectVo } from '@/api/sslxx/dormBuilding/types';
import { listDormCampus } from '@/api/xgss/xqxx/dormCampus';
import { getAreaTree } from '@/api/xgss/ssqxx/dormArea';
import { getBuildingTree } from '@/api/xgss/sslxx/dormBuilding';
import type { TreeSelectVo } from '@/api/xgss/ssqxx/dormArea/types';
import type { BuildingTreeSelectVo } from '@/api/xgss/sslxx/dormBuilding/types';
export default defineComponent({
name: 'ScopeDialog',
File renamed without changes.