Compare commits
1 Commits
a894367dcc
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15da7d589f |
@@ -1,140 +0,0 @@
|
|||||||
// import request from '@/views/conferencingRoom/utils/request.js'
|
|
||||||
// import { tansParams } from "@/utils/ruoyi";
|
|
||||||
|
|
||||||
import request from '@/utils/request'
|
|
||||||
|
|
||||||
// 创建房间获取token
|
|
||||||
export function getRoomToken(data) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/rooms/create`,
|
|
||||||
method: 'post',
|
|
||||||
data: data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//邀请用户参与房间
|
|
||||||
export function getInvite(room_uid,data) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/meeting/${room_uid}/invite`,
|
|
||||||
method: 'post',
|
|
||||||
data: data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建房间
|
|
||||||
export function createRoom(data) {
|
|
||||||
return request({
|
|
||||||
url: `/room/`,
|
|
||||||
method: 'post',
|
|
||||||
data: tansParams(data),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取所有房间列表
|
|
||||||
export function getRoomList() {
|
|
||||||
return request({
|
|
||||||
url: `/room/ `,
|
|
||||||
method: 'get',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//修改状态 加入 退出
|
|
||||||
export function getStatusApi(room_uid,data) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/meeting/${room_uid}/status`,
|
|
||||||
method: 'post',
|
|
||||||
data: data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//参与者获取token
|
|
||||||
export function getTokenApi(room_uid) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/meeting/${room_uid}/token`,
|
|
||||||
method: 'get',
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//用户退出房间
|
|
||||||
export function exitRoomApi(room_uid) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/meeting/${room_uid}/leave`,
|
|
||||||
method: 'post',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取当前房间信息
|
|
||||||
export function getRoomInfoApi(room_uid) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/meeting/${room_uid}/info`,
|
|
||||||
method: 'get',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取文件列表
|
|
||||||
export function getFileListApi(room_uid) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/conversion/${room_uid}/files`,
|
|
||||||
method: 'get',
|
|
||||||
// params:{
|
|
||||||
// service: room_uid
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//上传文件获取token
|
|
||||||
export function getUploadTokenApi(data) {
|
|
||||||
return request({
|
|
||||||
url: `/api/file/token`,
|
|
||||||
method: 'post',
|
|
||||||
data: data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//上传文件
|
|
||||||
export function uploadFileApi(token,data) {
|
|
||||||
return request({
|
|
||||||
url: `/api/file/${token}/upload`,
|
|
||||||
method: 'post',
|
|
||||||
data: data,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//提交一个文档转换任务,将指定的 Office 文档转换为 PDF 格式
|
|
||||||
export function convertFileApi(data,roomId) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/conversion/${ roomId }/convert`,
|
|
||||||
method: 'post',
|
|
||||||
data: data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取文档转换任务状态结构
|
|
||||||
export function getConvertStatusApi(taskId,roomId) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/conversion/${ roomId }/status/${taskId}`,
|
|
||||||
method: 'get',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取当前会议中所有参与者信息
|
|
||||||
export function getParticipantsApi(roomId) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/meeting/${ roomId }/participants`,
|
|
||||||
method: 'get',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 获取当前房间中视频
|
|
||||||
export function getvideoUrlApi(room_uid) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/room/${ room_uid }/recordings`,
|
|
||||||
method: 'get',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import request from '@/utils/request'
|
|
||||||
|
|
||||||
// 获取组织列表
|
|
||||||
export function getDirectories(data) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/auth/directories`,
|
|
||||||
method: 'get',
|
|
||||||
params:data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取指定目录下的用户列表
|
|
||||||
export function getDirectoriesUsers(directory_uuid,data) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/auth/directories/${directory_uuid}/users`,
|
|
||||||
method: 'get',
|
|
||||||
params:data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取参与者历史参会记录
|
|
||||||
export function getParticipantsHistoryApi(userId,data) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/rooms/${ userId }/participants/history`,
|
|
||||||
method: 'get',
|
|
||||||
params:data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户详细信息
|
|
||||||
export function getInfo(userUid,type) {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/auth/users/${userUid}`,
|
|
||||||
method: 'get',
|
|
||||||
params:type
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
10
src/api/interface.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询菜单列表
|
||||||
|
export const listMenu = (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/permissions/tree',
|
||||||
|
method: 'get',
|
||||||
|
params: data,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -59,3 +59,12 @@ export function checkPwdStrength(password) {
|
|||||||
data: params
|
data: params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取用户头像
|
||||||
|
export function getAvatarsApi(name) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/avatars/username/${name}/200`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,9 +1,70 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
// 获取路由
|
// 查询菜单列表
|
||||||
export const getRouters = () => {
|
export const listMenu = (data) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/menu/getRouters',
|
url: '/api/v1/permission/permissions/tree',
|
||||||
|
method: 'get',
|
||||||
|
params: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查询菜单下拉树结构
|
||||||
|
export const treeselect = () => {
|
||||||
|
return request({
|
||||||
|
url: '/system/menu/treeselect',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// 根据角色ID查询菜单下拉树结构
|
||||||
|
export const roleMenuTreeselect = (roleId) => {
|
||||||
|
return request({
|
||||||
|
url: '/system/menu/roleMenuTreeselect/' + roleId,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增菜单
|
||||||
|
export const addMenu = (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/permissions',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改菜单
|
||||||
|
export const updateMenu = (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/permissions/' + data.uid,
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除菜单
|
||||||
|
export const delMenu = (menuId) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/permissions/' + menuId,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//分配权限增量
|
||||||
|
export const dataScope = (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/roles/' + data.roleId + '/permissions/add',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//获取当前角色存在的权限菜单
|
||||||
|
export const getRolePermissions = (roleId) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/roles/' + roleId + '/all-permissions',
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
133
src/api/role.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
export const listRole = (query) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/roles',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询角色详细
|
||||||
|
*/
|
||||||
|
export const getRole = (roleId) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/roles/' + roleId,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增角色
|
||||||
|
*/
|
||||||
|
export const addRole = (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/roles',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改角色
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export const updateRole = (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/roles/' + data.uid,
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色数据权限
|
||||||
|
*/
|
||||||
|
export const dataScope = (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/system/role/dataScope',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色状态修改
|
||||||
|
*/
|
||||||
|
export const changeRoleStatus = (roleId, status) => {
|
||||||
|
const data = {
|
||||||
|
roleId,
|
||||||
|
status
|
||||||
|
};
|
||||||
|
return request({
|
||||||
|
url: '/system/role/changeStatus',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除角色
|
||||||
|
*/
|
||||||
|
export const delRole = (roleId) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/roles/' + roleId,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户已授权角色列表
|
||||||
|
*/
|
||||||
|
export const allocatedUserList = (query)=> {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/users/' + query.userId + '/roles',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询未分配角色列表
|
||||||
|
*/
|
||||||
|
export const unallocatedUserList = (query,userId) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/users/' + userId + '/roles',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消用户授权角色
|
||||||
|
*/
|
||||||
|
export const authUserCancel = (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/users/' + data.userId + '/roles/remove',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权用户选择的角色
|
||||||
|
*/
|
||||||
|
export const authUserSelectAll = (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/permission/users/' + data.userId + '/roles/add',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询未分配用户角色列表
|
||||||
|
*/
|
||||||
|
// export const unallocatedUserList = (query) => {
|
||||||
|
// return request({
|
||||||
|
// url: '/api/v1/permission/roles',
|
||||||
|
// method: 'get',
|
||||||
|
// params: query
|
||||||
|
// });
|
||||||
|
// };
|
||||||
69
src/api/room.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
export const listRoom = (query) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/rooms/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除房间
|
||||||
|
*/
|
||||||
|
export const delRoom = (roomId,data) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/rooms/' + roomId,
|
||||||
|
method: 'delete',
|
||||||
|
params: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询房间参与者列表
|
||||||
|
*/
|
||||||
|
export const participantUserList = (query)=> {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/rooms/participants/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除参与者信息
|
||||||
|
*/
|
||||||
|
export const delParticipant = (roomId,data) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/rooms/' + roomId +'/participants',
|
||||||
|
method: 'delete',
|
||||||
|
params: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提出房间参与者
|
||||||
|
*/
|
||||||
|
export const removeParticipant = (roomId,data) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/meeting/' + roomId + '/participant/remove' ,
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//静音 解除静音
|
||||||
|
export const muteParticipant = (roomId,data) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/meeting/' + roomId + '/participant/mute' ,
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
32
src/api/user.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户列表
|
||||||
|
* @param query
|
||||||
|
*/
|
||||||
|
export function listUser(data) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/auth/userList`,
|
||||||
|
method: 'get',
|
||||||
|
params:data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户
|
||||||
|
*/
|
||||||
|
export const updateUser = (data) => {
|
||||||
|
console.log(data,'data')
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/auth/userInfo/${data.uid}/update`,
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
listUser,
|
||||||
|
updateUser
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// $menu-bg-color: #8290f0;
|
$menu-bg-color: #409EFF;
|
||||||
$menu-bg-color: #434343;
|
// $menu-bg-color: #434343;
|
||||||
#app {
|
#app {
|
||||||
|
|
||||||
.main-container {
|
.main-container {
|
||||||
@@ -140,7 +140,7 @@ $menu-bg-color: #434343;
|
|||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
// width: 54px !important;
|
// width: 54px !important;
|
||||||
height: calc(100vh - 50px);
|
height: calc(100vh - 50px);
|
||||||
background-color: red;
|
// background-color: red;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ function handleLink(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
// if you go to the redirect page, do not update the breadcrumbs
|
|
||||||
if (route.path.startsWith('/redirect/')) {
|
if (route.path.startsWith('/redirect/')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
119
src/components/Pagination/index.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="{ 'hidden': hidden }" class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
:background="background"
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:layout="layout"
|
||||||
|
:page-sizes="pageSizes"
|
||||||
|
:pager-count="pagerCount"
|
||||||
|
:total="total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Pagination'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { scrollTo } from '@/utils/scroll-to.js'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
total: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: 20
|
||||||
|
},
|
||||||
|
pageSizes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [10, 20, 30, 50]
|
||||||
|
},
|
||||||
|
// 移动端页码按钮的数量端默认值5
|
||||||
|
pagerCount: {
|
||||||
|
type: Number,
|
||||||
|
default: document.body.clientWidth < 992 ? 5 : 7
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: String,
|
||||||
|
default: 'total, sizes, prev, pager, next, jumper'
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
autoScroll: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
hidden: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
float: {
|
||||||
|
type: String,
|
||||||
|
default: 'right'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:page', 'update:limit', 'pagination'])
|
||||||
|
|
||||||
|
const currentPage = computed({
|
||||||
|
get() {
|
||||||
|
return props.page
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
emit('update:page', val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageSize = computed({
|
||||||
|
get() {
|
||||||
|
return props.limit
|
||||||
|
},
|
||||||
|
set(val){
|
||||||
|
emit('update:limit', val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleSizeChange(val) {
|
||||||
|
if (currentPage.value * val > props.total) {
|
||||||
|
currentPage.value = 1
|
||||||
|
}
|
||||||
|
emit('pagination', { page: currentPage.value, limit: val })
|
||||||
|
if (props.autoScroll) {
|
||||||
|
scrollTo(0, 800)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCurrentChange(val) {
|
||||||
|
emit('pagination', { page: val, limit: pageSize.value })
|
||||||
|
if (props.autoScroll) {
|
||||||
|
scrollTo(0, 800)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pagination-container {
|
||||||
|
padding: 32px 16px;
|
||||||
|
.el-pagination{
|
||||||
|
float: v-bind(float);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pagination-container.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 24</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="切图" transform="translate(-48.000000, -152.000000)">
|
|
||||||
<g id="编组-4备份" transform="translate(48.000000, 152.000000)">
|
|
||||||
<line x1="6" y1="18" x2="16.4593589" y2="7.54064109" id="路径-20" stroke="#3381FF" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<polygon id="矩形" fill="#3381FF" points="12 6 18 6 18 12"></polygon>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 746 B |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 24</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="切图" transform="translate(-8.000000, -152.000000)">
|
|
||||||
<g id="编组-4备份" transform="translate(8.000000, 152.000000)">
|
|
||||||
<line x1="6" y1="18" x2="16.4593589" y2="7.54064109" id="路径-20" stroke="#444E60" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<polygon id="矩形" fill="#444E60" points="12 6 18 6 18 12"></polygon>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 744 B |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>编组</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-48.000000, -476.000000)" stroke="#3381FF">
|
|
||||||
<g id="编组-9备份" transform="translate(48.000000, 476.000000)">
|
|
||||||
<path d="M10.6666667,9.66666667 L10.6666667,6.16666667 C10.6666667,5.52233446 11.1890011,5 11.8333333,5 C12.4776655,5 13,5.52233446 13,6.16666667 L13,9.66666667 L13,9.66666667 C14.2886644,9.66666667 15.3333333,10.7113356 15.3333333,12 L15.3333333,12 L15.3333333,12 L8.33333333,12 C8.33333333,10.7113356 9.37800225,9.66666667 10.6666667,9.66666667 L10.6666667,9.66666667 L10.6666667,9.66666667 Z" id="路径-47"></path>
|
|
||||||
<path d="M15.3333333,12 L17.6666667,17.8333333 C13.8159078,18.6034851 9.85075886,18.6034851 6,17.8333333 L8.33333333,12" id="路径"></path>
|
|
||||||
<line x1="11.8333333" y1="17.8333333" x2="11.8333333" y2="15.5" id="路径-49"></line>
|
|
||||||
<line x1="9.5" y1="17.8333333" x2="9.5" y2="15.5" id="路径-51"></line>
|
|
||||||
<line x1="14.1666667" y1="17.8333333" x2="14.1666667" y2="15.5" id="路径-52"></line>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>编组</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="小班课" transform="translate(-16.000000, -549.000000)" stroke="#444E60">
|
|
||||||
<g id="编组-35" transform="translate(8.000000, 285.000000)">
|
|
||||||
<g id="编组-9" transform="translate(8.000000, 264.000000)">
|
|
||||||
<path d="M10.6666667,9.66666667 L10.6666667,6.16666667 C10.6666667,5.52233446 11.1890011,5 11.8333333,5 C12.4776655,5 13,5.52233446 13,6.16666667 L13,9.66666667 L13,9.66666667 C14.2886644,9.66666667 15.3333333,10.7113356 15.3333333,12 L15.3333333,12 L15.3333333,12 L8.33333333,12 C8.33333333,10.7113356 9.37800225,9.66666667 10.6666667,9.66666667 L10.6666667,9.66666667 L10.6666667,9.66666667 Z" id="路径-47"></path>
|
|
||||||
<path d="M15.3333333,12 L17.6666667,17.8333333 C13.8159078,18.6034851 9.85075886,18.6034851 6,17.8333333 L8.33333333,12" id="路径"></path>
|
|
||||||
<line x1="11.8333333" y1="17.8333333" x2="11.8333333" y2="15.5" id="路径-49"></line>
|
|
||||||
<line x1="9.5" y1="17.8333333" x2="9.5" y2="15.5" id="路径-51"></line>
|
|
||||||
<line x1="14.1666667" y1="17.8333333" x2="14.1666667" y2="15.5" id="路径-52"></line>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>click_p@1x</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="编组-4备份-2">
|
|
||||||
<polygon id="路径-5" stroke="#444E60" points="7 5.07179677 8.66987298 17.9641016 11.9090909 13.5745916 17.330127 12.9641016"></polygon>
|
|
||||||
<line x1="12" y1="13.7320508" x2="15" y2="18.9282032" id="路径-7" stroke="#444E60"></line>
|
|
||||||
<polygon id="路径-5" stroke="#3381FF" points="7 5.07179677 8.66987298 17.9641016 11.9090909 13.5745916 17.330127 12.9641016"></polygon>
|
|
||||||
<line x1="12" y1="13.7320508" x2="15" y2="18.9282032" id="路径-7" stroke="#3381FF"></line>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 918 B |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>click@1x</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="编组-4" stroke="#444E60">
|
|
||||||
<polygon id="路径-5" points="7 5.07179677 8.66987298 17.9641016 11.9090909 13.5745916 17.330127 12.9641016"></polygon>
|
|
||||||
<line x1="12" y1="13.7320508" x2="15" y2="18.9282032" id="路径-7"></line>
|
|
||||||
<polygon id="路径-5" points="7 5.07179677 8.66987298 17.9641016 11.9090909 13.5745916 17.330127 12.9641016"></polygon>
|
|
||||||
<line x1="12" y1="13.7320508" x2="15" y2="18.9282032" id="路径-7"></line>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 857 B |
@@ -1 +0,0 @@
|
|||||||
<svg t="1757644652588" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8843" width="18" height="18"><path d="M512 85.3248c235.5968 0 426.6752 169.728 426.6752 379.264a237.1328 237.1328 0 0 1-237.056 237.0304H617.728a71.0656 71.0656 0 0 0-71.1168 71.1168c0 17.9968 7.1168 34.6112 17.9968 46.9248a71.68 71.68 0 0 1 18.5088 47.872c0 39.3984-32.7168 71.1424-71.1168 71.1424-235.5968 0-426.6752-191.0784-426.6752-426.6752 0-235.5968 191.0784-426.6752 426.6752-426.6752z m-50.7392 687.4112a156.3136 156.3136 0 0 1 156.4672-156.4672h83.8912a151.808 151.808 0 0 0 151.7056-151.6288c0-160.0512-150.6816-293.9648-341.3248-293.9648a341.3504 341.3504 0 0 0-28.8512 681.472 155.648 155.648 0 0 1-21.888-79.36v-0.0512zM320 512a64 64 0 1 1 0-128 64 64 0 0 1 0 128z m384 0a64 64 0 1 1 0-128 64 64 0 0 1 0 128zM512 384a64 64 0 1 1 0-128 64 64 0 0 1 0 128z" p-id="8844" fill="#3381ff"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 924 B |
@@ -1 +0,0 @@
|
|||||||
<svg t="1757644652588" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8843" width="18" height="18"><path d="M512 85.3248c235.5968 0 426.6752 169.728 426.6752 379.264a237.1328 237.1328 0 0 1-237.056 237.0304H617.728a71.0656 71.0656 0 0 0-71.1168 71.1168c0 17.9968 7.1168 34.6112 17.9968 46.9248a71.68 71.68 0 0 1 18.5088 47.872c0 39.3984-32.7168 71.1424-71.1168 71.1424-235.5968 0-426.6752-191.0784-426.6752-426.6752 0-235.5968 191.0784-426.6752 426.6752-426.6752z m-50.7392 687.4112a156.3136 156.3136 0 0 1 156.4672-156.4672h83.8912a151.808 151.808 0 0 0 151.7056-151.6288c0-160.0512-150.6816-293.9648-341.3248-293.9648a341.3504 341.3504 0 0 0-28.8512 681.472 155.648 155.648 0 0 1-21.888-79.36v-0.0512zM320 512a64 64 0 1 1 0-128 64 64 0 0 1 0 128z m384 0a64 64 0 1 1 0-128 64 64 0 0 1 0 128zM512 384a64 64 0 1 1 0-128 64 64 0 0 1 0 128z" p-id="8844" fill="#444e60"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 924 B |
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>folder备份</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-48.000000, -368.000000)" stroke="#3381FF">
|
|
||||||
<g id="folder备份" transform="translate(48.000000, 368.000000)">
|
|
||||||
<rect id="矩形" x="6" y="6" width="12" height="12" rx="6"></rect>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 621 B |
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>folder备份</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-8.000000, -368.000000)" stroke="#444E60">
|
|
||||||
<g id="folder备份" transform="translate(8.000000, 368.000000)">
|
|
||||||
<rect id="矩形" x="6" y="6" width="12" height="12" rx="6"></rect>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 619 B |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="切图" transform="translate(-48.000000, -116.000000)" stroke="#3381FF">
|
|
||||||
<g id="编组-3" transform="translate(48.000000, 116.000000)">
|
|
||||||
<line x1="11" y1="8" x2="16" y2="13" id="路径-26"></line>
|
|
||||||
<path d="M15.5,6.16666667 L17.8333333,8.5 C18.4776655,9.14433221 18.4776655,10.1890011 17.8333333,10.8333333 L12,16.6666667 C10.7113356,17.9553311 8.62199775,17.9553311 7.33333333,16.6666667 L6.16666667,15.5 C5.52233446,14.8556678 5.52233446,13.8109989 6.16666667,13.1666667 L13.1666667,6.16666667 C13.8109989,5.52233446 14.8556678,5.52233446 15.5,6.16666667 Z" id="矩形" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="切图" transform="translate(-8.000000, -116.000000)" stroke="#444E60">
|
|
||||||
<g id="编组-3" transform="translate(8.000000, 116.000000)">
|
|
||||||
<line x1="11" y1="8" x2="16" y2="13" id="路径-26"></line>
|
|
||||||
<path d="M15.5,6.16666667 L17.8333333,8.5 C18.4776655,9.14433221 18.4776655,10.1890011 17.8333333,10.8333333 L12,16.6666667 C10.7113356,17.9553311 8.62199775,17.9553311 7.33333333,16.6666667 L6.16666667,15.5 C5.52233446,14.8556678 5.52233446,13.8109989 6.16666667,13.1666667 L13.1666667,6.16666667 C13.8109989,5.52233446 14.8556678,5.52233446 15.5,6.16666667 Z" id="矩形" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 24</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-48.000000, -224.000000)" stroke="#3381FF">
|
|
||||||
<g id="编组备份" transform="translate(48.000000, 224.000000)">
|
|
||||||
<path d="M12.5,5 C13.0522847,5 13.5,5.44771525 13.5,6 L13.5,7 C13.5,6.44771525 13.9477153,6 14.5,6 C15.0522847,6 15.5,6.44771525 15.5,7 L15.5,7 L15.5,9 C15.5,8.44771525 15.9477153,8 16.5,8 C17.0522847,8 17.5,8.44771525 17.5,9 L17.5,9 L17.5,15 C17.5,17.209139 15.709139,19 13.5,19 L12.1375846,19 C10.5374722,19 9.09131968,18.0464126 8.46100451,16.5756772 L6.77854301,12.6499337 C6.61031263,12.2573961 6.69801722,11.8019828 7,11.5 C7.27614237,11.2238576 7.72385763,11.2238576 8,11.5 L9.5,13 L9.5,7 L9.50672773,6.88337887 C9.56449284,6.38604019 9.98716416,6 10.5,6 C11.0522847,6 11.5,6.44771525 11.5,7 L11.5,7 L11.5,6 C11.5,5.44771525 11.9477153,5 12.5,5 Z M11.5,6 L11.5,10 M13.5,6 L13.5,10 M15.5,8 L15.5,10" id="形状结合备份" transform="translate(12.000000, 12.000000) rotate(45.000000) translate(-12.000000, -12.000000) "></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 24</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-8.000000, -224.000000)" stroke="#444E60">
|
|
||||||
<g id="编组备份" transform="translate(8.000000, 224.000000)">
|
|
||||||
<path d="M12.5,5 C13.0522847,5 13.5,5.44771525 13.5,6 L13.5,7 C13.5,6.44771525 13.9477153,6 14.5,6 C15.0522847,6 15.5,6.44771525 15.5,7 L15.5,7 L15.5,9 C15.5,8.44771525 15.9477153,8 16.5,8 C17.0522847,8 17.5,8.44771525 17.5,9 L17.5,9 L17.5,15 C17.5,17.209139 15.709139,19 13.5,19 L12.1375846,19 C10.5374722,19 9.09131968,18.0464126 8.46100451,16.5756772 L6.77854301,12.6499337 C6.61031263,12.2573961 6.69801722,11.8019828 7,11.5 C7.27614237,11.2238576 7.72385763,11.2238576 8,11.5 L9.5,13 L9.5,7 L9.50672773,6.88337887 C9.56449284,6.38604019 9.98716416,6 10.5,6 C11.0522847,6 11.5,6.44771525 11.5,7 L11.5,7 L11.5,6 C11.5,5.44771525 11.9477153,5 12.5,5 Z M11.5,6 L11.5,10 M13.5,6 L13.5,10 M15.5,8 L15.5,10" id="形状结合备份" transform="translate(12.000000, 12.000000) rotate(45.000000) translate(-12.000000, -12.000000) "></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="切图" transform="translate(-48.000000, -188.000000)">
|
|
||||||
<g id="编组-3备份" transform="translate(48.000000, 188.000000)">
|
|
||||||
<circle id="椭圆形" fill="#3381FF" cx="12" cy="12" r="2"></circle>
|
|
||||||
<line x1="12" y1="4" x2="12" y2="6" id="路径-4" stroke="#3381FF" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="12" y1="18" x2="12" y2="20" id="路径-4备份" stroke="#3381FF" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="20" y1="12" x2="18" y2="12" id="路径-4" stroke="#3381FF" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="6" y1="12" x2="4" y2="12" id="路径-4备份" stroke="#3381FF" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="17.6568542" y1="17.6568542" x2="16.2426407" y2="16.2426407" id="路径-4" stroke="#3381FF" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="7.75735931" y1="7.75735931" x2="6.34314575" y2="6.34314575" id="路径-4备份" stroke="#3381FF" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="6.34314575" y1="17.6568542" x2="7.75735931" y2="16.2426407" id="路径-4" stroke="#3381FF" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="16.2426407" y1="7.75735931" x2="17.6568542" y2="6.34314575" id="路径-4备份" stroke="#3381FF" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="切图" transform="translate(-8.000000, -188.000000)">
|
|
||||||
<g id="编组-3备份" transform="translate(8.000000, 188.000000)">
|
|
||||||
<circle id="椭圆形" fill="#444E60" cx="12" cy="12" r="2"></circle>
|
|
||||||
<line x1="12" y1="4" x2="12" y2="6" id="路径-4" stroke="#444E60" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="12" y1="18" x2="12" y2="20" id="路径-4备份" stroke="#444E60" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="20" y1="12" x2="18" y2="12" id="路径-4" stroke="#444E60" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="6" y1="12" x2="4" y2="12" id="路径-4备份" stroke="#444E60" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="17.6568542" y1="17.6568542" x2="16.2426407" y2="16.2426407" id="路径-4" stroke="#444E60" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="7.75735931" y1="7.75735931" x2="6.34314575" y2="6.34314575" id="路径-4备份" stroke="#444E60" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="6.34314575" y1="17.6568542" x2="7.75735931" y2="16.2426407" id="路径-4" stroke="#444E60" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
<line x1="16.2426407" y1="7.75735931" x2="17.6568542" y2="6.34314575" id="路径-4备份" stroke="#444E60" stroke-linecap="square" stroke-linejoin="round"></line>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="170px" height="29px" viewBox="0 0 170 29" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!-- Generator: Sketch 55.1 (78136) - https://sketchapp.com -->
|
|
||||||
<title>mask (1)</title>
|
|
||||||
<desc>Created with Sketch.</desc>
|
|
||||||
<g id="页面1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="mask-(1)" transform="translate(0.000000, -0.177515)" fill="#FFFFFF" fill-rule="nonzero">
|
|
||||||
<path d="M170,0.177514793 L170,29.1775148 L0,29.1775148 L0,0.177514793 L170,0.177514793 Z M155.702875,7.70508717 L155.434123,7.71050163 L8.49907471,13.4220166 C7.82142679,13.4483583 7.28571429,14.0026739 7.28571429,14.6775148 C7.28571429,15.3523557 7.82142679,15.9066713 8.49907471,15.933013 L8.49907471,15.933013 L155.434123,21.644528 C155.525243,21.6480696 155.616424,21.649841 155.707613,21.649841 C159.577292,21.649841 162.714286,18.5282242 162.714286,14.6775148 C162.714286,14.5867726 162.712506,14.4960388 162.708947,14.4053655 C162.557902,10.5575911 159.300853,7.56019825 155.434123,7.71050163 L155.702875,7.70508717 Z" id="mask"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>follow备份 3</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-48.000000, -44.000000)" stroke="#3381FF">
|
|
||||||
<g id="folder备份" transform="translate(48.000000, 44.000000)">
|
|
||||||
<path d="M15.2700286,11.5822879 L9.44305056,17.4092659 C9.06480988,17.7875066 8.55180543,18 8.01689232,18 L6,18 L6,18 L6,15.9831077 C6,15.4481946 6.2124934,14.9351901 6.59073408,14.5569494 L14.5569494,6.59073408 C15.3445949,5.80308864 16.6216205,5.80308864 17.4092659,6.59073408 C18.1969114,7.37837953 18.1969114,8.65540512 17.4092659,9.44305056 L17.4092659,9.44305056 L17.4092659,9.44305056 C18.1969114,10.230696 18.1969114,11.5077216 17.4092659,12.295367 L14.5569494,15.1476835 L14.5569494,15.1476835" id="路径"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>follow备份 3</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-8.000000, -44.000000)" stroke="#444E60">
|
|
||||||
<g id="folder备份" transform="translate(8.000000, 44.000000)">
|
|
||||||
<path d="M15.2700286,11.5822879 L9.44305056,17.4092659 C9.06480988,17.7875066 8.55180543,18 8.01689232,18 L6,18 L6,18 L6,15.9831077 C6,15.4481946 6.2124934,14.9351901 6.59073408,14.5569494 L14.5569494,6.59073408 C15.3445949,5.80308864 16.6216205,5.80308864 17.4092659,6.59073408 C18.1969114,7.37837953 18.1969114,8.65540512 17.4092659,9.44305056 L17.4092659,9.44305056 L17.4092659,9.44305056 C18.1969114,10.230696 18.1969114,11.5077216 17.4092659,12.295367 L14.5569494,15.1476835 L14.5569494,15.1476835" id="路径"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="工具栏" transform="translate(-128.000000, -128.000000)" stroke="#3381FF">
|
|
||||||
<g id="编组-34备份" transform="translate(56.000000, 88.000000)">
|
|
||||||
<g id="follow备份-23" transform="translate(72.000000, 40.000000)">
|
|
||||||
<path d="M11.8220813,5.53069562 C12.3696773,5.62507662 12.4625734,5.72554557 12.5206245,5.84774318 L12.5206245,5.84774318 L14.2189736,9.42277042 L17.9920285,9.99234378 C18.1520889,10.0165062 18.2857395,10.1091286 18.3751462,10.2348609 C18.4719375,10.3709779 18.5180744,10.5461839 18.4934299,10.727512 C18.4738012,10.8719348 18.4101452,11.0060802 18.3101095,11.107382 L18.3101095,11.107382 L15.6017319,13.8500424 L16.2437398,17.738772 C16.273578,17.9195063 16.2322362,18.0963001 16.1392467,18.2355891 C16.0531778,18.3645118 15.9218276,18.4616905 15.7619923,18.4909999 C15.6401188,18.5133481 15.5148269,18.4923492 15.4051219,18.4324315 L15.4051219,18.4324315 L11.9999312,16.5726127 L8.59474046,18.4324315 C8.45624603,18.5080732 8.30173213,18.5180361 8.16377893,18.4719016 C8.01822109,18.4232239 7.89275102,18.3137981 7.81733557,18.160431 C7.75356476,18.0307448 7.73238084,17.8825791 7.75612261,17.738772 L7.75612261,17.738772 L8.39813049,13.8500424 L5.68975282,11.107382 C5.56536722,10.981422 5.50330824,10.813681 5.50012823,10.6457587 C5.49693852,10.4773242 5.55293545,10.306672 5.67278368,10.1752191 C5.76224575,10.0770947 5.87963871,10.0116959 6.00783388,9.99234378 L6.00783388,9.99234378 L9.78088874,9.42277042 L11.4792378,5.84774318 C11.5527358,5.69302981 11.6769476,5.58159072 11.8220813,5.53069562 Z" id="星形"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="工具栏" transform="translate(-128.000000, -48.000000)" stroke="#444E60">
|
|
||||||
<g id="编组-34" transform="translate(56.000000, 8.000000)">
|
|
||||||
<g id="follow备份-23" transform="translate(72.000000, 40.000000)">
|
|
||||||
<path d="M11.8220813,5.53069562 C12.3696773,5.62507662 12.4625734,5.72554557 12.5206245,5.84774318 L12.5206245,5.84774318 L14.2189736,9.42277042 L17.9920285,9.99234378 C18.1520889,10.0165062 18.2857395,10.1091286 18.3751462,10.2348609 C18.4719375,10.3709779 18.5180744,10.5461839 18.4934299,10.727512 C18.4738012,10.8719348 18.4101452,11.0060802 18.3101095,11.107382 L18.3101095,11.107382 L15.6017319,13.8500424 L16.2437398,17.738772 C16.273578,17.9195063 16.2322362,18.0963001 16.1392467,18.2355891 C16.0531778,18.3645118 15.9218276,18.4616905 15.7619923,18.4909999 C15.6401188,18.5133481 15.5148269,18.4923492 15.4051219,18.4324315 L15.4051219,18.4324315 L11.9999312,16.5726127 L8.59474046,18.4324315 C8.45624603,18.5080732 8.30173213,18.5180361 8.16377893,18.4719016 C8.01822109,18.4232239 7.89275102,18.3137981 7.81733557,18.160431 C7.75356476,18.0307448 7.73238084,17.8825791 7.75612261,17.738772 L7.75612261,17.738772 L8.39813049,13.8500424 L5.68975282,11.107382 C5.56536722,10.981422 5.50330824,10.813681 5.50012823,10.6457587 C5.49693852,10.4773242 5.55293545,10.306672 5.67278368,10.1752191 C5.76224575,10.0770947 5.87963871,10.0116959 6.00783388,9.99234378 L6.00783388,9.99234378 L9.78088874,9.42277042 L11.4792378,5.84774318 C11.5527358,5.69302981 11.6769476,5.58159072 11.8220813,5.53069562 Z" id="星形"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>folder备份</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-48.000000, -332.000000)" stroke="#3381FF">
|
|
||||||
<g id="folder备份" transform="translate(48.000000, 332.000000)">
|
|
||||||
<rect id="矩形" x="6" y="6" width="12" height="12" rx="2"></rect>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 621 B |
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>folder备份</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-8.000000, -332.000000)" stroke="#444E60">
|
|
||||||
<g id="folder备份" transform="translate(8.000000, 332.000000)">
|
|
||||||
<rect id="矩形" x="6" y="6" width="12" height="12" rx="2"></rect>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 619 B |
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="工具栏" transform="translate(-64.000000, -128.000000)" stroke="#3381FF">
|
|
||||||
<g id="编组-34备份" transform="translate(56.000000, 88.000000)">
|
|
||||||
<g id="follow备份-21" transform="translate(8.000000, 40.000000)">
|
|
||||||
<path d="M12,5.5 C12.3399741,5.5 12.6799483,5.62969577 12.9393398,5.8890873 L12.9393398,5.8890873 L18.1109127,11.0606602 C18.3703042,11.3200517 18.5,11.6600259 18.5,12 C18.5,12.3399741 18.3703042,12.6799483 18.1109127,12.9393398 L18.1109127,12.9393398 L12.9393398,18.1109127 C12.6799483,18.3703042 12.3399741,18.5 12,18.5 C11.6600259,18.5 11.3200517,18.3703042 11.0606602,18.1109127 L11.0606602,18.1109127 L5.8890873,12.9393398 C5.62969577,12.6799483 5.5,12.3399741 5.5,12 C5.5,11.6600259 5.62969577,11.3200517 5.8890873,11.0606602 L5.8890873,11.0606602 L11.0606602,5.8890873 C11.3200517,5.62969577 11.6600259,5.5 12,5.5 Z" id="矩形"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="工具栏" transform="translate(-64.000000, -48.000000)" stroke="#444E60">
|
|
||||||
<g id="编组-34" transform="translate(56.000000, 8.000000)">
|
|
||||||
<g id="follow备份-21" transform="translate(8.000000, 40.000000)">
|
|
||||||
<path d="M12,5.5 C12.3399741,5.5 12.6799483,5.62969577 12.9393398,5.8890873 L12.9393398,5.8890873 L18.1109127,11.0606602 C18.3703042,11.3200517 18.5,11.6600259 18.5,12 C18.5,12.3399741 18.3703042,12.6799483 18.1109127,12.9393398 L18.1109127,12.9393398 L12.9393398,18.1109127 C12.6799483,18.3703042 12.3399741,18.5 12,18.5 C11.6600259,18.5 11.3200517,18.3703042 11.0606602,18.1109127 L11.0606602,18.1109127 L5.8890873,12.9393398 C5.62969577,12.6799483 5.5,12.3399741 5.5,12 C5.5,11.6600259 5.62969577,11.3200517 5.8890873,11.0606602 L5.8890873,11.0606602 L11.0606602,5.8890873 C11.3200517,5.62969577 11.6600259,5.5 12,5.5 Z" id="矩形"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!-- Generator: Sketch 55.1 (78136) - https://sketchapp.com -->
|
|
||||||
<title>selector</title>
|
|
||||||
<desc>Created with Sketch.</desc>
|
|
||||||
<g id="页面1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="selector" transform="translate(1.000000, 1.000000)">
|
|
||||||
<path d="M7,14 L2,14 C0.8954305,14 0,13.1045695 0,12 L0,2 C0,0.8954305 0.8954305,0 2,0 L12,0 C13.1045695,0 14,0.8954305 14,2 L14,7 L14,7" id="路径" stroke="#3381FF" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
||||||
<polygon id="路径-10" fill="#3381FF" fill-rule="nonzero" points="8 8 14 10 11.3333333 11.3333333 10 14"></polygon>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 861 B |
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>follow备份 14</title>
|
|
||||||
<g id="Flat" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="白板-关闭摄像头-大班课" transform="translate(-16.000000, -247.000000)">
|
|
||||||
<g id="编组-35" transform="translate(8.000000, 207.000000)">
|
|
||||||
<g id="folder备份" transform="translate(8.000000, 40.000000)">
|
|
||||||
<path d="M12,19 L7,19 C5.8954305,19 5,18.1045695 5,17 L5,7 C5,5.8954305 5.8954305,5 7,5 L17,5 C18.1045695,5 19,5.8954305 19,7 L19,12 L19,12" id="路径" stroke="#444E60" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
||||||
<polygon id="路径-10" fill="#444E60" points="13 13 19 15 16.3333333 16.3333333 15 19"></polygon>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 983 B |
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="工具栏" transform="translate(-160.000000, -128.000000)" stroke="#3381FF">
|
|
||||||
<g id="编组-34备份" transform="translate(56.000000, 88.000000)">
|
|
||||||
<g id="follow备份-24" transform="translate(104.000000, 40.000000)">
|
|
||||||
<path d="M16.5,6 C16.9142136,6 17.2892136,6.16789322 17.5606602,6.43933983 C17.8321068,6.71078644 18,7.08578644 18,7.5 L18,7.5 L18,13.7142857 C18,14.1284993 17.8321068,14.5034993 17.5606602,14.7749459 C17.2892136,15.0463925 16.9142136,15.2142857 16.5000418,15.2142857 L16.5000418,15.2142857 L12.862101,15.2139816 L10.4309693,17.8655011 L9.7122759,15.2139169 L7.5,15.2142857 C7.08578644,15.2142857 6.71078644,15.0463925 6.43933983,14.7749459 C6.16789322,14.5034993 6,14.1284993 6,13.7142857 L6,13.7142857 L6,7.5 C6,7.08578644 6.16789322,6.71078644 6.43933983,6.43933983 C6.71078644,6.16789322 7.08578644,6 7.5,6 L7.5,6 Z" id="形状结合"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="工具栏" transform="translate(-160.000000, -48.000000)" stroke="#444E60">
|
|
||||||
<g id="编组-34" transform="translate(56.000000, 8.000000)">
|
|
||||||
<g id="follow备份-24" transform="translate(104.000000, 40.000000)">
|
|
||||||
<path d="M16.5,6 C16.9142136,6 17.2892136,6.16789322 17.5606602,6.43933983 C17.8321068,6.71078644 18,7.08578644 18,7.5 L18,7.5 L18,13.7142857 C18,14.1284993 17.8321068,14.5034993 17.5606602,14.7749459 C17.2892136,15.0463925 16.9142136,15.2142857 16.5000418,15.2142857 L16.5000418,15.2142857 L12.862101,15.2139816 L10.4309693,17.8655011 L9.7122759,15.2139169 L7.5,15.2142857 C7.08578644,15.2142857 6.71078644,15.0463925 6.43933983,14.7749459 C6.16789322,14.5034993 6,14.1284993 6,13.7142857 L6,13.7142857 L6,7.5 C6,7.08578644 6.16789322,6.71078644 6.43933983,6.43933983 C6.71078644,6.16789322 7.08578644,6 7.5,6 L7.5,6 Z" id="形状结合"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>follow备份 7</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-48.000000, -404.000000)" stroke="#3381FF">
|
|
||||||
<g id="folder备份-2" transform="translate(48.000000, 404.000000)">
|
|
||||||
<line x1="6" y1="18" x2="18" y2="6" id="路径-2"></line>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 638 B |
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>follow备份 5</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-8.000000, -404.000000)" stroke="#444E60">
|
|
||||||
<g id="folder备份-2" transform="translate(8.000000, 404.000000)">
|
|
||||||
<line x1="6" y1="18" x2="18" y2="6" id="路径-2"></line>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 636 B |
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="4px" height="4px" viewBox="0 0 4 4" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!-- Generator: Sketch 55.1 (78136) - https://sketchapp.com -->
|
|
||||||
<title>矩形</title>
|
|
||||||
<desc>Created with Sketch.</desc>
|
|
||||||
<g id="页面1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<polygon id="矩形" fill="#3381FF" fill-rule="nonzero" points="4 0 4 4 0 4"></polygon>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 507 B |
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="4px" height="4px" viewBox="0 0 4 4" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!-- Generator: Sketch 55.1 (78136) - https://sketchapp.com -->
|
|
||||||
<title>矩形</title>
|
|
||||||
<desc>Created with Sketch.</desc>
|
|
||||||
<g id="页面1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<polygon id="矩形" fill="#444E60" fill-rule="nonzero" points="4 0 4 4 0 4"></polygon>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 507 B |
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-48.000000, -80.000000)" stroke="#3381FF">
|
|
||||||
<g id="编组-2" transform="translate(48.000000, 80.000000)">
|
|
||||||
<polyline id="路径-16" points="6 6 18 6 18 8"></polyline>
|
|
||||||
<line x1="12" y1="6" x2="12" y2="16" id="路径-17"></line>
|
|
||||||
<line x1="6" y1="6" x2="6" y2="8" id="路径-18"></line>
|
|
||||||
<line x1="10" y1="18" x2="14" y2="18" id="路径-19"></line>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 859 B |
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="切图" transform="translate(-8.000000, -80.000000)" stroke="#444E60">
|
|
||||||
<g id="编组-2" transform="translate(8.000000, 80.000000)">
|
|
||||||
<polyline id="路径-16" points="6 6 18 6 18 8"></polyline>
|
|
||||||
<line x1="12" y1="6" x2="12" y2="16" id="路径-17"></line>
|
|
||||||
<line x1="6" y1="6" x2="6" y2="8" id="路径-18"></line>
|
|
||||||
<line x1="10" y1="18" x2="14" y2="18" id="路径-19"></line>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 857 B |
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="工具栏" transform="translate(-96.000000, -128.000000)" stroke="#3381FF">
|
|
||||||
<g id="编组-34备份" transform="translate(56.000000, 88.000000)">
|
|
||||||
<g id="follow备份-22" transform="translate(40.000000, 40.000000)">
|
|
||||||
<path d="M11.8735432,6.51639519 L18.4433604,16.8729099 C18.5014646,16.9705991 18.513552,17.0816789 18.4849451,17.1824518 C18.4637477,17.2571235 18.4207741,17.3254323 18.3599622,17.3797162 L18.3599622,17.3797162 L5.9619473,17.5 C5.83202254,17.5 5.71357665,17.4508256 5.62815063,17.3687543 C5.5756394,17.3183052 5.53576798,17.2551406 5.51530514,17.1841993 L5.51530514,17.1841993 L11.5944453,6.72141053 C11.6575042,6.61539105 11.7604689,6.5460226 11.8735432,6.51639519 L11.8735432,6.51639519 Z" id="多边形"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>矩形备份 25</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="工具栏" transform="translate(-96.000000, -48.000000)" stroke="#444E60">
|
|
||||||
<g id="编组-34" transform="translate(56.000000, 8.000000)">
|
|
||||||
<g id="follow备份-22" transform="translate(40.000000, 40.000000)">
|
|
||||||
<path d="M11.8735432,6.51639519 L18.4433604,16.8729099 C18.5014646,16.9705991 18.513552,17.0816789 18.4849451,17.1824518 C18.4637477,17.2571235 18.4207741,17.3254323 18.3599622,17.3797162 L18.3599622,17.3797162 L5.9619473,17.5 C5.83202254,17.5 5.71357665,17.4508256 5.62815063,17.3687543 C5.5756394,17.3183052 5.53576798,17.2551406 5.51530514,17.1841993 L5.51530514,17.1841993 L11.5944453,6.72141053 C11.6575042,6.61539105 11.7604689,6.5460226 11.8735432,6.51639519 L11.8735432,6.51639519 Z" id="多边形"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
|
||||||
<title>upload-pressed</title>
|
|
||||||
<desc>Created with Sketch.</desc>
|
|
||||||
<g id="页面-4" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="Whiteboard-Guidelines" transform="translate(-736.000000, -295.000000)">
|
|
||||||
<g id="upload-pressed" transform="translate(736.000000, 295.000000)">
|
|
||||||
<rect id="矩形备份-18" fill="#FFFFFF" opacity="0.01" x="0" y="0" width="40" height="40" rx="2"></rect>
|
|
||||||
<polyline id="路径-11备份-5" stroke="#3381FF" stroke-linecap="round" stroke-linejoin="round" transform="translate(20.000000, 14.000000) rotate(-270.000000) translate(-20.000000, -14.000000) " points="22 18 20 16 18 14 20 12 22 10"></polyline>
|
|
||||||
<polyline id="Stroke-11备份-3" stroke="#3381FF" stroke-linecap="round" stroke-linejoin="round" points="28 24 28 28 12 28 12 24"></polyline>
|
|
||||||
<line x1="20" y1="24" x2="20" y2="12" id="Stroke-3备份-3" stroke="#3381FF" stroke-linecap="round" stroke-linejoin="round"></line>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
|
||||||
<title>upload</title>
|
|
||||||
<desc>Created with Sketch.</desc>
|
|
||||||
<g id="页面-4" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="Whiteboard-Guidelines" transform="translate(-696.000000, -295.000000)">
|
|
||||||
<g id="upload" transform="translate(696.000000, 295.000000)">
|
|
||||||
<rect id="矩形备份-18" fill="#FFFFFF" opacity="0.01" x="0" y="0" width="40" height="40" rx="2"></rect>
|
|
||||||
<polyline id="路径-11备份-5" stroke="#212324" stroke-linecap="round" stroke-linejoin="round" transform="translate(20.000000, 14.000000) rotate(-270.000000) translate(-20.000000, -14.000000) " points="22 18 20 16 18 14 20 12 22 10"></polyline>
|
|
||||||
<polyline id="Stroke-11备份-3" stroke="#212324" stroke-linecap="round" stroke-linejoin="round" points="28 24 28 28 12 28 12 24"></polyline>
|
|
||||||
<line x1="20" y1="24" x2="20" y2="12" id="Stroke-3备份-3" stroke="#212324" stroke-linecap="round" stroke-linejoin="round"></line>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,228 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="tool-mid-box-left">
|
|
||||||
<!-- 工具循环 -->
|
|
||||||
<div class="tool-box-cell-box-left" v-for="item in tools" :key="item.shapeType">
|
|
||||||
<!-- <el-tooltip :content="item.name" placement="top-end">
|
|
||||||
<div class="tool-box-cell" @click="clickAppliance(item.shapeType)">
|
|
||||||
<img :src="item.shapeType === currentShapType ? item.iconActive : item.icon" :alt="item.name" />
|
|
||||||
</div>
|
|
||||||
</el-tooltip> -->
|
|
||||||
<el-tooltip :content="item.name" placement="top-end">
|
|
||||||
<div class="tool-box-cell" @click="clickAppliance(item.shapeType)">
|
|
||||||
<img
|
|
||||||
:src="(
|
|
||||||
// 当前是该工具
|
|
||||||
item.shapeType === currentShapType
|
|
||||||
// 当前在选颜色 / 粗细时保持上一个绘图工具高亮
|
|
||||||
|| (['colorSelector','brushSize'].includes(currentShapType) && item.shapeType === lastDrawingTool)
|
|
||||||
)
|
|
||||||
? item.iconActive
|
|
||||||
: item.icon"
|
|
||||||
:alt="item.name"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</el-tooltip>
|
|
||||||
|
|
||||||
<!-- 颜色选择器 -->
|
|
||||||
<div v-if="item.shapeType === 'colorSelector' && currentShapType === 'colorSelector' && colorSelectorControl"
|
|
||||||
class="tool-popup">
|
|
||||||
<el-color-picker ref="colorPickerRef" v-model="selectedColor" v-model:visible="colorVisible" show-alpha
|
|
||||||
@change="handleColorChange" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 画笔大小选择器 -->
|
|
||||||
<div v-if="item.shapeType === 'brushSize' && currentShapType === 'brushSize' && brushSizeControl"
|
|
||||||
class="tool-popup">
|
|
||||||
<el-select ref="brushSizeRef" v-model="selectedThickness" placeholder="画笔粗细" size="small"
|
|
||||||
@change="handleThicknessChange" v-model:visible="brushSizeVisible" style="width: 60px">
|
|
||||||
<el-option v-for="size in thicknessOptions" :key="size" :label="size + 'px'" :value="size" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, nextTick, onMounted, onBeforeUnmount } from "vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
canvas: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
validator: (value) => value && typeof value.setDrawingTool === "function",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 工具图标导入
|
|
||||||
import pen from "./image/pencil.svg";
|
|
||||||
import penActive from "./image/pencil-active.svg";
|
|
||||||
import eraser from "./image/eraser.svg";
|
|
||||||
import eraserActive from "./image/eraser-active.svg";
|
|
||||||
import ellipse from "./image/ellipse.svg";
|
|
||||||
import ellipseActive from "./image/ellipse-active.svg";
|
|
||||||
import rectangle from "./image/rectangle.svg";
|
|
||||||
import rectangleActive from "./image/rectangle-active.svg";
|
|
||||||
import straight from "./image/straight.svg";
|
|
||||||
import straightActive from "./image/straight-active.svg";
|
|
||||||
import brushSize from "./image/brushSize.svg";
|
|
||||||
import brushSizeActive from "./image/brushSize-active.svg";
|
|
||||||
import colorSelector from "./image/colorSelector.svg";
|
|
||||||
import colorSelectorActive from "./image/colorSelector-active.svg";
|
|
||||||
|
|
||||||
const tools = ref([
|
|
||||||
{ name: "笔", icon: pen, iconActive: penActive, shapeType: "pencil" },
|
|
||||||
{ name: "圆形", icon: ellipse, iconActive: ellipseActive, shapeType: "circle" },
|
|
||||||
{ name: "矩形", icon: rectangle, iconActive: rectangleActive, shapeType: "rectangle" },
|
|
||||||
{ name: "直线", icon: straight, iconActive: straightActive, shapeType: "line" },
|
|
||||||
{ name: "选色器", icon: colorSelector, iconActive: colorSelectorActive, shapeType: "colorSelector" },
|
|
||||||
{ name: "画笔大小", icon: brushSize, iconActive: brushSizeActive, shapeType: "brushSize" },
|
|
||||||
{ name: "橡皮擦", icon: eraser, iconActive: eraserActive, shapeType: "eraser" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 状态
|
|
||||||
const selectedColor = ref("#ffcc00");
|
|
||||||
const selectedThickness = ref(2);
|
|
||||||
const thicknessOptions = [1, 2, 4, 8, 16];
|
|
||||||
|
|
||||||
const colorSelectorControl = ref(false);
|
|
||||||
const brushSizeControl = ref(false);
|
|
||||||
const colorVisible = ref(false);
|
|
||||||
const brushSizeVisible = ref(false);
|
|
||||||
|
|
||||||
const currentShapType = ref("pencil"); // 当前点击的工具
|
|
||||||
const lastDrawingTool = ref("pencil"); // 记录最后一个绘图工具
|
|
||||||
|
|
||||||
// refs
|
|
||||||
const colorPickerRef = ref();
|
|
||||||
const brushSizeRef = ref();
|
|
||||||
|
|
||||||
// 颜色选择逻辑
|
|
||||||
const handleColorChange = (color) => {
|
|
||||||
props.canvas?.setColor(color);
|
|
||||||
colorSelectorControl.value = false;
|
|
||||||
colorVisible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 粗细选择逻辑
|
|
||||||
const handleThicknessChange = (size) => {
|
|
||||||
props.canvas?.setThickness(size);
|
|
||||||
brushSizeControl.value = false;
|
|
||||||
brushSizeVisible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 点击工具逻辑
|
|
||||||
function clickAppliance(type) {
|
|
||||||
// 点击同一个工具时切换关闭
|
|
||||||
if (currentShapType.value === type) {
|
|
||||||
if (type === "colorSelector") {
|
|
||||||
colorSelectorControl.value = !colorSelectorControl.value;
|
|
||||||
colorVisible.value = colorSelectorControl.value;
|
|
||||||
} else if (type === "brushSize") {
|
|
||||||
brushSizeControl.value = !brushSizeControl.value;
|
|
||||||
brushSizeVisible.value = brushSizeControl.value;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换到新工具
|
|
||||||
currentShapType.value = type;
|
|
||||||
lastDrawingTool.value = type; // 记录最新绘图工具
|
|
||||||
if (type === "colorSelector") {
|
|
||||||
colorSelectorControl.value = true;
|
|
||||||
brushSizeControl.value = false;
|
|
||||||
nextTick(() => {
|
|
||||||
colorVisible.value = true;
|
|
||||||
});
|
|
||||||
} else if (type === "brushSize") {
|
|
||||||
brushSizeControl.value = true;
|
|
||||||
colorSelectorControl.value = false;
|
|
||||||
nextTick(() => {
|
|
||||||
brushSizeVisible.value = true;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
colorSelectorControl.value = false;
|
|
||||||
brushSizeControl.value = false;
|
|
||||||
colorVisible.value = false;
|
|
||||||
brushSizeVisible.value = false;
|
|
||||||
props.canvas?.setDrawingTool(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击工具栏外部关闭弹窗
|
|
||||||
function handleClickOutside(event) {
|
|
||||||
const toolBox = document.querySelector(".tool-mid-box-left");
|
|
||||||
if (!toolBox.contains(event.target)) {
|
|
||||||
// colorSelectorControl.value = false;
|
|
||||||
brushSizeControl.value = false;
|
|
||||||
// colorVisible.value = false;
|
|
||||||
brushSizeVisible.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!props.canvas || typeof props.canvas.setDrawingTool !== "function") {
|
|
||||||
console.error("Invalid canvas prop passed to ToolBox");
|
|
||||||
}
|
|
||||||
document.addEventListener("click", handleClickOutside);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
document.removeEventListener("click", handleClickOutside);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.tool-mid-box-left {
|
|
||||||
width: 40px;
|
|
||||||
display: flex;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: white;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 4px 0;
|
|
||||||
box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-box-cell {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-box-cell-box-left {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 2px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(33, 35, 36, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 弹出层样式:显示在图标右侧 */
|
|
||||||
.tool-popup {
|
|
||||||
position: absolute;
|
|
||||||
left: 40px;
|
|
||||||
/* 距离工具栏宽度 */
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background: white;
|
|
||||||
padding: 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
import EventEmitter from "@/utils/emitter";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Canvas绘图类,支持多种图形绘制和多人同步
|
|
||||||
* 使用百分比坐标系统确保跨设备一致性
|
|
||||||
*/
|
|
||||||
class Canvas extends EventEmitter {
|
|
||||||
constructor(canvasId) {
|
|
||||||
super();
|
|
||||||
this.canvas = document.getElementById(canvasId);
|
|
||||||
if (!this.canvas) {
|
|
||||||
throw new Error(`Canvas element with id ${canvasId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ctx = this.canvas.getContext('2d');
|
|
||||||
this.shapes = []; // 所有已绘制形状
|
|
||||||
this.currentShape = null; // 当前正在绘制的形状
|
|
||||||
this.isDrawing = false;
|
|
||||||
this.drawingTool = 'pencil';
|
|
||||||
this.pathOptimizationEnabled = true;
|
|
||||||
this.optimizationThreshold = 0.005;
|
|
||||||
this.currentColor = '#ffcc00';
|
|
||||||
this.currentThickness = 2;
|
|
||||||
|
|
||||||
this.resize();
|
|
||||||
|
|
||||||
// 绑定事件
|
|
||||||
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
||||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
||||||
this.handleMouseUp = this.handleMouseUp.bind(this);
|
|
||||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
|
||||||
|
|
||||||
this.canvas.addEventListener('mousedown', this.handleMouseDown);
|
|
||||||
this.canvas.addEventListener('mousemove', this.handleMouseMove);
|
|
||||||
this.canvas.addEventListener('mouseup', this.handleMouseUp);
|
|
||||||
this.canvas.addEventListener('mouseleave', this.handleMouseLeave);
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => this.resize());
|
|
||||||
}
|
|
||||||
|
|
||||||
resize() {
|
|
||||||
const parent = this.canvas.parentElement;
|
|
||||||
if (!parent) return;
|
|
||||||
|
|
||||||
const containerWidth = parent.offsetWidth - 150;
|
|
||||||
const containerHeight = parent.offsetHeight;
|
|
||||||
|
|
||||||
let width = containerWidth;
|
|
||||||
let height = Math.floor((width * 9) / 16);
|
|
||||||
|
|
||||||
if (height > containerHeight) {
|
|
||||||
height = containerHeight;
|
|
||||||
width = Math.floor((height * 16) / 9);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canvas.width === width && this.canvas.height === height) return;
|
|
||||||
|
|
||||||
this.canvas.width = width;
|
|
||||||
this.canvas.height = height;
|
|
||||||
this.canvas.style.width = width + "px";
|
|
||||||
this.canvas.style.height = height + "px";
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
setDrawingTool(tool) { this.drawingTool = tool; }
|
|
||||||
setColor(color) { this.currentColor = color; }
|
|
||||||
setThickness(size) { this.currentThickness = size; }
|
|
||||||
setPathOptimization(enabled) { this.pathOptimizationEnabled = enabled; }
|
|
||||||
setOptimizationThreshold(threshold) { this.optimizationThreshold = threshold; }
|
|
||||||
getShapes() { return this.shapes; }
|
|
||||||
setShapes(shapes) { this.shapes = shapes; this.render(); }
|
|
||||||
|
|
||||||
// 获取鼠标坐标,转换为百分比坐标
|
|
||||||
getMouseCoordinates(e) {
|
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
|
||||||
return {
|
|
||||||
x:((e.clientX - rect.left) / this.canvas.width).toFixed(4),
|
|
||||||
y:((e.clientY - rect.top) / this.canvas.height).toFixed(4)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseDown(e) {
|
|
||||||
this.isDrawing = true;
|
|
||||||
const coords = this.getMouseCoordinates(e);
|
|
||||||
|
|
||||||
if (this.drawingTool === 'pencil') {
|
|
||||||
this.currentShape = { type: 'pencil', data: { color: this.currentColor, path: [coords], thickness: this.currentThickness } };
|
|
||||||
} else if (this.drawingTool === 'line') {
|
|
||||||
this.currentShape = { type: 'line', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness } };
|
|
||||||
} else if (this.drawingTool === 'rectangle') {
|
|
||||||
this.currentShape = { type: 'rectangle', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness, fill: false } };
|
|
||||||
} else if (this.drawingTool === 'circle') {
|
|
||||||
this.currentShape = { type: 'circle', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness, fill: false } };
|
|
||||||
} else if (this.drawingTool === 'eraser') {
|
|
||||||
this.currentShape = { type: 'eraser', data: { color: '#ffffff', start: coords, end: coords, thickness: 3 } };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit('drawingStart', this.currentShape);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseMove(e) {
|
|
||||||
if (!this.isDrawing || !this.currentShape) return;
|
|
||||||
const coords = this.getMouseCoordinates(e);
|
|
||||||
|
|
||||||
if (this.drawingTool === 'pencil') {
|
|
||||||
this.currentShape.data.path.push(coords);
|
|
||||||
} else {
|
|
||||||
this.currentShape.data.end = coords;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
this.emit('drawingUpdate', this.currentShape);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseUp(e) {
|
|
||||||
if (!this.isDrawing || !this.currentShape) return;
|
|
||||||
this.isDrawing = false;
|
|
||||||
const coords = this.getMouseCoordinates(e);
|
|
||||||
if (this.drawingTool === 'pencil' && this.pathOptimizationEnabled && this.currentShape.data.path.length > 10) {
|
|
||||||
this.currentShape.data.path = this.optimizePath(this.currentShape.data.path);
|
|
||||||
} else {
|
|
||||||
this.currentShape.data.end = coords;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.shapes.push({ ...this.currentShape });
|
|
||||||
this.emit('drawingEnd', this.currentShape);
|
|
||||||
this.currentShape = null;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseLeave(e) {
|
|
||||||
if (this.isDrawing) this.handleMouseUp(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
optimizePath(path) {
|
|
||||||
if (path.length < 3) return path;
|
|
||||||
const optimizedPath = [path[0]];
|
|
||||||
for (let i = 1; i < path.length - 1;) {
|
|
||||||
let a = 1;
|
|
||||||
while (i + a < path.length && this.calculateDistance(path[i], path[i + a]) < this.optimizationThreshold) a++;
|
|
||||||
optimizedPath.push(path[i]);
|
|
||||||
i += a;
|
|
||||||
}
|
|
||||||
optimizedPath.push(path[path.length - 1]);
|
|
||||||
return optimizedPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateDistance(p1, p2) {
|
|
||||||
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
addShape(shapeData) {
|
|
||||||
if (Array.isArray(shapeData)) this.shapes.push(...shapeData);
|
|
||||||
else this.shapes.push(shapeData);
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearCanvas() {
|
|
||||||
this.shapes = [];
|
|
||||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
this.emit('clear');
|
|
||||||
}
|
|
||||||
|
|
||||||
exportToDataURL(type = 'image/png', quality = 1) {
|
|
||||||
return this.canvas.toDataURL(type, quality);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
|
|
||||||
// 绘制历史形状
|
|
||||||
this.shapes.forEach(shape => this.drawShape(shape));
|
|
||||||
|
|
||||||
// 绘制当前正在绘制的形状
|
|
||||||
if (this.currentShape) this.drawShape(this.currentShape);
|
|
||||||
|
|
||||||
// 画画布边框
|
|
||||||
this.ctx.save();
|
|
||||||
this.ctx.strokeStyle = "#000";
|
|
||||||
this.ctx.lineWidth = 2;
|
|
||||||
this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
this.ctx.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
drawShape(shape) {
|
|
||||||
switch (shape.type) {
|
|
||||||
case 'pencil': this.drawPencil(shape.data); break;
|
|
||||||
case 'line': this.drawLine(shape.data); break;
|
|
||||||
case 'rectangle': this.drawRectangle(shape.data); break;
|
|
||||||
case 'circle': this.drawCircle(shape.data); break;
|
|
||||||
case 'eraser': this.drawEraser(shape.data, shape); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drawPencil(p) {
|
|
||||||
if (p.path.length < 2) return;
|
|
||||||
const path = p.path.map(pt => ({ x: pt.x * this.canvas.width, y: pt.y * this.canvas.height }));
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.strokeStyle = p.color;
|
|
||||||
this.ctx.lineWidth = p.thickness;
|
|
||||||
this.ctx.lineCap = 'round';
|
|
||||||
this.ctx.lineJoin = 'round';
|
|
||||||
this.drawSmoothCurve(this.ctx, path);
|
|
||||||
this.ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
drawSmoothCurve(ctx, path) {
|
|
||||||
if (path.length < 3) { ctx.moveTo(path[0].x, path[0].y); for (let i = 1; i < path.length; i++) ctx.lineTo(path[i].x, path[i].y); return; }
|
|
||||||
ctx.moveTo(path[0].x, path[0].y);
|
|
||||||
const threshold = 5;
|
|
||||||
for (let i = 1; i < path.length - 2;) {
|
|
||||||
let a = 1;
|
|
||||||
while (i + a < path.length - 2 &&
|
|
||||||
Math.sqrt(Math.pow(path[i].x - path[i + a].x, 2) + Math.pow(path[i].y - path[i + a].y, 2)) < threshold) a++;
|
|
||||||
const xc = (path[i].x + path[i + a].x) / 2;
|
|
||||||
const yc = (path[i].y + path[i + a].y) / 2;
|
|
||||||
ctx.quadraticCurveTo(path[i].x, path[i].y, xc, yc);
|
|
||||||
i += a;
|
|
||||||
}
|
|
||||||
ctx.lineTo(path[path.length - 1].x, path[path.length - 1].y);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawLine(l) {
|
|
||||||
const sx = l.start.x * this.canvas.width, sy = l.start.y * this.canvas.height;
|
|
||||||
const ex = l.end.x * this.canvas.width, ey = l.end.y * this.canvas.height;
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.moveTo(sx, sy);
|
|
||||||
this.ctx.lineTo(ex, ey);
|
|
||||||
this.ctx.strokeStyle = l.color;
|
|
||||||
this.ctx.lineWidth = l.thickness;
|
|
||||||
this.ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
drawRectangle(r) {
|
|
||||||
const sx = r.start.x * this.canvas.width, sy = r.start.y * this.canvas.height;
|
|
||||||
const ex = r.end.x * this.canvas.width, ey = r.end.y * this.canvas.height;
|
|
||||||
const w = ex - sx, h = ey - sy;
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.rect(sx, sy, w, h);
|
|
||||||
if (r.fill) { this.ctx.fillStyle = r.color; this.ctx.fill(); }
|
|
||||||
else { this.ctx.strokeStyle = r.color; this.ctx.lineWidth = r.thickness; this.ctx.stroke(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
drawCircle(c) {
|
|
||||||
const sx = c.start.x * this.canvas.width, sy = c.start.y * this.canvas.height;
|
|
||||||
const ex = c.end.x * this.canvas.width, ey = c.end.y * this.canvas.height;
|
|
||||||
const cx = (sx + ex) / 2, cy = (sy + ey) / 2;
|
|
||||||
const rx = Math.abs(ex - sx) / 2, ry = Math.abs(ey - sy) / 2;
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.ellipse(cx, cy, rx, ry, 0, 0, 2 * Math.PI);
|
|
||||||
if (c.fill) { this.ctx.fillStyle = c.color; this.ctx.fill(); }
|
|
||||||
else { this.ctx.strokeStyle = c.color; this.ctx.lineWidth = c.thickness; this.ctx.stroke(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
drawEraser(e, shapeObj) {
|
|
||||||
const sx = e.start.x * this.canvas.width, sy = e.start.y * this.canvas.height;
|
|
||||||
const ex = e.end.x * this.canvas.width, ey = e.end.y * this.canvas.height;
|
|
||||||
const w = Math.abs(ex - sx), h = Math.abs(ey - sy);
|
|
||||||
const x = Math.min(sx, ex), y = Math.min(sy, ey);
|
|
||||||
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.rect(x, y, w, h);
|
|
||||||
this.ctx.fillStyle = 'rgba(255,255,255)';
|
|
||||||
this.ctx.fill();
|
|
||||||
|
|
||||||
// 仅当前正在绘制的橡皮擦显示边框
|
|
||||||
if (this.currentShape && shapeObj === this.currentShape) {
|
|
||||||
this.ctx.save();
|
|
||||||
this.ctx.strokeStyle = '#000';
|
|
||||||
this.ctx.lineWidth = 1;
|
|
||||||
this.ctx.strokeRect(x, y, w, h);
|
|
||||||
this.ctx.restore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.canvas.removeEventListener('mousedown', this.handleMouseDown);
|
|
||||||
this.canvas.removeEventListener('mousemove', this.handleMouseMove);
|
|
||||||
this.canvas.removeEventListener('mouseup', this.handleMouseUp);
|
|
||||||
this.canvas.removeEventListener('mouseleave', this.handleMouseLeave);
|
|
||||||
window.removeEventListener('resize', this.resize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Canvas;
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
import EventEmitter from "@/utils/emitter";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Canvas绘图类,支持多种图形绘制和多人同步
|
|
||||||
* 使用百分比坐标系统确保跨设备一致性
|
|
||||||
*/
|
|
||||||
class Canvas extends EventEmitter {
|
|
||||||
constructor(canvasId) {
|
|
||||||
super();
|
|
||||||
this.canvas = document.getElementById(canvasId);
|
|
||||||
if (!this.canvas) {
|
|
||||||
throw new Error(`Canvas element with id ${canvasId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ctx = this.canvas.getContext('2d');
|
|
||||||
this.shapes = []; // 所有已绘制形状
|
|
||||||
this.currentShape = null; // 当前正在绘制的形状
|
|
||||||
this.isDrawing = false;
|
|
||||||
this.drawingTool = 'pencil';
|
|
||||||
this.pathOptimizationEnabled = true;
|
|
||||||
this.optimizationThreshold = 0.005;
|
|
||||||
this.currentColor = '#ffcc00';
|
|
||||||
this.currentThickness = 2;
|
|
||||||
|
|
||||||
this.resize();
|
|
||||||
|
|
||||||
// 绑定事件
|
|
||||||
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
||||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
||||||
this.handleMouseUp = this.handleMouseUp.bind(this);
|
|
||||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
|
||||||
|
|
||||||
this.canvas.addEventListener('mousedown', this.handleMouseDown);
|
|
||||||
this.canvas.addEventListener('mousemove', this.handleMouseMove);
|
|
||||||
this.canvas.addEventListener('mouseup', this.handleMouseUp);
|
|
||||||
this.canvas.addEventListener('mouseleave', this.handleMouseLeave);
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => this.resize());
|
|
||||||
}
|
|
||||||
|
|
||||||
resize() {
|
|
||||||
const parent = this.canvas.parentElement;
|
|
||||||
if (!parent) return;
|
|
||||||
|
|
||||||
const containerWidth = parent.offsetWidth;
|
|
||||||
const containerHeight = parent.offsetHeight;
|
|
||||||
|
|
||||||
let width = containerWidth;
|
|
||||||
let height = Math.floor((width * 9) / 16);
|
|
||||||
|
|
||||||
if (height > containerHeight) {
|
|
||||||
height = containerHeight;
|
|
||||||
width = Math.floor((height * 16) / 9);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canvas.width === width && this.canvas.height === height) return;
|
|
||||||
|
|
||||||
this.canvas.width = width;
|
|
||||||
this.canvas.height = height;
|
|
||||||
this.canvas.style.width = width + "px";
|
|
||||||
this.canvas.style.height = height + "px";
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
setDrawingTool(tool) { this.drawingTool = tool; }
|
|
||||||
setColor(color) { this.currentColor = color; }
|
|
||||||
setThickness(size) { this.currentThickness = size; }
|
|
||||||
setPathOptimization(enabled) { this.pathOptimizationEnabled = enabled; }
|
|
||||||
setOptimizationThreshold(threshold) { this.optimizationThreshold = threshold; }
|
|
||||||
getShapes() { return this.shapes; }
|
|
||||||
setShapes(shapes) { this.shapes = shapes; this.render(); }
|
|
||||||
|
|
||||||
getMouseCoordinates(e) {
|
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
|
||||||
return {
|
|
||||||
x: (e.clientX - rect.left) / this.canvas.width,
|
|
||||||
y: (e.clientY - rect.top) / this.canvas.height
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseDown(e) {
|
|
||||||
this.isDrawing = true;
|
|
||||||
const coords = this.getMouseCoordinates(e);
|
|
||||||
|
|
||||||
if (this.drawingTool === 'pencil') {
|
|
||||||
this.currentShape = { type: 'pencil', data: { color: this.currentColor, path: [coords], thickness: this.currentThickness } };
|
|
||||||
} else if (this.drawingTool === 'line') {
|
|
||||||
this.currentShape = { type: 'line', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness } };
|
|
||||||
} else if (this.drawingTool === 'rectangle') {
|
|
||||||
this.currentShape = { type: 'rectangle', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness, fill: false } };
|
|
||||||
} else if (this.drawingTool === 'circle') {
|
|
||||||
this.currentShape = { type: 'circle', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness, fill: false } };
|
|
||||||
} else if (this.drawingTool === 'eraser') {
|
|
||||||
this.currentShape = { type: 'eraser', data: { color: '#ffffff', start: coords, end: coords, thickness: 3 } };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit('drawingStart', this.currentShape);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseMove(e) {
|
|
||||||
if (!this.isDrawing || !this.currentShape) return;
|
|
||||||
const coords = this.getMouseCoordinates(e);
|
|
||||||
|
|
||||||
if (this.drawingTool === 'pencil') {
|
|
||||||
this.currentShape.data.path.push(coords);
|
|
||||||
} else {
|
|
||||||
this.currentShape.data.end = coords;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
this.emit('drawingUpdate', this.currentShape);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseUp(e) {
|
|
||||||
if (!this.isDrawing || !this.currentShape) return;
|
|
||||||
this.isDrawing = false;
|
|
||||||
const coords = this.getMouseCoordinates(e);
|
|
||||||
if (this.drawingTool === 'pencil' && this.pathOptimizationEnabled && this.currentShape.data.path.length > 10) {
|
|
||||||
this.currentShape.data.path = this.optimizePath(this.currentShape.data.path);
|
|
||||||
} else {
|
|
||||||
this.currentShape.data.end = coords;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.shapes.push({ ...this.currentShape });
|
|
||||||
this.emit('drawingEnd', this.currentShape);
|
|
||||||
this.currentShape = null;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseLeave(e) {
|
|
||||||
if (this.isDrawing) this.handleMouseUp(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
optimizePath(path) {
|
|
||||||
if (path.length < 3) return path;
|
|
||||||
const optimizedPath = [path[0]];
|
|
||||||
for (let i = 1; i < path.length - 1;) {
|
|
||||||
let a = 1;
|
|
||||||
while (i + a < path.length && this.calculateDistance(path[i], path[i + a]) < this.optimizationThreshold) a++;
|
|
||||||
optimizedPath.push(path[i]);
|
|
||||||
i += a;
|
|
||||||
}
|
|
||||||
optimizedPath.push(path[path.length - 1]);
|
|
||||||
return optimizedPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateDistance(p1, p2) {
|
|
||||||
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
addShape(shapeData) {
|
|
||||||
if (Array.isArray(shapeData)) this.shapes.push(...shapeData);
|
|
||||||
else this.shapes.push(shapeData);
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearCanvas() {
|
|
||||||
this.shapes = [];
|
|
||||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
this.emit('clear');
|
|
||||||
}
|
|
||||||
|
|
||||||
exportToDataURL(type = 'image/png', quality = 1) {
|
|
||||||
return this.canvas.toDataURL(type, quality);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
|
|
||||||
// 绘制历史形状
|
|
||||||
this.shapes.forEach(shape => this.drawShape(shape));
|
|
||||||
|
|
||||||
// 绘制当前正在绘制的形状
|
|
||||||
if (this.currentShape) this.drawShape(this.currentShape);
|
|
||||||
|
|
||||||
// 画画布边框
|
|
||||||
this.ctx.save();
|
|
||||||
this.ctx.strokeStyle = "#000";
|
|
||||||
this.ctx.lineWidth = 2;
|
|
||||||
this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
this.ctx.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
drawShape(shape) {
|
|
||||||
switch (shape.type) {
|
|
||||||
case 'pencil': this.drawPencil(shape.data); break;
|
|
||||||
case 'line': this.drawLine(shape.data); break;
|
|
||||||
case 'rectangle': this.drawRectangle(shape.data); break;
|
|
||||||
case 'circle': this.drawCircle(shape.data); break;
|
|
||||||
case 'eraser': this.drawEraser(shape.data, shape); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drawPencil(p) {
|
|
||||||
if (p.path.length < 2) return;
|
|
||||||
const path = p.path.map(pt => ({ x: pt.x * this.canvas.width, y: pt.y * this.canvas.height }));
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.strokeStyle = p.color;
|
|
||||||
this.ctx.lineWidth = p.thickness;
|
|
||||||
this.ctx.lineCap = 'round';
|
|
||||||
this.ctx.lineJoin = 'round';
|
|
||||||
this.drawSmoothCurve(this.ctx, path);
|
|
||||||
this.ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
drawSmoothCurve(ctx, path) {
|
|
||||||
if (path.length < 3) { ctx.moveTo(path[0].x, path[0].y); for (let i = 1; i < path.length; i++) ctx.lineTo(path[i].x, path[i].y); return; }
|
|
||||||
ctx.moveTo(path[0].x, path[0].y);
|
|
||||||
const threshold = 5;
|
|
||||||
for (let i = 1; i < path.length - 2;) {
|
|
||||||
let a = 1;
|
|
||||||
while (i + a < path.length - 2 &&
|
|
||||||
Math.sqrt(Math.pow(path[i].x - path[i + a].x, 2) + Math.pow(path[i].y - path[i + a].y, 2)) < threshold) a++;
|
|
||||||
const xc = (path[i].x + path[i + a].x) / 2;
|
|
||||||
const yc = (path[i].y + path[i + a].y) / 2;
|
|
||||||
ctx.quadraticCurveTo(path[i].x, path[i].y, xc, yc);
|
|
||||||
i += a;
|
|
||||||
}
|
|
||||||
ctx.lineTo(path[path.length - 1].x, path[path.length - 1].y);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawLine(l) {
|
|
||||||
const sx = l.start.x * this.canvas.width, sy = l.start.y * this.canvas.height;
|
|
||||||
const ex = l.end.x * this.canvas.width, ey = l.end.y * this.canvas.height;
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.moveTo(sx, sy);
|
|
||||||
this.ctx.lineTo(ex, ey);
|
|
||||||
this.ctx.strokeStyle = l.color;
|
|
||||||
this.ctx.lineWidth = l.thickness;
|
|
||||||
this.ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
drawRectangle(r) {
|
|
||||||
const sx = r.start.x * this.canvas.width, sy = r.start.y * this.canvas.height;
|
|
||||||
const ex = r.end.x * this.canvas.width, ey = r.end.y * this.canvas.height;
|
|
||||||
const w = ex - sx, h = ey - sy;
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.rect(sx, sy, w, h);
|
|
||||||
if (r.fill) { this.ctx.fillStyle = r.color; this.ctx.fill(); }
|
|
||||||
else { this.ctx.strokeStyle = r.color; this.ctx.lineWidth = r.thickness; this.ctx.stroke(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
drawCircle(c) {
|
|
||||||
const sx = c.start.x * this.canvas.width, sy = c.start.y * this.canvas.height;
|
|
||||||
const ex = c.end.x * this.canvas.width, ey = c.end.y * this.canvas.height;
|
|
||||||
const cx = (sx + ex) / 2, cy = (sy + ey) / 2;
|
|
||||||
const rx = Math.abs(ex - sx) / 2, ry = Math.abs(ey - sy) / 2;
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.ellipse(cx, cy, rx, ry, 0, 0, 2 * Math.PI);
|
|
||||||
if (c.fill) { this.ctx.fillStyle = c.color; this.ctx.fill(); }
|
|
||||||
else { this.ctx.strokeStyle = c.color; this.ctx.lineWidth = c.thickness; this.ctx.stroke(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
drawEraser(e, shapeObj) {
|
|
||||||
const sx = e.start.x * this.canvas.width, sy = e.start.y * this.canvas.height;
|
|
||||||
const ex = e.end.x * this.canvas.width, ey = e.end.y * this.canvas.height;
|
|
||||||
const w = Math.abs(ex - sx), h = Math.abs(ey - sy);
|
|
||||||
const x = Math.min(sx, ex), y = Math.min(sy, ey);
|
|
||||||
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.rect(x, y, w, h);
|
|
||||||
this.ctx.fillStyle = 'rgba(255,255,255)';
|
|
||||||
this.ctx.fill();
|
|
||||||
|
|
||||||
// 仅当前正在绘制的橡皮擦显示边框
|
|
||||||
if (this.currentShape && shapeObj === this.currentShape) {
|
|
||||||
this.ctx.save();
|
|
||||||
this.ctx.strokeStyle = '#000';
|
|
||||||
this.ctx.lineWidth = 1;
|
|
||||||
this.ctx.strokeRect(x, y, w, h);
|
|
||||||
this.ctx.restore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.canvas.removeEventListener('mousedown', this.handleMouseDown);
|
|
||||||
this.canvas.removeEventListener('mousemove', this.handleMouseMove);
|
|
||||||
this.canvas.removeEventListener('mouseup', this.handleMouseUp);
|
|
||||||
this.canvas.removeEventListener('mouseleave', this.handleMouseLeave);
|
|
||||||
window.removeEventListener('resize', this.resize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Canvas;
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
v-model="inviteDialog"
|
|
||||||
title="远程协作"
|
|
||||||
width="400px"
|
|
||||||
:close-on-press-escape="false"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
:show-close="false"
|
|
||||||
>
|
|
||||||
|
|
||||||
<div style="width: 100%; margin-bottom: 30px; font-size: 20px">
|
|
||||||
"
|
|
||||||
{{
|
|
||||||
socketInformation.room_name
|
|
||||||
? socketInformation.room_name
|
|
||||||
: ''
|
|
||||||
}}
|
|
||||||
" 邀请您参加远程协作
|
|
||||||
</div>
|
|
||||||
<div style="text-align: center">
|
|
||||||
<el-button
|
|
||||||
size="large"
|
|
||||||
type="danger"
|
|
||||||
style="font-size: 16px"
|
|
||||||
@click="clickRefuseJoin"
|
|
||||||
>
|
|
||||||
拒 绝
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
size="large"
|
|
||||||
type="primary"
|
|
||||||
style="font-size: 16px"
|
|
||||||
@click="clickJoin"
|
|
||||||
>
|
|
||||||
加 入
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref ,onMounted} from 'vue'
|
|
||||||
import { getStatusApi } from '@/api/conferencingRoom.js'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { mqttClient } from "@/utils/mqtt.js";
|
|
||||||
import { useUserStore } from '@/stores/modules/user.js'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
const inviteDialog = ref(false)
|
|
||||||
const socketInformation = ref(null)
|
|
||||||
|
|
||||||
|
|
||||||
/** 拒绝加入 */
|
|
||||||
const clickRefuseJoin = async () => {
|
|
||||||
//status 1: 同意加入, 5: 拒绝加入
|
|
||||||
try{
|
|
||||||
const res = await getStatusApi(socketInformation.value.room_uid,{status:5})
|
|
||||||
if(res.meta.code == 200){
|
|
||||||
ElMessage({
|
|
||||||
message: '已拒绝加入该协作',
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
inviteDialog.value = false
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error,'error')
|
|
||||||
inviteDialog.value = false
|
|
||||||
} finally {
|
|
||||||
inviteDialog.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clickJoin = async () => {
|
|
||||||
const res = await getStatusApi(socketInformation.value.room_uid,{status:1})
|
|
||||||
if(res.meta.code == 200){
|
|
||||||
ElMessage({
|
|
||||||
message: '成功加入该协作',
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
inviteDialog.value = false
|
|
||||||
router.push({
|
|
||||||
path: '/conferencingRoom',
|
|
||||||
query:{
|
|
||||||
type:2,//创建房间,加入房间 2
|
|
||||||
room_uid:socketInformation.value.room_uid
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
inviteDialog.value = false
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 浏览器通知 */
|
|
||||||
const showNotification = (data) => {
|
|
||||||
if ('Notification' in window) {
|
|
||||||
Notification.requestPermission().then((permission) => {
|
|
||||||
if (permission === 'granted') {
|
|
||||||
const notification = new Notification('协作邀请', {
|
|
||||||
// body: String(data.room_name) + '邀请您参加远程协作'
|
|
||||||
body: '远程协作有新的邀请'
|
|
||||||
// icon: logo,
|
|
||||||
})
|
|
||||||
notification.onclick = () => { clickJoin() }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理加入房间和拒接房间 mqtt 消息 */
|
|
||||||
const processingSocket = (message) => {
|
|
||||||
const res = JSON.parse(message)
|
|
||||||
console.log(res,'收到用户信息 邀请')
|
|
||||||
if (!res?.status) {
|
|
||||||
socketInformation.value = res
|
|
||||||
inviteDialog.value = true
|
|
||||||
showNotification(socketInformation.value)
|
|
||||||
}else if(res.status == 5){
|
|
||||||
ElMessage({
|
|
||||||
message: `${res?.display_name}拒绝加入该协作`,
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
processingSocket,
|
|
||||||
});
|
|
||||||
|
|
||||||
// onMounted(async () => {
|
|
||||||
// await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
|
|
||||||
// const res = await userStore.getInfo()
|
|
||||||
// const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
|
|
||||||
// mqttClient.subscribe(topic, async (shapeData) => {
|
|
||||||
// // console.log(shapeData.toString(),'shapeData发送邀请')
|
|
||||||
// processingSocket(shapeData.toString())
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ const appStore = useAppStore()
|
|||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
function toggleSideBar() {
|
function toggleSideBar() {
|
||||||
appStore.toggleSideBar(true)
|
appStore.toggleSideBar(false)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div>
|
||||||
:class="{ 'has-logo': showLogo }"
|
|
||||||
:style="{
|
|
||||||
backgroundColor:
|
|
||||||
sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<!-- <logo v-if="showLogo" :collapse="isCollapse" /> -->
|
<!-- <logo v-if="showLogo" :collapse="isCollapse" /> -->
|
||||||
<el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
|
<el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
|
||||||
|
<transition mode="out-in">
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="activeMenu"
|
:default-active="activeMenu"
|
||||||
:collapse="false"
|
:collapse="isCollapse"
|
||||||
:background-color="
|
:background-color="
|
||||||
sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground
|
sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground
|
||||||
"
|
"
|
||||||
@@ -27,6 +22,7 @@
|
|||||||
:base-path="route.path"
|
:base-path="route.path"
|
||||||
/>
|
/>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
|
</transition>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -51,7 +47,6 @@ const isCollapse = computed(() => !appStore.sidebar.opened)
|
|||||||
|
|
||||||
const activeMenu = computed(() => {
|
const activeMenu = computed(() => {
|
||||||
const { meta, path } = route
|
const { meta, path } = route
|
||||||
// if set path, the sidebar will highlight the path you set
|
|
||||||
if (meta.activeMenu) {
|
if (meta.activeMenu) {
|
||||||
return meta.activeMenu
|
return meta.activeMenu
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ export { default as Navbar } from './Navbar.vue'
|
|||||||
export { default as Settings } from './Settings/index.vue'
|
export { default as Settings } from './Settings/index.vue'
|
||||||
export { default as TagsView } from './TagsView/index.vue'
|
export { default as TagsView } from './TagsView/index.vue'
|
||||||
export { default as ResetPwd } from './ResetPwd/index.vue'
|
export { default as ResetPwd } from './ResetPwd/index.vue'
|
||||||
export { default as InviteJoin } from './InviteJoin/index.vue'
|
|
||||||
@@ -2,11 +2,13 @@
|
|||||||
<div class="wrapper-content">
|
<div class="wrapper-content">
|
||||||
<div class="content-nav">
|
<div class="content-nav">
|
||||||
<div class="nav-left">
|
<div class="nav-left">
|
||||||
<div>xSynergy远程协作系统</div>
|
<div>xSynergy远程协作后台管理系统</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-right">
|
<div class="nav-right">
|
||||||
<el-dropdown trigger="click" @command="handleCommand">
|
<el-dropdown trigger="click" @command="handleCommand">
|
||||||
<div class="avatar-wrapper">
|
<div class="avatar-wrapper">
|
||||||
|
<img v-if="userStore.avatar" :src="userStore.avatar" />
|
||||||
|
<img v-else src="@/assets/images/profile.jpg"/>
|
||||||
<span class="username">{{ nickName }}</span>
|
<span class="username">{{ nickName }}</span>
|
||||||
<!-- <el-icon><caret-bottom /></el-icon> -->
|
<!-- <el-icon><caret-bottom /></el-icon> -->
|
||||||
</div>
|
</div>
|
||||||
@@ -32,12 +34,15 @@
|
|||||||
<div class="main-container" :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }">
|
<div class="main-container" :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }">
|
||||||
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||||
<div class="sidebar-right">
|
<div class="sidebar-right">
|
||||||
|
<!-- :class="{ 'fixed-header': fixedHeader }" -->
|
||||||
|
<div >
|
||||||
|
<navbar ref="navbarRef"/>
|
||||||
|
</div>
|
||||||
<app-main />
|
<app-main />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ResetPwd ref="resetPwdRef"/>
|
<ResetPwd ref="resetPwdRef"/>
|
||||||
<InviteJoin ref="inviteJoinRef"/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -45,25 +50,23 @@
|
|||||||
import { ElMessageBox ,ElMessage} from 'element-plus'
|
import { ElMessageBox ,ElMessage} from 'element-plus'
|
||||||
import { useWindowSize } from '@vueuse/core'
|
import { useWindowSize } from '@vueuse/core'
|
||||||
import Sidebar from './components/Sidebar/index.vue'
|
import Sidebar from './components/Sidebar/index.vue'
|
||||||
import { AppMain, TagsView ,ResetPwd,InviteJoin} from './components/index.js'
|
import { AppMain, TagsView ,ResetPwd,Navbar} from './components/index.js'
|
||||||
import { useAppStore } from '@/stores/modules/app.js'
|
import { useAppStore } from '@/stores/modules/app.js'
|
||||||
import { useSettingsStore } from '@/stores/modules/settings.js'
|
import { useSettingsStore } from '@/stores/modules/settings.js'
|
||||||
import { useUserStore } from '@/stores/modules/user.js'
|
import { useUserStore } from '@/stores/modules/user.js'
|
||||||
import { removeToken } from '@/utils/auth.js'
|
import { removeToken } from '@/utils/auth.js'
|
||||||
import { onMounted ,ref} from 'vue'
|
import { onMounted ,ref} from 'vue'
|
||||||
import { mqttClient } from "@/utils/mqtt.js";
|
|
||||||
|
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const useAppStoreStore = useAppStore()
|
const useAppStoreStore = useAppStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const inviteJoinRef = ref(null)
|
|
||||||
|
|
||||||
const theme = computed(() => settingsStore.theme)
|
const theme = computed(() => settingsStore.theme)
|
||||||
const sidebar = computed(() => useAppStoreStore.sidebar)
|
const sidebar = computed(() => useAppStoreStore.sidebar)
|
||||||
const device = computed(() => useAppStoreStore.device)
|
const device = computed(() => useAppStoreStore.device)
|
||||||
const needTagsView = computed(() => settingsStore.tagsView)
|
const needTagsView = computed(() => settingsStore.tagsView)
|
||||||
|
|
||||||
const nickName = computed(() => {
|
const nickName = computed(() => {
|
||||||
// 优先从 userStore 获取
|
// 优先从 userStore 获取
|
||||||
if (userStore.name) {
|
if (userStore.name) {
|
||||||
@@ -82,7 +85,6 @@ const nickName = computed(() => {
|
|||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const classObj = computed(() => ({
|
const classObj = computed(() => ({
|
||||||
hideSidebar: !sidebar.value.opened,
|
hideSidebar: !sidebar.value.opened,
|
||||||
openSidebar: sidebar.value.opened,
|
openSidebar: sidebar.value.opened,
|
||||||
@@ -141,17 +143,7 @@ function handleClickOutside() {
|
|||||||
useAppStoreStore.closeSideBar({ withoutAnimation: false })
|
useAppStoreStore.closeSideBar({ withoutAnimation: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {})
|
||||||
await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
|
|
||||||
const res = await userStore.getInfo()
|
|
||||||
const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
|
|
||||||
mqttClient.subscribe(topic, async (shapeData) => {
|
|
||||||
if(inviteJoinRef.value){
|
|
||||||
inviteJoinRef.value.processingSocket(shapeData.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -232,8 +224,8 @@ onMounted(async () => {
|
|||||||
-webkit-box-shadow: 2px 0 9px rgba(0, 21, 41, 0.35);
|
-webkit-box-shadow: 2px 0 9px rgba(0, 21, 41, 0.35);
|
||||||
box-shadow: 2px 0 9px rgba(0, 21, 41, 0.35);
|
box-shadow: 2px 0 9px rgba(0, 21, 41, 0.35);
|
||||||
padding: 3px 20px;
|
padding: 3px 20px;
|
||||||
// background-color: #8290f0;
|
background-color: #409EFF;
|
||||||
background-color: #434343;
|
// background-color: #434343;
|
||||||
.nav-left {
|
.nav-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -260,6 +252,11 @@ onMounted(async () => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-right: 30px;
|
margin-right: 30px;
|
||||||
|
img{
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
@@ -312,9 +309,9 @@ onMounted(async () => {
|
|||||||
transition: width 0.28s;
|
transition: width 0.28s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hideSidebar .fixed-header {
|
// .hideSidebar .fixed-header {
|
||||||
width: calc(100% - 54px);
|
// width: calc(100% - 54px);
|
||||||
}
|
// }
|
||||||
|
|
||||||
.sidebarHide .fixed-header {
|
.sidebarHide .fixed-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
62
src/main.js
@@ -4,8 +4,12 @@ import { createPinia } from 'pinia'
|
|||||||
import * as Sentry from '@sentry/vue';
|
import * as Sentry from '@sentry/vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deepClone
|
deepClone,
|
||||||
} from '@/utils/ruoyi'
|
parseTime,
|
||||||
|
} from '@/utils/ruoyi.js'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
@@ -20,35 +24,41 @@ import router from './router'
|
|||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
Sentry.init({
|
app.config.globalProperties.parseTime = parseTime;
|
||||||
app,
|
|
||||||
dsn: 'https://34a0d76174a64db09d31d13a8042560b@sentry.cnsdt.com/2', // 替换为你的 DSN
|
// Sentry.init({
|
||||||
integrations: [
|
// app,
|
||||||
// 浏览器性能追踪集成
|
// dsn: 'https://34a0d76174a64db09d31d13a8042560b@sentry.cnsdt.com/2', // 替换为你的 DSN
|
||||||
Sentry.browserTracingIntegration({
|
// integrations: [
|
||||||
router,
|
// // 浏览器性能追踪集成
|
||||||
}),
|
// Sentry.browserTracingIntegration({
|
||||||
// 会话回放集成
|
// router,
|
||||||
Sentry.replayIntegration({
|
// }),
|
||||||
maskAllText: false,
|
// // 会话回放集成
|
||||||
blockAllMedia: false,
|
// Sentry.replayIntegration({
|
||||||
}),
|
// maskAllText: false,
|
||||||
],
|
// blockAllMedia: false,
|
||||||
// 性能监控采样率
|
// }),
|
||||||
tracesSampleRate: 1.0, // 生产环境建议设置为 0.1-0.2
|
// ],
|
||||||
// 会话回放采样率
|
// // 性能监控采样率
|
||||||
replaysSessionSampleRate: 0.1,
|
// tracesSampleRate: 1.0, // 生产环境建议设置为 0.1-0.2
|
||||||
replaysOnErrorSampleRate: 1.0,
|
// // 会话回放采样率
|
||||||
// 环境配置
|
// replaysSessionSampleRate: 0.1,
|
||||||
environment: import.meta.env.MODE,
|
// replaysOnErrorSampleRate: 1.0,
|
||||||
// 开发环境下可禁用 Sentry
|
// // 环境配置
|
||||||
enabled: import.meta.env.PROD,
|
// environment: import.meta.env.MODE,
|
||||||
});
|
// // 开发环境下可禁用 Sentry
|
||||||
|
// enabled: import.meta.env.PROD,
|
||||||
|
// });
|
||||||
|
|
||||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
app.component(key, component)
|
app.component(key, component)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//全局组件
|
||||||
|
import Pagination from '@/components/Pagination/index.vue'
|
||||||
|
|
||||||
|
app.component('Pagination', Pagination)
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(ElementPlus, {
|
app.use(ElementPlus, {
|
||||||
|
|||||||
@@ -25,37 +25,146 @@ export const constantRoutes = [
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/whiteboard',
|
path: "/userManagement",
|
||||||
component: () => import('@/views/custom/tabulaRase/index.vue'),
|
redirect: "/userManagement/UserManagementPage",
|
||||||
meta: { title: "白板" },
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/coordinate",
|
|
||||||
redirect: "/coordinate/CoordinatePage",
|
|
||||||
component: Layout,
|
component: Layout,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'CoordinatePage',
|
path: 'UserManagementPage',
|
||||||
name: "Coordinate",
|
name: "UserManagement",
|
||||||
component: () => import("@/views/coordinate/personnelList/index.vue"),
|
component: () => import("@/views/userManagement/index.vue"),
|
||||||
meta: { title: "远程协作", icon: "client", affix: true },
|
meta: { title: "用户管理", icon: "client", affix: true },
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/conferencingRoom",
|
path: "/roleManagement",
|
||||||
|
redirect: "/roleManagement/RoleManagementPage",
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'RoleManagementPage',
|
||||||
|
name: "RoleManagement",
|
||||||
|
component: () => import("@/views/roleManagement/index.vue"),
|
||||||
|
meta: { title: "角色管理", icon: "client", affix: true },
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/menuManagement",
|
||||||
|
redirect: "/menuManagement/MenuManagementPage",
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'MenuManagementPage',
|
||||||
|
name: "MenuManagement",
|
||||||
|
component: () => import("@/views/menuManagement/index.vue"),
|
||||||
|
meta: { title: "菜单管理", icon: "client", affix: true },
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/roomManagement",
|
||||||
|
redirect: "/roomManagement/RoomManagementPage",
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'RoomManagementPage',
|
||||||
|
name: "RoomManagement",
|
||||||
|
component: () => import("@/views/roomManagement/index.vue"),
|
||||||
|
meta: { title: "房间管理", icon: "client", affix: true },
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/interfaceManagement",
|
||||||
|
redirect: "/interfaceManagement/InterfaceManagementPage",
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'InterfaceManagementPage',
|
||||||
|
name: "InterfaceManagement",
|
||||||
|
component: () => import("@/views/interfaceManagement/index.vue"),
|
||||||
|
meta: { title: "接口管理", icon: "client", affix: true },
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// path: "/patrolMission",
|
||||||
|
// redirect: "/patrolMission/PatrolMissionPage",
|
||||||
// component: Layout,
|
// component: Layout,
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// path: 'PatrolMissionPage',
|
||||||
|
// name: "PatrolMission",
|
||||||
|
// component: () => import("@/views/patrolMission/index.vue"),
|
||||||
|
// meta: { title: "设备知识库", icon: "client", affix: true },
|
||||||
|
// },
|
||||||
|
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
path: '/room-auth',
|
||||||
|
component: Layout,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: 'room/:roomId(\\w+)',
|
||||||
name: "ConferencingRoom",
|
component: () => import('@/views/roomManagement/authRoom.vue'),
|
||||||
component: () => import("@/views/conferencingRoom/index.vue"),
|
name: 'AuthRoom',
|
||||||
meta: { title: "会议房间", icon: "client", affix: true },
|
meta: { title: '参与者信息', activeMenu: '/roomManagement/RoomManagementPage', affix: true, icon: '' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/user-auth',
|
||||||
|
component: Layout,
|
||||||
|
hidden: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'role/:userId(\\w+)',
|
||||||
|
component: () => import('@/views/userManagement/authRole.vue'),
|
||||||
|
name: 'AuthRole',
|
||||||
|
meta: { title: '分配角色', activeMenu: '/userManagement/UserManagementPage', affix: true, icon: '' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// path: '/role-auth',
|
||||||
|
// component: Layout,
|
||||||
|
// hidden: true,
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// path: 'menu/:roleId(\\w+)',
|
||||||
|
// component: () => import('@/views/roleManagement/authMenu.vue'),
|
||||||
|
// name: 'AuthUser',
|
||||||
|
// meta: { title: '分配菜单',activeMenu: '/roleManagement/RoleManagementPage', affix: true , icon: '' }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: "/system",
|
||||||
|
// redirect: "/system/menuManagement",
|
||||||
|
// component: Layout,
|
||||||
|
// meta: { title: "系统管理", icon: "system", affix: true },
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// path: "menuManagement",
|
||||||
|
// name: "MenuManagement",
|
||||||
|
// component: () => import("@/views/menuManagement/index.vue"),
|
||||||
|
// meta: { title: "菜单管理", icon: "menu", affix: true },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: "userManagement",
|
||||||
|
// name: "UserManagement",
|
||||||
|
// component: () => import("@/views/userManagement/index.vue"),
|
||||||
|
// meta: { title: "用户管理", icon: "user", affix: true },
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
// 错误页面路由
|
// 错误页面路由
|
||||||
{
|
{
|
||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
@@ -68,72 +177,11 @@ export const constantRoutes = [
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
meta: { title: "401未授权" }
|
meta: { title: "401未授权" }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/assistWx',
|
|
||||||
component: () => import('@/views/coordinate/personnelList/components/assistWx/index.vue'),
|
|
||||||
meta: { title: "白板" },
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const dynamicRoutes = [
|
export const dynamicRoutes = [
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// const router = createRouter({
|
|
||||||
// history: createWebHashHistory(import.meta.env.BASE_URL),
|
|
||||||
// routes: [
|
|
||||||
// // {
|
|
||||||
// // path: '/',
|
|
||||||
// // component: () => import("@/views/custom/tabulaRase/index.vue"),
|
|
||||||
// // },
|
|
||||||
// {
|
|
||||||
// path: '/',
|
|
||||||
// redirect: '/login', // 这里做重定向
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: '/whiteboard',
|
|
||||||
// component: () => import('@/views/custom/tabulaRase/index.vue'),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: "/login",
|
|
||||||
// component: () => import("@/views/login.vue"),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: "/coordinate",
|
|
||||||
// component: Layout,
|
|
||||||
// meta: { title: "远程协作", icon: "client", affix: true },
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// path: '',
|
|
||||||
// name: "Coordinate",
|
|
||||||
// component: () => import("@/views/coordinate/personnelList/index.vue")
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: "/conferencingRoom",
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// path: '',
|
|
||||||
// name: "ConferencingRoom",
|
|
||||||
// component: () => import("@/views/conferencingRoom/index.vue")
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
|
|
||||||
|
|
||||||
// // 错误页面路由
|
|
||||||
// {
|
|
||||||
// path: "/:pathMatch(.*)*",
|
|
||||||
// component: () => import("@/views/error/404.vue"),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: "/401",
|
|
||||||
// component: () => import("@/views/error/401.vue"),
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
// })
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
// history: createWebHistory(import.meta.env.VITE_BASE_PATH),
|
// history: createWebHistory(import.meta.env.VITE_BASE_PATH),
|
||||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const useAppStore = defineStore(
|
|||||||
{
|
{
|
||||||
state: () => ({
|
state: () => ({
|
||||||
sidebar: {
|
sidebar: {
|
||||||
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
|
opened: true,
|
||||||
withoutAnimation: false,
|
withoutAnimation: false,
|
||||||
hide: false
|
hide: false
|
||||||
},
|
},
|
||||||
@@ -20,14 +20,8 @@ export const useAppStore = defineStore(
|
|||||||
}
|
}
|
||||||
this.sidebar.opened = !this.sidebar.opened
|
this.sidebar.opened = !this.sidebar.opened
|
||||||
this.sidebar.withoutAnimation = withoutAnimation
|
this.sidebar.withoutAnimation = withoutAnimation
|
||||||
if (this.sidebar.opened) {
|
|
||||||
// Cookies.set('sidebarStatus', 1)
|
|
||||||
} else {
|
|
||||||
// Cookies.set('sidebarStatus', 0)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
closeSideBar({ withoutAnimation }) {
|
closeSideBar({ withoutAnimation }) {
|
||||||
// Cookies.set('sidebarStatus', 0)
|
|
||||||
this.sidebar.opened = false
|
this.sidebar.opened = false
|
||||||
this.sidebar.withoutAnimation = withoutAnimation
|
this.sidebar.withoutAnimation = withoutAnimation
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const useSettingsStore = defineStore(
|
|||||||
{
|
{
|
||||||
state: () => ({
|
state: () => ({
|
||||||
title: '',
|
title: '',
|
||||||
theme: storageSetting.theme === undefined ? '#434343' : storageSetting.theme,
|
theme: storageSetting.theme === undefined ? '#409EFF' : storageSetting.theme,
|
||||||
sideTheme: storageSetting.sideTheme === undefined ? sideTheme : storageSetting.sideTheme,
|
sideTheme: storageSetting.sideTheme === undefined ? sideTheme : storageSetting.sideTheme,
|
||||||
showSettings: showSettings,
|
showSettings: showSettings,
|
||||||
topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
|
topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { login, logout, getInfo } from '@/api/login.js'
|
import { login, logout, getInfo ,getAvatarsApi} from '@/api/login.js'
|
||||||
import { getToken, setToken, removeToken } from '@/utils/auth.js'
|
import { getToken, setToken, removeToken ,getUserInfo} from '@/utils/auth.js'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
@@ -23,9 +23,14 @@ export const useUserStore = defineStore(
|
|||||||
ElMessage({ message: res.meta?.message || '登录失败', type: 'error' });
|
ElMessage({ message: res.meta?.message || '登录失败', type: 'error' });
|
||||||
return Promise.reject(res);
|
return Promise.reject(res);
|
||||||
}
|
}
|
||||||
|
if(!res.data.user.is_admin){
|
||||||
|
ElMessage({ message: '暂无权限登录,请联系管理员', type: 'error' });
|
||||||
|
return Promise.reject(res);
|
||||||
|
}
|
||||||
const { token, user } = res.data;
|
const { token, user } = res.data;
|
||||||
this.name = user.name;
|
this.name = user.name;
|
||||||
sessionStorage.setItem('userData', JSON.stringify(user));
|
sessionStorage.setItem('userData', JSON.stringify(user));
|
||||||
|
await this.getAvatars();
|
||||||
setToken(token);
|
setToken(token);
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
|
||||||
@@ -54,6 +59,18 @@ export const useUserStore = defineStore(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// 获取用户头像
|
||||||
|
async getAvatars() {
|
||||||
|
try {
|
||||||
|
const userData = await getUserInfo()
|
||||||
|
const res = await getAvatarsApi(userData.uid);
|
||||||
|
const url = URL.createObjectURL(res);
|
||||||
|
this.avatar = url;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取头像失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
// 退出系统
|
// 退出系统
|
||||||
async logOut() {
|
async logOut() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
59
src/utils/scroll-to.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const easeInOutQuad = (t, b, c, d) => {
|
||||||
|
t /= d / 2;
|
||||||
|
if (t < 1) {
|
||||||
|
return (c / 2) * t * t + b;
|
||||||
|
}
|
||||||
|
t--;
|
||||||
|
return (-c / 2) * (t * (t - 2) - 1) + b;
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestAnimFrame = (function () {
|
||||||
|
return (
|
||||||
|
window.requestAnimationFrame ||
|
||||||
|
(window).webkitRequestAnimationFrame ||
|
||||||
|
(window).mozRequestAnimationFrame ||
|
||||||
|
function (callback) {
|
||||||
|
window.setTimeout(callback, 1000 / 60);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} amount
|
||||||
|
*/
|
||||||
|
const move = (amount) => {
|
||||||
|
document.documentElement.scrollTop = amount;
|
||||||
|
(document.body.parentNode).scrollTop = amount;
|
||||||
|
document.body.scrollTop = amount;
|
||||||
|
};
|
||||||
|
|
||||||
|
const position = () => {
|
||||||
|
return document.documentElement.scrollTop || (document.body.parentNode).scrollTop || document.body.scrollTop;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} to
|
||||||
|
* @param {number} duration
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
export const scrollTo = (to, duration, callback) => {
|
||||||
|
const start = position();
|
||||||
|
const change = to - start;
|
||||||
|
const increment = 20;
|
||||||
|
let currentTime = 0;
|
||||||
|
duration = typeof duration === 'undefined' ? 500 : duration;
|
||||||
|
const animateScroll = function () {
|
||||||
|
currentTime += increment;
|
||||||
|
const val = easeInOutQuad(currentTime, start, change, duration);
|
||||||
|
move(val);
|
||||||
|
if (currentTime < duration) {
|
||||||
|
requestAnimFrame(animateScroll);
|
||||||
|
} else {
|
||||||
|
if (callback && typeof callback === 'function') {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
animateScroll();
|
||||||
|
};
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { mqttClient } from "./mqtt";
|
|
||||||
import { getWhiteboardShapes, getWhiteboardHistory } from "@/views/custom/api";
|
|
||||||
import { useMeterStore } from '@/stores/modules/meter';
|
|
||||||
import { encode, decode } from '@msgpack/msgpack'
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import { emitter } from "@/utils/bus.js";
|
|
||||||
const meterStore = useMeterStore();
|
|
||||||
meterStore.initUdid();
|
|
||||||
|
|
||||||
let isRemote = false;
|
|
||||||
let canvasInstance = null;
|
|
||||||
|
|
||||||
// 获取本地缓存 userData
|
|
||||||
function getLocalUserData() {
|
|
||||||
const dataStr = sessionStorage.getItem('userData');
|
|
||||||
if (!dataStr) return null;
|
|
||||||
try {
|
|
||||||
return JSON.parse(dataStr);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("解析 userData 失败:", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WhiteboardSync = {
|
|
||||||
async init(canvas, roomUid) {
|
|
||||||
if (!canvas || !roomUid) return;
|
|
||||||
canvasInstance = canvas;
|
|
||||||
|
|
||||||
const localUser = getLocalUserData();
|
|
||||||
const localUid = localUser?.uid;
|
|
||||||
try {
|
|
||||||
// 先连接 MQTT
|
|
||||||
await mqttClient.connect(meterStore.getSudid());
|
|
||||||
// 获取历史数据
|
|
||||||
const res = await getWhiteboardHistory({ after_timestamp: 0 }, roomUid);
|
|
||||||
if (res.meta.code === 200 && res.data.shapes.length > 0) {
|
|
||||||
canvasInstance.addShape(res.data.shapes);
|
|
||||||
}else if(res.meta.code === 401){
|
|
||||||
emitter.emit('whiteboardFailed',true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订阅当前房间
|
|
||||||
const topic = `xSynergy/ROOM/${roomUid}/whiteboard/#`;
|
|
||||||
mqttClient.subscribe(topic, async (shapeData) => {
|
|
||||||
const shapeDataNew = JSON.parse(shapeData.toString())
|
|
||||||
// const shapeDataNew = decode(message);
|
|
||||||
try {
|
|
||||||
isRemote = true;
|
|
||||||
// 如果 shape 来自本地用户,则跳过
|
|
||||||
if (shapeDataNew.user_uid === localUid) return;
|
|
||||||
const res = await getWhiteboardHistory({ after_timestamp: shapeDataNew.created_at }, roomUid);
|
|
||||||
if (res.meta.code === 200) {
|
|
||||||
canvasInstance.addShape(res.data.shapes);
|
|
||||||
} else {
|
|
||||||
ElMessage.error("获取历史数据失败");
|
|
||||||
console.error("获取历史数据失败");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("处理MQTT数据失败:", e);
|
|
||||||
} finally {
|
|
||||||
isRemote = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.log("初始化多人同步失败:", err)
|
|
||||||
// console.error("❌ 连接或订阅失败:", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听画布事件:新增图形
|
|
||||||
canvas.on('drawingEnd', async (shape) => {
|
|
||||||
// 如果来自远程,或不是需要同步的类型,跳过
|
|
||||||
if (isRemote || !['pencil', 'line', 'rectangle', 'circle', 'eraser'].includes(shape.type)) return;
|
|
||||||
|
|
||||||
// 如果是本地用户自己的 shape,则不调用接口
|
|
||||||
if (shape.user_uid && shape.user_uid === localUid) return;
|
|
||||||
|
|
||||||
shape.room_uid = roomUid;
|
|
||||||
try {
|
|
||||||
await getWhiteboardShapes(shape, roomUid);
|
|
||||||
} catch (err) {
|
|
||||||
ElMessage.error("提交形状失败");
|
|
||||||
console.error("提交形状失败:", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听画布事件:清空
|
|
||||||
canvas.on('clear', async () => {
|
|
||||||
if (!isRemote) {
|
|
||||||
try {
|
|
||||||
// TODO: 调用接口,后端再发 MQTT
|
|
||||||
// await clearWhiteboard(roomUid);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("提交清空失败:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import { mqttClient } from "./mqtt";
|
|
||||||
import { getWhiteboardShapes, getWhiteboardHistory } from "@/views/custom/api";
|
|
||||||
import { useMeterStore } from '@/stores/modules/meter';
|
|
||||||
import { encode, decode } from '@msgpack/msgpack'
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
|
|
||||||
const meterStore = useMeterStore();
|
|
||||||
meterStore.initUdid();
|
|
||||||
|
|
||||||
let isRemote = false;
|
|
||||||
let canvasInstance = null;
|
|
||||||
|
|
||||||
// 获取本地缓存 userData
|
|
||||||
function getLocalUserData() {
|
|
||||||
const dataStr = sessionStorage.getItem('userData');
|
|
||||||
if (!dataStr) return null;
|
|
||||||
try {
|
|
||||||
return JSON.parse(dataStr);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("解析 userData 失败:", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WhiteboardSync = {
|
|
||||||
async init(canvas, roomUid) {
|
|
||||||
if (!canvas || !roomUid) return;
|
|
||||||
console.log('初始化多人同步:', roomUid);
|
|
||||||
canvasInstance = canvas;
|
|
||||||
|
|
||||||
const localUser = getLocalUserData();
|
|
||||||
const localUid = localUser?.uid;
|
|
||||||
try {
|
|
||||||
// 先连接 MQTT
|
|
||||||
await mqttClient.connect(meterStore.getSudid());
|
|
||||||
console.log("✅ MQTT 已连接");
|
|
||||||
|
|
||||||
// 获取历史数据
|
|
||||||
const res = await getWhiteboardHistory({ after_timestamp: 0 }, roomUid);
|
|
||||||
if (res.meta.code === 200 && res.data.shapes.length > 0) {
|
|
||||||
canvasInstance.addShape(res.data.shapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订阅当前房间
|
|
||||||
const topic = `xSynergy/ROOM/${roomUid}/whiteboard/#`;
|
|
||||||
mqttClient.subscribe(topic, async (shapeData) => {
|
|
||||||
const shapeDataNew = JSON.parse(shapeData.toString())
|
|
||||||
// const shapeDataNew = decode(message);
|
|
||||||
// console.log(shapeDataNew, '格式解码')
|
|
||||||
try {
|
|
||||||
isRemote = true;
|
|
||||||
// 如果 shape 来自本地用户,则跳过
|
|
||||||
if (shapeDataNew.user_uid === localUid) return;
|
|
||||||
const res = await getWhiteboardHistory({ after_timestamp: shapeDataNew.created_at }, roomUid);
|
|
||||||
if (res.meta.code === 200) {
|
|
||||||
canvasInstance.addShape(res.data.shapes);
|
|
||||||
} else {
|
|
||||||
ElMessage.error("获取历史数据失败");
|
|
||||||
console.error("获取历史数据失败");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("处理MQTT数据失败:", e);
|
|
||||||
} finally {
|
|
||||||
isRemote = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("✅ 已订阅:", topic);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("初始化多人同步失败:", err)
|
|
||||||
// console.error("❌ 连接或订阅失败:", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听画布事件:新增图形
|
|
||||||
canvas.on('drawingEnd', async (shape) => {
|
|
||||||
// 如果来自远程,或不是需要同步的类型,跳过
|
|
||||||
if (isRemote || !['pencil', 'line', 'rectangle', 'circle', 'eraser'].includes(shape.type)) return;
|
|
||||||
|
|
||||||
// 如果是本地用户自己的 shape,则不调用接口
|
|
||||||
if (shape.user_uid && shape.user_uid === localUid) return;
|
|
||||||
|
|
||||||
shape.room_uid = roomUid;
|
|
||||||
try {
|
|
||||||
await getWhiteboardShapes(shape, roomUid);
|
|
||||||
} catch (err) {
|
|
||||||
ElMessage.error("提交形状失败");
|
|
||||||
console.error("提交形状失败:", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听画布事件:清空
|
|
||||||
canvas.on('clear', async () => {
|
|
||||||
if (!isRemote) {
|
|
||||||
try {
|
|
||||||
// TODO: 调用接口,后端再发 MQTT
|
|
||||||
// await clearWhiteboard(roomUid);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("提交清空失败:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { ElMessage } from 'element-plus';
|
|
||||||
export function errorHandling(error,type) {
|
|
||||||
switch (error.name) {
|
|
||||||
case 'NotAllowedError':
|
|
||||||
ElMessage.error(`用户拒绝了权限请求,请允许此网站使用${type}权限`);
|
|
||||||
break;
|
|
||||||
case 'NotFoundError':
|
|
||||||
ElMessage.error(`未检测到可用的${type}设备,请检查${type}是否已正确连接`);
|
|
||||||
break;
|
|
||||||
case 'NotSupportedError':
|
|
||||||
ElMessage.error(`当前浏览器不支持${type}功能,请使用现代浏览器如Chrome、Firefox或Edge`);
|
|
||||||
break;
|
|
||||||
case 'NotReadableError':
|
|
||||||
ElMessage.error(`${type}设备正被其他应用程序占用,请关闭其他使用${type}的应用后重试`);
|
|
||||||
break;
|
|
||||||
case 'OverconstrainedError':
|
|
||||||
ElMessage.error(`${type}配置不兼容,请尝试调整${type}设置`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ElMessage.error('服务错误,请刷新重试');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理数据接收事件
|
|
||||||
export function handleDataReceived(payload, participant, kind) {
|
|
||||||
try {
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
const strData = decoder.decode(payload);
|
|
||||||
ElMessage.info(`收到消息 from ${participant.identity}: ${strData}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('处理接收消息失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleReconnected() {
|
|
||||||
ElMessage.success('已重新连接到房间');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取设备名称
|
|
||||||
export function getDeviceName(devices, deviceId) {
|
|
||||||
const device = devices.find(d => d.deviceId === deviceId);
|
|
||||||
return device ? (device.label || '未知设备') : '未知设备';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleVideoLoaded(identity, type) {
|
|
||||||
// console.log(`视频加载完成: ${identity}的${type}视频`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleConnectionStateChanged(state) {
|
|
||||||
// console.log('连接状态改变:', state);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 平滑曲线绘制函数
|
|
||||||
export function drawSmoothCurve(ctx, path) {
|
|
||||||
if (path.length < 3) {
|
|
||||||
// 如果点太少,直接绘制直线
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(path[0].x, path[0].y);
|
|
||||||
for (let i = 1; i < path.length; i++) {
|
|
||||||
ctx.lineTo(path[i].x, path[i].y);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(path[0].x, path[0].y);
|
|
||||||
|
|
||||||
const threshold = 5; // 距离阈值,用于控制平滑度
|
|
||||||
|
|
||||||
for (let i = 1; i < path.length - 2;) {
|
|
||||||
let a = 1;
|
|
||||||
// 寻找下一个足够远的点
|
|
||||||
while (i + a < path.length - 2 &&
|
|
||||||
Math.sqrt(Math.pow(path[i].x - path[i + a].x, 2) +
|
|
||||||
Math.pow(path[i].y - path[i + a].y, 2)) < threshold) {
|
|
||||||
a++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const xc = (path[i].x + path[i + a].x) / 2;
|
|
||||||
const yc = (path[i].y + path[i + a].y) / 2;
|
|
||||||
|
|
||||||
ctx.quadraticCurveTo(path[i].x, path[i].y, xc, yc);
|
|
||||||
i += a;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 连接最后两个点
|
|
||||||
ctx.lineTo(path[path.length - 1].x, path[path.length - 1].y);
|
|
||||||
}
|
|
||||||
|
|
||||||
//计算文件的SHA1值
|
|
||||||
export async function calculateFileSHA1(file) {
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
const hashBuffer = await crypto.subtle.digest('SHA-1', arrayBuffer);
|
|
||||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
||||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
||||||
return hashHex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用于比较两个对象是否相等
|
|
||||||
export function simpleDeepEqual(obj1, obj2) {
|
|
||||||
return JSON.stringify(obj1) === JSON.stringify(obj2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成唯一ID
|
|
||||||
export function generateElementId() {
|
|
||||||
return `laser_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
}
|
|
||||||
@@ -1,366 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-drawer v-model="drawerVisible" direction="rtl" title="请选择要加入房间的人员" size="40%">
|
|
||||||
<template #header>
|
|
||||||
<h4>请选择要加入房间的人员</h4>
|
|
||||||
</template>
|
|
||||||
<div class="drawer-content">
|
|
||||||
<!-- 搜索框 -->
|
|
||||||
<div class="search-section">
|
|
||||||
<el-input
|
|
||||||
v-model="searchKeyword"
|
|
||||||
placeholder="搜索人员或部门"
|
|
||||||
clearable
|
|
||||||
prefix-icon="Search"
|
|
||||||
@input="handleSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 已选人员展示 -->
|
|
||||||
<div class="selected-section" v-if="selectedUsers.length > 0">
|
|
||||||
<div class="selected-header">
|
|
||||||
<span>已选择 ({{ selectedUsers.length }})</span>
|
|
||||||
<el-button type="text" @click="clearAllSelected">清空</el-button>
|
|
||||||
</div>
|
|
||||||
<div class="selected-tags">
|
|
||||||
<el-tag
|
|
||||||
v-for="user in selectedUsers"
|
|
||||||
:key="user.uid"
|
|
||||||
closable
|
|
||||||
@close="removeSelectedUser(user)"
|
|
||||||
class="selected-tag"
|
|
||||||
>
|
|
||||||
{{ user.name }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 树形结构 -->
|
|
||||||
<el-scrollbar v-loading="leftListLoading" class="left-list-scrollbar" height="calc(100vh - 240px)">
|
|
||||||
<el-tree
|
|
||||||
ref="treeRef"
|
|
||||||
lazy
|
|
||||||
:load="handleLoadNode"
|
|
||||||
:props="treeProps"
|
|
||||||
node-key="uid"
|
|
||||||
show-checkbox
|
|
||||||
:default-expand-all="false"
|
|
||||||
:expand-on-click-node="false"
|
|
||||||
:check-strictly="false"
|
|
||||||
:filter-node-method="filterNode"
|
|
||||||
@check="handleCheckChange"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<template #default="{ node, data }">
|
|
||||||
<div class="tree-item">
|
|
||||||
<div class="tree-item-content">
|
|
||||||
<span v-if="data.uid" class="user-icon">👤</span>
|
|
||||||
<span v-else class="dept-icon">📁</span>
|
|
||||||
<span class="tree-item-text">{{ data.name }}</span>
|
|
||||||
<span v-if="data.users_count" class="user-count">({{ data.users_count }})</span>
|
|
||||||
<!-- <span v-if="isNodeDisabled(data)" class="already-joined-tag">已在房间中</span> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-tree>
|
|
||||||
</el-scrollbar>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="footer-actions">
|
|
||||||
<el-button @click="cancelClick">取 消</el-button>
|
|
||||||
<el-button type="primary" @click="confirmClick" :disabled="selectedUsers.length === 0">
|
|
||||||
确 定 ({{ selectedUsers.length }})
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-drawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
getDirectories,
|
|
||||||
getDirectoriesUsers
|
|
||||||
} from '@/api/coordinate.js'
|
|
||||||
import { getParticipantsApi } from '@/api/conferencingRoom.js'
|
|
||||||
import { nextTick, reactive, toRefs, watch, onMounted, ref } from "vue";
|
|
||||||
|
|
||||||
// 定义 emit
|
|
||||||
const emit = defineEmits(["confirmSelection"]);
|
|
||||||
|
|
||||||
// 接收 props
|
|
||||||
const props = defineProps({
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const drawerVisible = ref(false);
|
|
||||||
const treeRef = ref();
|
|
||||||
const searchKeyword = ref('');
|
|
||||||
const selectedUsers = ref([]);
|
|
||||||
const checkedNodes = ref([]);
|
|
||||||
const leftListLoading = ref(false)
|
|
||||||
const joinedUserIds = ref([]); // 存储已在房间中的用户ID
|
|
||||||
// 树形配置
|
|
||||||
const treeProps = reactive({
|
|
||||||
children: 'users',
|
|
||||||
label: 'name',
|
|
||||||
value: 'uid',
|
|
||||||
disabled: (data) => {
|
|
||||||
// 只有人员节点(有uid)且该用户已在房间中时才禁用
|
|
||||||
return data.uid && joinedUserIds.value.includes(data.uid);
|
|
||||||
},
|
|
||||||
isLeaf: (node) => {
|
|
||||||
return !!node.uid; // 有 uid 的为叶子节点(人员)
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const isNodeDisabled = (data) => {
|
|
||||||
// 只有人员节点(有uid)且该用户已在房间中时才禁用
|
|
||||||
return data.uid && joinedUserIds.value.includes(data.uid);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// 显示抽屉
|
|
||||||
async function show(roomId) {
|
|
||||||
drawerVisible.value = true;
|
|
||||||
// 重置选择状态
|
|
||||||
selectedUsers.value = [];
|
|
||||||
checkedNodes.value = [];
|
|
||||||
searchKeyword.value = '';
|
|
||||||
|
|
||||||
// 获取已在房间中的用户
|
|
||||||
try {
|
|
||||||
const res = await getParticipantsApi(roomId);
|
|
||||||
// 提取状态为1(在房间中)的用户ID
|
|
||||||
joinedUserIds.value = res.data
|
|
||||||
.filter(item => item.status == 1)
|
|
||||||
.map(item => item.user_uid);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取房间参与者失败:', error);
|
|
||||||
joinedUserIds.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 延迟加载树形数据,确保 DOM 已渲染
|
|
||||||
nextTick(() => {
|
|
||||||
if (treeRef.value) {
|
|
||||||
treeRef.value.setCheckedKeys([]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async function getJoinUsers(){
|
|
||||||
const res = await getParticipantsApi(roomId)
|
|
||||||
const joinUsers = res.data.filter(item => item.status == 1).map(item => item.user_uid)
|
|
||||||
return joinUsers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认选择
|
|
||||||
function confirmClick() {
|
|
||||||
if (selectedUsers.value.length > 0) {
|
|
||||||
emit('confirmSelection', selectedUsers.value);
|
|
||||||
drawerVisible.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消选择
|
|
||||||
function cancelClick() {
|
|
||||||
drawerVisible.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载树节点
|
|
||||||
const handleLoadNode = async (node, resolve) => {
|
|
||||||
if(node?.level === 0){
|
|
||||||
loadNode(resolve,'',node?.level)
|
|
||||||
}else if(node?.level > 0){
|
|
||||||
if(node.data.directory_uid){
|
|
||||||
loadUserNode(resolve,node.data.directory_uid,node?.level)
|
|
||||||
}else{
|
|
||||||
resolve(resolve)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载节点数据
|
|
||||||
const loadNode = async (resolve, id) => {
|
|
||||||
try {
|
|
||||||
leftListLoading.value = true
|
|
||||||
let res = await getDirectories({level:1})
|
|
||||||
if(res.meta.code == 200){
|
|
||||||
resolve(res.data)
|
|
||||||
}
|
|
||||||
leftListLoading.value = false
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
leftListLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadUserNode = async(resolve,id,level)=>{
|
|
||||||
try {
|
|
||||||
leftListLoading.value = true
|
|
||||||
let userData = []
|
|
||||||
let orgData = []
|
|
||||||
const resOrg = await getDirectories({level: 1,parent_uuid:id})
|
|
||||||
if(resOrg?.data){
|
|
||||||
orgData = resOrg.data
|
|
||||||
}
|
|
||||||
if(id){
|
|
||||||
const res = await getDirectoriesUsers(id,{directory_uuid:id})
|
|
||||||
userData = res.data
|
|
||||||
}
|
|
||||||
resolve([...orgData, ...userData])
|
|
||||||
leftListLoading.value = false
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
leftListLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理复选框选择变化
|
|
||||||
const handleCheckChange = (checkedNode, checkedNodesInfo) => {
|
|
||||||
// 获取所有选中的节点
|
|
||||||
const checkedKeys = treeRef.value.getCheckedKeys();
|
|
||||||
const halfCheckedKeys = treeRef.value.getHalfCheckedKeys();
|
|
||||||
|
|
||||||
// 获取所有选中节点数据
|
|
||||||
const allCheckedNodes = treeRef.value.getCheckedNodes(false, true);
|
|
||||||
|
|
||||||
// 筛选出人员节点(有 uid 的节点)且不在已加入用户列表中的
|
|
||||||
const userNodes = allCheckedNodes.filter(node =>
|
|
||||||
node.uid && !joinedUserIds.value.includes(node.uid)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 更新已选用户列表
|
|
||||||
selectedUsers.value = userNodes;
|
|
||||||
checkedNodes.value = allCheckedNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除已选用户
|
|
||||||
const removeSelectedUser = (user) => {
|
|
||||||
// 从已选列表中移除
|
|
||||||
const index = selectedUsers.value.findIndex(u => u.uid === user.uid);
|
|
||||||
if (index !== -1) {
|
|
||||||
selectedUsers.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新树的选中状态
|
|
||||||
if (treeRef.value) {
|
|
||||||
treeRef.value.setChecked(user.uid, false, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空所有选择
|
|
||||||
const clearAllSelected = () => {
|
|
||||||
selectedUsers.value = [];
|
|
||||||
checkedNodes.value = [];
|
|
||||||
if (treeRef.value) {
|
|
||||||
treeRef.value.setCheckedKeys([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索过滤
|
|
||||||
const handleSearch = () => {
|
|
||||||
if (treeRef.value) {
|
|
||||||
treeRef.value.filter(searchKeyword.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 节点过滤方法
|
|
||||||
const filterNode = (value, data) => {
|
|
||||||
if (!value) return true;
|
|
||||||
return data.name && data.name.toLowerCase().includes(value.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 暴露方法给父组件
|
|
||||||
defineExpose({
|
|
||||||
show,
|
|
||||||
getSelectedUsers: () => selectedUsers.value
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.drawer-content {
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-section {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-section {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding: 12px;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #606266;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-tags {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-tag {
|
|
||||||
max-width: 120px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 36px;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
.tree-item-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-icon, .dept-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item-text {
|
|
||||||
color: #333333;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-count {
|
|
||||||
margin-left: 6px;
|
|
||||||
color: #999999;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-tree-node__content) {
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-tree-node.is-current > .el-tree-node__content) {
|
|
||||||
background-color: #f0f7ff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,871 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogFormVisible"
|
|
||||||
:title="title"
|
|
||||||
width="80%"
|
|
||||||
:before-close="handleClose"
|
|
||||||
class="file-preview-dialog"
|
|
||||||
>
|
|
||||||
<div class="preview-container">
|
|
||||||
<!-- 加载状态 -->
|
|
||||||
<div v-if="loading" class="loading-container">
|
|
||||||
<el-icon class="is-loading" size="48">
|
|
||||||
<Loading />
|
|
||||||
</el-icon>
|
|
||||||
<p>{{ loadingText }}</p>
|
|
||||||
<!-- <p v-if="convertTaskId" class="task-id">任务ID: {{ convertTaskId }}</p> -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 转换状态 -->
|
|
||||||
<div v-else-if="converting" class="loading-container">
|
|
||||||
<el-icon class="is-loading" size="48">
|
|
||||||
<Loading />
|
|
||||||
</el-icon>
|
|
||||||
<p>{{ conversionMessage }}</p>
|
|
||||||
<!-- <p class="task-id">任务ID: {{ convertTaskId }}</p> -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 下载状态 -->
|
|
||||||
<div v-else-if="downloading" class="loading-container">
|
|
||||||
<el-icon class="is-loading" size="48">
|
|
||||||
<Loading />
|
|
||||||
</el-icon>
|
|
||||||
<p>正在下载文件,请稍候...</p>
|
|
||||||
<p class="download-progress" v-if="downloadProgress > 0">
|
|
||||||
下载进度: {{ downloadProgress }}%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 错误状态 -->
|
|
||||||
<div v-else-if="error" class="error-container">
|
|
||||||
<el-icon size="48" color="#F56C6C">
|
|
||||||
<CircleClose />
|
|
||||||
</el-icon>
|
|
||||||
<p>文件预览失败</p>
|
|
||||||
<p class="error-message">{{ errorMessage }}</p>
|
|
||||||
<el-button type="primary" @click="retryPreview">重试</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 文件预览内容 -->
|
|
||||||
<div v-else class="file-content">
|
|
||||||
<!-- 图片预览 -->
|
|
||||||
<div v-if="isImage" class="image-preview">
|
|
||||||
<img :src="previewUrl" :alt="fileName" @load="handleImageLoad" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PDF预览 - 使用 vue-pdf-embed -->
|
|
||||||
<div v-else-if="isPdf" class="pdf-preview">
|
|
||||||
<div class="pdf-controls" v-if="pageCount > 0">
|
|
||||||
<el-button-group>
|
|
||||||
<el-button :disabled="currentPage <= 1" @click="previousPage">
|
|
||||||
<el-icon><ArrowLeft /></el-icon>
|
|
||||||
上一页
|
|
||||||
</el-button>
|
|
||||||
<el-button>
|
|
||||||
{{ currentPage }} / {{ pageCount }}
|
|
||||||
</el-button>
|
|
||||||
<el-button :disabled="currentPage >= pageCount" @click="nextPage">
|
|
||||||
下一页
|
|
||||||
<el-icon><ArrowRight /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</el-button-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pdf-viewer-container">
|
|
||||||
<VuePdfEmbed
|
|
||||||
:source="pdfSource"
|
|
||||||
:page="currentPage"
|
|
||||||
:scale="scale"
|
|
||||||
@loaded="handlePdfLoaded"
|
|
||||||
@rendered="handlePdfRendered"
|
|
||||||
@error="handlePdfError"
|
|
||||||
class="pdf-viewer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 视频预览 -->
|
|
||||||
<div v-else-if="isVideo" class="video-preview">
|
|
||||||
<video controls :src="previewUrl" class="video-player">
|
|
||||||
您的浏览器不支持视频播放
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 音频预览 -->
|
|
||||||
<div v-else-if="isAudio" class="audio-preview">
|
|
||||||
<audio controls :src="previewUrl" class="audio-player">
|
|
||||||
您的浏览器不支持音频播放
|
|
||||||
</audio>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 文本预览 -->
|
|
||||||
<div v-else-if="isText" class="text-preview">
|
|
||||||
<pre>{{ textContent }}</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Office文档预览(转换后) -->
|
|
||||||
<div v-else-if="isConvertedOffice" class="office-preview">
|
|
||||||
<!-- 转换后的Office文件也是PDF,使用相同的PDF预览器 -->
|
|
||||||
<div class="pdf-controls" v-if="pageCount > 0">
|
|
||||||
<el-button-group>
|
|
||||||
<el-button :disabled="currentPage <= 1" @click="previousPage">
|
|
||||||
<el-icon><ArrowLeft /></el-icon>
|
|
||||||
上一页
|
|
||||||
</el-button>
|
|
||||||
<el-button>
|
|
||||||
{{ currentPage }} / {{ pageCount }}
|
|
||||||
</el-button>
|
|
||||||
<el-button :disabled="currentPage >= pageCount" @click="nextPage">
|
|
||||||
下一页
|
|
||||||
<el-icon><ArrowRight /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</el-button-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pdf-viewer-container">
|
|
||||||
<VuePdfEmbed
|
|
||||||
:source="pdfSource"
|
|
||||||
:page="currentPage"
|
|
||||||
:scale="scale"
|
|
||||||
@loaded="handlePdfLoaded"
|
|
||||||
@rendered="handlePdfRendered"
|
|
||||||
@error="handlePdfError"
|
|
||||||
class="pdf-viewer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 不支持预览的文件类型 -->
|
|
||||||
<div v-else class="unsupported-preview">
|
|
||||||
<el-icon size="64" color="#909399">
|
|
||||||
<Document />
|
|
||||||
</el-icon>
|
|
||||||
<p>不支持在线预览此文件类型</p>
|
|
||||||
<p class="file-name">{{ fileName }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 底部操作栏 -->
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button type="primary" @click="close">
|
|
||||||
关闭
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogFileVisible"
|
|
||||||
title="文件转换"
|
|
||||||
width="50%"
|
|
||||||
class="file-preview-dialog"
|
|
||||||
>
|
|
||||||
<div class="preview-container">
|
|
||||||
<div class="loading-container">
|
|
||||||
<el-icon class="is-loading" size="48">
|
|
||||||
<Loading />
|
|
||||||
</el-icon>
|
|
||||||
<p>正在下载文件,请稍候...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, getCurrentInstance, onUnmounted, nextTick, onMounted } from 'vue'
|
|
||||||
import { convertFileApi, getConvertStatusApi } from '@/api/conferencingRoom'
|
|
||||||
import { ElMessage ,ElMessageBox} from 'element-plus'
|
|
||||||
import {
|
|
||||||
Loading,
|
|
||||||
CircleClose,
|
|
||||||
Document,
|
|
||||||
ArrowLeft,
|
|
||||||
ArrowRight,
|
|
||||||
} from '@element-plus/icons-vue'
|
|
||||||
import VuePdfEmbed from 'vue-pdf-embed'
|
|
||||||
import { mqttClient } from "@/utils/mqtt.js";
|
|
||||||
import { emitter } from "@/utils/bus.js";
|
|
||||||
|
|
||||||
// 定义props
|
|
||||||
const props = defineProps({
|
|
||||||
fileType: {
|
|
||||||
type: Array,
|
|
||||||
default: () => ["pdf", "png", "jpg", "jpeg", "gif", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "mp4", "mp3"],
|
|
||||||
},
|
|
||||||
roomId: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义emits
|
|
||||||
const emit = defineEmits(['fetch-data'])
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
const enumType = ["doc", "docx", "xls", "xlsx", "ppt", "pptx"]
|
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const dialogFormVisible = ref(false)
|
|
||||||
const title = ref('')
|
|
||||||
const loading = ref(false)
|
|
||||||
const converting = ref(false)
|
|
||||||
const downloading = ref(false)
|
|
||||||
const downloadProgress = ref(0)
|
|
||||||
const loadingText = ref('正在加载...')
|
|
||||||
const conversionMessage = ref('正在转换文件...')
|
|
||||||
const error = ref(false)
|
|
||||||
const errorMessage = ref('')
|
|
||||||
const previewUrl = ref('')
|
|
||||||
const fileName = ref('')
|
|
||||||
const textContent = ref('')
|
|
||||||
const currentFileData = ref(null)
|
|
||||||
const convertTaskId = ref('')
|
|
||||||
const dialogFileVisible = ref(false)
|
|
||||||
|
|
||||||
|
|
||||||
// PDF相关状态
|
|
||||||
const pdfSource = ref('')
|
|
||||||
const currentPage = ref(1)
|
|
||||||
const pageCount = ref(0)
|
|
||||||
const scale = ref(1.0)
|
|
||||||
const pdfDocument = ref(null)
|
|
||||||
|
|
||||||
// MQTT相关
|
|
||||||
const isMqttConnected = ref(false)
|
|
||||||
|
|
||||||
|
|
||||||
// 计算属性
|
|
||||||
const isImage = computed(() => {
|
|
||||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
return ['png', 'jpg', 'jpeg', 'gif'].includes(ext)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isPdf = computed(() => {
|
|
||||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
return ext === 'pdf'
|
|
||||||
})
|
|
||||||
|
|
||||||
const isVideo = computed(() => {
|
|
||||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
return ['mp4'].includes(ext)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isAudio = computed(() => {
|
|
||||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
return ['mp3'].includes(ext)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isText = computed(() => {
|
|
||||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
return ext === 'txt'
|
|
||||||
})
|
|
||||||
|
|
||||||
const isOffice = computed(() => {
|
|
||||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
return enumType.includes(ext)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isConvertedOffice = computed(() => {
|
|
||||||
return previewUrl.value && previewUrl.value.endsWith('.pdf') && isOffice.value
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件挂载时初始化MQTT连接
|
|
||||||
onMounted(async () => {
|
|
||||||
})
|
|
||||||
emitter.on('subscribeToFileConversionStatusTopic',subscribeToFileConversionStatusTopic)
|
|
||||||
emitter.on('fileUploadStatus',fileUploadStatus)
|
|
||||||
emitter.on('subscribeToFilePreviewTopic',subscribeToFilePreviewTopic)
|
|
||||||
emitter.on('fileSuccess',fileSuccess)
|
|
||||||
|
|
||||||
function fileSuccess(){
|
|
||||||
dialogFileVisible.value = true
|
|
||||||
}
|
|
||||||
//
|
|
||||||
function subscribeToFileConversionStatusTopic(data){
|
|
||||||
try {
|
|
||||||
const userId = JSON.parse(sessionStorage.getItem('userData'))?.uid
|
|
||||||
if (!userId) {
|
|
||||||
console.error('用户ID不存在')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const topic = `xsynergy/room/${data.roomId}/file/${userId}/conversion_status`
|
|
||||||
mqttClient.subscribe(topic, handlePdfMessage)
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('订阅pdf转换事件失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribeToFilePreviewTopic(data){
|
|
||||||
try {
|
|
||||||
const topic = `xsynergy/room/${data.roomId}/file/preview`
|
|
||||||
mqttClient.subscribe(topic, handleFileUploadMessage)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('订阅文件上传事件失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//提交转换任务,但文件暂未转换完成
|
|
||||||
function fileUploadStatus(data){
|
|
||||||
// console.log('文件上传成功mqtt消息')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化MQTT连接 pdf转换成功
|
|
||||||
async function initMqttConnection() {
|
|
||||||
try {
|
|
||||||
if (isMqttConnected.value) return
|
|
||||||
const clientId = `PdfConversion_${Date.now()}`
|
|
||||||
await mqttClient.connect(clientId)
|
|
||||||
isMqttConnected.value = true
|
|
||||||
// 订阅主题
|
|
||||||
subscribeToPdfConversionTopic()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('MQTT连接失败:', error)
|
|
||||||
ElMessage.error('文件转换服务连接失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribeToPdfConversionTopic() {
|
|
||||||
try {
|
|
||||||
const userId = JSON.parse(sessionStorage.getItem('userData'))?.uid
|
|
||||||
if (!userId) {
|
|
||||||
console.error('用户ID不存在')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const topic = `xsynergy/room/${props.roomId}/file/${userId}/conversion_status`
|
|
||||||
mqttClient.subscribe(topic, handlePdfMessage)
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('订阅pdf转换事件失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFileUploadMessage(payload, topic){
|
|
||||||
try {
|
|
||||||
const messageStr = payload.toString()
|
|
||||||
const data = JSON.parse(messageStr)
|
|
||||||
// emitter.emit('fileUploadStatus')
|
|
||||||
console.log(data,'data文件转换完成,预览通知')
|
|
||||||
const userId = JSON.parse(sessionStorage.getItem('userData'))?.uid
|
|
||||||
if(dialogFileVisible.value){
|
|
||||||
dialogFileVisible.value = false
|
|
||||||
}
|
|
||||||
if(dialogFormVisible.value && userId != data.user_uid){
|
|
||||||
// 显示确认对话框
|
|
||||||
ElMessageBox.confirm(
|
|
||||||
`用户${data.user_uid}上传了${data.file_name}文件,是否立即预览?`,
|
|
||||||
'文件更新提示',
|
|
||||||
{
|
|
||||||
confirmButtonText: '预览',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
closeOnClickModal: false,
|
|
||||||
closeOnPressEscape: false,
|
|
||||||
showClose: false
|
|
||||||
}
|
|
||||||
).then(() => {
|
|
||||||
// 用户点击"预览"
|
|
||||||
resetPreviewState()
|
|
||||||
getPreviewFileUrl(data)
|
|
||||||
ElMessage({
|
|
||||||
message: '已切换到新文件预览',
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
}).catch(() => {
|
|
||||||
// 用户点击"取消"
|
|
||||||
ElMessage({
|
|
||||||
message: '已取消预览新文件,继续查看当前文件',
|
|
||||||
type: 'info'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
// resetPreviewState()
|
|
||||||
// getPreviewFileUrl(data)
|
|
||||||
} else {
|
|
||||||
ElMessage({
|
|
||||||
message: `用户${data.user_uid}上传了${data.file_name}文件`,
|
|
||||||
type: 'info',
|
|
||||||
})
|
|
||||||
showEdit(data)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('处理转换状态消息失败:', error)
|
|
||||||
handleError('处理转换状态失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增重置预览状态的方法
|
|
||||||
function resetPreviewState() {
|
|
||||||
loading.value = true
|
|
||||||
converting.value = false
|
|
||||||
downloading.value = false
|
|
||||||
downloadProgress.value = 0
|
|
||||||
error.value = false
|
|
||||||
previewUrl.value = ''
|
|
||||||
textContent.value = ''
|
|
||||||
resetPdfState()
|
|
||||||
|
|
||||||
// 强制重新渲染
|
|
||||||
nextTick(() => {
|
|
||||||
// 确保DOM更新
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePdfMessage(payload, topic){
|
|
||||||
try {
|
|
||||||
const messageStr = payload.toString()
|
|
||||||
const data = JSON.parse(messageStr)
|
|
||||||
switch (data.status) {
|
|
||||||
case 'converting':
|
|
||||||
break
|
|
||||||
case 'completed':
|
|
||||||
getConvertedFile(data.task_id)
|
|
||||||
break
|
|
||||||
case 'failed':
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
console.warn('未知的转换状态:', data.status)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('处理转换状态消息失败:', error)
|
|
||||||
handleError('处理转换状态失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取转换后的文件
|
|
||||||
const getConvertedFile = async (taskId) => {
|
|
||||||
try {
|
|
||||||
if (!taskId) {
|
|
||||||
throw new Error('任务ID不存在')
|
|
||||||
}
|
|
||||||
const fileRes = await getConvertStatusApi(taskId,props.roomId)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('获取转换文件失败:', err)
|
|
||||||
handleError('获取转换文件失败', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改 getPreviewFileUrl 方法,确保状态正确更新
|
|
||||||
async function getPreviewFileUrl(file){
|
|
||||||
fileName.value = file.file_name || file.source_url.split('/').pop()
|
|
||||||
const fileExt = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
try {
|
|
||||||
// 根据文件类型处理转换后的文件
|
|
||||||
if (fileExt === 'pdf' || enumType.includes(fileExt)) {
|
|
||||||
// PDF和Office文档使用PDF预览器
|
|
||||||
await loadPdfFile(file.preview_url || file.file_url)
|
|
||||||
} else if (fileExt === 'txt') {
|
|
||||||
// 文本文件
|
|
||||||
await loadTextFile(file.preview_url || file.file_url)
|
|
||||||
} else if (['png', 'jpg', 'jpeg', 'gif'].includes(fileExt)) {
|
|
||||||
// 图片文件直接预览
|
|
||||||
previewUrl.value = file.preview_url || file.file_url
|
|
||||||
// 确保图片重新加载
|
|
||||||
nextTick(() => {
|
|
||||||
loading.value = false
|
|
||||||
})
|
|
||||||
} else if (fileExt === 'mp4') {
|
|
||||||
// 视频文件
|
|
||||||
previewUrl.value = file.preview_url || file.file_url
|
|
||||||
nextTick(() => {
|
|
||||||
loading.value = false
|
|
||||||
})
|
|
||||||
} else if (fileExt === 'mp3') {
|
|
||||||
// 音频文件
|
|
||||||
previewUrl.value = file.preview_url || file.file_url
|
|
||||||
nextTick(() => {
|
|
||||||
loading.value = false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 其他文件类型
|
|
||||||
previewUrl.value = file.preview_url || file.file_url
|
|
||||||
nextTick(() => {
|
|
||||||
loading.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载预览文件失败:', error)
|
|
||||||
handleError('加载预览文件失败', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 显示弹框
|
|
||||||
const showEdit = async (data) => {
|
|
||||||
// 重置状态
|
|
||||||
resetPreviewState()
|
|
||||||
title.value = '文件预览'
|
|
||||||
dialogFormVisible.value = true
|
|
||||||
currentFileData.value = data
|
|
||||||
// fileName.value = data.file_name || data.source_url.split('/').pop()
|
|
||||||
|
|
||||||
await getPreviewFileUrl(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载PDF文件
|
|
||||||
const loadPdfFile = async (fileUrl) => {
|
|
||||||
try {
|
|
||||||
loading.value = false
|
|
||||||
downloading.value = true
|
|
||||||
downloadProgress.value = 0
|
|
||||||
|
|
||||||
const response = await fetch(fileUrl)
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('文件下载失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentLength = response.headers.get('content-length')
|
|
||||||
const total = parseInt(contentLength, 10)
|
|
||||||
let loaded = 0
|
|
||||||
const reader = response.body.getReader()
|
|
||||||
const chunks = []
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read()
|
|
||||||
if (done) break
|
|
||||||
chunks.push(value)
|
|
||||||
loaded += value.length
|
|
||||||
if (total) {
|
|
||||||
downloadProgress.value = Math.round((loaded / total) * 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob(chunks)
|
|
||||||
const blobUrl = URL.createObjectURL(blob)
|
|
||||||
|
|
||||||
// 先清理之前的URL
|
|
||||||
if (pdfSource.value) {
|
|
||||||
URL.revokeObjectURL(pdfSource.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
previewUrl.value = fileUrl
|
|
||||||
pdfSource.value = blobUrl
|
|
||||||
|
|
||||||
// 重置PDF状态
|
|
||||||
currentPage.value = 1
|
|
||||||
pageCount.value = 0
|
|
||||||
|
|
||||||
downloading.value = false
|
|
||||||
downloadProgress.value = 0
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
downloading.value = false
|
|
||||||
downloadProgress.value = 0
|
|
||||||
handleError('PDF文件下载失败', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载文本文件
|
|
||||||
const loadTextFile = async (fileUrl) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(fileUrl)
|
|
||||||
if (!response.ok) throw new Error('无法加载文本文件')
|
|
||||||
const text = await response.text()
|
|
||||||
textContent.value = text
|
|
||||||
loading.value = false
|
|
||||||
} catch (err) {
|
|
||||||
handleError('文本文件加载失败', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传文件获取office获取pdf文件
|
|
||||||
async function getFilePdf(fileUrl) {
|
|
||||||
try {
|
|
||||||
loadingText.value = '正在转换文件...'
|
|
||||||
const res = await convertFileApi({ file_url: fileUrl },props.roomId)
|
|
||||||
if (res.meta.code !== 200) {
|
|
||||||
throw new Error(res.meta.msg || '文件转换失败')
|
|
||||||
}
|
|
||||||
convertTaskId.value = res.data.task_id
|
|
||||||
// 等待MQTT消息,不进行轮询
|
|
||||||
loading.value = false
|
|
||||||
converting.value = true
|
|
||||||
conversionMessage.value = '已提交转换任务,等待处理...'
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
handleError('文件转换失败', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PDF相关方法
|
|
||||||
const handlePdfLoaded = (data) => {
|
|
||||||
pageCount.value = data.numPages
|
|
||||||
pdfDocument.value = data
|
|
||||||
loading.value = false
|
|
||||||
converting.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePdfRendered = () => {
|
|
||||||
// console.log('PDF页面渲染完成')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePdfError = (error) => {
|
|
||||||
console.error('PDF加载错误:', error)
|
|
||||||
handleError('PDF文件加载失败', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousPage = () => {
|
|
||||||
if (currentPage.value > 1) {
|
|
||||||
currentPage.value--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextPage = () => {
|
|
||||||
if (currentPage.value < pageCount.value) {
|
|
||||||
currentPage.value++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetPdfState = () => {
|
|
||||||
pdfSource.value = ''
|
|
||||||
currentPage.value = 1
|
|
||||||
pageCount.value = 0
|
|
||||||
scale.value = 1.0
|
|
||||||
pdfDocument.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理图片加载完成
|
|
||||||
const handleImageLoad = () => {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理错误
|
|
||||||
const handleError = (message, error) => {
|
|
||||||
error.value = true
|
|
||||||
errorMessage.value = message
|
|
||||||
loading.value = false
|
|
||||||
converting.value = false
|
|
||||||
downloading.value = false
|
|
||||||
downloadProgress.value = 0
|
|
||||||
ElMessage.error(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重试预览
|
|
||||||
const retryPreview = () => {
|
|
||||||
if (currentFileData.value) {
|
|
||||||
showEdit(currentFileData.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭按钮点击事件
|
|
||||||
const close = () => {
|
|
||||||
dialogFormVisible.value = false
|
|
||||||
resetState()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理对话框关闭
|
|
||||||
const handleClose = (done) => {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置状态
|
|
||||||
const resetState = () => {
|
|
||||||
loading.value = false
|
|
||||||
converting.value = false
|
|
||||||
downloading.value = false
|
|
||||||
downloadProgress.value = 0
|
|
||||||
error.value = false
|
|
||||||
previewUrl.value = ''
|
|
||||||
textContent.value = ''
|
|
||||||
currentFileData.value = null
|
|
||||||
convertTaskId.value = ''
|
|
||||||
resetPdfState()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时清理
|
|
||||||
onUnmounted(() => {
|
|
||||||
// 可以在这里取消特定主题的订阅,但不断开连接
|
|
||||||
// const userId = JSON.parse(sessionStorage.getItem('userData'))?.uid
|
|
||||||
// if (userId) {
|
|
||||||
// const topic = `xsynergy/room/${props.roomId}/file/${userId}/conversion_status`
|
|
||||||
// mqttClient.unsubscribe(topic, handlePdfMessage)
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
|
|
||||||
// 暴露方法给父组件
|
|
||||||
defineExpose({
|
|
||||||
showEdit,
|
|
||||||
close,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.file-preview-dialog {
|
|
||||||
.preview-container {
|
|
||||||
min-height: 400px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.loading-container, .error-container {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 0;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-top: 16px;
|
|
||||||
color: #606266;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #F56C6C;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-progress {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #409EFF;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.image-preview {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 70vh;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-preview, .office-preview {
|
|
||||||
.pdf-controls {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding: 8px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-viewer-container {
|
|
||||||
height: 70vh;
|
|
||||||
overflow: auto;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #f9f9f9;
|
|
||||||
|
|
||||||
.pdf-viewer {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
:deep(.vue-pdf-embed) {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(canvas) {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-preview {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.video-player {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 70vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-preview {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 0;
|
|
||||||
|
|
||||||
.audio-player {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-preview {
|
|
||||||
height: 70vh;
|
|
||||||
overflow: auto;
|
|
||||||
background: #f5f5f5;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.unsupported-preview {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 0;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-top: 16px;
|
|
||||||
color: #606266;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #909399;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.file-preview-dialog {
|
|
||||||
width: 95% !important;
|
|
||||||
|
|
||||||
.preview-container {
|
|
||||||
min-height: 300px;
|
|
||||||
|
|
||||||
.file-content {
|
|
||||||
.pdf-preview, .office-preview {
|
|
||||||
.pdf-viewer-container {
|
|
||||||
height: 50vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-preview {
|
|
||||||
height: 50vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-controls {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.el-button-group {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.el-button {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 8px 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-drawer v-model="drawerVisible" direction="rtl" title="文件列表" size="40%">
|
|
||||||
<template #header>
|
|
||||||
<h4>文件列表</h4>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="drawer-content">
|
|
||||||
<!-- 上传按钮 -->
|
|
||||||
<div class="upload-section">
|
|
||||||
<el-button type="primary" size="small" @click="handleUpload">
|
|
||||||
<el-icon><Upload /></el-icon>
|
|
||||||
上传文件
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 文件列表 -->
|
|
||||||
<div class="file-list" v-loading="loading">
|
|
||||||
<!-- 文件列表内容 -->
|
|
||||||
<div v-for="item in fileList" :key="item.id || item.fileKey" class="file-item">
|
|
||||||
<div class="file-info">
|
|
||||||
<div class="file-icon">
|
|
||||||
<img :src="getFileIcon(item.file_name)" alt="文件图标" class="file-icon-img">
|
|
||||||
</div>
|
|
||||||
<div class="file-details">
|
|
||||||
<div class="file-name" :title="item.file_name">{{ item.file_name }}</div>
|
|
||||||
<!-- <div class="file-meta">
|
|
||||||
<span class="file-size">{{ formatFileSize(item.fileSize) }}</span>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="file-actions">
|
|
||||||
<el-button type="primary" size="small" :disabled="!item.preview_url" @click="handlePreview(item)">预览</el-button>
|
|
||||||
<el-button type="success" size="small" @click="handleDownload(item)">下载</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 空状态 -->
|
|
||||||
<div v-if="fileList.length === 0" class="empty-state">
|
|
||||||
<el-empty description="暂无文件" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-drawer>
|
|
||||||
<!-- 文件上传 -->
|
|
||||||
<UpLoadFile
|
|
||||||
ref="uploadRef"
|
|
||||||
:fileType='["pdf", "png", "jpg", "jpeg","gif","doc","docx","xls","xlsx","ppt","pptx","txt","mp4","mp3"]'
|
|
||||||
:roomId="roomId"
|
|
||||||
@upload-success="handleUploadSuccess"
|
|
||||||
/>
|
|
||||||
<!-- 文件预览 -->
|
|
||||||
<BrowseFile ref="browseFileRef" :roomId="roomId"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
getFileListApi,
|
|
||||||
} from '@/api/conferencingRoom.js'
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { Upload } from '@element-plus/icons-vue'
|
|
||||||
import UpLoadFile from './upLoadFile.vue'
|
|
||||||
import BrowseFile from './browseFile.vue'
|
|
||||||
import fileLogo from '@/assets/images/file-logo.png';
|
|
||||||
import { emitter } from "@/utils/bus.js";
|
|
||||||
// 定义 emit
|
|
||||||
const emit = defineEmits([""]);
|
|
||||||
|
|
||||||
// 接收 props
|
|
||||||
const props = defineProps({
|
|
||||||
roomId: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.on('fileUploadStatus',async ()=>{
|
|
||||||
if(drawerVisible.value){
|
|
||||||
await getFileList()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const drawerVisible = ref(false);
|
|
||||||
const fileList = ref([]);
|
|
||||||
const loading = ref(false);
|
|
||||||
const uploadRef = ref(null);
|
|
||||||
|
|
||||||
//文件预览
|
|
||||||
const browseFileRef = ref(null);
|
|
||||||
|
|
||||||
// 根据文件扩展名获取文件图标
|
|
||||||
function getFileIcon(fileName) {
|
|
||||||
return fileLogo;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化文件大小
|
|
||||||
function formatFileSize(size) {
|
|
||||||
if (!size) return '未知大小';
|
|
||||||
|
|
||||||
if (size < 1024) {
|
|
||||||
return size + ' B';
|
|
||||||
} else if (size < 1024 * 1024) {
|
|
||||||
return (size / 1024).toFixed(2) + ' KB';
|
|
||||||
} else {
|
|
||||||
return (size / (1024 * 1024)).toFixed(2) + ' MB';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文件下载功能
|
|
||||||
function handleDownload(file) {
|
|
||||||
try {
|
|
||||||
const iframe = document.createElement('iframe');
|
|
||||||
iframe.style.display = 'none';
|
|
||||||
iframe.src = file.source_url;
|
|
||||||
document.body.appendChild(iframe);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.removeChild(iframe);
|
|
||||||
}, 5000); // 5秒后清理iframe
|
|
||||||
|
|
||||||
ElMessage.success('开始下载文件');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('iframe下载失败:', error);
|
|
||||||
ElMessage.error('下载失败,请检查浏览器设置');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文件预览功能
|
|
||||||
function handlePreview(file) {
|
|
||||||
if (!file.preview_url) {
|
|
||||||
ElMessage.error('文件链接无效');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
browseFileRef.value.showEdit(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传文件
|
|
||||||
function handleUpload() {
|
|
||||||
uploadRef.value.showEdit()
|
|
||||||
// 这里可以添加文件上传逻辑
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示抽屉
|
|
||||||
async function show() {
|
|
||||||
drawerVisible.value = true;
|
|
||||||
await getFileList()
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取文件列表
|
|
||||||
async function getFileList(){
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
// xsy
|
|
||||||
const res = await getFileListApi(props.roomId);
|
|
||||||
if (res.meta.code !== 200) {
|
|
||||||
ElMessage.error(res.meta.msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fileList.value = res.data.files || [];
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('获取文件列表失败:', error);
|
|
||||||
ElMessage.error('获取文件列表失败');
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//文件上传成功
|
|
||||||
async function handleUploadSuccess(){
|
|
||||||
if(drawerVisible.value){
|
|
||||||
await getFileList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 暴露方法给父组件
|
|
||||||
defineExpose({
|
|
||||||
show,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.drawer-content {
|
|
||||||
padding: 0 10px;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-section {
|
|
||||||
padding: 15px 0;
|
|
||||||
border-bottom: 1px solid #e8e8e8;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-list {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 0;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0; /* 防止内容溢出 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-icon {
|
|
||||||
margin-right: 12px;
|
|
||||||
flex-shrink: 0; /* 防止图标被压缩 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-icon-img {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-details {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0; /* 防止文本溢出 */
|
|
||||||
overflow: hidden; /* 隐藏溢出内容 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-meta {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-shrink: 0; /* 防止按钮被压缩 */
|
|
||||||
margin-left: 10px; /* 添加左边距,与文件信息保持距离 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-state {
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,726 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogFormVisible"
|
|
||||||
:title="title"
|
|
||||||
width="80%"
|
|
||||||
:before-close="handleClose"
|
|
||||||
class="file-preview-dialog"
|
|
||||||
>
|
|
||||||
<div class="preview-container">
|
|
||||||
<!-- 加载状态 -->
|
|
||||||
<div v-if="loading" class="loading-container">
|
|
||||||
<el-icon class="is-loading" size="48">
|
|
||||||
<Loading />
|
|
||||||
</el-icon>
|
|
||||||
<p>{{ loadingText }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 下载状态 -->
|
|
||||||
<div v-else-if="downloading" class="loading-container">
|
|
||||||
<el-icon class="is-loading" size="48">
|
|
||||||
<Loading />
|
|
||||||
</el-icon>
|
|
||||||
<p>正在下载文件,请稍候...</p>
|
|
||||||
<p class="download-progress" v-if="downloadProgress > 0">
|
|
||||||
下载进度: {{ downloadProgress }}%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 错误状态 -->
|
|
||||||
<div v-else-if="error" class="error-container">
|
|
||||||
<el-icon size="48" color="#F56C6C">
|
|
||||||
<CircleClose />
|
|
||||||
</el-icon>
|
|
||||||
<p>文件预览失败</p>
|
|
||||||
<p class="error-message">{{ errorMessage }}</p>
|
|
||||||
<el-button type="primary" @click="retryPreview">重试</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 文件预览内容 -->
|
|
||||||
<div v-else class="file-content">
|
|
||||||
<!-- 图片预览 -->
|
|
||||||
<div v-if="isImage" class="image-preview">
|
|
||||||
<img :src="previewUrl" :alt="fileName" @load="handleImageLoad" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PDF预览 - 使用 vue-pdf-embed -->
|
|
||||||
<div v-else-if="isPdf" class="pdf-preview">
|
|
||||||
<div class="pdf-controls" v-if="pageCount > 0">
|
|
||||||
<el-button-group>
|
|
||||||
<el-button :disabled="currentPage <= 1" @click="previousPage">
|
|
||||||
<el-icon><ArrowLeft /></el-icon>
|
|
||||||
上一页
|
|
||||||
</el-button>
|
|
||||||
<el-button>
|
|
||||||
{{ currentPage }} / {{ pageCount }}
|
|
||||||
</el-button>
|
|
||||||
<el-button :disabled="currentPage >= pageCount" @click="nextPage">
|
|
||||||
下一页
|
|
||||||
<el-icon><ArrowRight /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</el-button-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pdf-viewer-container">
|
|
||||||
<VuePdfEmbed
|
|
||||||
:source="pdfSource"
|
|
||||||
:page="currentPage"
|
|
||||||
:scale="scale"
|
|
||||||
@loaded="handlePdfLoaded"
|
|
||||||
@rendered="handlePdfRendered"
|
|
||||||
@error="handlePdfError"
|
|
||||||
class="pdf-viewer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 视频预览 -->
|
|
||||||
<div v-else-if="isVideo" class="video-preview">
|
|
||||||
<video controls :src="previewUrl" class="video-player">
|
|
||||||
您的浏览器不支持视频播放
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 音频预览 -->
|
|
||||||
<div v-else-if="isAudio" class="audio-preview">
|
|
||||||
<audio controls :src="previewUrl" class="audio-player">
|
|
||||||
您的浏览器不支持音频播放
|
|
||||||
</audio>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 文本预览 -->
|
|
||||||
<div v-else-if="isText" class="text-preview">
|
|
||||||
<pre>{{ textContent }}</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Office文档预览(转换后) -->
|
|
||||||
<div v-else-if="isConvertedOffice" class="office-preview">
|
|
||||||
<!-- 转换后的Office文件也是PDF,使用相同的PDF预览器 -->
|
|
||||||
<div class="pdf-controls" v-if="pageCount > 0">
|
|
||||||
<el-button-group>
|
|
||||||
<el-button :disabled="currentPage <= 1" @click="previousPage">
|
|
||||||
<el-icon><ArrowLeft /></el-icon>
|
|
||||||
上一页
|
|
||||||
</el-button>
|
|
||||||
<el-button>
|
|
||||||
{{ currentPage }} / {{ pageCount }}
|
|
||||||
</el-button>
|
|
||||||
<el-button :disabled="currentPage >= pageCount" @click="nextPage">
|
|
||||||
下一页
|
|
||||||
<el-icon><ArrowRight /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</el-button-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pdf-viewer-container">
|
|
||||||
<VuePdfEmbed
|
|
||||||
:source="pdfSource"
|
|
||||||
:page="currentPage"
|
|
||||||
:scale="scale"
|
|
||||||
@loaded="handlePdfLoaded"
|
|
||||||
@rendered="handlePdfRendered"
|
|
||||||
@error="handlePdfError"
|
|
||||||
class="pdf-viewer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 不支持预览的文件类型 -->
|
|
||||||
<div v-else class="unsupported-preview">
|
|
||||||
<el-icon size="64" color="#909399">
|
|
||||||
<Document />
|
|
||||||
</el-icon>
|
|
||||||
<p>不支持在线预览此文件类型</p>
|
|
||||||
<p class="file-name">{{ fileName }}</p>
|
|
||||||
<!-- <el-button type="primary" @click="downloadFile">下载文件</el-button> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 底部操作栏 -->
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<!-- <el-button @click="downloadFile" :disabled="loading">
|
|
||||||
<el-icon><Download /></el-icon>
|
|
||||||
下载
|
|
||||||
</el-button> -->
|
|
||||||
<el-button type="primary" @click="close">
|
|
||||||
关闭
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, getCurrentInstance, onUnmounted, nextTick } from 'vue'
|
|
||||||
import { convertFileApi, getConvertStatusApi } from '@/api/conferencingRoom'
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import {
|
|
||||||
Loading,
|
|
||||||
CircleClose,
|
|
||||||
Document,
|
|
||||||
Download,
|
|
||||||
ArrowLeft,
|
|
||||||
ArrowRight,
|
|
||||||
Plus,
|
|
||||||
Minus,
|
|
||||||
Refresh
|
|
||||||
} from '@element-plus/icons-vue'
|
|
||||||
import VuePdfEmbed from 'vue-pdf-embed'
|
|
||||||
|
|
||||||
// 定义props
|
|
||||||
const props = defineProps({
|
|
||||||
fileType: {
|
|
||||||
type: Array,
|
|
||||||
default: () => ["pdf", "png", "jpg", "jpeg", "gif", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "mp4", "mp3"],
|
|
||||||
},
|
|
||||||
roomId: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义emits
|
|
||||||
const emit = defineEmits(['fetch-data'])
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
const enumType = ["doc", "docx", "xls", "xlsx", "ppt", "pptx"]
|
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const dialogFormVisible = ref(false)
|
|
||||||
const title = ref('')
|
|
||||||
const loading = ref(false)
|
|
||||||
const downloading = ref(false) // 新增:下载状态
|
|
||||||
const downloadProgress = ref(0) // 新增:下载进度
|
|
||||||
const loadingText = ref('正在加载...')
|
|
||||||
const error = ref(false)
|
|
||||||
const errorMessage = ref('')
|
|
||||||
const previewUrl = ref('')
|
|
||||||
const fileName = ref('')
|
|
||||||
const textContent = ref('')
|
|
||||||
const currentFileData = ref(null)
|
|
||||||
const convertTaskId = ref('')
|
|
||||||
const statusCheckInterval = ref(null)
|
|
||||||
|
|
||||||
// PDF相关状态
|
|
||||||
const pdfSource = ref('')
|
|
||||||
const currentPage = ref(1)
|
|
||||||
const pageCount = ref(0)
|
|
||||||
const scale = ref(1.0)
|
|
||||||
const pdfDocument = ref(null)
|
|
||||||
|
|
||||||
// 计算属性
|
|
||||||
const isImage = computed(() => {
|
|
||||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
return ['png', 'jpg', 'jpeg', 'gif'].includes(ext)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isPdf = computed(() => {
|
|
||||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
return ext === 'pdf'
|
|
||||||
})
|
|
||||||
|
|
||||||
const isVideo = computed(() => {
|
|
||||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
return ['mp4'].includes(ext)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isAudio = computed(() => {
|
|
||||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
return ['mp3'].includes(ext)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isText = computed(() => {
|
|
||||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
return ext === 'txt'
|
|
||||||
})
|
|
||||||
|
|
||||||
const isOffice = computed(() => {
|
|
||||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
return enumType.includes(ext)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isConvertedOffice = computed(() => {
|
|
||||||
return previewUrl.value && previewUrl.value.endsWith('.pdf') && isOffice.value
|
|
||||||
})
|
|
||||||
|
|
||||||
// 显示弹框
|
|
||||||
const showEdit = async (data) => {
|
|
||||||
// 重置状态
|
|
||||||
loading.value = true
|
|
||||||
downloading.value = false
|
|
||||||
downloadProgress.value = 0
|
|
||||||
title.value = '文件预览'
|
|
||||||
dialogFormVisible.value = true
|
|
||||||
currentFileData.value = data
|
|
||||||
fileName.value = data.fileName || data.fileUrl.split('/').pop()
|
|
||||||
|
|
||||||
error.value = false
|
|
||||||
previewUrl.value = ''
|
|
||||||
textContent.value = ''
|
|
||||||
resetPdfState()
|
|
||||||
const fileExt = fileName.value.split('.').pop().toLowerCase()
|
|
||||||
try {
|
|
||||||
if (enumType.includes(fileExt)) {
|
|
||||||
// Office文档需要转换
|
|
||||||
await getFilePdf(data.fileUrl)
|
|
||||||
} else if (fileExt === 'txt') {
|
|
||||||
// 文本文件需要特殊处理
|
|
||||||
await loadTextFile(data.fileUrl)
|
|
||||||
} else if (fileExt === 'pdf') {
|
|
||||||
// PDF文件使用vue-pdf-embed预览
|
|
||||||
await loadPdfFile(data.fileUrl)
|
|
||||||
} else {
|
|
||||||
// 其他文件直接预览
|
|
||||||
previewUrl.value = data.fileUrl
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
handleError('文件加载失败', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载PDF文件
|
|
||||||
const loadPdfFile = async (fileUrl) => {
|
|
||||||
try {
|
|
||||||
// 先显示下载状态
|
|
||||||
loading.value = false
|
|
||||||
downloading.value = true
|
|
||||||
downloadProgress.value = 0
|
|
||||||
|
|
||||||
// 使用fetch下载文件并跟踪进度
|
|
||||||
const response = await fetch(fileUrl)
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('文件下载失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentLength = response.headers.get('content-length')
|
|
||||||
const total = parseInt(contentLength, 10)
|
|
||||||
let loaded = 0
|
|
||||||
|
|
||||||
// 创建读取器
|
|
||||||
const reader = response.body.getReader()
|
|
||||||
const chunks = []
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read()
|
|
||||||
|
|
||||||
if (done) break
|
|
||||||
|
|
||||||
chunks.push(value)
|
|
||||||
loaded += value.length
|
|
||||||
|
|
||||||
// 更新下载进度
|
|
||||||
if (total) {
|
|
||||||
downloadProgress.value = Math.round((loaded / total) * 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建Blob URL
|
|
||||||
const blob = new Blob(chunks)
|
|
||||||
const blobUrl = URL.createObjectURL(blob)
|
|
||||||
|
|
||||||
// 设置PDF源
|
|
||||||
previewUrl.value = fileUrl
|
|
||||||
pdfSource.value = blobUrl
|
|
||||||
|
|
||||||
// 重置状态
|
|
||||||
downloading.value = false
|
|
||||||
downloadProgress.value = 0
|
|
||||||
|
|
||||||
// 使用nextTick确保DOM更新
|
|
||||||
nextTick(() => {
|
|
||||||
// PDF组件会自动开始加载,loading状态会在handlePdfLoaded中设置为false
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
downloading.value = false
|
|
||||||
downloadProgress.value = 0
|
|
||||||
handleError('PDF文件下载失败', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载文本文件
|
|
||||||
const loadTextFile = async (fileUrl) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(fileUrl)
|
|
||||||
if (!response.ok) throw new Error('无法加载文本文件')
|
|
||||||
const text = await response.text()
|
|
||||||
textContent.value = text
|
|
||||||
loading.value = false
|
|
||||||
} catch (err) {
|
|
||||||
handleError('文本文件加载失败', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传文件获取office获取pdf文件
|
|
||||||
async function getFilePdf(fileUrl) {
|
|
||||||
try {
|
|
||||||
loadingText.value = '正在转换文件...'
|
|
||||||
const res = await convertFileApi({ file_url: fileUrl })
|
|
||||||
|
|
||||||
if (res.meta.code !== 200) {
|
|
||||||
throw new Error(res.meta.msg || '文件转换失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
convertTaskId.value = res.data.task_id
|
|
||||||
loadingText.value = '正在等待转换完成...'
|
|
||||||
|
|
||||||
// 开始轮询转换状态
|
|
||||||
startStatusPolling()
|
|
||||||
} catch (err) {
|
|
||||||
handleError('文件转换失败', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始轮询转换状态
|
|
||||||
const startStatusPolling = () => {
|
|
||||||
// 清除之前的轮询
|
|
||||||
if (statusCheckInterval.value) {
|
|
||||||
clearInterval(statusCheckInterval.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置新的轮询
|
|
||||||
statusCheckInterval.value = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const fileRes = await getConvertStatusApi(convertTaskId.value)
|
|
||||||
|
|
||||||
if (fileRes.meta.code === 200) {
|
|
||||||
if (fileRes.data.status === 'completed') {
|
|
||||||
// 转换完成
|
|
||||||
clearInterval(statusCheckInterval.value)
|
|
||||||
previewUrl.value = fileRes.data.output_file
|
|
||||||
pdfSource.value = fileRes.data.output_file
|
|
||||||
loading.value = false
|
|
||||||
} else if (fileRes.data.status === 'failed') {
|
|
||||||
// 转换失败
|
|
||||||
clearInterval(statusCheckInterval.value)
|
|
||||||
throw new Error('文件转换失败')
|
|
||||||
}
|
|
||||||
// 其他状态继续等待
|
|
||||||
} else {
|
|
||||||
throw new Error(fileRes.meta.msg || '获取转换状态失败')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
clearInterval(statusCheckInterval.value)
|
|
||||||
handleError('获取转换状态失败', err)
|
|
||||||
}
|
|
||||||
}, 2000) // 每2秒检查一次
|
|
||||||
}
|
|
||||||
|
|
||||||
// PDF相关方法
|
|
||||||
const handlePdfLoaded = (data) => {
|
|
||||||
pageCount.value = data.numPages
|
|
||||||
pdfDocument.value = data
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePdfRendered = () => {
|
|
||||||
// console.log('PDF页面渲染完成')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePdfError = (error) => {
|
|
||||||
console.error('PDF加载错误:', error)
|
|
||||||
handleError('PDF文件加载失败', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousPage = () => {
|
|
||||||
if (currentPage.value > 1) {
|
|
||||||
currentPage.value--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextPage = () => {
|
|
||||||
if (currentPage.value < pageCount.value) {
|
|
||||||
currentPage.value++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const zoomIn = () => {
|
|
||||||
if (scale.value < 2) {
|
|
||||||
scale.value = Math.round((scale.value + 0.1) * 10) / 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const zoomOut = () => {
|
|
||||||
if (scale.value > 0.5) {
|
|
||||||
scale.value = Math.round((scale.value - 0.1) * 10) / 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetZoom = () => {
|
|
||||||
scale.value = 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetPdfState = () => {
|
|
||||||
pdfSource.value = ''
|
|
||||||
currentPage.value = 1
|
|
||||||
pageCount.value = 0
|
|
||||||
scale.value = 1.0
|
|
||||||
pdfDocument.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理图片加载完成
|
|
||||||
const handleImageLoad = () => {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理错误
|
|
||||||
const handleError = (message, error) => {
|
|
||||||
console.error('File preview error:', error)
|
|
||||||
error.value = true
|
|
||||||
errorMessage.value = message
|
|
||||||
loading.value = false
|
|
||||||
downloading.value = false
|
|
||||||
downloadProgress.value = 0
|
|
||||||
ElMessage.error(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重试预览
|
|
||||||
const retryPreview = () => {
|
|
||||||
if (currentFileData.value) {
|
|
||||||
showEdit(currentFileData.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载文件
|
|
||||||
const downloadFile = () => {
|
|
||||||
if (currentFileData.value && currentFileData.value.fileUrl) {
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = currentFileData.value.fileUrl
|
|
||||||
link.download = fileName.value
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
} else {
|
|
||||||
ElMessage.warning('无法下载文件')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭按钮点击事件
|
|
||||||
const close = () => {
|
|
||||||
dialogFormVisible.value = false
|
|
||||||
resetState()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理对话框关闭
|
|
||||||
const handleClose = (done) => {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置状态
|
|
||||||
const resetState = () => {
|
|
||||||
// 清除轮询
|
|
||||||
if (statusCheckInterval.value) {
|
|
||||||
clearInterval(statusCheckInterval.value)
|
|
||||||
statusCheckInterval.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置其他状态
|
|
||||||
loading.value = false
|
|
||||||
downloading.value = false
|
|
||||||
downloadProgress.value = 0
|
|
||||||
error.value = false
|
|
||||||
previewUrl.value = ''
|
|
||||||
textContent.value = ''
|
|
||||||
currentFileData.value = null
|
|
||||||
convertTaskId.value = ''
|
|
||||||
resetPdfState()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时清理
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (statusCheckInterval.value) {
|
|
||||||
clearInterval(statusCheckInterval.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 暴露方法给父组件
|
|
||||||
defineExpose({
|
|
||||||
showEdit,
|
|
||||||
close,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.file-preview-dialog {
|
|
||||||
.preview-container {
|
|
||||||
min-height: 400px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.loading-container, .error-container {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 0;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-top: 16px;
|
|
||||||
color: #606266;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #F56C6C;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-progress {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #409EFF;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.image-preview {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 70vh;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-preview, .office-preview {
|
|
||||||
.pdf-controls {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding: 8px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-viewer-container {
|
|
||||||
height: 70vh;
|
|
||||||
overflow: auto;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #f9f9f9;
|
|
||||||
|
|
||||||
.pdf-viewer {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
:deep(.vue-pdf-embed) {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(canvas) {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-preview {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.video-player {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 70vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-preview {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 0;
|
|
||||||
|
|
||||||
.audio-player {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-preview {
|
|
||||||
height: 70vh;
|
|
||||||
overflow: auto;
|
|
||||||
background: #f5f5f5;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.unsupported-preview {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 0;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-top: 16px;
|
|
||||||
color: #606266;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #909399;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.file-preview-dialog {
|
|
||||||
width: 95% !important;
|
|
||||||
|
|
||||||
.preview-container {
|
|
||||||
min-height: 300px;
|
|
||||||
|
|
||||||
.file-content {
|
|
||||||
.pdf-preview, .office-preview {
|
|
||||||
.pdf-viewer-container {
|
|
||||||
height: 50vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-preview {
|
|
||||||
height: 50vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-controls {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.el-button-group {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.el-button {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 8px 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogFormVisible"
|
|
||||||
:title="title"
|
|
||||||
width="403px"
|
|
||||||
@close="close"
|
|
||||||
>
|
|
||||||
<el-upload
|
|
||||||
ref="uploadRef"
|
|
||||||
:accept="acceptString"
|
|
||||||
:show-file-list="false"
|
|
||||||
:limit="999"
|
|
||||||
style="width: 100%; text-align: center"
|
|
||||||
:before-upload="handleBeforeUpload"
|
|
||||||
>
|
|
||||||
<template #trigger>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
class="el-button-custom-css blue-css"
|
|
||||||
:loading="uploadLoading"
|
|
||||||
:disabled="uploadLoading"
|
|
||||||
>
|
|
||||||
{{ uploadLoading ? '上传中...' : '上传文件' }}
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-upload>
|
|
||||||
<div style="margin-top: 20px">
|
|
||||||
请上传格式为:
|
|
||||||
<b style="color: #f56c6c">
|
|
||||||
{{ acceptString }}
|
|
||||||
</b>
|
|
||||||
的文件
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, getCurrentInstance } from 'vue'
|
|
||||||
import axios from 'axios'
|
|
||||||
import { getUploadTokenApi, uploadFileApi,convertFileApi } from '@/api/conferencingRoom'
|
|
||||||
import { calculateFileSHA1 } from '@/views/conferencingRoom/business/index.js'
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import { mqttClient } from "@/utils/mqtt.js";
|
|
||||||
import { emitter } from "@/utils/bus.js";
|
|
||||||
// 定义props
|
|
||||||
const props = defineProps({
|
|
||||||
fileType: {
|
|
||||||
type: Array,
|
|
||||||
default: () => ["pdf", "png", "jpg", "jpeg", "gif", "doc", "docx", "xls", "xlsx", "ppt", "pptx"],
|
|
||||||
},
|
|
||||||
roomId: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义emits
|
|
||||||
const emit = defineEmits(['upload-success'])
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const dialogFormVisible = ref(false)
|
|
||||||
const title = ref('')
|
|
||||||
const fileList = ref([])
|
|
||||||
const fileIds = ref([])
|
|
||||||
const uploadRef = ref(null)
|
|
||||||
const showList = ref(true)
|
|
||||||
const saveLoading = ref(false)
|
|
||||||
|
|
||||||
const uploadToken = ref('')
|
|
||||||
const fileUrl = ref('')
|
|
||||||
const currentUploadFile = ref(null) // 存储当前要上传的文件
|
|
||||||
const uploadLoading = ref(false) // 上传loading状态
|
|
||||||
|
|
||||||
const roomId = ref()
|
|
||||||
const uploaderInfo = ref('')
|
|
||||||
|
|
||||||
// 计算属性:将文件类型数组转换为accept字符串
|
|
||||||
const acceptString = computed(() => {
|
|
||||||
return props.fileType.map(type => `.${type}`).join(', ')
|
|
||||||
})
|
|
||||||
|
|
||||||
emitter.on('subscribeToFileUploadTopic',subscribeToFileUploadTopic)
|
|
||||||
|
|
||||||
|
|
||||||
function subscribeToFileUploadTopic(data){
|
|
||||||
try {
|
|
||||||
// 订阅文件上传状态主题
|
|
||||||
roomId.value = data.roomId
|
|
||||||
const topic = `xsynergy/room/${data.roomId}/file/upload`
|
|
||||||
mqttClient.subscribe(topic, handleFileUploadMessage)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('订阅文件上传事件失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//订阅文件上传状态主题
|
|
||||||
function handleFileUploadMessage(payload, topic){
|
|
||||||
try {
|
|
||||||
const messageStr = payload.toString()
|
|
||||||
const data = JSON.parse(messageStr)
|
|
||||||
emitter.emit('fileUploadStatus',data)
|
|
||||||
emit('upload-success')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('文件长传状态消息失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传前校检格式和大小
|
|
||||||
const handleBeforeUpload = async (file) => {
|
|
||||||
// 如果正在上传中,阻止新文件上传
|
|
||||||
if (uploadLoading.value) {
|
|
||||||
ElMessage.warning('文件正在上传中,请稍候...')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文件扩展名
|
|
||||||
const fileExtension = file.name.toLowerCase().slice(((file.name.lastIndexOf(".") - 1) >>> 0) + 2)
|
|
||||||
// 校验文件格式
|
|
||||||
if (!props.fileType.includes(fileExtension)) {
|
|
||||||
ElMessage.error(`文件格式不支持,请上传 ${acceptString.value} 格式的文件`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校检文件大小
|
|
||||||
const isLt = file.size / 1024 / 1024 < 50
|
|
||||||
if (!isLt) {
|
|
||||||
ElMessage.error(`上传文件大小不能超过 50 MB!`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始上传,设置loading状态
|
|
||||||
uploadLoading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 保存当前文件引用
|
|
||||||
currentUploadFile.value = file
|
|
||||||
|
|
||||||
// 计算文件SHA1
|
|
||||||
const sha1 = await calculateFileSHA1(file)
|
|
||||||
|
|
||||||
// 获取上传token
|
|
||||||
const res = await getUploadTokenApi({
|
|
||||||
service: props.roomId,
|
|
||||||
hash: sha1,
|
|
||||||
ext: fileExtension,
|
|
||||||
})
|
|
||||||
|
|
||||||
if(res.meta.code != 200){
|
|
||||||
ElMessage.error(res.meta.msg)
|
|
||||||
uploadLoading.value = false // 出错时取消loading
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if(res.data.exists){
|
|
||||||
// 文件已存在,直接获取文件URL
|
|
||||||
fileUrl.value = res.data.fileUrl
|
|
||||||
ElMessage.info('文件已存在,无需重复上传')
|
|
||||||
// dialogFormVisible.value = false
|
|
||||||
uploadLoading.value = false
|
|
||||||
} else {
|
|
||||||
// 文件不存在,获取token并执行上传
|
|
||||||
uploadToken.value = res.data.token
|
|
||||||
// 执行上传操作
|
|
||||||
await handleHttpRequest(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('上传过程出错:', error)
|
|
||||||
ElMessage.error('上传过程出错,请重试')
|
|
||||||
uploadLoading.value = false // 出错时取消loading
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return false // 阻止默认上传行为,因为我们使用自定义上传
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义上传
|
|
||||||
const handleHttpRequest = async (file) => {
|
|
||||||
if (!uploadToken.value) {
|
|
||||||
ElMessage.error('上传凭证不存在')
|
|
||||||
uploadLoading.value = false // 取消loading
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let params = new FormData()
|
|
||||||
params.append('file', file)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await uploadFileApi(uploadToken.value, params)
|
|
||||||
if(res.meta.code != 200){
|
|
||||||
ElMessage.error(res.meta.msg)
|
|
||||||
uploadLoading.value = false // 出错时取消loading
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
fileUrl.value = res.data.fileUrl
|
|
||||||
getFileTaskId(res.data.fileUrl)
|
|
||||||
ElMessage.success('文件上传成功')
|
|
||||||
// publishFileUploadData(file);
|
|
||||||
|
|
||||||
// 上传成功,取消loading
|
|
||||||
uploadLoading.value = false
|
|
||||||
dialogFormVisible.value = false
|
|
||||||
|
|
||||||
emitter.emit('fileSuccess')
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
console.error('上传文件失败:', error)
|
|
||||||
ElMessage.error('上传文件失败')
|
|
||||||
uploadLoading.value = false // 出错时取消loading
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function getFileTaskId(fileUrl) {
|
|
||||||
try {
|
|
||||||
const res = await convertFileApi({ file_url: fileUrl },props.roomId)
|
|
||||||
if (res.meta.code !== 200) {
|
|
||||||
throw new Error(res.meta.msg || '文件转换失败')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
ElMessage.error('文件转换失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function publishFileUploadData(fileData) {
|
|
||||||
try {
|
|
||||||
const message = {
|
|
||||||
uploaderName: uploaderInfo.value?.name || '',
|
|
||||||
uploaderUid: uploaderInfo.value?.uid || '',
|
|
||||||
fileName: fileData.name,
|
|
||||||
fileUrl: fileUrl.value,
|
|
||||||
roomId: roomId.value,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
mqttClient.publish(`xSynergy/File/Upload/${roomId.value}`, message);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('发布文件数据失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传前校检格式和大小
|
|
||||||
const beforeUploadser = (file) => {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示弹框
|
|
||||||
const showEdit = () => {
|
|
||||||
title.value = '上传文件'
|
|
||||||
dialogFormVisible.value = true
|
|
||||||
uploaderInfo.value = JSON.parse(sessionStorage.getItem('userData'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭按钮点击事件
|
|
||||||
const close = () => {
|
|
||||||
fileList.value = []
|
|
||||||
currentUploadFile.value = null
|
|
||||||
uploadToken.value = ''
|
|
||||||
fileUrl.value = ''
|
|
||||||
uploadLoading.value = false // 关闭时重置loading状态
|
|
||||||
dialogFormVisible.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存按钮点击事件
|
|
||||||
const save = () => {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 暴露方法给父组件
|
|
||||||
defineExpose({
|
|
||||||
showEdit,
|
|
||||||
close,
|
|
||||||
save
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.avatar-uploader {
|
|
||||||
.el-upload {
|
|
||||||
width: 345px;
|
|
||||||
height: 180px;
|
|
||||||
}
|
|
||||||
.el-upload-list__item-thumbnail {
|
|
||||||
height: 180px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 765px) {
|
|
||||||
.el-dialog {
|
|
||||||
width: 80% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import {
|
|
||||||
ElNotification,
|
|
||||||
ElMessageBox,
|
|
||||||
ElMessage,
|
|
||||||
} from "element-plus";
|
|
||||||
import { tansParams } from "@/utils/ruoyi";
|
|
||||||
import cache from "@/plugins/cache";
|
|
||||||
import { getToken, removeToken } from "@/utils/auth";
|
|
||||||
import router from '@/router';
|
|
||||||
import { useMeterStore } from '@/stores/modules/meter'
|
|
||||||
|
|
||||||
axios.defaults.headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
||||||
const meterStore = useMeterStore()
|
|
||||||
meterStore.initUdid()
|
|
||||||
// 创建axios实例
|
|
||||||
const service = axios.create({
|
|
||||||
// axios中请求配置有baseURL选项,表示请求URL公共部分
|
|
||||||
baseURL: import.meta.env.VITE_APP_BASE_API_livekit,
|
|
||||||
// 超时
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
// request拦截器
|
|
||||||
service.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
// 是否需要防止数据重复提交
|
|
||||||
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false;
|
|
||||||
if (meterStore.getSudid()) {
|
|
||||||
config.headers["X-User-Agent"] = `gxtech/web 1.0.0: c=GxTech, udid=${meterStore.getSudid()}, sv=15.4.1, app=stt`;
|
|
||||||
}
|
|
||||||
// get请求映射params参数
|
|
||||||
if (config.method === "get" && config.params) {
|
|
||||||
let url = config.url + "?" + tansParams(config.params);
|
|
||||||
url = url.slice(0, -1);
|
|
||||||
config.params = {};
|
|
||||||
config.url = url;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!isRepeatSubmit &&
|
|
||||||
(config.method === "post" || config.method === "put")
|
|
||||||
) {
|
|
||||||
const requestObj = {
|
|
||||||
url: config.url,
|
|
||||||
data:
|
|
||||||
typeof config.data === "object"
|
|
||||||
? JSON.stringify(config.data)
|
|
||||||
: config.data,
|
|
||||||
time: new Date().getTime(),
|
|
||||||
};
|
|
||||||
const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小
|
|
||||||
const limitSize = 5 * 1024 * 1024; // 限制存放数据5M
|
|
||||||
if (requestSize >= limitSize) {
|
|
||||||
console.warn(
|
|
||||||
`[${config.url}]: ` +
|
|
||||||
"请求数据大小超出允许的5M限制,无法进行防重复提交验证。"
|
|
||||||
);
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
const sessionObj = cache.session.getJSON("sessionObj");
|
|
||||||
if (
|
|
||||||
sessionObj === undefined ||
|
|
||||||
sessionObj === null ||
|
|
||||||
sessionObj === ""
|
|
||||||
) {
|
|
||||||
cache.session.setJSON("sessionObj", requestObj);
|
|
||||||
} else {
|
|
||||||
const s_url = sessionObj.url; // 请求地址
|
|
||||||
const s_data = sessionObj.data; // 请求数据
|
|
||||||
const s_time = sessionObj.time; // 请求时间
|
|
||||||
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
|
|
||||||
if (
|
|
||||||
s_data === requestObj.data &&
|
|
||||||
requestObj.time - s_time < interval &&
|
|
||||||
s_url === requestObj.url
|
|
||||||
) {
|
|
||||||
const message = "数据正在处理,请勿重复提交";
|
|
||||||
console.warn(`[${s_url}]: ` + message);
|
|
||||||
return Promise.reject(new Error(message));
|
|
||||||
} else {
|
|
||||||
cache.session.setJSON("sessionObj", requestObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
service.interceptors.response.use(
|
|
||||||
(response) => {
|
|
||||||
// 1. 检查响应是否存在
|
|
||||||
if (!response) {
|
|
||||||
ElMessage.error('无响应数据');
|
|
||||||
return Promise.reject(new Error('无响应数据'));
|
|
||||||
}
|
|
||||||
// 2. 安全获取响应数据和状态码
|
|
||||||
const responseData = response.data || {};
|
|
||||||
const statusCode = response.status;
|
|
||||||
const businessCode = responseData.meta?.code || statusCode;
|
|
||||||
|
|
||||||
// 3. 二进制数据直接返回
|
|
||||||
if (
|
|
||||||
response.request.responseType === 'blob' ||
|
|
||||||
response.request.responseType === 'arraybuffer'
|
|
||||||
) {
|
|
||||||
return responseData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 根据业务码处理不同情况
|
|
||||||
switch (businessCode) {
|
|
||||||
case 200:
|
|
||||||
case 201:
|
|
||||||
return Promise.resolve(responseData);
|
|
||||||
|
|
||||||
case 401:
|
|
||||||
console.log('未授权', responseData)
|
|
||||||
return Promise.resolve(responseData);
|
|
||||||
// return handleUnauthorized().then(() => {
|
|
||||||
// return Promise.reject({ code: 401, message: '未授权' });
|
|
||||||
// });
|
|
||||||
|
|
||||||
case 500:
|
|
||||||
const serverErrorMsg = responseData.meta?.message || '服务器内部错误';
|
|
||||||
ElMessage({ message: serverErrorMsg, type: 'error' });
|
|
||||||
return Promise.reject({ code: 500, message: serverErrorMsg });
|
|
||||||
|
|
||||||
default:
|
|
||||||
const errorMsg = responseData.meta?.message || `业务错误 (${businessCode})`;
|
|
||||||
ElNotification.error({ title: errorMsg });
|
|
||||||
return Promise.reject({ code: businessCode, message: errorMsg });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
let { message } = error;
|
|
||||||
let code = error?.response?.status || -1;
|
|
||||||
if (message == 'Network Error') {
|
|
||||||
message = '后端接口连接异常';
|
|
||||||
ElMessage({ message, type: 'error', duration: 5 * 1000 });
|
|
||||||
} else if (message.includes('timeout')) {
|
|
||||||
message = '系统接口请求超时';
|
|
||||||
ElMessage({ message, type: 'error', duration: 5 * 1000 });
|
|
||||||
} else if (message.includes('Request failed with status code')) {
|
|
||||||
// message = '系统接口' + message.substr(message.length - 3) + '异常';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回结构化错误
|
|
||||||
return Promise.reject({
|
|
||||||
code,
|
|
||||||
message,
|
|
||||||
raw: error // 保留原始 error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 单独处理401未授权
|
|
||||||
function handleUnauthorized() {
|
|
||||||
return ElMessageBox.confirm(
|
|
||||||
'认证信息已失效,您可以继续留在该页面,或者重新登录',
|
|
||||||
'系统提示',
|
|
||||||
{
|
|
||||||
confirmButtonText: '重新登录',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
removeToken()
|
|
||||||
if (router.currentRoute.path !== '/login') {
|
|
||||||
router.push({
|
|
||||||
path: '/login',
|
|
||||||
query: { redirect: router.currentRoute.fullPath }
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 如果在登录页,强制刷新以清除残留状态
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return Promise.reject('用户取消操作');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default service;
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogFormVisible"
|
|
||||||
width="80%"
|
|
||||||
:show-close="false"
|
|
||||||
:destroy-on-close="true"
|
|
||||||
:modal="false"
|
|
||||||
:lock-scroll="true"
|
|
||||||
:before-close="handleClose"
|
|
||||||
class="call-dialog"
|
|
||||||
>
|
|
||||||
<div class="call-wrapper">
|
|
||||||
<!-- 上方头像与状态 -->
|
|
||||||
<div class="avatar-section">
|
|
||||||
<img class="avatar" :src="avatarUrl" alt="头像" />
|
|
||||||
<div class="user-name">{{ userName }}</div>
|
|
||||||
<div class="status-text">{{ statusText }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 底部操作按钮 -->
|
|
||||||
<div class="control-section">
|
|
||||||
<!-- 呼叫模式 -->
|
|
||||||
<template v-if="mode === 'call'">
|
|
||||||
<el-button
|
|
||||||
v-if="callStatus === 'calling'"
|
|
||||||
type="danger"
|
|
||||||
round
|
|
||||||
class="control-btn hangup"
|
|
||||||
@click="hangup"
|
|
||||||
>
|
|
||||||
挂断
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-else
|
|
||||||
type="primary"
|
|
||||||
round
|
|
||||||
class="control-btn call"
|
|
||||||
@click="startCall"
|
|
||||||
>
|
|
||||||
呼叫中...
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 接听模式 -->
|
|
||||||
<template v-else>
|
|
||||||
<el-button
|
|
||||||
type="success"
|
|
||||||
round
|
|
||||||
class="control-btn accept"
|
|
||||||
@click="acceptCall"
|
|
||||||
>
|
|
||||||
接听
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
type="danger"
|
|
||||||
round
|
|
||||||
class="control-btn hangup"
|
|
||||||
@click="hangup"
|
|
||||||
>
|
|
||||||
挂断
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed } from "vue";
|
|
||||||
import { ElMessage } from "element-plus";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
mode: { type: String, default: "call" }, // call: 呼叫模式, receive: 接听模式
|
|
||||||
avatarUrl: {
|
|
||||||
type: String,
|
|
||||||
default: "https://cdn-icons-png.flaticon.com/512/1946/1946429.png",
|
|
||||||
},
|
|
||||||
userName: { type: String, default: "对方用户" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const dialogFormVisible = ref(false);
|
|
||||||
const callStatus = ref("calling"); // calling | active | ended
|
|
||||||
|
|
||||||
const statusText = computed(() => {
|
|
||||||
if (props.mode === "call") {
|
|
||||||
if (callStatus.value === "calling") return "正在呼叫对方...";
|
|
||||||
if (callStatus.value === "active") return "通话中";
|
|
||||||
return "通话结束";
|
|
||||||
} else {
|
|
||||||
return callStatus.value === "active" ? "通话中" : "对方来电...";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function startCall() {
|
|
||||||
callStatus.value = "active";
|
|
||||||
ElMessage.success("开始通话");
|
|
||||||
}
|
|
||||||
|
|
||||||
function acceptCall() {
|
|
||||||
callStatus.value = "active";
|
|
||||||
ElMessage.success("已接听");
|
|
||||||
}
|
|
||||||
|
|
||||||
function hangup() {
|
|
||||||
callStatus.value = "ended";
|
|
||||||
ElMessage.error("通话已结束");
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClose() {
|
|
||||||
dialogFormVisible.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
dialogFormVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
show,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* ::v-deep .el-dialog__header{
|
|
||||||
background-color:red;
|
|
||||||
} */
|
|
||||||
/* 去除 el-dialog 默认样式,让它全屏覆盖且透明 */
|
|
||||||
.call-dialog :deep(.el-dialog) {
|
|
||||||
background: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
height: calc(100vh - 30vh);
|
|
||||||
width: 100%;
|
|
||||||
max-width: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.call-dialog :deep(.el-dialog__body) {
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 通话主背景 */
|
|
||||||
.call-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
height: calc(100vh - 30vh);
|
|
||||||
width: 100%;
|
|
||||||
background: linear-gradient(180deg, #0f2027, #203a43, #2c5364);
|
|
||||||
color: #fff;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 80px 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 头像部分 */
|
|
||||||
.avatar-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 180px;
|
|
||||||
height: 180px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 6px solid rgba(255, 255, 255, 0.25);
|
|
||||||
box-shadow: 0 0 40px rgba(0, 0, 0, 0.4);
|
|
||||||
object-fit: cover;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-name {
|
|
||||||
margin-top: 24px;
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
margin-top: 12px;
|
|
||||||
font-size: 18px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制按钮部分 */
|
|
||||||
.control-section {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-btn {
|
|
||||||
width: 160px;
|
|
||||||
height: 60px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-btn:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-btn.accept {
|
|
||||||
background: linear-gradient(135deg, #00c853, #4caf50);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-btn.hangup {
|
|
||||||
background: linear-gradient(135deg, #ff1744, #d50000);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-btn.call {
|
|
||||||
background: linear-gradient(135deg, #2196f3, #1976d2);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -1,682 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<div class="left-list" v-loading="leftListLoading || loading">
|
|
||||||
<div class="list-tab">
|
|
||||||
<div
|
|
||||||
:class="'list-tab-item ' + (leftTab == 1 ? 'pitch-on' : '')"
|
|
||||||
@click="() => (leftTab = 1)"
|
|
||||||
>
|
|
||||||
<img src="@/assets/images/Gc_114_line-Level-action.png" v-if="leftTab == 1" />
|
|
||||||
<img src="@/assets/images/Gc_114_line-Level.png" v-else />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
:class="'list-tab-item ' + (leftTab == 2 ? 'pitch-on' : '')"
|
|
||||||
@click="() => (leftTab = 2)"
|
|
||||||
>
|
|
||||||
<img src="@/assets/images/book-read-fill-action.png" v-if="leftTab == 2" />
|
|
||||||
<img src="@/assets/images/book-read-fill.png" v-else />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="list-content">
|
|
||||||
<div class="content-top-input" v-if="leftTab == 1">
|
|
||||||
<!-- <el-input
|
|
||||||
v-model="queryFrom.nickName"
|
|
||||||
placeholder="搜索成员"
|
|
||||||
type="text"
|
|
||||||
prefix-icon="Search"
|
|
||||||
@change="searchList"
|
|
||||||
/> -->
|
|
||||||
<el-select
|
|
||||||
v-model="participant_user"
|
|
||||||
multiple
|
|
||||||
filterable
|
|
||||||
clearable
|
|
||||||
remote
|
|
||||||
reserve-keyword
|
|
||||||
placeholder="搜索成员"
|
|
||||||
:remote-method="remoteMethod"
|
|
||||||
:loading="loading"
|
|
||||||
collapse-tags
|
|
||||||
collapse-tags-tooltip
|
|
||||||
style="width: 100%"
|
|
||||||
@change="searchList"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in userList"
|
|
||||||
:key="item.uid"
|
|
||||||
:label="item.name"
|
|
||||||
:value="item.uid"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
<div class="content-top-input" v-if="leftTab == 2">
|
|
||||||
<el-input
|
|
||||||
v-model="queryFrom.nickName"
|
|
||||||
placeholder="搜索成员"
|
|
||||||
type="text"
|
|
||||||
prefix-icon="Search"
|
|
||||||
@change="searchList"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="content-datapicker" v-if="leftTab == 1">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="queryFrom.leftDatePicker"
|
|
||||||
type="datetimerange"
|
|
||||||
:shortcuts="shortcuts"
|
|
||||||
range-separator="到"
|
|
||||||
start-placeholder="开始时间"
|
|
||||||
end-placeholder="结束时间"
|
|
||||||
format="YYYY-MM-DD HH:mm"
|
|
||||||
value-format="YYYY-MM-DD HH:mm"
|
|
||||||
@change="searchList"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<el-scrollbar class="left-list-scrollbar" v-if="isShow && leftTab == 1">
|
|
||||||
<div
|
|
||||||
class="content-list"
|
|
||||||
v-infinite-scroll="infinite"
|
|
||||||
v-if="dataList?.length"
|
|
||||||
>
|
|
||||||
<!-- @click="updateDetail(item)" -->
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in dataList"
|
|
||||||
:key="index"
|
|
||||||
class="content-list-item"
|
|
||||||
@click="updateDetail(item)"
|
|
||||||
:style="
|
|
||||||
item.id == assistanceId
|
|
||||||
? 'border-color: #409EFF; '
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div class="list-item-top">
|
|
||||||
<span>
|
|
||||||
{{ parseTime(item.created_at, '{m}月{d}日') }}
|
|
||||||
{{ weekName[new Date(item.created_at).getDay()] }}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{{ parseTime(item.created_at, '{y}年') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="list-item-content">
|
|
||||||
<div class="list-item-content-text">
|
|
||||||
<div style="display: flex; flex-wrap: wrap">
|
|
||||||
<span
|
|
||||||
v-for="(items, indexs) in item.all_participants"
|
|
||||||
:key="indexs"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
items.display_name +
|
|
||||||
(indexs + 1 == item.all_participants.length
|
|
||||||
? ''
|
|
||||||
: '、')
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span>
|
|
||||||
发起人:{{ item.all_participants.find(item => item.participant_role == 'moderator')?.display_name || ''}}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
时间:{{
|
|
||||||
parseTime(item.created_at, '{h}:{i}') +
|
|
||||||
' ~ ' +
|
|
||||||
(item.updated_at ? parseTime(item.updated_at, '{h}:{i}') : '')
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: center" v-if="dataList?.length">
|
|
||||||
<p v-if="more">Loading...</p>
|
|
||||||
<p v-else>No more</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="list-empty">
|
|
||||||
<el-empty description="暂无记录" />
|
|
||||||
</div>
|
|
||||||
</el-scrollbar>
|
|
||||||
<div v-if="!isShow && leftTab == 1" class="list-empty">
|
|
||||||
<el-empty description="暂无记录" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-scrollbar
|
|
||||||
class="left-list-scrollbar1"
|
|
||||||
height="calc(100vh - 120px)"
|
|
||||||
v-if="leftTab == 2"
|
|
||||||
>
|
|
||||||
<el-tree
|
|
||||||
ref="treeRef"
|
|
||||||
lazy
|
|
||||||
:load="HandleLoadNode"
|
|
||||||
:filter-node-method="filterNode"
|
|
||||||
highlight-current
|
|
||||||
:props="treeProps"
|
|
||||||
style="width: 100%"
|
|
||||||
@node-click="updateDetail"
|
|
||||||
>
|
|
||||||
<template #default="{ data }">
|
|
||||||
<div class="tree-item">
|
|
||||||
{{ data.name }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-tree>
|
|
||||||
</el-scrollbar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
// getAssistanceList,
|
|
||||||
getParticipantsHistoryApi,
|
|
||||||
getDirectories,
|
|
||||||
getDirectoriesUsers,
|
|
||||||
getInfo,
|
|
||||||
} from '@/api/coordinate.js'
|
|
||||||
import { nextTick, reactive, toRefs, watch, onMounted } from 'vue'
|
|
||||||
import { deepClone,parseTime ,createThrottle} from '@/utils/ruoyi.js'
|
|
||||||
import { getUserInfo } from '@/utils/auth.js'
|
|
||||||
|
|
||||||
// 接收 props
|
|
||||||
const props = defineProps({
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义 emit
|
|
||||||
const emit = defineEmits(['updateDetail', 'updateTab'])
|
|
||||||
|
|
||||||
// state
|
|
||||||
const state = reactive({
|
|
||||||
isFirst: true,
|
|
||||||
leftTab: 1,
|
|
||||||
queryFrom: {
|
|
||||||
page_size: 10,
|
|
||||||
page: 1,
|
|
||||||
nickName: '',
|
|
||||||
leftDatePicker: null,
|
|
||||||
participant_user_uids:''
|
|
||||||
},
|
|
||||||
participant_user:[],
|
|
||||||
leftListLoading: true,
|
|
||||||
loading: false,
|
|
||||||
dataList: [],
|
|
||||||
more: false,
|
|
||||||
isShow: false,
|
|
||||||
shortcuts: [
|
|
||||||
{
|
|
||||||
text: '本周',
|
|
||||||
value: () => {
|
|
||||||
const now = new Date()
|
|
||||||
const nowDaty = now.getDay()
|
|
||||||
const start = new Date(now)
|
|
||||||
start.setDate(now.getDate() - nowDaty)
|
|
||||||
const end = new Date(start)
|
|
||||||
end.setDate(start.getDate() + 6)
|
|
||||||
return [start, end]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '最近三周',
|
|
||||||
value: () => {
|
|
||||||
const now = new Date()
|
|
||||||
const nowDaty = now.getDay()
|
|
||||||
let end = new Date(now)
|
|
||||||
end.setDate(now.getDate() + (6 - nowDaty))
|
|
||||||
const start = new Date(end)
|
|
||||||
start.setDate(start.getDate() - 20)
|
|
||||||
return [start, end]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '本月',
|
|
||||||
value: () => {
|
|
||||||
const now = new Date()
|
|
||||||
const start = new Date(now.getFullYear(), now.getMonth(), 1)
|
|
||||||
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0)
|
|
||||||
return [start, end]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
weekName: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
|
|
||||||
treeRef: null,
|
|
||||||
|
|
||||||
treeProps: {
|
|
||||||
children: 'users',
|
|
||||||
label: 'name',
|
|
||||||
value: 'uid',
|
|
||||||
isLeaf: (node) => {
|
|
||||||
if(node.uid) return true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
assistanceId: '',
|
|
||||||
userList:[],
|
|
||||||
})
|
|
||||||
// state.loading = true;
|
|
||||||
const remoteMethod = createThrottle(async (query) => {
|
|
||||||
if (!query || query.trim() === '') {
|
|
||||||
state.userList = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await getInfo(query, { search_type: 'name' });
|
|
||||||
state.userList = res.data || [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('搜索用户失败:', error);
|
|
||||||
state.userList = [];
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 树状列表筛选
|
|
||||||
*/
|
|
||||||
const filterNode = (value, data) => {
|
|
||||||
if (!value) return true
|
|
||||||
return data.name.includes(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 搜索框变化
|
|
||||||
*/
|
|
||||||
const searchList = () => {
|
|
||||||
if (state.leftTab == 1) {
|
|
||||||
state.queryFrom.page = 1
|
|
||||||
state.dataList = []
|
|
||||||
state.queryFrom.participant_user_uids = state.participant_user.join(',')
|
|
||||||
getList()
|
|
||||||
} else {
|
|
||||||
state.treeRef.filter(state.queryFrom.nickName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 变更详情
|
|
||||||
*/
|
|
||||||
const updateDetail = (item) => {
|
|
||||||
if (state.leftTab == 1) {
|
|
||||||
state.assistanceId = item.id
|
|
||||||
emit('updateDetail', item)
|
|
||||||
} else {
|
|
||||||
if (item.uid) {
|
|
||||||
emit('updateDetail', item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 触底加载
|
|
||||||
*/
|
|
||||||
const infinite = () => {
|
|
||||||
if (state.more) {
|
|
||||||
state.queryFrom.page++
|
|
||||||
getList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 协作记录
|
|
||||||
*/
|
|
||||||
const getList = async () => {
|
|
||||||
try {
|
|
||||||
state.leftListLoading = true
|
|
||||||
let query = deepClone(state.queryFrom)
|
|
||||||
// if (query.leftDatePicker?.length) {
|
|
||||||
// query.beginSignTime = query.leftDatePicker[0]
|
|
||||||
// query.endSignTime = query.leftDatePicker[1]
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (query.leftDatePicker?.length) {
|
|
||||||
const startTime = new Date(query.leftDatePicker[0]);
|
|
||||||
const endTime = new Date(query.leftDatePicker[1]);
|
|
||||||
|
|
||||||
// 添加日期有效性验证
|
|
||||||
if (!isNaN(startTime.getTime())) {
|
|
||||||
query.start_time = Math.floor(startTime.getTime());
|
|
||||||
} else {
|
|
||||||
console.error('开始时间格式无效:', query.leftDatePicker[0]);
|
|
||||||
query.start_time = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNaN(endTime.getTime())) {
|
|
||||||
query.end_time = Math.floor(endTime.getTime());
|
|
||||||
} else {
|
|
||||||
console.error('结束时间格式无效:', query.leftDatePicker[1]);
|
|
||||||
query.end_time = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delete query.leftDatePicker
|
|
||||||
const userData = await getUserInfo()
|
|
||||||
if(!userData) return
|
|
||||||
let infoData = await getParticipantsHistoryApi( userData?.uid ,{ ...query })
|
|
||||||
state.dataList = infoData.data.history?.length
|
|
||||||
? state.dataList.concat(infoData.data.history)
|
|
||||||
: []
|
|
||||||
|
|
||||||
if (state.isFirst) {
|
|
||||||
emit('updateDetail', state.dataList?.length ? state.dataList[0] : null)
|
|
||||||
state.assistanceId = state.dataList?.length
|
|
||||||
? state.dataList[0].id
|
|
||||||
: ''
|
|
||||||
state.isFirst = false
|
|
||||||
}
|
|
||||||
|
|
||||||
state.more = state.dataList?.length < infoData.data.total
|
|
||||||
state.isShow = Boolean(state.dataList?.length)
|
|
||||||
state.leftListLoading = false
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
state.leftListLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通讯录 人员信息树
|
|
||||||
*/
|
|
||||||
const HandleLoadNode = async (node, resolve) => {
|
|
||||||
if(node?.level === 0){
|
|
||||||
loadNode(resolve,'',node?.level)
|
|
||||||
}else if(node?.level > 0){
|
|
||||||
if(node.data.directory_uid){
|
|
||||||
loadUserNode(resolve,node.data.directory_uid,node?.level)
|
|
||||||
}else{
|
|
||||||
resolve(resolve)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadNode = async(resolve,id,level)=>{
|
|
||||||
try {
|
|
||||||
state.leftListLoading = true
|
|
||||||
let res = await getDirectories({level:1})
|
|
||||||
if(res.meta.code == 200){
|
|
||||||
resolve(res.data)
|
|
||||||
}
|
|
||||||
state.leftListLoading = false
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
state.leftListLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadUserNode = async(resolve,id,level)=>{
|
|
||||||
try {
|
|
||||||
state.leftListLoading = true
|
|
||||||
let userData = []
|
|
||||||
let orgData = []
|
|
||||||
const resOrg = await getDirectories({level: 1,parent_uuid:id})
|
|
||||||
if(resOrg?.data){
|
|
||||||
orgData = resOrg.data
|
|
||||||
}
|
|
||||||
if(id){
|
|
||||||
const res = await getDirectoriesUsers(id,{directory_uuid:id})
|
|
||||||
userData = res.data
|
|
||||||
}
|
|
||||||
resolve([...orgData, ...userData])
|
|
||||||
state.leftListLoading = false
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
state.leftListLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUser = (list) => {
|
|
||||||
for (let i = 0; i < list.length; i++) {
|
|
||||||
if (list[i].type == 2) {
|
|
||||||
return list[i]
|
|
||||||
} else if (list[i].children?.length) {
|
|
||||||
let user = getUser(list[i].children)
|
|
||||||
if (user != null) {
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听 props.loading
|
|
||||||
*/
|
|
||||||
watch(
|
|
||||||
() => props.loading,
|
|
||||||
(newValue) => {
|
|
||||||
state.loading = newValue
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听 tab 切换
|
|
||||||
*/
|
|
||||||
watch(
|
|
||||||
() => state.leftTab,
|
|
||||||
(newValue) => {
|
|
||||||
emit('updateTab', newValue)
|
|
||||||
state.dataList = []
|
|
||||||
state.isFirst = true
|
|
||||||
state.queryFrom = {
|
|
||||||
page_size: 10,
|
|
||||||
page: 1,
|
|
||||||
nickName: '',
|
|
||||||
participant_user_uids:'',
|
|
||||||
leftDatePicker: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newValue == 1) {
|
|
||||||
state.isShow = false
|
|
||||||
getList()
|
|
||||||
} else {
|
|
||||||
HandleLoadNode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
state.dataList = []
|
|
||||||
state.isFirst = true
|
|
||||||
getList()
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 暴露给模板
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
isFirst, leftTab, queryFrom, leftListLoading, loading,
|
|
||||||
dataList, more, isShow, shortcuts, weekName,
|
|
||||||
treeRef, treeProps, assistanceId,userList,participant_user
|
|
||||||
} = toRefs(state)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-list {
|
|
||||||
display: flex;
|
|
||||||
height: calc(100vh - 40px);
|
|
||||||
|
|
||||||
.list-tab {
|
|
||||||
@extend .flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-right: 6px;
|
|
||||||
|
|
||||||
.list-tab-item {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.list-content {
|
|
||||||
@extend .flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
width: calc(100% - 56px);
|
|
||||||
box-shadow: 0px 5px 15px 0px rgba(153, 153, 153, 0.3);
|
|
||||||
|
|
||||||
.content-top-input {
|
|
||||||
@extend .flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
padding: 6px 20px;
|
|
||||||
background: #666666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-datapicker {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-list-scrollbar {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100vh - 170px);
|
|
||||||
padding: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-list-scrollbar1 {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100vh - 120px);
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-list {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-list-item {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
background: #f5f7fa;
|
|
||||||
border: 1.5px solid #c9d4e6;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.list-item-top {
|
|
||||||
@extend .flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
padding: 0 13px;
|
|
||||||
background: #c9d4e6;
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: #333333;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-item-content {
|
|
||||||
@extend .flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 13px;
|
|
||||||
background: #f5f7fa;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 27px;
|
|
||||||
height: 27px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-item-content-text {
|
|
||||||
@extend .flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
div {
|
|
||||||
@extend .flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin: 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: 6px;
|
|
||||||
color: #999;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.list-empty {
|
|
||||||
@extend .flex;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100vh - 170px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item {
|
|
||||||
@extend .flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
height: 45px;
|
|
||||||
font-size: 18px;
|
|
||||||
|
|
||||||
|
|
||||||
.tree-item-img1 {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item-text1 {
|
|
||||||
margin-left: 10px;
|
|
||||||
color: #333333;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.tree-item-text2 {
|
|
||||||
color: #333333;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
.tree-item-text3 {
|
|
||||||
margin-left: 15px;
|
|
||||||
color: #999999;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-tree-node__content {
|
|
||||||
@extend .flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
height: 45px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .list-content .el-input__wrapper {
|
|
||||||
height: 38px;
|
|
||||||
border-radius: 19px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .content-datapicker .el-date-editor {
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
background: #e6f1ff;
|
|
||||||
border-radius: 0;
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .left-list-scrollbar .el-scrollbar__wrap {
|
|
||||||
height: calc(100vh - 170px);
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .left-list-scrollbar1 .el-scrollbar__wrap {
|
|
||||||
height: calc(100vh - 120px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,707 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="app-container" v-loading="load" :element-loading-text="loadText">
|
|
||||||
<el-row :gutter="6" style="padding: 0 10px; background: #fefefe">
|
|
||||||
<el-col :xs="24" :sm="24" :md="8" :lg="6">
|
|
||||||
<leftTab
|
|
||||||
@updateDetail="updateDetail"
|
|
||||||
@updateTab="updateTab"
|
|
||||||
:loading="!detail?.appId && !detail?.userId && isShow"
|
|
||||||
/>
|
|
||||||
</el-col>
|
|
||||||
<el-col :xs="24" :sm="24" :md="16" :lg="18">
|
|
||||||
<div
|
|
||||||
class="right-content"
|
|
||||||
v-loading='isShowLoading'
|
|
||||||
>
|
|
||||||
<div class="right-content-title">
|
|
||||||
{{ tabValue == 1 ? '协作信息' : '员工信息' }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="agency-detail-massage-cont right-content-message"
|
|
||||||
v-if="isShow && tabValue == 1"
|
|
||||||
>
|
|
||||||
<div class="agency-detail-cont-item">
|
|
||||||
<span class="agency-detail-item-title">发起人</span>
|
|
||||||
<span class="agency-detail-item-content">
|
|
||||||
{{ detail.initiator }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="agency-detail-cont-item">
|
|
||||||
<span class="agency-detail-item-title">协作时间</span>
|
|
||||||
<span class="agency-detail-item-content">
|
|
||||||
{{
|
|
||||||
detail.created_at ?
|
|
||||||
parseTime(detail.created_at, '{y}年{m}月{d}日') +
|
|
||||||
' ' +
|
|
||||||
weekName[new Date(detail.created_at).getDay()] +
|
|
||||||
' ' +
|
|
||||||
parseTime(detail.created_at, '{h}:{i}')
|
|
||||||
: '暂无'
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="agency-detail-cont-item">
|
|
||||||
<span class="agency-detail-item-title">成员</span>
|
|
||||||
<span
|
|
||||||
class="agency-detail-item-content"
|
|
||||||
v-if="detail?.all_participants?.length"
|
|
||||||
style="display: flex; flex-wrap: wrap"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-for="(items, indexs) in detail.all_participants"
|
|
||||||
:key="indexs"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
items.display_name +
|
|
||||||
(indexs + 1 == detail.all_participants.length
|
|
||||||
? ''
|
|
||||||
: '、')
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="agency-detail-cont-item">
|
|
||||||
<span class="agency-detail-item-title">协作时长</span>
|
|
||||||
<span class="agency-detail-item-content">
|
|
||||||
{{ getTime() }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="right-content-file" v-if="isShow && tabValue == 1">
|
|
||||||
<el-row :gutter="15">
|
|
||||||
<el-col :xs="24" :sm="24" :md="16" :lg="18">
|
|
||||||
<div class="content-file-video">
|
|
||||||
<div class="file-top">协作视频</div>
|
|
||||||
<div class="file-video-bottom">
|
|
||||||
<!-- autoplay="autoplay" -->
|
|
||||||
<video
|
|
||||||
v-if="detail.remoteVideoFile?.storage_url"
|
|
||||||
:src="detail.remoteVideoFile.storage_url"
|
|
||||||
id="videoPlayer"
|
|
||||||
loop
|
|
||||||
autoplay
|
|
||||||
controls
|
|
||||||
></video>
|
|
||||||
<div v-else class="video-null">暂无视频</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
<el-col :xs="24" :sm="24" :md="8" :lg="6">
|
|
||||||
<div>
|
|
||||||
<div class="file-top">
|
|
||||||
附件({{
|
|
||||||
detail?.fileList?.length ? detail.fileList.length : 0
|
|
||||||
}})
|
|
||||||
</div>
|
|
||||||
<div class="content-file-list">
|
|
||||||
<el-scrollbar
|
|
||||||
class="file-list"
|
|
||||||
height="calc(100vh - 380px)"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="file-list-content"
|
|
||||||
v-if="detail?.fileList?.length"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="file-list-item"
|
|
||||||
v-for="(item, index) in detail.fileList"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
<div class="file-list-item-icon"></div>
|
|
||||||
<div class="file-list-item-text">
|
|
||||||
<div class="list-item-text text-out-of-hiding-1">
|
|
||||||
{{ item.file_name }}
|
|
||||||
</div>
|
|
||||||
<el-icon
|
|
||||||
:size="18"
|
|
||||||
color="#0d74ff"
|
|
||||||
style="cursor: pointer"
|
|
||||||
@click="handlePreview(item)"
|
|
||||||
>
|
|
||||||
<View />
|
|
||||||
</el-icon>
|
|
||||||
<el-link
|
|
||||||
:href="item.source_url"
|
|
||||||
type="primary"
|
|
||||||
target="_blank"
|
|
||||||
:underline="false"
|
|
||||||
>
|
|
||||||
<el-icon
|
|
||||||
:size="18"
|
|
||||||
color="#0d74ff"
|
|
||||||
style="cursor: pointer"
|
|
||||||
>
|
|
||||||
<Download />
|
|
||||||
</el-icon>
|
|
||||||
</el-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-scrollbar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="message-user"
|
|
||||||
v-else-if="isShow && tabValue == 2"
|
|
||||||
style="height: calc(100vh - 90px)"
|
|
||||||
v-loading="userLoading"
|
|
||||||
>
|
|
||||||
<div class="message-user-card">
|
|
||||||
<div class="user-card-nickName">
|
|
||||||
<img v-if="detail.avatar" :src="detail.avatar" />
|
|
||||||
<img v-else src="@/assets/images/profile.jpg" />
|
|
||||||
<span>{{ detail.nickName || detail.name || '暂无信息' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="user-card-information">
|
|
||||||
<div class="user-information-item">
|
|
||||||
<div class="user-information-title">
|
|
||||||
<img src="@/assets/images/user-information1.png" alt="" />
|
|
||||||
<span>性别</span>
|
|
||||||
</div>
|
|
||||||
<div class="user-information-text">
|
|
||||||
<!-- <dict-tag
|
|
||||||
v-if="detail.sex"
|
|
||||||
:options="sys_user_sex"
|
|
||||||
:value="detail.sex"
|
|
||||||
/>
|
|
||||||
<div v-else>
|
|
||||||
{{ '暂无' }}
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="user-information-item">
|
|
||||||
<div class="user-information-title">
|
|
||||||
<img src="@/assets/images/user-information2.png" alt="" />
|
|
||||||
<span>手机号</span>
|
|
||||||
</div>
|
|
||||||
<div class="user-information-text">
|
|
||||||
{{ detail.phonenumber || '暂无' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="user-information-item">
|
|
||||||
<div class="user-information-title">
|
|
||||||
<img src="@/assets/images/user-information3.png" alt="" />
|
|
||||||
<span>邮箱</span>
|
|
||||||
</div>
|
|
||||||
<div class="user-information-text">
|
|
||||||
{{ detail.email || '暂无' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="user-information-item">
|
|
||||||
<div class="user-information-title">
|
|
||||||
<img src="@/assets/images/user-information4.png" alt="" />
|
|
||||||
<span>所属部门</span>
|
|
||||||
</div>
|
|
||||||
<div class="user-information-text">
|
|
||||||
{{ detail.organization || '暂无' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="user-card-btn">
|
|
||||||
<el-button type="info" @click="clickInitiate">
|
|
||||||
发起协作
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="message-null">
|
|
||||||
<el-empty description="暂无内容" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<!-- <el-dialog
|
|
||||||
v-model="inviteDialog"
|
|
||||||
title="远程协作"
|
|
||||||
width="400px"
|
|
||||||
:close-on-press-escape="false"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
:show-close="false"
|
|
||||||
>
|
|
||||||
|
|
||||||
<div style="width: 100%; margin-bottom: 30px; font-size: 20px">
|
|
||||||
"
|
|
||||||
{{
|
|
||||||
socketInformation.room_name
|
|
||||||
? socketInformation.room_name
|
|
||||||
: ''
|
|
||||||
}}
|
|
||||||
" 邀请您参加远程协作
|
|
||||||
</div>
|
|
||||||
<div style="text-align: center">
|
|
||||||
<el-button
|
|
||||||
size="large"
|
|
||||||
type="danger"
|
|
||||||
style="font-size: 16px"
|
|
||||||
@click="clickRefuseJoin"
|
|
||||||
>
|
|
||||||
拒 绝
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
size="large"
|
|
||||||
type="primary"
|
|
||||||
style="font-size: 16px"
|
|
||||||
@click="clickJoin"
|
|
||||||
>
|
|
||||||
加 入
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-dialog> -->
|
|
||||||
</div>
|
|
||||||
<!-- 文件预览 -->
|
|
||||||
<BrowseFile ref="browseFileRef" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onActivated, onMounted, reactive, toRefs, watch, getCurrentInstance ,ref} from 'vue'
|
|
||||||
import leftTab from './components/leftTab/index.vue'
|
|
||||||
import AssistWx from './components/assistWx/index.vue'
|
|
||||||
import BrowseFile from '@/views/conferencingRoom/components/fileUpload/browseFile.vue'
|
|
||||||
import { getInfo } from '@/api/login.js'
|
|
||||||
import { getStatusApi ,getFileListApi ,getvideoUrlApi} from '@/api/conferencingRoom.js'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import { useRoomStore } from '@/stores/modules/room'
|
|
||||||
import { useUserStore } from '@/stores/modules/user.js'
|
|
||||||
import { mqttClient } from "@/utils/mqtt.js";
|
|
||||||
import { getToken ,getUserInfo} from '@/utils/auth.js';
|
|
||||||
import { deepClone,parseTime } from '@/utils/ruoyi.js'
|
|
||||||
const roomStore = useRoomStore()
|
|
||||||
const userStore = useUserStore()
|
|
||||||
const router = useRouter()
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
const state = reactive({
|
|
||||||
detail: {},
|
|
||||||
weekName: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
|
|
||||||
tabValue: 1,
|
|
||||||
isShow: true,
|
|
||||||
isShowLoading: false,
|
|
||||||
load: false,
|
|
||||||
loadText: '数据加载中',
|
|
||||||
isLinkKnow: 'F',
|
|
||||||
socketInformation: null,
|
|
||||||
inviteDialog: false,
|
|
||||||
cooperation: import.meta.env.VITE_APP_COOPERATION_TYPE,
|
|
||||||
})
|
|
||||||
const userLoading = ref(false); // 用户信息加载状态
|
|
||||||
//文件预览
|
|
||||||
const browseFileRef = ref(null);
|
|
||||||
|
|
||||||
const isEmptyObject = (obj) => {
|
|
||||||
return !obj || Object.keys(obj).length === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 发起协作邀请 */
|
|
||||||
const clickInitiate = () => {
|
|
||||||
let userData = null
|
|
||||||
try {
|
|
||||||
userData = JSON.parse(sessionStorage.getItem('userData')) || null
|
|
||||||
} catch (e) {
|
|
||||||
console.error('解析 userData 失败:', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (isEmptyObject(state.detail)) {
|
|
||||||
ElMessage({
|
|
||||||
message: '请先选择人员',
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if(state.detail.uid == userData.uid){
|
|
||||||
ElMessage({
|
|
||||||
message: '不能邀请自己',
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
roomStore.setUserUid(userData.uid)
|
|
||||||
roomStore.setDetailUid(state.detail.uid)
|
|
||||||
roomStore.setDetailName(state.detail.name)
|
|
||||||
router.push({
|
|
||||||
path: '/conferencingRoom',
|
|
||||||
query:{
|
|
||||||
type:1//创建房间,加入房间 2
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 修改展示列表 */
|
|
||||||
const updateTab = (newValue) => {
|
|
||||||
state.detail = {}
|
|
||||||
state.tabValue = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 修改展示区内容 */
|
|
||||||
const updateDetail = async (details) => {
|
|
||||||
userLoading.value = true
|
|
||||||
if (details) {
|
|
||||||
state.detail = {}
|
|
||||||
if (state.tabValue == 1) {
|
|
||||||
state.isShowLoading = true
|
|
||||||
getTheFileList(details)
|
|
||||||
} else {
|
|
||||||
const res = await getInfo(details.uid)
|
|
||||||
state.detail = res.data
|
|
||||||
userLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
userLoading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTheFileList = async (details) => {
|
|
||||||
try {
|
|
||||||
let detail = deepClone(details);
|
|
||||||
const [fileResponse, videoResponse] = await Promise.all([
|
|
||||||
getFileListApi(details.room_uid),
|
|
||||||
getvideoUrlApi(details.room_uid)
|
|
||||||
]);
|
|
||||||
const processedDetail = {
|
|
||||||
...details,
|
|
||||||
fileList: fileResponse.data?.files || [],
|
|
||||||
remoteVideoFile: videoResponse.data?.recordings?.[0] || {},
|
|
||||||
initiator: details.all_participants?.find(item =>
|
|
||||||
item.participant_role === 'moderator' // 使用严格相等
|
|
||||||
)?.display_name || '未知发起人'
|
|
||||||
};
|
|
||||||
state.detail = processedDetail;
|
|
||||||
state.isShowLoading = false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取文件列表失败:', error);
|
|
||||||
// 可以根据需要添加错误处理逻辑
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取通话时长 */
|
|
||||||
const getTime = () => {
|
|
||||||
let begin = new Date(state.detail.created_at).getTime()
|
|
||||||
let end = new Date(state.detail.updated_at).getTime()
|
|
||||||
if (begin && end) {
|
|
||||||
let diff = end - begin
|
|
||||||
const h = Math.floor(diff / (1000 * 60 * 60))
|
|
||||||
const m = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
|
||||||
const s = Math.floor((diff % (1000 * 60)) / 1000)
|
|
||||||
return h > 0 ? `${h}小时 ${m}分钟 ${s}秒`
|
|
||||||
: m > 0 ? `${m}分钟 ${s}秒`
|
|
||||||
: `${s}秒`
|
|
||||||
} else {
|
|
||||||
return '暂无'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//文件预览
|
|
||||||
function handlePreview(file) {
|
|
||||||
if (!file.preview_url) {
|
|
||||||
ElMessage.error('文件链接无效');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
browseFileRef.value.showEdit(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 加入会议 */
|
|
||||||
// const clickJoin = async () => {
|
|
||||||
// const res = await getStatusApi(state.socketInformation.room_uid,{status:1})
|
|
||||||
// if(res.meta.code == 200){
|
|
||||||
// ElMessage({
|
|
||||||
// message: '成功加入该协作',
|
|
||||||
// type: 'success',
|
|
||||||
// })
|
|
||||||
// state.inviteDialog = false
|
|
||||||
// router.push({
|
|
||||||
// path: '/conferencingRoom',
|
|
||||||
// query:{
|
|
||||||
// type:2,//创建房间,加入房间 2
|
|
||||||
// room_uid:state.socketInformation.room_uid
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// state.inviteDialog = false
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
/** 拒绝加入 */
|
|
||||||
// const clickRefuseJoin = async () => {
|
|
||||||
// //status 1: 同意加入, 5: 拒绝加入
|
|
||||||
// const res = await getStatusApi(state.socketInformation.room_uid,{status:5})
|
|
||||||
// if(res.meta.code == 200){
|
|
||||||
// ElMessage({
|
|
||||||
// message: '已拒绝加入该协作',
|
|
||||||
// type: 'error',
|
|
||||||
// })
|
|
||||||
// state.inviteDialog = false
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/** 处理加入房间和拒接房间 mqtt 消息 */
|
|
||||||
// const processingSocket = (message) => {
|
|
||||||
// const res = JSON.parse(message)
|
|
||||||
// if (!res?.status) {
|
|
||||||
// state.socketInformation = res
|
|
||||||
// state.inviteDialog = true
|
|
||||||
// showNotification(state.socketInformation)
|
|
||||||
// }else if(res.status == 5){
|
|
||||||
// ElMessage({
|
|
||||||
// message: `${res?.display_name}拒绝加入该协作`,
|
|
||||||
// type: 'error',
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/** 浏览器通知 */
|
|
||||||
// const showNotification = (data) => {
|
|
||||||
// if ('Notification' in window) {
|
|
||||||
// Notification.requestPermission().then((permission) => {
|
|
||||||
// if (permission === 'granted') {
|
|
||||||
// const notification = new Notification('协作邀请', {
|
|
||||||
// // body: String(data.room_name) + '邀请您参加远程协作'
|
|
||||||
// body: '远程协作有新的邀请'
|
|
||||||
// // icon: logo,
|
|
||||||
// })
|
|
||||||
// notification.onclick = () => { clickJoin() }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// 暴露给模板
|
|
||||||
const { detail, weekName, tabValue, isShow, load, loadText, isLinkKnow, socketInformation, inviteDialog, cooperation,isShowLoading } = toRefs(state)
|
|
||||||
// onMounted(async () => {
|
|
||||||
// await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
|
|
||||||
// const res = await userStore.getInfo()
|
|
||||||
// const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
|
|
||||||
// mqttClient.subscribe(topic, async (shapeData) => {
|
|
||||||
// // console.log(shapeData.toString(),'shapeData发送邀请')
|
|
||||||
// processingSocket(shapeData.toString())
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-container {
|
|
||||||
padding: 20px;
|
|
||||||
// margin: 0 17px;
|
|
||||||
// height: calc(100vh - 50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-null {
|
|
||||||
@extend .flex;
|
|
||||||
height: calc(100vh - 90px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-content {
|
|
||||||
height: calc(100vh - 40px);
|
|
||||||
background: #f4f9ff;
|
|
||||||
|
|
||||||
.right-content-title {
|
|
||||||
@extend .flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
padding: 0 20px;
|
|
||||||
background: #666666;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-content-message {
|
|
||||||
width: calc(100% - 30px);
|
|
||||||
height: 150px;
|
|
||||||
margin: 15px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-content-file {
|
|
||||||
width: calc(100% - 30px);
|
|
||||||
margin: 0 15px 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-top {
|
|
||||||
@extend .flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
height: 50px;
|
|
||||||
padding-left: 20px;
|
|
||||||
background: #e6f1ff;
|
|
||||||
color: #333;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-file-video {
|
|
||||||
.file-video-bottom {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100vh - 350px);
|
|
||||||
padding: 15px;
|
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.content-file-list {
|
|
||||||
background: #fff;
|
|
||||||
padding: 15px 5px 15px 10px;
|
|
||||||
|
|
||||||
.file-list {
|
|
||||||
width: 100%;
|
|
||||||
padding-right: 10px;
|
|
||||||
|
|
||||||
.file-list-content {
|
|
||||||
@extend .flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.file-list-item {
|
|
||||||
@extend .flex;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
|
|
||||||
.file-list-item-icon {
|
|
||||||
width: 13px;
|
|
||||||
height: 13px;
|
|
||||||
margin-right: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #89b2ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-list-item-text {
|
|
||||||
@extend .flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: calc(100% - 23px);
|
|
||||||
padding: 13px 6px 13px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #f4f9ff;
|
|
||||||
|
|
||||||
.list-item-text {
|
|
||||||
display: inline-block;
|
|
||||||
width: calc(100% - 50px);
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-user {
|
|
||||||
@extend .flex;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
.message-user-card {
|
|
||||||
@extend .flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 500px;
|
|
||||||
height: 95%;
|
|
||||||
max-height: 550px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.user-card-nickName {
|
|
||||||
@extend .flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 40%;
|
|
||||||
background: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
rgba(153, 153, 153, 0.22) 0%,
|
|
||||||
rgba(153, 153, 153, 0) 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
margin-top: 10%;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #167bff;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: #051435;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-card-information {
|
|
||||||
@extend .flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-around;
|
|
||||||
width: 100%;
|
|
||||||
height: 45%;
|
|
||||||
|
|
||||||
.user-information-item {
|
|
||||||
@extend .flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.user-information-title {
|
|
||||||
@extend .flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
width: 150px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: #999;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-information-text {
|
|
||||||
width: 130px;
|
|
||||||
color: #333333;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.user-card-btn {
|
|
||||||
@extend .flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 15%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-null {
|
|
||||||
color: #999;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .user-card-btn .el-button {
|
|
||||||
width: 70%;
|
|
||||||
height: 55%;
|
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import request from "@/utils/request.js";
|
|
||||||
|
|
||||||
// 上传绘制数据
|
|
||||||
export const getWhiteboardShapes = (data, room_uid) => {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/whiteboard/rooms/${room_uid}/shapes`,
|
|
||||||
method: "put",
|
|
||||||
data: data
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取以往绘制数据
|
|
||||||
export const getWhiteboardHistory = (data, room_uid) => {
|
|
||||||
return request({
|
|
||||||
url: `/api/v1/whiteboard/rooms/${room_uid}/history`,
|
|
||||||
method: "get",
|
|
||||||
params: data
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//获取用户信息
|
|
||||||
// export const getWhiteboardHistory = (data, room_uid) => {
|
|
||||||
// return request({
|
|
||||||
// url: `/api/v1/user/self`,
|
|
||||||
// method: "get",
|
|
||||||
// params: data
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="wrapper-content">
|
|
||||||
<div>
|
|
||||||
<!-- 未加入时显示按钮 -->
|
|
||||||
<div v-if="!hasJoined" class="login-button-container">
|
|
||||||
<el-button type="primary" size="large" link @click="joinWhiteboard">
|
|
||||||
正在进入互动画板。。。
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 已加入时显示白板 -->
|
|
||||||
<div v-else class="whiteboard-wrapper">
|
|
||||||
<ToolBox v-if="canvas" class="toolbox" :canvas="canvas" />
|
|
||||||
<canvas id="whiteboard" class="whiteboard-canvas"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, nextTick, onUnmounted, onMounted } from "vue";
|
|
||||||
import { ElLoading, ElMessage } from "element-plus";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import { useRoute } from "vue-router";
|
|
||||||
import { mqttClient } from "@/utils/mqtt";
|
|
||||||
import { WhiteboardSync } from "@/utils/whiteboardSync";
|
|
||||||
import ToolBox from "@/components/ToolBox/index.vue";
|
|
||||||
import Canvas from "@/core/index.js";
|
|
||||||
import { getInfo } from "@/api/login";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
roomId: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const hasJoined = ref(false); // 是否加入白板
|
|
||||||
const canvas = ref(null);
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
/** 进入白板 */
|
|
||||||
async function joinWhiteboard() {
|
|
||||||
const loading = ElLoading.service({
|
|
||||||
lock: true,
|
|
||||||
text: "正在进入互动画板...",
|
|
||||||
background: "rgba(0, 0, 0, 0.4)",
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const clientId = `whiteboard-${uuidv4()}`;
|
|
||||||
await mqttClient.connect(clientId);
|
|
||||||
// console.log("✅ 已连接 MQTT:", clientId);
|
|
||||||
|
|
||||||
hasJoined.value = true;
|
|
||||||
|
|
||||||
// 等待 DOM 更新后再初始化画布
|
|
||||||
await nextTick();
|
|
||||||
initWhiteboard();
|
|
||||||
|
|
||||||
ElMessage.success("已进入互动画板 🎉");
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ 连接失败:", err);
|
|
||||||
ElMessage.error("连接白板失败,请重试");
|
|
||||||
} finally {
|
|
||||||
loading.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化白板 */
|
|
||||||
function initWhiteboard() {
|
|
||||||
const el = document.getElementById("whiteboard");
|
|
||||||
if (!el) {
|
|
||||||
console.error("⚠️ 找不到 canvas 元素");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 初始化画布
|
|
||||||
canvas.value = new Canvas("whiteboard");
|
|
||||||
|
|
||||||
// 获取房间号
|
|
||||||
const roomUid = route.query.room_uid || props.roomId || "default-room";
|
|
||||||
// 初始化多人同步
|
|
||||||
WhiteboardSync.init(canvas.value, roomUid);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
joinWhiteboard()
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (canvas.value) canvas.value.destroy();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 外层容器全屏居中 */
|
|
||||||
.wrapper-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: #fff;
|
|
||||||
position: relative; /* 关键:为绝对定位子元素提供参照 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 登录按钮容器居中 */
|
|
||||||
.login-button-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 白板容器 */
|
|
||||||
.whiteboard-wrapper {
|
|
||||||
position: relative;
|
|
||||||
width: 72vw;
|
|
||||||
height: 69vh;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 画布占满白板容器 */
|
|
||||||
.whiteboard-canvas {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 工具栏固定在 wrapper-content 内 */
|
|
||||||
.toolbox {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 40px; /* 距离左侧的间距 */
|
|
||||||
transform: translateY(-50%);
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="wrapper-content">
|
|
||||||
<div v-if="showLogin">
|
|
||||||
<!-- 登录界面 -->
|
|
||||||
<Login @loginSuccess="handleLoginSuccess" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<!-- 未加入时显示按钮 -->
|
|
||||||
<div v-if="!hasJoined" class="login-button-container">
|
|
||||||
<el-button type="primary" size="large" round plain @click="joinWhiteboard">
|
|
||||||
加入互动画板
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 已加入时显示白板 -->
|
|
||||||
<div v-else class="whiteboard-wrapper">
|
|
||||||
<ToolBox v-if="canvas" class="toolbox" :canvas="canvas" />
|
|
||||||
<canvas id="whiteboard" class="whiteboard-canvas"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, nextTick, onUnmounted, onMounted } from "vue";
|
|
||||||
import { ElLoading, ElMessage } from "element-plus";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import { useRoute } from "vue-router";
|
|
||||||
import { mqttClient } from "@/utils/mqtt";
|
|
||||||
import { WhiteboardSync } from "@/utils/whiteboardSync";
|
|
||||||
import ToolBox from "@/components/ToolBox/index.vue";
|
|
||||||
import Login from "@/components/Login/index.vue";
|
|
||||||
import Canvas from "@/core/index.js";
|
|
||||||
import { getInfo } from "@/api/login";
|
|
||||||
|
|
||||||
const showLogin = ref(false); // 是否显示登录页面
|
|
||||||
const hasJoined = ref(false); // 是否加入白板
|
|
||||||
const canvas = ref(null);
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
/** 进入白板 */
|
|
||||||
async function joinWhiteboard() {
|
|
||||||
const loading = ElLoading.service({
|
|
||||||
lock: true,
|
|
||||||
text: "正在进入互动画板...",
|
|
||||||
background: "rgba(0, 0, 0, 0.4)",
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const clientId = `whiteboard-${uuidv4()}`;
|
|
||||||
await mqttClient.connect(clientId);
|
|
||||||
console.log("✅ 已连接 MQTT:", clientId);
|
|
||||||
|
|
||||||
hasJoined.value = true;
|
|
||||||
|
|
||||||
// 等待 DOM 更新后再初始化画布
|
|
||||||
await nextTick();
|
|
||||||
initWhiteboard();
|
|
||||||
|
|
||||||
ElMessage.success("已进入互动画板 🎉");
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ 连接失败:", err);
|
|
||||||
ElMessage.error("连接白板失败,请重试");
|
|
||||||
} finally {
|
|
||||||
loading.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 登录成功回调 */
|
|
||||||
function handleLoginSuccess() {
|
|
||||||
showLogin.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化白板 */
|
|
||||||
function initWhiteboard() {
|
|
||||||
const el = document.getElementById("whiteboard");
|
|
||||||
if (!el) {
|
|
||||||
console.error("⚠️ 找不到 canvas 元素");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置画布宽高(保持16:9)
|
|
||||||
const { width, height } = getCanvasSize(el.parentElement);
|
|
||||||
Object.assign(el, { width, height });
|
|
||||||
Object.assign(el.style, {
|
|
||||||
width: `${width}px`,
|
|
||||||
height: `${height}px`,
|
|
||||||
border: "2px solid #000",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化画布
|
|
||||||
canvas.value = new Canvas("whiteboard");
|
|
||||||
|
|
||||||
// 获取房间号
|
|
||||||
const roomUid = route.query.room_uid || "default-room";
|
|
||||||
|
|
||||||
// 初始化多人同步
|
|
||||||
WhiteboardSync.init(canvas.value, roomUid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 计算画布大小,保持16:9 */
|
|
||||||
function getCanvasSize(container) {
|
|
||||||
const containerWidth = container.offsetWidth;
|
|
||||||
const containerHeight = container.offsetHeight;
|
|
||||||
|
|
||||||
let width = containerWidth;
|
|
||||||
let height = Math.floor((width * 9) / 16);
|
|
||||||
|
|
||||||
if (height > containerHeight) {
|
|
||||||
height = containerHeight;
|
|
||||||
width = Math.floor((height * 16) / 9);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { width, height };
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
showLogin.value = false;
|
|
||||||
const res = await getInfo("self");
|
|
||||||
if (res.meta.code === 401) {
|
|
||||||
showLogin.value = true;
|
|
||||||
} else {
|
|
||||||
showLogin.value = false;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("⚠️ 用户信息校验失败:", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (canvas.value) canvas.value.destroy();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 外层容器全屏居中 */
|
|
||||||
.wrapper-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background: #fff;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 登录按钮容器居中 */
|
|
||||||
.login-button-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 白板容器 */
|
|
||||||
.whiteboard-wrapper {
|
|
||||||
position: relative;
|
|
||||||
width: 90vw;
|
|
||||||
max-width: 1280px;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 画布占满白板容器 */
|
|
||||||
.whiteboard-canvas {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 工具栏左侧垂直居中 */
|
|
||||||
.toolbox {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 10px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
317
src/views/interfaceManagement/index.vue
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2">
|
||||||
|
<transition>
|
||||||
|
<div class="mb-[10px]">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
|
||||||
|
<!-- <el-form-item label="菜单名称" prop="permission_name">
|
||||||
|
<el-input v-model="queryParams.permission_name" placeholder="请输入菜单名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item> -->
|
||||||
|
<!-- <el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="菜单状态" clearable>
|
||||||
|
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||||
|
</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 shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="primary" plain icon="Plus" @click="handleAdd()">新增 </el-button>
|
||||||
|
</el-col>
|
||||||
|
<!-- <el-col :span="1.5">
|
||||||
|
<el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
|
||||||
|
</el-col> -->
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="menuList"
|
||||||
|
row-key="uid"
|
||||||
|
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||||
|
ref="menuTableRef"
|
||||||
|
:default-expand-all="isExpandAll"
|
||||||
|
>
|
||||||
|
<el-table-column prop="permission_name" label="接口名称" :show-overflow-tooltip="true" width="160"></el-table-column>
|
||||||
|
<el-table-column prop="permission_code" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
|
||||||
|
<el-table-column prop="resource_path" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
|
||||||
|
<el-table-column prop="description" label="权限描述" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column prop="http_method" label="请求方式" :show-overflow-tooltip="true" width="80" />
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ parseTime(scope.row.created_at) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column fixed="right" label="操作" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tooltip content="修改" placement="top">
|
||||||
|
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"/>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="新增" placement="top">
|
||||||
|
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)"/>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="删除" placement="top">
|
||||||
|
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"/>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog :title="dialog.title" v-model="dialog.visible" destroy-on-close append-to-body width="750px">
|
||||||
|
<el-form ref="menuFormRef" :model="form" :rules="rules" label-width="100px">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="上级接口">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="form.parent_uid"
|
||||||
|
:data="menuOptions"
|
||||||
|
:props="{ value: 'uid', label: 'permission_name', children: 'children' }"
|
||||||
|
value-key="uid"
|
||||||
|
placeholder="选择上级接口"
|
||||||
|
check-strictly
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="接口名称" prop="permission_name">
|
||||||
|
<el-input v-model="form.permission_name" placeholder="请输入接口名称"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item prop="permission_code">
|
||||||
|
<el-input v-model="form.permission_code" placeholder="请输入权限标识" maxlength="100" />
|
||||||
|
<template #label>
|
||||||
|
<span>
|
||||||
|
<el-tooltip content="控制器中定义的权限字符,如:xsy-delete-role')" placement="top">
|
||||||
|
<el-icon>
|
||||||
|
<question-filled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
权限字符
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item prop="resource_path">
|
||||||
|
<template #label>
|
||||||
|
<span>
|
||||||
|
<el-tooltip content="接口路径,如:`/permission/roles/:role_uid`" placement="top">
|
||||||
|
<el-icon>
|
||||||
|
<question-filled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
接口 路径
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="form.resource_path" placeholder="请输入接口路径" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item prop="http_method">
|
||||||
|
<template #label>
|
||||||
|
<span>
|
||||||
|
<el-tooltip content="请求方法,如:`GET`、`POST`、`PUT`、`DELETE`等" placement="top">
|
||||||
|
<el-icon>
|
||||||
|
<question-filled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
请求方法
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="form.http_method" placeholder="请输入请求方法" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="权限描述" prop="description">
|
||||||
|
<el-input v-model="form.description" type="textarea" placeholder="请输入权限描述"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="menuManagement">
|
||||||
|
import { listMenu } from '@/api/interface.js';
|
||||||
|
import { addMenu, delMenu, updateMenu } from '@/api/menu.js';
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
|
const menuList = ref([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const menuOptions = ref([])
|
||||||
|
const isExpandAll = ref(false)
|
||||||
|
|
||||||
|
const dialog = reactive({
|
||||||
|
visible: false,
|
||||||
|
title: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryFormRef = ref();
|
||||||
|
const menuFormRef = ref();
|
||||||
|
const menuTableRef = ref();
|
||||||
|
const initFormData = {
|
||||||
|
permission_name: '',
|
||||||
|
permission_type: 2,//1:菜单 3:按钮 2:数据权限
|
||||||
|
|
||||||
|
permission_code: '',
|
||||||
|
resource_path: '',
|
||||||
|
http_method: '',
|
||||||
|
description: '',
|
||||||
|
parent_uid: '',
|
||||||
|
}
|
||||||
|
const data = reactive({
|
||||||
|
form: { ...initFormData },
|
||||||
|
queryParams: {
|
||||||
|
//1:菜单 3:按钮 2:数据权限
|
||||||
|
permission_types: '2'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
permission_name: [{ required: true, message: "接口名称不能为空", trigger: "blur" }],
|
||||||
|
permission_code: [{ required: true, message: "权限标识不能为空", trigger: "blur" }],
|
||||||
|
permission_type: [{ required: true, message: "接口类型不能为空", trigger: "change" }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const { queryParams, form, rules } = toRefs(data)
|
||||||
|
/** 查询接口列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
const res = await listMenu(queryParams.value);
|
||||||
|
menuList.value = res.data
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
/** 查询下拉树结构 */
|
||||||
|
const getTreeselect = async () => {
|
||||||
|
menuOptions.value = []
|
||||||
|
const response = await listMenu(queryParams.value);
|
||||||
|
const menu = { uid: '0', permission_name: "主类目", children: [] }
|
||||||
|
menu.children = response.data
|
||||||
|
menuOptions.value.push(menu)
|
||||||
|
}
|
||||||
|
/** 取消按钮 */
|
||||||
|
const cancel = () => {
|
||||||
|
// reset()
|
||||||
|
dialog.visible = false
|
||||||
|
}
|
||||||
|
/** 表单重置 */
|
||||||
|
const reset = () => {
|
||||||
|
form.value = { ...initFormData };
|
||||||
|
menuFormRef.value?.resetFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
}
|
||||||
|
/** 新增按钮操作 */
|
||||||
|
const handleAdd = (row) => {
|
||||||
|
reset();
|
||||||
|
getTreeselect();
|
||||||
|
row && row.uid ? form.value.parent_uid = row.uid : form.value.parent_uid = '';
|
||||||
|
dialog.visible = true;
|
||||||
|
dialog.title = "添加接口";
|
||||||
|
}
|
||||||
|
/** 展开/折叠操作 */
|
||||||
|
const handleToggleExpandAll = () => {
|
||||||
|
isExpandAll.value = !isExpandAll.value;
|
||||||
|
toggleExpandAll(menuList.value, isExpandAll.value)
|
||||||
|
}
|
||||||
|
/** 展开/折叠所有 */
|
||||||
|
const toggleExpandAll = (data, status) => {
|
||||||
|
data.forEach((item) => {
|
||||||
|
menuTableRef.value?.toggleRowExpansion(item, status)
|
||||||
|
if (item.children && item.children.length > 0) toggleExpandAll(item.children, status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/** 修改按钮操作 */
|
||||||
|
const handleUpdate = async (row) => {
|
||||||
|
reset();
|
||||||
|
await getTreeselect();
|
||||||
|
if(row.parent_uid == ''){
|
||||||
|
row.parent_uid = '0'
|
||||||
|
}
|
||||||
|
form.value = row;
|
||||||
|
dialog.visible = true;
|
||||||
|
dialog.title = "修改接口";
|
||||||
|
}
|
||||||
|
/** 提交按钮 */
|
||||||
|
const submitForm = () => {
|
||||||
|
try {
|
||||||
|
menuFormRef.value?.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
form.value.uid ? await updateMenu(form.value) : await addMenu(form.value);
|
||||||
|
ElMessage.success('操作成功');
|
||||||
|
dialog.visible = false;
|
||||||
|
await getList();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('操作失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确认要删除"${row.permission_name}"的数据项?`,
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const menuid = row?.uid;
|
||||||
|
const res = await delMenu(menuid);
|
||||||
|
if(res.meta.code !== 200){
|
||||||
|
ElMessage.error(res.data.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await getList();
|
||||||
|
ElMessage.success(`${row.permission_name}权限删除成功`);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err?.toString() || '';
|
||||||
|
if (errorMsg.includes('cancel') ||
|
||||||
|
errorMsg.includes('取消') ||
|
||||||
|
errorMsg === 'cancel' ||
|
||||||
|
err === 'cancel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errorMsg.includes('close') || err === 'close') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.error(`${row.permission_name}权限删除失败`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
<div class="selected-rectangle"></div>
|
<div class="selected-rectangle"></div>
|
||||||
<el-form ref="loginRef" class="form-info" :model="loginForm" :rules="loginRules">
|
<el-form ref="loginRef" class="form-info" :model="loginForm" :rules="loginRules">
|
||||||
<h3 class="title">欢迎登录</h3>
|
<h2 class="title">欢迎登录</h2>
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input v-model="loginForm.username" auto-complete="off" placeholder="请输入您的账号" size="large"
|
<el-input v-model="loginForm.username" auto-complete="off" placeholder="请输入您的账号" size="large"
|
||||||
type="text"></el-input>
|
type="text"></el-input>
|
||||||
@@ -118,20 +118,20 @@ const loading = ref(false)
|
|||||||
await router.push(redirect.value);
|
await router.push(redirect.value);
|
||||||
} else {
|
} else {
|
||||||
console.warn('重定向路径无效,跳转到默认页面');
|
console.warn('重定向路径无效,跳转到默认页面');
|
||||||
await router.push('/coordinate');
|
await router.push('/userManagement');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('重定向跳转失败,跳转到默认页面:', error);
|
console.warn('重定向跳转失败,跳转到默认页面:', error);
|
||||||
await router.push('/coordinate');
|
await router.push('/userManagement');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 没有重定向或重定向到登录页,跳转到默认页面
|
// 没有重定向或重定向到登录页,跳转到默认页面
|
||||||
await router.push('/coordinate');
|
await router.push('/userManagement');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录跳转异常:', error);
|
console.error('登录跳转异常:', error);
|
||||||
// 降级处理:跳转到默认页面
|
// 降级处理:跳转到默认页面
|
||||||
await router.push('/coordinate');
|
await router.push('/userManagement');
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
329
src/views/menuManagement/index.vue
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2">
|
||||||
|
<transition>
|
||||||
|
<div class="mb-[10px]">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
|
||||||
|
<!-- <el-form-item label="菜单名称" prop="permission_name">
|
||||||
|
<el-input v-model="queryParams.permission_name" placeholder="请输入菜单名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item> -->
|
||||||
|
<!-- <el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="菜单状态" clearable>
|
||||||
|
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||||
|
</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 shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="primary" plain icon="Plus" @click="handleAdd()">新增 </el-button>
|
||||||
|
</el-col>
|
||||||
|
<!-- <el-col :span="1.5">
|
||||||
|
<el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
|
||||||
|
</el-col> -->
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="menuList"
|
||||||
|
row-key="uid"
|
||||||
|
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||||
|
ref="menuTableRef"
|
||||||
|
:default-expand-all="isExpandAll"
|
||||||
|
>
|
||||||
|
<el-table-column prop="permission_name" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
|
||||||
|
<el-table-column prop="permission_code" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
|
||||||
|
<el-table-column prop="resource_path" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
|
||||||
|
<el-table-column label="类型" align="center" prop="permission_type">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-if="scope.row.permission_type === 1" type="primary">菜单</el-tag>
|
||||||
|
<el-tag v-if="scope.row.permission_type === 3" type="info">按钮</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="description" label="权限描述" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column prop="http_method" label="请求方式" :show-overflow-tooltip="true" width="80" />
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ parseTime(scope.row.created_at) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column fixed="right" label="操作" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tooltip content="修改" placement="top">
|
||||||
|
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"/>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="新增" placement="top">
|
||||||
|
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)"/>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="删除" placement="top">
|
||||||
|
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"/>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog :title="dialog.title" v-model="dialog.visible" destroy-on-close append-to-body width="750px">
|
||||||
|
<el-form ref="menuFormRef" :model="form" :rules="rules" label-width="100px">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="上级菜单">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="form.parent_uid"
|
||||||
|
:data="menuOptions"
|
||||||
|
:props="{ value: 'uid', label: 'permission_name', children: 'children' }"
|
||||||
|
value-key="uid"
|
||||||
|
placeholder="选择上级菜单"
|
||||||
|
check-strictly
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="菜单名称" prop="permission_name">
|
||||||
|
<el-input v-model="form.permission_name" placeholder="请输入菜单名称"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="菜单类型" prop="permission_type">
|
||||||
|
<el-radio-group v-model="form.permission_type">
|
||||||
|
<el-radio :label="1">菜单</el-radio>
|
||||||
|
<el-radio :label="3">按钮</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item prop="permission_code">
|
||||||
|
<el-input v-model="form.permission_code" placeholder="请输入权限标识" maxlength="100" />
|
||||||
|
<template #label>
|
||||||
|
<span>
|
||||||
|
<el-tooltip content="控制器中定义的权限字符,如:user:list')" placement="top">
|
||||||
|
<el-icon>
|
||||||
|
<question-filled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
权限字符
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24" v-if="form.permission_type === 1">
|
||||||
|
<el-form-item prop="resource_path">
|
||||||
|
<template #label>
|
||||||
|
<span>
|
||||||
|
<el-tooltip content="访问的路径,如:`/user/index`" placement="top">
|
||||||
|
<el-icon>
|
||||||
|
<question-filled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
路径
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="form.resource_path" placeholder="请输入组件路径" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<!-- <el-col :span="24" v-if="form.permission_type === 3">
|
||||||
|
<el-form-item prop="http_method">
|
||||||
|
<template #label>
|
||||||
|
<span>
|
||||||
|
<el-tooltip content="请求方法,如:`GET`、`POST`、`PUT`、`DELETE`等" placement="top">
|
||||||
|
<el-icon>
|
||||||
|
<question-filled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
请求方法
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="form.http_method" placeholder="请输入请求方法" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col> -->
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="权限描述" prop="description">
|
||||||
|
<el-input v-model="form.description" type="textarea" placeholder="请输入权限描述"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="menuManagement">
|
||||||
|
import { addMenu, delMenu, listMenu, updateMenu } from '@/api/menu.js';
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
|
const menuList = ref([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const menuOptions = ref([])
|
||||||
|
const isExpandAll = ref(false)
|
||||||
|
|
||||||
|
const dialog = reactive({
|
||||||
|
visible: false,
|
||||||
|
title: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryFormRef = ref();
|
||||||
|
const menuFormRef = ref();
|
||||||
|
const menuTableRef = ref();
|
||||||
|
const initFormData = {
|
||||||
|
permission_name: '',
|
||||||
|
permission_type: 1,// 1:菜单 3:按钮 2:接口
|
||||||
|
permission_code: '',
|
||||||
|
resource_path: '',
|
||||||
|
http_method: '',
|
||||||
|
description: '',
|
||||||
|
parent_uid: '',
|
||||||
|
}
|
||||||
|
const data = reactive({
|
||||||
|
form: { ...initFormData },
|
||||||
|
queryParams: {
|
||||||
|
permission_types:'1,3'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
permission_name: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
|
||||||
|
permission_code: [{ required: true, message: "权限标识不能为空", trigger: "blur" }],
|
||||||
|
permission_type: [{ required: true, message: "菜单类型不能为空", trigger: "change" }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const { queryParams, form, rules } = toRefs(data)
|
||||||
|
/** 查询菜单列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
const res = await listMenu(queryParams.value);
|
||||||
|
menuList.value = res.data
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
/** 查询菜单下拉树结构 */
|
||||||
|
const getTreeselect = async () => {
|
||||||
|
menuOptions.value = []
|
||||||
|
const response = await listMenu(queryParams.value);
|
||||||
|
const menu = { uid: '0', permission_name: "主类目", children: [] }
|
||||||
|
menu.children = response.data
|
||||||
|
menuOptions.value.push(menu)
|
||||||
|
}
|
||||||
|
/** 取消按钮 */
|
||||||
|
const cancel = () => {
|
||||||
|
// reset()
|
||||||
|
dialog.visible = false
|
||||||
|
}
|
||||||
|
/** 表单重置 */
|
||||||
|
const reset = () => {
|
||||||
|
form.value = { ...initFormData };
|
||||||
|
menuFormRef.value?.resetFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
}
|
||||||
|
/** 新增按钮操作 */
|
||||||
|
const handleAdd = (row) => {
|
||||||
|
reset();
|
||||||
|
getTreeselect();
|
||||||
|
row && row.uid ? form.value.parent_uid = row.uid : form.value.parent_uid = '';
|
||||||
|
dialog.visible = true;
|
||||||
|
dialog.title = "添加菜单";
|
||||||
|
}
|
||||||
|
/** 展开/折叠操作 */
|
||||||
|
const handleToggleExpandAll = () => {
|
||||||
|
isExpandAll.value = !isExpandAll.value;
|
||||||
|
toggleExpandAll(menuList.value, isExpandAll.value)
|
||||||
|
}
|
||||||
|
/** 展开/折叠所有 */
|
||||||
|
const toggleExpandAll = (data, status) => {
|
||||||
|
data.forEach((item) => {
|
||||||
|
menuTableRef.value?.toggleRowExpansion(item, status)
|
||||||
|
if (item.children && item.children.length > 0) toggleExpandAll(item.children, status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/** 修改按钮操作 */
|
||||||
|
const handleUpdate = async (row) => {
|
||||||
|
reset();
|
||||||
|
await getTreeselect();
|
||||||
|
if(row.parent_uid == ''){
|
||||||
|
row.parent_uid = '0'
|
||||||
|
}
|
||||||
|
form.value = row;
|
||||||
|
dialog.visible = true;
|
||||||
|
dialog.title = "修改菜单";
|
||||||
|
}
|
||||||
|
/** 提交按钮 */
|
||||||
|
const submitForm = () => {
|
||||||
|
try {
|
||||||
|
menuFormRef.value?.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
form.value.uid ? await updateMenu(form.value) : await addMenu(form.value);
|
||||||
|
ElMessage.success('操作成功');
|
||||||
|
dialog.visible = false;
|
||||||
|
await getList();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('操作失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确认要删除"${row.permission_name}"的数据项?`,
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const menuid = row?.uid;
|
||||||
|
const res = await delMenu(menuid);
|
||||||
|
if(res.meta.code !== 200){
|
||||||
|
ElMessage.error(res.data.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await getList();
|
||||||
|
ElMessage.success(`${row.permission_name}权限删除成功`);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err?.toString() || '';
|
||||||
|
if (errorMsg.includes('cancel') ||
|
||||||
|
errorMsg.includes('取消') ||
|
||||||
|
errorMsg === 'cancel' ||
|
||||||
|
err === 'cancel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errorMsg.includes('close') || err === 'close') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.error(`${row.permission_name}权限删除失败`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
144
src/views/patrolMission/distributionMenu.vue
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<el-row>
|
||||||
|
<el-dialog :title="title" v-model="visible" width="50%" top="5vh" append-to-body>
|
||||||
|
<el-form :model="form" label-width="80px" ref="dataScopeRef">
|
||||||
|
<el-form-item label="权限列表">
|
||||||
|
<el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand($event)">展开/折叠</el-checkbox>
|
||||||
|
<!-- <el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event)">全选/全不选</el-checkbox> -->
|
||||||
|
<el-tree
|
||||||
|
class="tree-border"
|
||||||
|
:data="deptOptions"
|
||||||
|
show-checkbox
|
||||||
|
default-expand-all
|
||||||
|
ref="deptRef"
|
||||||
|
node-key="uid"
|
||||||
|
:check-strictly="false"
|
||||||
|
empty-text="加载中,请稍候"
|
||||||
|
:props="{ label: 'permission_name', children: 'children' }"
|
||||||
|
></el-tree>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitDataScope">确 定</el-button>
|
||||||
|
<el-button @click="cancelDataScope">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="distributionMenu">
|
||||||
|
import { listMenu, dataScope ,getRolePermissions} from '@/api/menu.js';
|
||||||
|
|
||||||
|
const form = ref(
|
||||||
|
{
|
||||||
|
permission_uids: [],
|
||||||
|
roleId: undefined
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const deptRef = ref()
|
||||||
|
const dataScopeRef = ref()
|
||||||
|
const deptOptions = ref([])
|
||||||
|
const deptNodeAll = ref(false)
|
||||||
|
const deptExpand = ref(true)
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const title = ref('')
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
userId:'',
|
||||||
|
role_name: undefined,
|
||||||
|
role_code: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableRef = ref();
|
||||||
|
const queryFormRef = ref();
|
||||||
|
|
||||||
|
const show = async (rows,type) => {
|
||||||
|
title.value = type == 'menu' ? '分配菜单权限' : '分配按钮权限'
|
||||||
|
await getList(type);
|
||||||
|
visible.value = true;
|
||||||
|
form.value.roleId = rows.uid;
|
||||||
|
const res = await getRolePermissions(rows.uid);
|
||||||
|
const checkedKeys = []
|
||||||
|
if(res?.meta.code == 200){
|
||||||
|
const checkedKeys = res.data ? res.data.map(item => item.uid) : [];
|
||||||
|
deptRef.value?.setCheckedKeys(checkedKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取权限树
|
||||||
|
const getList = async (type) => {
|
||||||
|
loading.value = true;
|
||||||
|
const params = {
|
||||||
|
permission_types: type == 'menu' ? '1,3': '2'
|
||||||
|
}
|
||||||
|
const res = await listMenu(params);
|
||||||
|
deptOptions.value = res.data;
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 树权限(全选/全不选) */
|
||||||
|
const handleCheckedTreeNodeAll = (value) => {
|
||||||
|
deptRef.value?.setCheckedNodes(value ? deptOptions.value : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 树权限(展开/折叠)*/
|
||||||
|
const handleCheckedTreeExpand = (value) => {
|
||||||
|
let treeList = deptOptions.value;
|
||||||
|
for (let i = 0; i < treeList.length; i++) {
|
||||||
|
if (deptRef.value) {
|
||||||
|
deptRef.value.store.nodesMap[treeList[i].uid].expanded = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 所有菜单节点数据 */
|
||||||
|
const getDeptAllCheckedKeys = async () => {
|
||||||
|
// 目前被选中的菜单节点
|
||||||
|
let checkedKeys = deptRef.value?.getCheckedKeys();
|
||||||
|
// 半选中的菜单节点
|
||||||
|
let halfCheckedKeys = deptRef.value?.getHalfCheckedKeys();
|
||||||
|
if (halfCheckedKeys) {
|
||||||
|
checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
|
||||||
|
}
|
||||||
|
return checkedKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交按钮(数据权限)
|
||||||
|
const submitDataScope = async () => {
|
||||||
|
if (form.value.roleId) {
|
||||||
|
form.value.permission_uids = await getDeptAllCheckedKeys();
|
||||||
|
const res = await dataScope(form.value);
|
||||||
|
if (res.meta.code === 200) {
|
||||||
|
ElMessage.success(`分配成功`);
|
||||||
|
visible.value = false;
|
||||||
|
getList();
|
||||||
|
}else {
|
||||||
|
ElMessage.error(`分配失败`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消按钮(数据权限)*/
|
||||||
|
const cancelDataScope = () => {
|
||||||
|
dataScopeRef.value?.resetFields();
|
||||||
|
form.value = {
|
||||||
|
permission_uids: [],
|
||||||
|
roleId: undefined
|
||||||
|
};
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
// 暴露
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
||||||
346
src/views/patrolMission/index.vue
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2">
|
||||||
|
<transition>
|
||||||
|
<div class="mb-[10px]">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
|
||||||
|
<!-- <el-form-item label="角色名称" prop="role_name">
|
||||||
|
<el-input v-model="queryParams.role_name" placeholder="请输入角色名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="权限字符" prop="role_code">
|
||||||
|
<el-input v-model="queryParams.role_code" placeholder="请输入权限字符" clearable style="width: 240px" @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item> -->
|
||||||
|
<el-form-item label="任务开始时间" label-width="120px" style="width: 400px">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||||
|
></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery" icon="Search">搜索</el-button>
|
||||||
|
<el-button @click="resetQuery" icon="Refresh">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="primary" plain @click="handleAdd()" icon="Plus">新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-table ref="roleTableRef" v-loading="loading" :data="roleList">
|
||||||
|
<el-table-column label="任务名称" prop="role_name" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="执行人" prop="role_code" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="作业项数" prop="description"/>
|
||||||
|
<el-table-column label="作业状态" align="center" width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="任务状态" align="center" width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="任务起止时间" align="center" prop="createTime">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ parseTime(scope.row.created_at) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="任务结束时间" align="center" prop="createTime">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ parseTime(scope.row.created_at) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column fixed="right" label="操作" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tooltip content="修改" placement="top">
|
||||||
|
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="删除" placement="top">
|
||||||
|
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<!-- <el-tooltip content="分配菜单权限" placement="top">
|
||||||
|
<el-button link type="primary" icon="Menu" @click="handleAuthMenu(scope.row,'menu')"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="分配接口权限" placement="top">
|
||||||
|
<el-button link type="primary" icon="DocumentAdd" @click="handleAuthMenu(scope.row,'interface')"></el-button>
|
||||||
|
</el-tooltip> -->
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-if="total > 0"
|
||||||
|
v-model:total="total"
|
||||||
|
v-model:page="queryParams.page"
|
||||||
|
v-model:limit="queryParams.page_size"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
|
||||||
|
<el-form ref="roleFormRef" :model="form" :rules="rules" label-width="100px">
|
||||||
|
<el-form-item label="角色名称" prop="role_name">
|
||||||
|
<el-input v-model="form.role_name" placeholder="请输入角色名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="role_code">
|
||||||
|
<template #label>
|
||||||
|
<span>
|
||||||
|
<el-tooltip content="控制器中定义的权限字符,如:admin" placement="top">
|
||||||
|
<el-icon><question-filled /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
权限字符
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="form.role_code" placeholder="请输入权限字符" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.description" type="textarea" placeholder="请输入内容"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 分配权限 -->
|
||||||
|
<DistributionMenu ref="distributionMenuRef"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="roleManagement">
|
||||||
|
import { addRole, changeRoleStatus, delRole, getRole, listRole, updateRole } from "@/api/role.js";
|
||||||
|
import { roleMenuTreeselect, treeselect } from '@/api/menu.js';
|
||||||
|
import { ElMessage ,ElMessageBox} from "element-plus";
|
||||||
|
import DistributionMenu from '@/views/roleManagement/distributionMenu.vue'
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { proxy } = getCurrentInstance();
|
||||||
|
|
||||||
|
const roleList = ref();
|
||||||
|
const loading = ref(true)
|
||||||
|
const total = ref(0)
|
||||||
|
const dateRange = ref(['', ''])
|
||||||
|
const menuOptions = ref([])
|
||||||
|
const sys_normal_disable = ref([
|
||||||
|
{ value: "1", label: "正常" },
|
||||||
|
{ value: "0", label: "停用" },
|
||||||
|
])
|
||||||
|
|
||||||
|
const queryFormRef = ref();
|
||||||
|
const roleFormRef = ref();
|
||||||
|
const menuRef = ref();
|
||||||
|
const deptRef = ref();
|
||||||
|
const distributionMenuRef = ref();
|
||||||
|
|
||||||
|
const initForm = {
|
||||||
|
uid: undefined,
|
||||||
|
role_name: '',
|
||||||
|
role_code: '',
|
||||||
|
description: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = reactive({
|
||||||
|
form: { ...initForm },
|
||||||
|
queryParams: {
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
role_name: '',
|
||||||
|
role_code: '',
|
||||||
|
created_from: '',
|
||||||
|
created_to:'' ,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
role_name: [{ required: true, message: "角色名称不能为空", trigger: "blur" }],
|
||||||
|
role_code: [{ required: true, message: "权限字符不能为空", trigger: "blur" }],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const { form, queryParams, rules } = toRefs(data)
|
||||||
|
|
||||||
|
const dialog = reactive({
|
||||||
|
visible: false,
|
||||||
|
title: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询角色列表
|
||||||
|
*/
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
queryParams.value.created_from = dateRange.value[0];
|
||||||
|
queryParams.value.created_to = dateRange.value[1];
|
||||||
|
const res = await listRole(queryParams.value);
|
||||||
|
loading.value = false;
|
||||||
|
roleList.value = res.data.roles;
|
||||||
|
total.value = res.data.pagination.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索按钮操作
|
||||||
|
*/
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.value.page = 1;
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跳转菜单分配 */
|
||||||
|
// const handleAuthMenu = (row) => {
|
||||||
|
// const roleId = row.uid;
|
||||||
|
// router.push("/role-auth/menu/" + roleId);
|
||||||
|
// }
|
||||||
|
const handleAuthMenu = (row,type) => {
|
||||||
|
distributionMenuRef.value?.show(row,type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
dateRange.value = ['', '']
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
}
|
||||||
|
/**删除按钮操作 */
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确认要删除"${row.role_name}" 角色吗?`,
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const roleid = row?.uid;
|
||||||
|
const res = await delRole(roleid);
|
||||||
|
if(res.meta.code !== 200){
|
||||||
|
ElMessage.error(res.data.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getList();
|
||||||
|
ElMessage.success(`${row.role_name}角色删除成功`);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err?.toString() || '';
|
||||||
|
if (errorMsg.includes('cancel') ||
|
||||||
|
errorMsg.includes('取消') ||
|
||||||
|
errorMsg === 'cancel' ||
|
||||||
|
err === 'cancel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errorMsg.includes('close') || err === 'close') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.error(`${row.role_name}角色删除失败`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 角色状态修改 */
|
||||||
|
const handleStatusChange = async (row) => {
|
||||||
|
let text = row.status === "1" ? "启用" : "停用";
|
||||||
|
try {
|
||||||
|
await proxy?.$modal.confirm('确认要"' + text + '""' + row.role_name + '"角色吗?');
|
||||||
|
await changeRoleStatus(row.roleId, row.status);
|
||||||
|
proxy?.$modal.msgSuccess(text + "成功");
|
||||||
|
} catch {
|
||||||
|
row.status = row.status === "0" ? "1" : "0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 所有部门节点数据 */
|
||||||
|
const getDeptAllCheckedKeys = () => {
|
||||||
|
// 目前被选中的部门节点
|
||||||
|
let checkedKeys = deptRef.value?.getCheckedKeys();
|
||||||
|
// 半选中的部门节点
|
||||||
|
let halfCheckedKeys = deptRef.value?.getHalfCheckedKeys();
|
||||||
|
if (halfCheckedKeys) {
|
||||||
|
checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
|
||||||
|
}
|
||||||
|
return checkedKeys
|
||||||
|
}
|
||||||
|
/** 重置新增的表单以及其他数据 */
|
||||||
|
const reset = () => {
|
||||||
|
menuRef.value?.setCheckedKeys([]);
|
||||||
|
form.value = { ...initForm };
|
||||||
|
roleFormRef.value?.resetFields();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加角色 */
|
||||||
|
const handleAdd = () => {
|
||||||
|
reset();
|
||||||
|
dialog.visible = true;
|
||||||
|
dialog.title = "添加角色";
|
||||||
|
}
|
||||||
|
/** 修改角色 */
|
||||||
|
const handleUpdate = async (row) => {
|
||||||
|
try{
|
||||||
|
reset();
|
||||||
|
const roleId = row?.uid
|
||||||
|
const {meta, data } = await getRole(roleId);
|
||||||
|
if(meta.code !== 200){
|
||||||
|
ElMessage.error(meta.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.assign(form.value, data);
|
||||||
|
dialog.title = "修改角色";
|
||||||
|
dialog.visible = true;
|
||||||
|
}catch(err){
|
||||||
|
ElMessage.error(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
/** 根据角色ID查询菜单树结构 */
|
||||||
|
const getRoleMenuTreeselect = (roleId) => {
|
||||||
|
return roleMenuTreeselect(roleId).then((res) => {
|
||||||
|
menuOptions.value = res.data.menus;
|
||||||
|
return res.data;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 所有菜单节点数据 */
|
||||||
|
const getMenuAllCheckedKeys = () => {
|
||||||
|
// 目前被选中的菜单节点
|
||||||
|
let checkedKeys = menuRef.value?.getCheckedKeys();
|
||||||
|
// 半选中的菜单节点
|
||||||
|
let halfCheckedKeys = menuRef.value?.getHalfCheckedKeys();
|
||||||
|
if (halfCheckedKeys) {
|
||||||
|
checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
|
||||||
|
}
|
||||||
|
return checkedKeys;
|
||||||
|
}
|
||||||
|
/** 提交按钮 */
|
||||||
|
const submitForm = () => {
|
||||||
|
roleFormRef.value?.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
form.value.uid ? await updateRole(form.value) : await addRole(form.value);
|
||||||
|
const text = form.value.uid ? "更新成功" : "添加成功";
|
||||||
|
ElMessage.success(text);
|
||||||
|
dialog.visible = false
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/** 取消按钮 */
|
||||||
|
const cancel = () => {
|
||||||
|
reset()
|
||||||
|
dialog.visible = false;
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
134
src/views/roleManagement/SelectMenu.vue
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<el-row>
|
||||||
|
<el-dialog title="选择角色" v-model="visible" width="70%" top="5vh" append-to-body>
|
||||||
|
<el-form :model="queryParams" ref="queryFormRef" :inline="true">
|
||||||
|
<el-form-item label="角色名称" prop="role_name">
|
||||||
|
<el-input v-model="queryParams.role_name" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="权限字符" prop="role_code">
|
||||||
|
<el-input v-model="queryParams.role_code" placeholder="请输入权限字符" clearable @keyup.enter="handleQuery" />
|
||||||
|
</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-row>
|
||||||
|
<el-table v-loading="loading" @row-click="clickRow" ref="tableRef" :data="userList" @selection-change="handleSelectionChange" height="260px">
|
||||||
|
<el-table-column type="selection" width="55"></el-table-column>
|
||||||
|
<el-table-column label="角色名称" prop="role_name" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="权限字符" prop="role_code" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="描述" prop="description" :show-overflow-tooltip="true"/>
|
||||||
|
<el-table-column label="创建时间" align="center" prop="created_at" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ parseTime(scope.row.created_at) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination v-if="total > 0" :total="total" v-model:page="queryParams.page" v-model:limit="queryParams.page_size" @pagination="getList" />
|
||||||
|
</el-row>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="handleSelectUser">确 定</el-button>
|
||||||
|
<el-button @click="visible = false">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="SelectUser">
|
||||||
|
import { authUserSelectAll, unallocatedUserList } from "@/api/role.js";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
userId: {
|
||||||
|
type: [Number, String]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance();
|
||||||
|
const sys_normal_disable = ref([
|
||||||
|
{
|
||||||
|
label: '启用',
|
||||||
|
value: '1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '停用',
|
||||||
|
value: '0'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const userList = ref([]);
|
||||||
|
const visible = ref(false);
|
||||||
|
const total = ref(0);
|
||||||
|
const roleIds = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
userId:'',
|
||||||
|
role_name: undefined,
|
||||||
|
role_code: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableRef = ref();
|
||||||
|
const queryFormRef = ref();
|
||||||
|
|
||||||
|
const show = () => {
|
||||||
|
queryParams.userId = props.userId;
|
||||||
|
getList();
|
||||||
|
visible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择行
|
||||||
|
*/
|
||||||
|
const clickRow = (row) => {
|
||||||
|
tableRef.value?.toggleRowSelection(row, false);
|
||||||
|
}
|
||||||
|
/** 多选框选中数据 */
|
||||||
|
const handleSelectionChange = (selection) => {
|
||||||
|
roleIds.value = selection.map((item) => item.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询数据 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await unallocatedUserList(queryParams);
|
||||||
|
userList.value = res.data.roles;
|
||||||
|
total.value = res.data.pagination.total;
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.page = 1;
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits(["ok"]);
|
||||||
|
/**选择授权用户操作 */
|
||||||
|
const handleSelectUser = async () => {
|
||||||
|
const userId = queryParams.userId;
|
||||||
|
const ids = roleIds.value;
|
||||||
|
if (ids.length == 0) {
|
||||||
|
ElMessage.error('请选择要分配的角色');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await authUserSelectAll({ userId, role_uids: ids });
|
||||||
|
ElMessage.success('分配成功');
|
||||||
|
emit('ok');
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
// 暴露
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
186
src/views/roleManagement/authMenu.vue
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2">
|
||||||
|
<transition>
|
||||||
|
<div class="search mb-[10px]">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form :model="queryParams" ref="queryFormRef" :inline="true">
|
||||||
|
<el-form-item label="用户名称" prop="role_name">
|
||||||
|
<el-input v-model="queryParams.role_name" placeholder="请输入用户名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="权限字符" prop="role_code">
|
||||||
|
<el-input v-model="queryParams.role_code" placeholder="请输入权限字符" clearable style="width: 240px" @keyup.enter="handleQuery" />
|
||||||
|
</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 shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="primary" plain icon="Plus" @click="openSelectRole">添加角色</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="danger" plain icon="CircleClose" :disabled="multiple" @click="cancelAuthUserAll">
|
||||||
|
批量取消授权
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="warning" plain icon="Close" @click="handleClose">关闭</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column label="角色名称" prop="role_name" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="权限字符" prop="role_code" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="描述" prop="description" :show-overflow-tooltip="true"/>
|
||||||
|
<el-table-column label="创建时间" align="center" prop="created_at" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ parseTime(scope.row.created_at) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tooltip content="取消授权" placement="top">
|
||||||
|
<el-button link type="primary" icon="CircleClose" @click="cancelAuthUser(scope.row)"> </el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.page" v-model:limit="queryParams.page_size" @pagination="getList" />
|
||||||
|
<select-menu ref="selectRef" :userId="queryParams.userId" @ok="handleQuery" />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="authRole">
|
||||||
|
import { allocatedUserList, authUserCancel } from "@/api/role.js";
|
||||||
|
import SelectMenu from "./SelectMenu.vue"; // 引入选择角色组件
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const { proxy } = getCurrentInstance();
|
||||||
|
|
||||||
|
const userList = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const multiple = ref(true);
|
||||||
|
const total = ref(0);
|
||||||
|
const roleIds = ref([]);
|
||||||
|
|
||||||
|
const sys_normal_disable = ref([
|
||||||
|
{
|
||||||
|
label: '启用',
|
||||||
|
value: '1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '停用',
|
||||||
|
value: '0'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const queryFormRef = ref();
|
||||||
|
const selectRef = ref();
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
userId: route.params.userId,
|
||||||
|
role_name: undefined,
|
||||||
|
role_code: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 查询当前用户下的角色 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await allocatedUserList(queryParams);
|
||||||
|
userList.value = res.data.roles;
|
||||||
|
total.value = res.data.pagination.total;
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
// 返回按钮
|
||||||
|
const handleClose = () => {
|
||||||
|
router.push('/userManagement/UserManagementPage')
|
||||||
|
|
||||||
|
}
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNum = 1;
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
}
|
||||||
|
// 多选框选中数据
|
||||||
|
const handleSelectionChange = (selection) => {
|
||||||
|
roleIds.value = selection.map(item => item.uid);
|
||||||
|
multiple.value = !selection.length;
|
||||||
|
}
|
||||||
|
/** 打开授权用户表弹窗 */
|
||||||
|
const openSelectRole = () => {
|
||||||
|
selectRef.value?.show();
|
||||||
|
}
|
||||||
|
/** 取消授权按钮操作 */
|
||||||
|
const cancelAuthUser = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确认要取消该用户"${row.role_name}" 角色吗?`,
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if(!route.params.userId){
|
||||||
|
ElMessage.error(`不能取消当前登录用户的角色`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await authUserCancel({ userId: route.params.userId, role_uids: [row.uid] });
|
||||||
|
await getList();
|
||||||
|
ElMessage.success(`取消授权成功`);
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error(`取消授权失败`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** 批量取消授权按钮操作 */
|
||||||
|
const cancelAuthUserAll = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`是否取消选中角色授权数据项?`,
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if(!route.params.userId){
|
||||||
|
ElMessage.error(`不能取消当前登录用户的角色`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uIds = roleIds.value;
|
||||||
|
await authUserCancel({ userId: route.params.userId, role_uids:uIds });
|
||||||
|
await getList();
|
||||||
|
ElMessage.success(`取消授权成功`);
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error(`取消授权失败`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
144
src/views/roleManagement/distributionMenu.vue
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<el-row>
|
||||||
|
<el-dialog :title="title" v-model="visible" width="50%" top="5vh" append-to-body>
|
||||||
|
<el-form :model="form" label-width="80px" ref="dataScopeRef">
|
||||||
|
<el-form-item label="权限列表">
|
||||||
|
<el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand($event)">展开/折叠</el-checkbox>
|
||||||
|
<!-- <el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event)">全选/全不选</el-checkbox> -->
|
||||||
|
<el-tree
|
||||||
|
class="tree-border"
|
||||||
|
:data="deptOptions"
|
||||||
|
show-checkbox
|
||||||
|
default-expand-all
|
||||||
|
ref="deptRef"
|
||||||
|
node-key="uid"
|
||||||
|
:check-strictly="false"
|
||||||
|
empty-text="加载中,请稍候"
|
||||||
|
:props="{ label: 'permission_name', children: 'children' }"
|
||||||
|
></el-tree>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitDataScope">确 定</el-button>
|
||||||
|
<el-button @click="cancelDataScope">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="distributionMenu">
|
||||||
|
import { listMenu, dataScope ,getRolePermissions} from '@/api/menu.js';
|
||||||
|
|
||||||
|
const form = ref(
|
||||||
|
{
|
||||||
|
permission_uids: [],
|
||||||
|
roleId: undefined
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const deptRef = ref()
|
||||||
|
const dataScopeRef = ref()
|
||||||
|
const deptOptions = ref([])
|
||||||
|
const deptNodeAll = ref(false)
|
||||||
|
const deptExpand = ref(true)
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const title = ref('')
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
userId:'',
|
||||||
|
role_name: undefined,
|
||||||
|
role_code: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableRef = ref();
|
||||||
|
const queryFormRef = ref();
|
||||||
|
|
||||||
|
const show = async (rows,type) => {
|
||||||
|
title.value = type == 'menu' ? '分配菜单权限' : '分配按钮权限'
|
||||||
|
await getList(type);
|
||||||
|
visible.value = true;
|
||||||
|
form.value.roleId = rows.uid;
|
||||||
|
const res = await getRolePermissions(rows.uid);
|
||||||
|
const checkedKeys = []
|
||||||
|
if(res?.meta.code == 200){
|
||||||
|
const checkedKeys = res.data ? res.data.map(item => item.uid) : [];
|
||||||
|
deptRef.value?.setCheckedKeys(checkedKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取权限树
|
||||||
|
const getList = async (type) => {
|
||||||
|
loading.value = true;
|
||||||
|
const params = {
|
||||||
|
permission_types: type == 'menu' ? '1,3': '2'
|
||||||
|
}
|
||||||
|
const res = await listMenu(params);
|
||||||
|
deptOptions.value = res.data;
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 树权限(全选/全不选) */
|
||||||
|
const handleCheckedTreeNodeAll = (value) => {
|
||||||
|
deptRef.value?.setCheckedNodes(value ? deptOptions.value : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 树权限(展开/折叠)*/
|
||||||
|
const handleCheckedTreeExpand = (value) => {
|
||||||
|
let treeList = deptOptions.value;
|
||||||
|
for (let i = 0; i < treeList.length; i++) {
|
||||||
|
if (deptRef.value) {
|
||||||
|
deptRef.value.store.nodesMap[treeList[i].uid].expanded = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 所有菜单节点数据 */
|
||||||
|
const getDeptAllCheckedKeys = async () => {
|
||||||
|
// 目前被选中的菜单节点
|
||||||
|
let checkedKeys = deptRef.value?.getCheckedKeys();
|
||||||
|
// 半选中的菜单节点
|
||||||
|
let halfCheckedKeys = deptRef.value?.getHalfCheckedKeys();
|
||||||
|
if (halfCheckedKeys) {
|
||||||
|
checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
|
||||||
|
}
|
||||||
|
return checkedKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交按钮(数据权限)
|
||||||
|
const submitDataScope = async () => {
|
||||||
|
if (form.value.roleId) {
|
||||||
|
form.value.permission_uids = await getDeptAllCheckedKeys();
|
||||||
|
const res = await dataScope(form.value);
|
||||||
|
if (res.meta.code === 200) {
|
||||||
|
ElMessage.success(`分配成功`);
|
||||||
|
visible.value = false;
|
||||||
|
getList();
|
||||||
|
}else {
|
||||||
|
ElMessage.error(`分配失败`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消按钮(数据权限)*/
|
||||||
|
const cancelDataScope = () => {
|
||||||
|
dataScopeRef.value?.resetFields();
|
||||||
|
form.value = {
|
||||||
|
permission_uids: [],
|
||||||
|
roleId: undefined
|
||||||
|
};
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
// 暴露
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
||||||
337
src/views/roleManagement/index.vue
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2">
|
||||||
|
<transition>
|
||||||
|
<div class="mb-[10px]">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
|
||||||
|
<el-form-item label="角色名称" prop="role_name">
|
||||||
|
<el-input v-model="queryParams.role_name" placeholder="请输入角色名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="权限字符" prop="role_code">
|
||||||
|
<el-input v-model="queryParams.role_code" placeholder="请输入权限字符" clearable style="width: 240px" @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间" style="width: 308px">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||||
|
></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery" icon="Search">搜索</el-button>
|
||||||
|
<el-button @click="resetQuery" icon="Refresh">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="primary" plain @click="handleAdd()" icon="Plus">新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-table ref="roleTableRef" v-loading="loading" :data="roleList">
|
||||||
|
<el-table-column label="角色名称" prop="role_name" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="权限字符" prop="role_code" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="描述" prop="description"/>
|
||||||
|
<!-- <el-table-column label="状态" align="center" width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
|
||||||
|
</template>
|
||||||
|
</el-table-column> -->
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ parseTime(scope.row.created_at) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column fixed="right" label="操作" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tooltip content="修改" placement="top">
|
||||||
|
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="删除" placement="top">
|
||||||
|
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="分配菜单权限" placement="top">
|
||||||
|
<el-button link type="primary" icon="Menu" @click="handleAuthMenu(scope.row,'menu')"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="分配接口权限" placement="top">
|
||||||
|
<el-button link type="primary" icon="DocumentAdd" @click="handleAuthMenu(scope.row,'interface')"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-if="total > 0"
|
||||||
|
v-model:total="total"
|
||||||
|
v-model:page="queryParams.page"
|
||||||
|
v-model:limit="queryParams.page_size"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
|
||||||
|
<el-form ref="roleFormRef" :model="form" :rules="rules" label-width="100px">
|
||||||
|
<el-form-item label="角色名称" prop="role_name">
|
||||||
|
<el-input v-model="form.role_name" placeholder="请输入角色名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="role_code">
|
||||||
|
<template #label>
|
||||||
|
<span>
|
||||||
|
<el-tooltip content="控制器中定义的权限字符,如:admin" placement="top">
|
||||||
|
<el-icon><question-filled /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
权限字符
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="form.role_code" placeholder="请输入权限字符" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.description" type="textarea" placeholder="请输入内容"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 分配权限 -->
|
||||||
|
<DistributionMenu ref="distributionMenuRef"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="roleManagement">
|
||||||
|
import { addRole, changeRoleStatus, delRole, getRole, listRole, updateRole } from "@/api/role.js";
|
||||||
|
import { roleMenuTreeselect, treeselect } from '@/api/menu.js';
|
||||||
|
import { ElMessage ,ElMessageBox} from "element-plus";
|
||||||
|
import DistributionMenu from '@/views/roleManagement/distributionMenu.vue'
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { proxy } = getCurrentInstance();
|
||||||
|
|
||||||
|
const roleList = ref();
|
||||||
|
const loading = ref(true)
|
||||||
|
const total = ref(0)
|
||||||
|
const dateRange = ref(['', ''])
|
||||||
|
const menuOptions = ref([])
|
||||||
|
const sys_normal_disable = ref([
|
||||||
|
{ value: "1", label: "正常" },
|
||||||
|
{ value: "0", label: "停用" },
|
||||||
|
])
|
||||||
|
|
||||||
|
const queryFormRef = ref();
|
||||||
|
const roleFormRef = ref();
|
||||||
|
const menuRef = ref();
|
||||||
|
const deptRef = ref();
|
||||||
|
const distributionMenuRef = ref();
|
||||||
|
|
||||||
|
const initForm = {
|
||||||
|
uid: undefined,
|
||||||
|
role_name: '',
|
||||||
|
role_code: '',
|
||||||
|
description: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = reactive({
|
||||||
|
form: { ...initForm },
|
||||||
|
queryParams: {
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
role_name: '',
|
||||||
|
role_code: '',
|
||||||
|
created_from: '',
|
||||||
|
created_to:'' ,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
role_name: [{ required: true, message: "角色名称不能为空", trigger: "blur" }],
|
||||||
|
role_code: [{ required: true, message: "权限字符不能为空", trigger: "blur" }],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const { form, queryParams, rules } = toRefs(data)
|
||||||
|
|
||||||
|
const dialog = reactive({
|
||||||
|
visible: false,
|
||||||
|
title: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询角色列表
|
||||||
|
*/
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
queryParams.value.created_from = dateRange.value[0];
|
||||||
|
queryParams.value.created_to = dateRange.value[1];
|
||||||
|
const res = await listRole(queryParams.value);
|
||||||
|
loading.value = false;
|
||||||
|
roleList.value = res.data.roles;
|
||||||
|
total.value = res.data.pagination.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索按钮操作
|
||||||
|
*/
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.value.page = 1;
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跳转菜单分配 */
|
||||||
|
// const handleAuthMenu = (row) => {
|
||||||
|
// const roleId = row.uid;
|
||||||
|
// router.push("/role-auth/menu/" + roleId);
|
||||||
|
// }
|
||||||
|
const handleAuthMenu = (row,type) => {
|
||||||
|
distributionMenuRef.value?.show(row,type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
dateRange.value = ['', '']
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
}
|
||||||
|
/**删除按钮操作 */
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确认要删除"${row.role_name}" 角色吗?`,
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const roleid = row?.uid;
|
||||||
|
const res = await delRole(roleid);
|
||||||
|
if(res.meta.code !== 200){
|
||||||
|
ElMessage.error(res.data.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getList();
|
||||||
|
ElMessage.success(`${row.role_name}角色删除成功`);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err?.toString() || '';
|
||||||
|
if (errorMsg.includes('cancel') ||
|
||||||
|
errorMsg.includes('取消') ||
|
||||||
|
errorMsg === 'cancel' ||
|
||||||
|
err === 'cancel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errorMsg.includes('close') || err === 'close') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.error(`${row.role_name}角色删除失败`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 角色状态修改 */
|
||||||
|
const handleStatusChange = async (row) => {
|
||||||
|
let text = row.status === "1" ? "启用" : "停用";
|
||||||
|
try {
|
||||||
|
await proxy?.$modal.confirm('确认要"' + text + '""' + row.role_name + '"角色吗?');
|
||||||
|
await changeRoleStatus(row.roleId, row.status);
|
||||||
|
proxy?.$modal.msgSuccess(text + "成功");
|
||||||
|
} catch {
|
||||||
|
row.status = row.status === "0" ? "1" : "0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 所有部门节点数据 */
|
||||||
|
const getDeptAllCheckedKeys = () => {
|
||||||
|
// 目前被选中的部门节点
|
||||||
|
let checkedKeys = deptRef.value?.getCheckedKeys();
|
||||||
|
// 半选中的部门节点
|
||||||
|
let halfCheckedKeys = deptRef.value?.getHalfCheckedKeys();
|
||||||
|
if (halfCheckedKeys) {
|
||||||
|
checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
|
||||||
|
}
|
||||||
|
return checkedKeys
|
||||||
|
}
|
||||||
|
/** 重置新增的表单以及其他数据 */
|
||||||
|
const reset = () => {
|
||||||
|
menuRef.value?.setCheckedKeys([]);
|
||||||
|
form.value = { ...initForm };
|
||||||
|
roleFormRef.value?.resetFields();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加角色 */
|
||||||
|
const handleAdd = () => {
|
||||||
|
reset();
|
||||||
|
dialog.visible = true;
|
||||||
|
dialog.title = "添加角色";
|
||||||
|
}
|
||||||
|
/** 修改角色 */
|
||||||
|
const handleUpdate = async (row) => {
|
||||||
|
try{
|
||||||
|
reset();
|
||||||
|
const roleId = row?.uid
|
||||||
|
const {meta, data } = await getRole(roleId);
|
||||||
|
if(meta.code !== 200){
|
||||||
|
ElMessage.error(meta.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.assign(form.value, data);
|
||||||
|
dialog.title = "修改角色";
|
||||||
|
dialog.visible = true;
|
||||||
|
}catch(err){
|
||||||
|
ElMessage.error(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
/** 根据角色ID查询菜单树结构 */
|
||||||
|
const getRoleMenuTreeselect = (roleId) => {
|
||||||
|
return roleMenuTreeselect(roleId).then((res) => {
|
||||||
|
menuOptions.value = res.data.menus;
|
||||||
|
return res.data;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 所有菜单节点数据 */
|
||||||
|
const getMenuAllCheckedKeys = () => {
|
||||||
|
// 目前被选中的菜单节点
|
||||||
|
let checkedKeys = menuRef.value?.getCheckedKeys();
|
||||||
|
// 半选中的菜单节点
|
||||||
|
let halfCheckedKeys = menuRef.value?.getHalfCheckedKeys();
|
||||||
|
if (halfCheckedKeys) {
|
||||||
|
checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
|
||||||
|
}
|
||||||
|
return checkedKeys;
|
||||||
|
}
|
||||||
|
/** 提交按钮 */
|
||||||
|
const submitForm = () => {
|
||||||
|
roleFormRef.value?.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
form.value.uid ? await updateRole(form.value) : await addRole(form.value);
|
||||||
|
const text = form.value.uid ? "更新成功" : "添加成功";
|
||||||
|
ElMessage.success(text);
|
||||||
|
dialog.visible = false
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/** 取消按钮 */
|
||||||
|
const cancel = () => {
|
||||||
|
reset()
|
||||||
|
dialog.visible = false;
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
301
src/views/roomManagement/authRoom.vue
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2">
|
||||||
|
<transition>
|
||||||
|
<div class="search mb-[10px]">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form :model="queryParams" ref="queryFormRef" :inline="true">
|
||||||
|
<el-form-item label="参与者名称" prop="display_name" :label-width="120">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.display_name"
|
||||||
|
placeholder="请输入参与者名称"
|
||||||
|
clearable
|
||||||
|
style="width: 240px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="状态" clearable>
|
||||||
|
<el-option v-for="dict in statusList" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||||
|
</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 shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="warning" plain icon="Close" @click="handleClose">关闭</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-table v-loading="loading" :data="userList">
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
label="参与者名称"
|
||||||
|
prop="display_name"
|
||||||
|
:show-overflow-tooltip="true"
|
||||||
|
/>
|
||||||
|
<el-table-column label="状态" align="center" prop="status">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="STATUS_CONFIG[scope.row.status]?.type">
|
||||||
|
{{ STATUS_CONFIG[scope.row.status]?.text }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="参与角色" align="center" prop="participant_role">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.participant_role == "moderator" ? "发起者" : "参与者" }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="参与时间" align="center" prop="duration">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatMinutesSeconds(scope.row.duration) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" align="center" prop="created_at" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ parseTime(scope.row.created_at) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="操作"
|
||||||
|
align="center"
|
||||||
|
class-name="small-padding fixed-width"
|
||||||
|
>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tooltip content="踢出房间" placement="top" v-if="scope.row.status === 1">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
icon="CircleClose"
|
||||||
|
@click="removeParticipantHandld(scope.row)"
|
||||||
|
>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="静音" placement="top" v-if="scope.row.status === 1">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
icon="Mute"
|
||||||
|
@click="muteParticipantHandld(scope.row)"
|
||||||
|
>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<!-- <el-tooltip content="解除静音" placement="top" v-if="scope.row.status === 1">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
icon="Microphone"
|
||||||
|
@click="muteParticipantHandld(scope.row)"
|
||||||
|
>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip> -->
|
||||||
|
<el-tooltip content="删除" placement="top" >
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
icon="Delete"
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.page"
|
||||||
|
v-model:limit="queryParams.page_size"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="authRole">
|
||||||
|
import { participantUserList, delParticipant ,removeParticipant ,muteParticipant} from "@/api/room.js";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const { proxy } = getCurrentInstance();
|
||||||
|
|
||||||
|
const STATUS_CONFIG = {
|
||||||
|
1: { type: "success", text: "已同意并加入" },
|
||||||
|
2: { type: "info", text: "离开房间" },
|
||||||
|
3: { type: "warning", text: "被踢出房间" },
|
||||||
|
4: { type: "warning", text: "已邀请但未同意" },
|
||||||
|
5: { type: "danger", text: "拒绝加入" },
|
||||||
|
6: { type: "danger", text: "异常退出房间" },
|
||||||
|
}
|
||||||
|
const statusList = [
|
||||||
|
{
|
||||||
|
label: "已同意并加入",
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "离开房间",
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "被踢出房间",
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "已邀请但未同意",
|
||||||
|
value: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "拒绝加入",
|
||||||
|
value: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "异常退出房间",
|
||||||
|
value: 6,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const userList = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
|
const queryFormRef = ref();
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
room_uid: route.params.roomId,
|
||||||
|
display_name: undefined,
|
||||||
|
status: undefined,
|
||||||
|
});
|
||||||
|
// 格式化时分秒时间
|
||||||
|
function formatMinutesSeconds(seconds) {
|
||||||
|
if (!seconds && seconds !== 0) return "0秒";
|
||||||
|
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${seconds}秒`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
|
||||||
|
if (secs === 0) {
|
||||||
|
return `${mins}分钟`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${mins}分${secs}秒`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询当前用户下的角色 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await participantUserList(queryParams);
|
||||||
|
userList.value = res.data.data;
|
||||||
|
total.value = res.data.total;
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
// 返回按钮
|
||||||
|
const handleClose = () => {
|
||||||
|
router.push("/roomManagement/RoomManagementPage");
|
||||||
|
};
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.page = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const muteParticipantHandld = async (row) =>{
|
||||||
|
try{
|
||||||
|
//muted true 静音 false 解除禁用
|
||||||
|
const res = await muteParticipant(row.room_uid, { muted: true ,participant_uid:row.user_uid});
|
||||||
|
if (res.meta.code !== 200) {
|
||||||
|
ElMessage.error(res.data.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getList();
|
||||||
|
ElMessage.success(`${row.display_name}参与者静音成功`);
|
||||||
|
|
||||||
|
}catch(err){
|
||||||
|
ElMessage.error(`${row.display_name}参与者静音失败`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const removeParticipantHandld = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认要踢出"${row.display_name}" 参与者吗?`, "提示", {
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await removeParticipant(row.room_uid, { participant_uid: row.user_uid });
|
||||||
|
if (res.meta.code !== 200) {
|
||||||
|
ElMessage.error(res.data.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getList();
|
||||||
|
ElMessage.success(`${row.display_name}参与者踢出成功`);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err?.toString() || '';
|
||||||
|
if (errorMsg.includes('cancel') ||
|
||||||
|
errorMsg.includes('取消') ||
|
||||||
|
errorMsg === 'cancel' ||
|
||||||
|
err === 'cancel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errorMsg.includes('close') || err === 'close') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.error(`${row.display_name}参与者踢出失败`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**删除按钮操作 */
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认要删除"${row.display_name}" 参与者吗?`, "提示", {
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
// delete_participants:是否同时删除参与者信息,默认false
|
||||||
|
const res = await delParticipant(row.room_uid, { user_uid: row.user_uid });
|
||||||
|
if (res.meta.code !== 200) {
|
||||||
|
ElMessage.error(res.data.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getList();
|
||||||
|
ElMessage.success(`${row.display_name}参与者删除成功`);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err?.toString() || '';
|
||||||
|
if (errorMsg.includes('cancel') ||
|
||||||
|
errorMsg.includes('取消') ||
|
||||||
|
errorMsg === 'cancel' ||
|
||||||
|
err === 'cancel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errorMsg.includes('close') || err === 'close') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.error(`${row.display_name}参与者删除失败`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
210
src/views/roomManagement/index.vue
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-2">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :lg="24" :xs="24">
|
||||||
|
<transition>
|
||||||
|
<div class="mb-[10px]" v-show="showSearch">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
|
||||||
|
<el-form-item label="房间名称" prop="name">
|
||||||
|
<el-input v-model="queryParams.name" placeholder="请输入房间名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="发起人" prop="owner_uid">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.owner_uid"
|
||||||
|
placeholder="请输入发起人"
|
||||||
|
clearable
|
||||||
|
style="width: 240px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="房间状态" clearable style="width: 240px">
|
||||||
|
<el-option v-for="dict in sys_normal_status" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间" style="width: 308px;">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||||
|
></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery" icon="Search">搜索</el-button>
|
||||||
|
<el-button @click="resetQuery" icon="Refresh">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row :gutter="10">
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-table v-loading="loading" :data="roomList" width='100%'>
|
||||||
|
<el-table-column label="房间编号" align="center" prop="uid" />
|
||||||
|
<el-table-column label="房间名称" align="center" prop="name" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="发起人" align="center" prop="owner_uid"/>
|
||||||
|
<el-table-column label="最大人数" align="center" prop="max_participants"/>
|
||||||
|
<!-- <el-table-column label="备注" align="center" prop="description" :show-overflow-tooltip="true"/> -->
|
||||||
|
<el-table-column label="状态" align="center" prop="status">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="STATUS_CONFIG[scope.row.status]?.type">
|
||||||
|
{{ STATUS_CONFIG[scope.row.status]?.text }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" align="center" prop="created_at">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ parseTime(scope.row.created_at) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" fixed="right" class-name="small-padding fixed-width">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tooltip content="删除" placement="top" v-if="scope.row.status === 3">
|
||||||
|
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="参与者信息" placement="top">
|
||||||
|
<el-button link type="primary" icon="User" @click="handleAuthRole(scope.row)"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination
|
||||||
|
v-show="total > 0"
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.page"
|
||||||
|
v-model:limit="queryParams.page_size"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="userManagement">
|
||||||
|
import { listRoom ,delRoom} from "@/api/room.js"
|
||||||
|
import { ElMessage ,ElMessageBox} from "element-plus";
|
||||||
|
const router = useRouter();
|
||||||
|
const roomList = ref();
|
||||||
|
const loading = ref(true);
|
||||||
|
const showSearch = ref(true);
|
||||||
|
const total = ref(0);
|
||||||
|
const dateRange = ref(['', '']);
|
||||||
|
const sys_normal_status = ref([
|
||||||
|
{
|
||||||
|
label: '进行中',
|
||||||
|
value: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '已结束',
|
||||||
|
value: 3
|
||||||
|
}
|
||||||
|
])
|
||||||
|
const STATUS_CONFIG = ref({
|
||||||
|
1: { type: 'success', text: '进行中' },
|
||||||
|
3: { type: 'danger', text: '已结束' },
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const queryFormRef = ref();
|
||||||
|
|
||||||
|
const data = reactive({
|
||||||
|
queryParams: {
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
name: '',
|
||||||
|
owner_uid: '',
|
||||||
|
status: undefined,
|
||||||
|
min_created_at: '',
|
||||||
|
max_created_at: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { queryParams } = toRefs(data)
|
||||||
|
|
||||||
|
|
||||||
|
/** 查询用户列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
if(dateRange.value.length > 0 && dateRange.value[0] && dateRange.value[1]){
|
||||||
|
queryParams.value.min_created_at = +new Date(dateRange.value[0]);
|
||||||
|
queryParams.value.max_created_at = +new Date(dateRange.value[1]);
|
||||||
|
}
|
||||||
|
const res = await listRoom(queryParams.value);
|
||||||
|
loading.value = false;
|
||||||
|
roomList.value = res.data.data;
|
||||||
|
total.value = res.data.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跳转参与者信息 */
|
||||||
|
const handleAuthRole = (row) => {
|
||||||
|
router.push("/room-auth/room/" + row.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.value.page = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
dateRange.value = ['', '']
|
||||||
|
queryParams.value.min_created_at = ''
|
||||||
|
queryParams.value.max_created_at = ''
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
queryParams.value.page = 1;
|
||||||
|
handleQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**删除按钮操作 */
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确认要删除"${row.name}" 房间吗?`,
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// delete_participants:是否同时删除参与者信息,默认false
|
||||||
|
const res = await delRoom(row.uid,{delete_participants:true});
|
||||||
|
if(res.meta.code !== 200){
|
||||||
|
ElMessage.error(res.data.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getList();
|
||||||
|
ElMessage.success(`${row.name}房间删除成功`);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err?.toString() || '';
|
||||||
|
if (errorMsg.includes('cancel') ||
|
||||||
|
errorMsg.includes('取消') ||
|
||||||
|
errorMsg === 'cancel' ||
|
||||||
|
err === 'cancel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errorMsg.includes('close') || err === 'close') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.error(`${row.name}房间删除失败`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList()
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||