You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
713 lines
18 KiB
713 lines
18 KiB
<template>
|
|
<div class="page-container">
|
|
<div class="background-decoration"></div>
|
|
|
|
<div class="main-content">
|
|
<div class="room-panel">
|
|
<div class="panel-header">
|
|
<a href="javascript:;" class="back-login" @click="backToLogin">
|
|
<i class="el-icon-back"></i> 返回登录
|
|
</a>
|
|
<h2>房间选择</h2>
|
|
<p>选择或创建您要加入的协作房间</p>
|
|
</div>
|
|
|
|
<div class="room-list">
|
|
<div
|
|
v-for="room in getParentRooms"
|
|
:key="room.id"
|
|
class="room-group"
|
|
>
|
|
<div
|
|
class="room-item parent-room"
|
|
:class="{ 'active': selectedRoom === room.id }"
|
|
@click="handleParentRoomClick(room)"
|
|
@contextmenu.prevent="showContextMenu($event, room)"
|
|
>
|
|
<div class="room-info">
|
|
<div class="room-icon">
|
|
<i class="fa fa-building"></i>
|
|
</div>
|
|
<div class="room-details">
|
|
<h3>{{ room.name }}</h3>
|
|
<p class="room-stats">{{ getChildRooms(room.id).length }} 个子房间</p>
|
|
</div>
|
|
</div>
|
|
<div class="room-actions">
|
|
<div class="expand-icon">
|
|
<i class="fa" :class="expandedRooms.includes(room.id) ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
|
|
</div>
|
|
<div class="delete-room-btn" @click.stop="handleDelete(room)">
|
|
<i class="el-icon-delete" style="color: #ff4949; font-size: 18px; margin-right: 5px;"></i>
|
|
</div>
|
|
<div v-if="expandedRooms.includes(room.id)" class="add-child-btn"
|
|
@click.stop="showAddChildRoomDialog(room)">
|
|
<i class="el-icon-plus"></i>
|
|
</div>
|
|
<div class="select-indicator">
|
|
<i v-if="selectedRoom === room.id" class="fa fa-check"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="expandedRooms.includes(room.id)" class="child-rooms">
|
|
<div
|
|
v-for="childRoom in getChildRooms(room.id)"
|
|
:key="childRoom.id"
|
|
class="room-item child-room"
|
|
:class="{ 'active': selectedRoom === childRoom.id }"
|
|
@click="selectRoom(childRoom)"
|
|
@contextmenu.prevent="showContextMenu($event, childRoom)"
|
|
>
|
|
<div class="room-info">
|
|
<div class="room-icon">
|
|
<i class="fa fa-room"></i>
|
|
</div>
|
|
<div class="room-details">
|
|
<h3>{{ childRoom.name }}</h3>
|
|
</div>
|
|
</div>
|
|
<div class="room-actions">
|
|
<div class="delete-room-btn" @click.stop="handleDelete(childRoom)">
|
|
<i class="el-icon-delete" style="color: #ff4949; font-size: 16px; margin-right: 5px;"></i>
|
|
</div>
|
|
<div class="select-indicator">
|
|
<i v-if="selectedRoom === childRoom.id" class="fa fa-check"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="getParentRooms.length === 0" class="empty-state">
|
|
<i class="fa fa-sitemap"></i>
|
|
<h3>暂无房间</h3>
|
|
<p>点击下方按钮创建您的第一个房间</p>
|
|
</div>
|
|
</div>
|
|
<div class="action-buttons">
|
|
<button @click="showAddRoomDialog" class="btn-secondary">
|
|
<i class="fa fa-plus"></i> 新增大房间
|
|
</button>
|
|
<button
|
|
@click="enterRoom"
|
|
class="btn-primary"
|
|
:disabled="!canEnterRoom"
|
|
:class="{ 'disabled': !canEnterRoom }"
|
|
:title="enterRoomDisabledTip"
|
|
>
|
|
<i class="fa fa-sign-in"></i> 进入房间
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="footer-info">
|
|
<p>© 2026 网络化任务规划系统 | 支持Windows/国产化系统互通</p>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="contextMenu.visible"
|
|
:style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
|
|
class="context-menu"
|
|
>
|
|
<div @click="editRoom" class="menu-item">
|
|
<i class="fa fa-edit"></i>
|
|
<span>修改房间</span>
|
|
</div>
|
|
<div @click="handleDelete(contextMenu.room)" class="menu-item menu-item-danger">
|
|
<i class="fa fa-trash"></i>
|
|
<span>删除房间</span>
|
|
</div>
|
|
</div>
|
|
<el-dialog
|
|
:title="dialog.mode === 'add' ? '新增房间' : '修改房间'"
|
|
:visible.sync="dialog.visible"
|
|
width="400px"
|
|
append-to-body
|
|
:close-on-click-modal="false"
|
|
custom-class="custom-el-dialog"
|
|
>
|
|
<el-form label-position="top">
|
|
<el-form-item label="房间名称">
|
|
<el-input
|
|
v-model="dialog.form.name"
|
|
placeholder="请输入房间名称"
|
|
@keyup.enter.native="saveRoom"
|
|
clearable
|
|
></el-input>
|
|
</el-form-item>
|
|
</el-form>
|
|
<div slot="footer" class="dialog-footer">
|
|
<el-button @click="closeDialog" class="cancel-btn">取 消</el-button>
|
|
<el-button type="primary" @click="saveRoom" class="confirm-btn">确 定</el-button>
|
|
</div>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import {addRooms, updateRooms, delRooms, listRooms} from "@/api/system/rooms";
|
|
|
|
export default {
|
|
name: 'RoomSelect',
|
|
data() {
|
|
return {
|
|
selectedRoom: null,
|
|
expandedRooms: [],
|
|
contextMenu: {visible: false, x: 0, y: 0, room: null},
|
|
dialog: {
|
|
visible: false,
|
|
mode: 'add',
|
|
form: {id: null, name: '', parentId: null}
|
|
},
|
|
rooms: []
|
|
}
|
|
},
|
|
mounted() {
|
|
document.addEventListener('click', this.hideContextMenu);
|
|
this.getList();
|
|
},
|
|
beforeDestroy() {
|
|
document.removeEventListener('click', this.hideContextMenu)
|
|
},
|
|
computed: {
|
|
getParentRooms() {
|
|
return this.rooms.filter(room => room.parentId === null)
|
|
},
|
|
/** 选中的房间对象 */
|
|
selectedRoomObj() {
|
|
if (!this.selectedRoom) return null
|
|
return this.rooms.find(r => r.id === this.selectedRoom)
|
|
},
|
|
/** 是否可进入:小房间直接进;大房间需有子房间才能进 */
|
|
canEnterRoom() {
|
|
if (!this.selectedRoom || !this.selectedRoomObj) return false
|
|
if (this.selectedRoomObj.parentId != null) return true // 小房间
|
|
return this.getChildRooms(this.selectedRoom).length > 0 // 大房间需有子房间
|
|
},
|
|
/** 进入按钮禁用时的提示 */
|
|
enterRoomDisabledTip() {
|
|
if (!this.selectedRoom) return '请先选择房间'
|
|
if (!this.selectedRoomObj) return ''
|
|
if (this.selectedRoomObj.parentId != null) return ''
|
|
if (this.getChildRooms(this.selectedRoom).length === 0) return '该大房间暂无子房间,无法进入'
|
|
return ''
|
|
}
|
|
},
|
|
methods: {
|
|
/** 返回登录页:登出并跳转到登录界面 */
|
|
backToLogin() {
|
|
this.$confirm('确定退出当前账号并返回登录页吗?', '提示', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}).then(() => {
|
|
this.$store.dispatch('LogOut').then(() => {
|
|
this.$router.push({ path: '/login' }).catch(() => {})
|
|
}).catch(() => {})
|
|
}).catch(() => {})
|
|
},
|
|
getList() {
|
|
// 传 pageSize 拉取全部房间,避免分页只返回前 10 条导致新建房间不显示
|
|
listRooms({ pageNum: 1, pageSize: 9999 }).then(response => {
|
|
this.rooms = response.rows || response || [];
|
|
}).catch(err => {
|
|
console.error("加载列表失败,请检查后端接口权限", err);
|
|
});
|
|
},
|
|
getChildRooms(parentId) {
|
|
return this.rooms.filter(room => room.parentId === parentId)
|
|
},
|
|
/** 大房间点击:第一次展开,第二次选中 */
|
|
handleParentRoomClick(room) {
|
|
const isExpanded = this.expandedRooms.includes(room.id)
|
|
if (!isExpanded) {
|
|
this.expandedRooms.push(room.id)
|
|
} else {
|
|
this.selectedRoom = room.id
|
|
}
|
|
},
|
|
toggleRoomExpansion(room) {
|
|
const index = this.expandedRooms.indexOf(room.id)
|
|
if (index > -1) {
|
|
this.expandedRooms.splice(index, 1)
|
|
} else {
|
|
this.expandedRooms.push(room.id)
|
|
}
|
|
},
|
|
selectRoom(room) {
|
|
this.selectedRoom = room.id
|
|
},
|
|
enterRoom() {
|
|
if (this.selectedRoom) {
|
|
this.$router.push({
|
|
path: '/childRoom',
|
|
query: { roomId: this.selectedRoom }
|
|
});
|
|
}
|
|
},
|
|
showContextMenu(event, room) {
|
|
const padding = 12
|
|
const menuWidth = 160
|
|
const menuHeight = 90
|
|
const winW = window.innerWidth
|
|
const winH = window.innerHeight
|
|
let x = event.clientX
|
|
let y = event.clientY
|
|
if (x + menuWidth + padding > winW) x = winW - menuWidth - padding
|
|
if (x < padding) x = padding
|
|
if (y + menuHeight + padding > winH) y = winH - menuHeight - padding
|
|
if (y < padding) y = padding
|
|
this.contextMenu.visible = true
|
|
this.contextMenu.x = x
|
|
this.contextMenu.y = y
|
|
this.contextMenu.room = room
|
|
},
|
|
hideContextMenu() {
|
|
this.contextMenu.visible = false
|
|
},
|
|
showAddRoomDialog() {
|
|
this.dialog.mode = 'add'
|
|
this.dialog.form = {id: null, name: '', parentId: null}
|
|
this.dialog.visible = true
|
|
},
|
|
showAddChildRoomDialog(parentRoom) {
|
|
this.dialog.mode = 'add'
|
|
this.dialog.form = {id: null, name: '', parentId: parentRoom.id}
|
|
this.dialog.visible = true
|
|
},
|
|
editRoom() {
|
|
this.dialog.mode = 'edit'
|
|
this.dialog.form = {
|
|
id: this.contextMenu.room.id,
|
|
name: this.contextMenu.room.name,
|
|
parentId: this.contextMenu.room.parentId
|
|
}
|
|
this.dialog.visible = true
|
|
this.hideContextMenu()
|
|
},
|
|
handleDelete(room) {
|
|
this.$modal.confirm('是否确认删除房间:"' + room.name + '"?').then(() => {
|
|
return delRooms(room.id);
|
|
}).then(() => {
|
|
this.$modal.msgSuccess("删除成功");
|
|
if (this.selectedRoom === room.id) this.selectedRoom = null;
|
|
this.getList();
|
|
}).catch(() => {
|
|
});
|
|
this.hideContextMenu();
|
|
},
|
|
saveRoom() {
|
|
if (!this.dialog.form.name.trim()) {
|
|
this.$modal.msgError("请输入房间名称");
|
|
return;
|
|
}
|
|
const action = this.dialog.mode === 'add' ? addRooms : updateRooms;
|
|
action(this.dialog.form).then(response => {
|
|
this.$modal.msgSuccess(this.dialog.mode === 'add' ? "新增成功" : "修改成功");
|
|
this.closeDialog();
|
|
this.getList();
|
|
}).catch(err => {
|
|
});
|
|
},
|
|
closeDialog() {
|
|
this.dialog.visible = false
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 核心布局 */
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.page-container {
|
|
background: linear-gradient(135deg, #ffffff 0%, #f5f9ff 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
color: #1a202c;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.background-decoration {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-image: radial-gradient(circle at 20% 20%, rgba(22, 93, 255, 0.05) 0%, transparent 50%);
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
|
|
.main-content {
|
|
width: 100%;
|
|
max-width: 400px;
|
|
z-index: 1;
|
|
animation: fadeIn 0.5s ease-out;
|
|
}
|
|
|
|
/* 列表面板 */
|
|
.room-panel {
|
|
background-color: white;
|
|
border-radius: 16px;
|
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.panel-header {
|
|
padding: 20px 24px 16px;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
text-align: center;
|
|
position: relative;
|
|
}
|
|
|
|
.back-login {
|
|
position: absolute;
|
|
left: 24px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
font-size: 14px;
|
|
color: #64748b;
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.back-login:hover {
|
|
color: #165dff;
|
|
}
|
|
|
|
.panel-header h2 {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
margin: 0 0 6px 0;
|
|
color: #1e293b;
|
|
}
|
|
|
|
.panel-header p {
|
|
font-size: 13px;
|
|
color: #64748b;
|
|
margin: 0;
|
|
}
|
|
|
|
.room-list {
|
|
padding: 0;
|
|
max-height: 480px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.room-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 14px 24px;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid #f1f5f9;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.parent-room {
|
|
background-color: white;
|
|
border-left: 4px solid transparent;
|
|
}
|
|
|
|
.parent-room.active {
|
|
background-color: #ebf3ff;
|
|
border-left-color: #165dff;
|
|
}
|
|
|
|
.child-room {
|
|
background-color: #fafcfe;
|
|
padding-left: 60px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.child-room.active {
|
|
background-color: #ebf3ff;
|
|
border-left-color: #165dff;
|
|
}
|
|
|
|
.room-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.room-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.parent-room .room-icon {
|
|
background: #ebf3ff;
|
|
color: #165dff;
|
|
}
|
|
|
|
.child-room .room-icon {
|
|
background: #ebf3ff;
|
|
color: #165dff;
|
|
}
|
|
|
|
.room-details h3 {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.room-stats {
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.room-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
}
|
|
|
|
.add-child-btn {
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
color: #64748b;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.add-child-btn:hover {
|
|
color: #165dff;
|
|
}
|
|
|
|
.delete-room-btn {
|
|
cursor: pointer;
|
|
opacity: 0.6;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.delete-room-btn:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 32px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.empty-state i {
|
|
font-size: 40px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
/* 底部按钮 */
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding: 18px 24px;
|
|
background-color: #f8fafc;
|
|
border-top: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.btn-primary {
|
|
flex: 1;
|
|
padding: 12px;
|
|
border-radius: 10px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
background: #165dff;
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
box-shadow: 0 4px 12px rgba(22, 93, 255, 0.2);
|
|
}
|
|
|
|
.btn-primary:hover:not(.disabled) {
|
|
background: #165dff;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn-primary.disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.btn-secondary {
|
|
flex: 1;
|
|
padding: 12px;
|
|
border-radius: 10px;
|
|
font-size: 14px;
|
|
background: white;
|
|
color: #475569;
|
|
border: 1px solid #e2e8f0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #f1f5f9;
|
|
border-color: #cbd5e1;
|
|
}
|
|
|
|
.footer-info {
|
|
text-align: center;
|
|
margin-top: 24px;
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
/* 右键菜单 */
|
|
.context-menu {
|
|
position: fixed;
|
|
background: white;
|
|
border-radius: 10px;
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
|
padding: 6px 0;
|
|
min-width: 140px;
|
|
z-index: 2000;
|
|
border: 1px solid #f1f5f9;
|
|
}
|
|
|
|
.menu-item {
|
|
padding: 10px 16px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 13px;
|
|
color: #475569;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.menu-item:hover {
|
|
background-color: #f8fafc;
|
|
color: #165dff;
|
|
}
|
|
|
|
.menu-item-danger {
|
|
color: #ef4444;
|
|
}
|
|
|
|
.menu-item-danger:hover {
|
|
background-color: #fef2f2;
|
|
color: #ef4444;
|
|
}
|
|
|
|
/* --- Element UI 局部重写 --- */
|
|
::v-deep .custom-el-dialog {
|
|
border-radius: 20px !important;
|
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15) !important;
|
|
}
|
|
|
|
::v-deep .el-dialog__header {
|
|
padding: 24px 24px 10px !important;
|
|
text-align: center;
|
|
}
|
|
|
|
::v-deep .el-dialog__title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
}
|
|
|
|
::v-deep .el-form-item__label {
|
|
font-weight: 500;
|
|
color: #475569;
|
|
padding-bottom: 8px !important;
|
|
}
|
|
|
|
::v-deep .el-input__inner {
|
|
border-radius: 10px !important;
|
|
height: 48px !important;
|
|
border: 1px solid #e2e8f0 !important;
|
|
transition: all 0.2s !important;
|
|
}
|
|
|
|
::v-deep .el-input__inner:focus {
|
|
border-color: #165dff !important;
|
|
box-shadow: 0 0 0 3px rgba(22, 93, 255, 0.1) !important;
|
|
}
|
|
|
|
::v-deep .el-dialog__footer {
|
|
padding: 0 24px 24px !important;
|
|
}
|
|
|
|
.dialog-footer {
|
|
display: flex !important;
|
|
justify-content: space-between !important;
|
|
gap: 12px !important;
|
|
width: 100% !important;
|
|
}
|
|
|
|
::v-deep .el-dialog__footer .el-button {
|
|
flex: 1 !important;
|
|
margin: 0 !important;
|
|
height: 44px !important;
|
|
border-radius: 10px !important;
|
|
font-size: 14px !important;
|
|
font-weight: 500 !important;
|
|
display: inline-flex !important;
|
|
align-items: center !important;
|
|
justify-content: center !important;
|
|
}
|
|
|
|
::v-deep .el-button--primary {
|
|
background-color: #165dff !important;
|
|
border-color: #165dff !important;
|
|
box-shadow: 0 4px 10px rgba(22, 93, 255, 0.2) !important;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style>
|
|
/* 针对全局弹出的 Message 组件进行修饰 */
|
|
.el-message {
|
|
min-width: 320px !important;
|
|
max-width: 420px !important;
|
|
border-radius: 12px !important;
|
|
padding: 14px 20px !important;
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12) !important;
|
|
border: 1px solid rgba(0, 0, 0, 0.05) !important;
|
|
top: 40px !important;
|
|
z-index: 99999 !important;
|
|
}
|
|
|
|
/* 消息内容文字美化 */
|
|
.el-message__content {
|
|
font-size: 14px !important;
|
|
font-weight: 600 !important;
|
|
line-height: 1.4 !important;
|
|
}
|
|
|
|
/* 修正图标大小 */
|
|
.el-message__icon {
|
|
font-size: 18px !important;
|
|
}
|
|
</style>
|
|
|