feat:更新代码
Some checks failed
Deploy To Dev / deploy (push) Has been cancelled

This commit is contained in:
leilei
2026-01-14 17:08:07 +08:00
124 changed files with 3964 additions and 10524 deletions

1
dist/assets/401-B78AUXmB.js vendored Normal file
View File

@@ -0,0 +1 @@
import{_ as d,g as f,r as m,c as k,f as o,w as n,h as s,o as g,p as a,k as e}from"./index-DyWI3x4X.js";const h=""+new URL("401-HGF6Q5qM.gif",import.meta.url).href,w={class:"errPage-container"},x={class:"list-unstyled"},b={class:"link-type"},v=["src"],y={__name:"401",setup(B){let{proxy:r}=f();const u=m(h+"?"+ +new Date);function c(){r.$route.query.noGoBack?r.$router.push({path:"/"}):r.$router.go(-1)}return(C,t)=>{const _=s("el-button"),i=s("router-link"),l=s("el-col"),p=s("el-row");return g(),k("div",w,[o(_,{icon:"arrow-left",class:"pan-back-btn",onClick:c},{default:n(()=>[...t[0]||(t[0]=[a(" 返回 ",-1)])]),_:1}),o(p,null,{default:n(()=>[o(l,{span:12},{default:n(()=>[t[2]||(t[2]=e("h1",{class:"text-jumbo text-ginormous"}," 401错误! ",-1)),t[3]||(t[3]=e("h2",null,"您没有访问权限!",-1)),t[4]||(t[4]=e("h6",null,"对不起,您没有访问权限,请不要进行非法操作!您可以返回主页面",-1)),e("ul",x,[e("li",b,[o(i,{to:"/"},{default:n(()=>[...t[1]||(t[1]=[a(" 回首页 ",-1)])]),_:1})])])]),_:1}),o(l,{span:12},{default:n(()=>[e("img",{src:u.value,width:"313",height:"428",alt:"Girl has dropped her ice cream."},null,8,v)]),_:1})]),_:1})])}}},I=d(y,[["__scopeId","data-v-2c8b7582"]]);export{I as default};

BIN
dist/assets/401-B78AUXmB.js.gz vendored Normal file

Binary file not shown.

1
dist/assets/404-BS97o3Mb.js vendored Normal file
View File

@@ -0,0 +1 @@
import{_ as o,B as l,c as r,k as t,C as _,f as c,q as n,i as d,w as p,h as m,o as u,p as v}from"./index-DyWI3x4X.js";const h=""+new URL("404-N4aRkdWY.png",import.meta.url).href,a=""+new URL("404_cloud-CPexjtDj.png",import.meta.url).href,f={class:"wscn-http404-container"},g={class:"wscn-http404"},x={class:"bullshit"},k={class:"bullshit__headline"},w={__name:"404",setup(b){let e=l(()=>"找不到网页!");return(N,s)=>{const i=m("router-link");return u(),r("div",f,[t("div",g,[s[3]||(s[3]=_('<div class="pic-404" data-v-328ae272><img class="pic-404__parent" src="'+h+'" alt="404" data-v-328ae272><img class="pic-404__child left" src="'+a+'" alt="404" data-v-328ae272><img class="pic-404__child mid" src="'+a+'" alt="404" data-v-328ae272><img class="pic-404__child right" src="'+a+'" alt="404" data-v-328ae272></div>',1)),t("div",x,[s[1]||(s[1]=t("div",{class:"bullshit__oops"}," 404错误! ",-1)),t("div",k,n(d(e)),1),s[2]||(s[2]=t("div",{class:"bullshit__info"}," 对不起您正在寻找的页面不存在。尝试检查URL的错误然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。 ",-1)),c(i,{to:"/index",class:"bullshit__return-home"},{default:p(()=>[...s[0]||(s[0]=[v(" 返回首页 ",-1)])]),_:1})])])])}}},C=o(w,[["__scopeId","data-v-328ae272"]]);export{C as default};

BIN
dist/assets/404-BS97o3Mb.js.gz vendored Normal file

Binary file not shown.

1
dist/assets/authRole-WYvqp_Mv.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/authRole-WYvqp_Mv.js.gz vendored Normal file

Binary file not shown.

1
dist/assets/authRoom-BBxwRGru.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/authRoom-BBxwRGru.js.gz vendored Normal file

Binary file not shown.

1
dist/assets/index-BRI8Z0ig.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/index-BRI8Z0ig.js.gz vendored Normal file

Binary file not shown.

1
dist/assets/index-BTCuff5d.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/index-BTCuff5d.js.gz vendored Normal file

Binary file not shown.

1
dist/assets/index-BnIeKfOQ.js vendored Normal file
View File

@@ -0,0 +1 @@
import{u as s,a as c,c as n,o as u}from"./index-DyWI3x4X.js";const i={__name:"index",setup(p){const e=s(),t=c(),{params:o,query:a}=e,{path:r}=o;return t.replace({path:"/"+r,query:a}),(_,m)=>(u(),n("div"))}};export{i as default};

1
dist/assets/index-CeULEb6a.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/index-CeULEb6a.js.gz vendored Normal file

Binary file not shown.

1
dist/assets/index-CiEK6-Lk.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/index-CiEK6-Lk.js.gz vendored Normal file

Binary file not shown.

80
dist/assets/index-DyWI3x4X.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/index-DyWI3x4X.js.gz vendored Normal file

Binary file not shown.

1
dist/assets/index-DzgyBp0k.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/index-DzgyBp0k.js.gz vendored Normal file

Binary file not shown.

1
dist/assets/index-Nyh-4EGz.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/index-Nyh-4EGz.css.gz vendored Normal file

Binary file not shown.

1
dist/assets/login-C2MWIt8z.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/login-C2MWIt8z.css.gz vendored Normal file

Binary file not shown.

14
dist/assets/login-X-xHawH5.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/assets/login-X-xHawH5.js.gz vendored Normal file

Binary file not shown.

1
dist/assets/menu-DaF34q_h.js vendored Normal file
View File

@@ -0,0 +1 @@
import{A as e}from"./index-DyWI3x4X.js";const i=s=>e({url:"/api/v1/permission/permissions/tree",method:"get",params:s}),o=s=>e({url:"/api/v1/permission/permissions",method:"post",data:s}),t=s=>e({url:"/api/v1/permission/permissions/"+s.uid,method:"put",data:s}),n=s=>e({url:"/api/v1/permission/permissions/"+s,method:"delete"}),p=s=>e({url:"/api/v1/permission/roles/"+s.roleId+"/permissions/add",method:"post",data:s}),a=s=>e({url:"/api/v1/permission/roles/"+s+"/all-permissions",method:"get"});export{o as a,p as b,n as d,a as g,i as l,t as u};

1
dist/assets/role-e2W5ZQl3.js vendored Normal file
View File

@@ -0,0 +1 @@
import{A as s}from"./index-DyWI3x4X.js";const t=e=>s({url:"/api/v1/permission/roles",method:"get",params:e}),a=e=>s({url:"/api/v1/permission/roles/"+e,method:"get"}),l=e=>s({url:"/api/v1/permission/roles",method:"post",data:e}),i=e=>s({url:"/api/v1/permission/roles/"+e.uid,method:"put",data:e}),u=e=>s({url:"/api/v1/permission/roles/"+e,method:"delete"}),n=e=>s({url:"/api/v1/permission/users/"+e.userId+"/roles",method:"get",params:e}),p=(e,r)=>s({url:"/api/v1/permission/users/"+r+"/roles",method:"get",params:e}),d=e=>s({url:"/api/v1/permission/users/"+e.userId+"/roles/remove",method:"post",data:e}),m=e=>s({url:"/api/v1/permission/users/"+e.userId+"/roles/add",method:"post",data:e});export{l as a,p as b,m as c,u as d,n as e,d as f,a as g,t as l,i as u};

1
dist/assets/room-CCkgiVs_.js vendored Normal file
View File

@@ -0,0 +1 @@
import{A as r}from"./index-DyWI3x4X.js";const o=t=>r({url:"/api/v1/rooms/list",method:"get",params:t}),s=(t,a)=>r({url:"/api/v1/rooms/"+t,method:"delete",params:a}),i=t=>r({url:"/api/v1/rooms/participants/list",method:"get",params:t}),p=(t,a)=>r({url:"/api/v1/rooms/"+t+"/participants",method:"delete",params:a}),m=(t,a)=>r({url:"/api/v1/meeting/"+t+"/participant/remove",method:"post",data:a}),n=(t,a)=>r({url:"/api/v1/meeting/"+t+"/participant/mute",method:"post",data:a});export{p as a,s as d,o as l,n as m,i as p,m as r};

View File

@@ -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',
})
}

View File

@@ -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
View 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,
});
};

View File

@@ -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'
});
}

View File

@@ -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
View 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
View 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
View 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
};

View File

@@ -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;
} }

View File

@@ -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
} }

View 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>

View File

@@ -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

View File

@@ -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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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
} }

View File

@@ -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'

View File

@@ -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,6 +143,7 @@ function handleClickOutside() {
useAppStoreStore.closeSideBar({ withoutAnimation: false }) useAppStoreStore.closeSideBar({ withoutAnimation: false })
} }
<<<<<<< HEAD
onMounted(async () => { onMounted(async () => {
await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`); await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
const res = await userStore.getInfo() const res = await userStore.getInfo()
@@ -152,6 +155,9 @@ onMounted(async () => {
}); });
}) })
=======
onMounted(async () => {})
>>>>>>> 15da7d589f6c82c522e5f98cee178f72d20bdf48
</script> </script>
@@ -232,8 +238,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 +266,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 +323,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%;

View File

@@ -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, {

View File

@@ -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),

View File

@@ -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
}, },

View File

@@ -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,

View File

@@ -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
View 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();
};

View File

@@ -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)}`;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

Some files were not shown because too many files have changed in this diff Show More