Compare commits

...

11 Commits

Author SHA1 Message Date
leilei
15da7d589f feat:更新代码 2026-01-14 15:26:10 +08:00
leilei
a894367dcc feat:更新协作信息 2025-11-18 17:42:15 +08:00
leilei
ca93e91326 feat:更新白板 2025-11-11 18:01:59 +08:00
leilei
6fbe53009c feat:修改文件上传,白板关闭不影响其他用户等 2025-11-10 17:55:52 +08:00
leilei
df359d01cc feat:修改存在的问题 2025-11-03 09:03:32 +08:00
leilei
528170fe2f feat:更新激光笔功能 2025-10-23 10:22:56 +08:00
leilei
f19ab86ada feat:集成Sentry 2025-10-21 15:23:39 +08:00
leilei
2b8e7349e6 feat:更新画板工具栏显示问题 2025-10-21 11:26:12 +08:00
leilei
e0001ba430 feat:更新侧边导航,修改密码等 2025-10-20 17:41:54 +08:00
leilei
db72ea9f33 feat:跟新密码加密 共享白板功能 2025-10-11 17:28:20 +08:00
leilei
e429e4286a feat:更新协同合作 2025-09-30 17:58:47 +08:00
139 changed files with 13743 additions and 5487 deletions

View File

@@ -6,7 +6,8 @@ VITE_APP_PORT = 80
VITE_BASE_PATH = '/'
# 应用模板管理后台/生产环境
VITE_APP_BASE_API = 'https://xsynergy.gxtech.ltd'
# VITE_APP_BASE_API = 'https://xsynergy.gxtech.ltd'
VITE_APP_BASE_API = ''
# 公网为“web” 私有化为不跳转为“private” 私有化跳转为“skip”
VITE_APP_COOPERATION_TYPE = 'skip'

BIN
dist.zip Normal file

Binary file not shown.

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>互动白板</title>
<title>xSynergy远程协作系统</title>
</head>
<body>
<div id="app"></div>

6966
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,9 +11,12 @@
"dependencies": {
"@element-plus/icons-vue": "^2.0.10",
"@msgpack/msgpack": "^3.1.2",
"@sentry/vue": "^10.20.0",
"@vueuse/core": "^9.5.0",
"autoprefixer": "^10.4.21",
"axios": "^0.27.2",
"code-inspector-plugin": "^0.20.12",
"crypto-js": "^4.2.0",
"element-plus": "^2.2.27",
"js-cookie": "^3.0.1",
"livekit-client": "^2.7.5",
@@ -23,14 +26,20 @@
"postcss": "^8.5.6",
"tailwindcss": "^3.4.1",
"uuid": "^11.1.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-svg-icons": "^2.0.1",
"vue": "^3.5.12",
"vue-pdf": "^4.3.0",
"vue-pdf-embed": "^2.1.3",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"@vue/compiler-sfc": "^3.2.45",
"sass": "^1.56.1",
"unplugin-auto-import": "^20.1.0",
"unplugin-auto-import": "^0.11.4",
"unplugin-vue-components": "^29.0.0",
"unplugin-vue-setup-extend-plus": "^0.4.9",
"vite": "^5.4.10"
}
}

View File

@@ -1,6 +1,15 @@
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import { onMounted } from 'vue'
import { useUserStore } from '@/stores/modules/user.js'
// 在根组件预先初始化用户 store
onMounted(() => {
try {
const userStore = useUserStore()
} catch (error) {
console.warn('App.vue: Pinia 初始化中...', error)
}
})
</script>
<template>

View File

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

View File

@@ -1,19 +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
})
}

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

@@ -34,3 +34,37 @@ export function logout() {
method: 'post'
})
}
//修改密码
export function changePwd(oldPassword, newPassword) {
const params = {
old_password: oldPassword,
new_password: newPassword
}
return request({
url: '/api/v1/auth/change-password',
method: 'post',
data: params
})
}
//验证密码强度
export function checkPwdStrength(password) {
const params = {
password: password
}
return request({
url: '/api/v1/auth/check-password-strength',
method: 'post',
data: params
})
}
// 获取用户头像
export function getAvatarsApi(name) {
return request({
url: `/api/v1/avatars/username/${name}/200`,
method: 'get',
responseType: 'blob'
});
}

70
src/api/menu.js Normal file
View File

@@ -0,0 +1,70 @@
import request from '@/utils/request'
// 查询菜单列表
export const listMenu = (data) => {
return request({
url: '/api/v1/permission/permissions/tree',
method: 'get',
params: data
});
};
// 查询菜单下拉树结构
export const treeselect = () => {
return request({
url: '/system/menu/treeselect',
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
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
<filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
</defs>
<g id="配置面板" width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
<g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
<g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
<mask id="mask-3" fill="white">
<use xlink:href="#path-2"></use>
</mask>
<g id="Rectangle-18">
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
<use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
</g>
<rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
<rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
<filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
</defs>
<g id="配置面板" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="setting-copy-2" transform="translate(-1254.000000, -136.000000)">
<g id="Group-8" transform="translate(1167.000000, 0.000000)">
<g id="Group-5" filter="url(#filter-1)" transform="translate(89.000000, 137.000000)">
<mask id="mask-3" fill="white">
<use xlink:href="#path-2"></use>
</mask>
<g id="Rectangle-18">
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
<use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
</g>
<rect id="Rectangle-18" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
<rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -1,4 +1,5 @@
$menu-bg-color: #8290f0;
$menu-bg-color: #409EFF;
// $menu-bg-color: #434343;
#app {
.main-container {
@@ -92,12 +93,12 @@ $menu-bg-color: #8290f0;
.sub-menu-title-noDropdown,
.el-sub-menu__title {
&:hover {
background-color: #c4cbf3 !important;
background-color: #e5e5e5 !important;
}
}
.el-menu-item:hover {
background-color: #c4cbf3 !important;
background-color: #e5e5e5 !important;
}
& .theme-dark .is-active>.el-sub-menu__title {
@@ -109,7 +110,7 @@ $menu-bg-color: #8290f0;
min-width: $base-sidebar-width !important;
&:hover {
background-color: #c4cbf3 !important;
background-color: #e5e5e5 !important;
}
}
@@ -139,7 +140,7 @@ $menu-bg-color: #8290f0;
.sidebar-container {
// width: 54px !important;
height: calc(100vh - 50px);
background-color: red;
// background-color: red;
}

View File

@@ -0,0 +1,80 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
class="no-redirect"
>
{{ item.meta.title }}
</span>
<a v-else @click.prevent="handleLink(item)">
{{ item.meta.title }}
</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script setup>
const route = useRoute()
const router = useRouter()
const levelList = ref([])
function getBreadcrumb() {
// only show routes with meta.title
let matched = route.matched.filter((item) => item.meta && item.meta.title)
const first = matched[0]
// 判断是否为首页
if (!isDashboard(first)) {
matched = matched
}
levelList.value = matched.filter(
(item) => item.meta && item.meta.title && item.meta.breadcrumb !== false
)
}
function isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim() === 'Index'
}
function handleLink(item) {
const { redirect, path } = item
if (redirect) {
router.push(redirect)
return
}
router.push(path)
}
watchEffect(() => {
if (route.path.startsWith('/redirect/')) {
return
}
getBreadcrumb()
})
getBreadcrumb()
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-flex;
align-items: center;
height: 56px;
font-size: 16px;
line-height: 1;
margin-left: 8px;
.no-redirect {
color: #333333;
font-weight: 600;
cursor: text;
}
}
::v-deep .el-breadcrumb__separator {
color: #333333;
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script setup>
defineProps({
isActive: {
type: Boolean,
default: false
}
})
const emit = defineEmits()
const toggleClick = () => {
emit('toggleClick');
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

View File

@@ -26,15 +26,18 @@
</template>
<script setup>
import useUserStore from '@/stores/modules/user'
import { watch, ref, getCurrentInstance } from 'vue'
import { useUserStore } from '@/stores/modules/user.js'
import { watch, ref, getCurrentInstance,onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMeterStore } from '@/stores/modules/meter'
import CryptoJS from 'crypto-js';
const userStore = useUserStore()
const router = useRouter()
const { proxy } = getCurrentInstance()
const redirect = ref(undefined);
const emit = defineEmits(['loginSuccess'])
const meterStore = useMeterStore()
const loginForm = ref({
username: '',
@@ -52,14 +55,31 @@ function handleLogin() {
proxy.$refs.loginRef.validate((valid) => {
if (valid) {
loading.value = true
if(!localStorage?.getItem('UDID')){
ElMessage({
message: '服务错误,请刷新页面',
type: 'warning',
})
return
}
const secretKey = ((loginForm.value.username + localStorage?.getItem('UDID')).toLowerCase()).replaceAll('-', ''); // 用户名+UDID(32位16进制全小写)
const randomChars = generateRandomChars(6);
const message = `Gx${randomChars}${loginForm.value.password}`;
const ciphertext = CryptoJS.Blowfish.encrypt(message, secretKey).toString();
// 调用action的登录方法
userStore
.login({
password: loginForm.value.password,
password: ciphertext,
// password: loginForm.value.password,
username: loginForm.value.username,
})
.then(async (res) => {
const userInfo = JSON.parse(localStorage.getItem('userData'))
const userInfo = JSON.parse(sessionStorage.getItem('userData'))
emit('loginSuccess', userInfo)
})
.catch((e) => {
@@ -71,6 +91,16 @@ function handleLogin() {
requestNotificationPermission()
}
// 生成随机字符串
function generateRandomChars(length) {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* @description 请求浏览器的通知权限
* @returns {*}
@@ -83,13 +113,17 @@ function requestNotificationPermission() {
}
}
onMounted(() => {
meterStore.initUdid()
})
</script>
<style lang="scss" scoped>
.wrapper-content {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
height: calc(100vh - 60px);
padding: 0 40px;
background: linear-gradient(135deg, #f5f7fa 0%, #eef1f6 100%);
}
@@ -160,6 +194,7 @@ function requestNotificationPermission() {
transition: background 0.3s ease, transform 0.2s ease;
&:hover {
// background: linear-gradient(135deg, #000, #000);
background: linear-gradient(135deg, #00a5a1, #008f8b);
transform: translateY(-2px);
}

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

@@ -0,0 +1,3 @@
<template >
<router-view />
</template>

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

@@ -0,0 +1,210 @@
<template>
<el-menu
:default-active="activeMenu"
mode="horizontal"
@select="handleSelect"
:ellipsis="false"
>
<template v-for="(item, index) in topMenus">
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber"
><svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"
/>
{{ item.meta.title }}</el-menu-item
>
</template>
<!-- 顶部菜单超出数量折叠 -->
<el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
<template #title>更多菜单</template>
<template v-for="(item, index) in topMenus">
<el-menu-item
:index="item.path"
:key="index"
v-if="index >= visibleNumber"
><svg-icon :icon-class="item.meta.icon" />
{{ item.meta.title }}</el-menu-item
>
</template>
</el-sub-menu>
</el-menu>
</template>
<script setup>
import { constantRoutes } from "@/router/index.js"
import { isHttp } from '@/utils/validate.js'
import { useAppStore } from '@/stores/modules/app.js'
import { useSettingsStore } from '@/stores/modules/settings.js'
// 顶部栏初始数
const visibleNumber = ref(null);
// 当前激活菜单的 index
const currentIndex = ref(null);
// 隐藏侧边栏路由
const hideList = ['/index', '/user/profile'];
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const route = useRoute();
const router = useRouter();
// 主题颜色
const theme = computed(() => settingsStore.theme);
// 顶部显示菜单
const topMenus = computed(() => {
let topMenus = [];
routers.value.map((menu) => {
if (menu.hidden !== true) {
// 兼容顶部栏一级菜单内部跳转
if (menu.path === "/") {
topMenus.push(menu.children[0]);
} else {
topMenus.push(menu);
}
}
})
return topMenus;
})
// 设置子路由
const childrenMenus = computed(() => {
let childrenMenus = [];
routers.value.map((router) => {
for (let item in router.children) {
if (router.children[item].parentPath === undefined) {
if(router.path === "/") {
router.children[item].path = "/" + router.children[item].path;
} else {
if(!isHttp(router.children[item].path)) {
router.children[item].path = router.path + "/" + router.children[item].path;
}
}
router.children[item].parentPath = router.path;
}
childrenMenus.push(router.children[item]);
}
})
return constantRoutes.concat(childrenMenus);
})
// 默认激活的菜单
const activeMenu = computed(() => {
const path = route.path;
let activePath = path;
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
const tmpPath = path.substring(1, path.length);
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
if (!route.meta.link) {
appStore.toggleSideBarHide(false);
}
} else if(!route.children) {
activePath = path;
appStore.toggleSideBarHide(true);
}
activeRoutes(activePath);
return activePath;
})
function setVisibleNumber() {
const width = document.body.getBoundingClientRect().width / 3;
visibleNumber.value = parseInt(width / 85);
}
function handleSelect(key, keyPath) {
currentIndex.value = key;
const route = routers.value.find(item => item.path === key);
if (isHttp(key)) {
// http(s):// 路径新窗口打开
window.open(key, "_blank");
} else if (!route || !route.children) {
// 没有子路由路径内部打开
const routeMenu = childrenMenus.value.find(item => item.path === key);
if (routeMenu && routeMenu.query) {
let query = JSON.parse(routeMenu.query);
router.push({ path: key, query: query });
} else {
router.push({ path: key });
}
appStore.toggleSideBarHide(true);
} else {
// 显示左侧联动菜单
activeRoutes(key);
appStore.toggleSideBarHide(false);
}
}
function activeRoutes(key) {
let routes = [];
if (childrenMenus.value && childrenMenus.value.length > 0) {
childrenMenus.value.map((item) => {
if (key == item.parentPath || (key == "index" && "" == item.path)) {
routes.push(item);
}
});
}
if(routes.length > 0) {
} else {
appStore.toggleSideBarHide(true);
}
return routes;
}
onMounted(() => {
window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber)
})
onMounted(() => {
setVisibleNumber()
})
</script>
<style lang="scss">
.topmenu-container.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #999093 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
border-bottom: 2px solid #{'var(--theme)'} !important;
color: #303133;
}
/* sub-menu item */
.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #999093 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
/* 背景色隐藏 */
.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
background-color: #ffffff !important;
}
/* 图标右间距 */
.topmenu-container .svg-icon {
margin-right: 4px;
}
/* topmenu more arrow */
.topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
position: static;
vertical-align: middle;
margin-left: 8px;
margin-top: 0px;
}
</style>

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

@@ -0,0 +1,49 @@
<template>
<section class="app-main">
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="tagsViewStore.cachedViews">
<component
v-if="!route.meta.link"
:is="Component"
:key="route.path"
/>
</keep-alive>
</transition>
</router-view>
<iframe-toggle />
</section>
</template>
<script setup>
import iframeToggle from "./IframeToggle/index.vue";
import useTagsViewStore from "@/stores/modules/tagsView.js";
const tagsViewStore = useTagsViewStore();
</script>
<style lang="scss" scoped>
.app-main {
/* 50= navbar 50 */
min-height: calc(100vh - 176px);
width: 100%;
position: relative;
overflow: hidden;
}
.fixed-header + .app-main {
padding-top: 50px;
}
.hasTagsView {
.app-main {
/* 84 = navbar + tags-view = 50 + 34 */
// min-height: calc(100vh - 84px);
}
.fixed-header + .app-main {
padding-top: 84px;
}
}
</style>

View File

@@ -0,0 +1,19 @@
<template>
<transition-group name="fade-transform" mode="out-in">
<inner-link
v-for="(item, index) in tagsViewStore.iframeViews"
:key="item.path"
:iframeId="'iframe' + index"
v-show="route.path === item.path"
:src="item.meta.link"
></inner-link>
</transition-group>
</template>
<script setup>
import InnerLink from "../InnerLink/index.vue"
import useTagsViewStore from '@/stores/modules/tagsView.js'
const route = useRoute();
const tagsViewStore = useTagsViewStore()
</script>

View File

@@ -0,0 +1,24 @@
<template>
<div :style="'height:' + height">
<iframe
:id="iframeId"
style="width: 100%; height: 100%"
:src="src"
frameborder="no"
></iframe>
</div>
</template>
<script setup>
const props = defineProps({
src: {
type: String,
default: "/"
},
iframeId: {
type: String
}
});
const height = ref(document.documentElement.clientHeight - 94.5 + "px");
</script>

View File

@@ -0,0 +1,69 @@
<template>
<div class="navbar">
<hamburger
id="hamburger-container"
:is-active="appStore.sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
<breadcrumb
id="breadcrumb-container"
class="breadcrumb-container"
v-if="!settingsStore.topNav"
/>
<top-nav id="topmenu-container" class="topmenu-container" v-if="settingsStore.topNav" />
</div>
</template>
<script setup>
import Breadcrumb from '@/components/Breadcrumb/index.vue'
import TopNav from '@/components/TopNav/index.vue'
import Hamburger from '@/components/Hamburger/index.vue'
import { useAppStore } from '@/stores/modules/app.js'
import { useSettingsStore } from '@/stores/modules/settings.js'
const appStore = useAppStore()
const settingsStore = useSettingsStore()
function toggleSideBar() {
appStore.toggleSideBar(false)
}
</script>
<style lang="scss" scoped>
.navbar {
height: 56px;
overflow: hidden;
position: relative;
.hamburger-container {
line-height: 56px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.breadcrumb-container {
display: flex;
align-content: center;
float: left;
height: 100%;
}
.topmenu-container {
position: absolute;
left: 50px;
}
.errLog-container {
display: inline-block;
vertical-align: top;
}
}
</style>

View File

@@ -0,0 +1,162 @@
<template>
<div>
<el-dialog
v-model="dialogVisible"
title="修改密码"
width="500"
:show-close='false'
:close-on-click-modal="false"
>
<div v-loading="loading">
<el-form ref="pwdRef" :model="user" :rules="rules" label-width="100px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input
v-model="user.oldPassword"
placeholder="请输入旧密码"
type="password"
show-password
/>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input
v-model="user.newPassword"
placeholder="请输入新密码"
type="password"
show-password
/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="user.confirmPassword"
placeholder="请确认新密码"
type="password"
show-password
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">保存</el-button>
<el-button type="danger" @click="close">关闭</el-button>
</el-form-item>
</el-form>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { changePwd ,checkPwdStrength } from '@/api/login.js'
import { useUserStore } from '@/stores/modules/user.js'
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const dialogVisible = ref(false)
const loading = ref(false)
// 密码规则
function passwords(value) {
let validatorA = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/
let validatorB = /^(?=.*\d)(?=.*[a-z])(?=.*[!@#$%^&*()\-_=+{};:,<.>]).{8,}$/
let validatorC = /^(?=.*\d)(?=.*[A-Z])(?=.*[!@#$%^&*()\-_=+{};:,<.>]).{8,}$/
let validatorD = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()\-_=+{};:,<.>]).{8,}$/
if (
validatorA.test(value) ||
validatorB.test(value) ||
validatorC.test(value) ||
validatorD.test(value)
) {
return true
} else {
return false
}
}
// 新增用户检验密码规则
const validatorPasswords = async (rule, value, callback) => {
const resPwd = await checkPwdStrength(user.newPassword);
if(resPwd.meta.code !== 200) {
ElMessage.error(resPwd.meta?.message || '密码强度校验失败');
return
}else{
if(!resPwd.data.is_strong){
callback(new Error(resPwd.data.suggestions.join(', ')|| '密码强度校验失败'))
return
}
}
}
const user = reactive({
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined,
})
const equalToPassword = (rule, value, callback) => {
if (user.newPassword !== value) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
const rules = ref({
oldPassword: [{ required: true, message: '旧密码不能为空', trigger: 'blur' }],
newPassword: [
{ required: true, message: '新密码不能为空', trigger: 'blur' },
{ required: true, validator: validatorPasswords, trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '确认密码不能为空', trigger: 'blur' },
{ required: true, validator: equalToPassword, trigger: 'blur' },
],
})
/** 提交按钮 */
async function submit() {
loading.value = true;
try {
const valid = await proxy.$refs.pwdRef.validate();
if (!valid) return;
const resPwd = await checkPwdStrength(user.newPassword);
if(resPwd.meta.code !== 200) {
ElMessage.error(resPwd.meta?.message || '密码强度校验失败');
return
}else{
if(!resPwd.data.is_strong){
ElMessage.error(resPwd.data.suggestions.join(', ') || '密码强度校验失败');
return
}
}
const res = await changePwd(user.oldPassword, user.newPassword);
if(res.meta.code !== 200){
ElMessage.error(res.meta?.message || '密码修改失败');
return
}else{
ElMessage.success('密码修改成功,请重新登录');
}
await userStore.logOut();
location.href = '/';
} catch (error) {
console.log(error,'error')
} finally {
loading.value = false;
}
}
/** 关闭按钮 */
function close() {
dialogVisible.value = false
proxy.$refs.pwdRef.resetFields()
}
function open(){
dialogVisible.value = true
}
defineExpose({
open
})
</script>

View File

@@ -0,0 +1,238 @@
<template>
<el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px">
<div class="setting-drawer-title">
<h3 class="drawer-title">主题风格设置</h3>
</div>
<div class="setting-drawer-block-checbox">
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
<img src="@/assets/images/dark.svg" alt="dark" />
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
<i aria-label="图标: check" class="anticon anticon-check">
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
</svg>
</i>
</div>
</div>
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
<img src="@/assets/images/light.svg" alt="light" />
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
<i aria-label="图标: check" class="anticon anticon-check">
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
</svg>
</i>
</div>
</div>
</div>
<div class="drawer-item">
<span>主题颜色</span>
<span class="comp-style">
<el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange"/>
</span>
</div>
<el-divider />
<h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item">
<span>开启 TopNav</span>
<span class="comp-style">
<el-switch v-model="topNav" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>开启 Tags-Views</span>
<span class="comp-style">
<el-switch v-model="tagsView" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>固定 Header</span>
<span class="comp-style">
<el-switch v-model="fixedHeader" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>显示 Logo</span>
<span class="comp-style">
<el-switch v-model="sidebarLogo" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>动态标题</span>
<span class="comp-style">
<el-switch v-model="dynamicTitle" class="drawer-switch" />
</span>
</div>
<el-divider />
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
<el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
</el-drawer>
</template>
<script setup>
import variables from '@/assets/styles/variables.module.scss'
// import originElementPlus from 'element-plus/theme-chalk/index.css'
import axios from 'axios'
import { ElLoading, ElMessage } from 'element-plus'
import { useDynamicTitle } from '@/utils/dynamicTitle.js'
import { useAppStore } from '@/stores/modules/app.js'
import { useSettingsStore } from '@/stores/modules/settings.js'
import { handleThemeStyle } from '@/utils/theme'
const { proxy } = getCurrentInstance();
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const showSettings = ref(false);
const theme = ref(settingsStore.theme);
const sideTheme = ref(settingsStore.sideTheme);
const storeSettings = computed(() => settingsStore);
const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"]);
/** 是否需要topnav */
const topNav = computed({
get: () => storeSettings.value.topNav,
set: (val) => {
settingsStore.changeSetting({ key: 'topNav', value: val })
if (!val) {
appStore.toggleSideBarHide(false);
}
}
})
/** 是否需要tagview */
const tagsView = computed({
get: () => storeSettings.value.tagsView,
set: (val) => {
settingsStore.changeSetting({ key: 'tagsView', value: val })
}
})
/**是否需要固定头部 */
const fixedHeader = computed({
get: () => storeSettings.value.fixedHeader,
set: (val) => {
settingsStore.changeSetting({ key: 'fixedHeader', value: val })
}
})
/**是否需要侧边栏的logo */
const sidebarLogo = computed({
get: () => storeSettings.value.sidebarLogo,
set: (val) => {
settingsStore.changeSetting({ key: 'sidebarLogo', value: val })
}
})
/**是否需要侧边栏的动态网页的title */
const dynamicTitle = computed({
get: () => storeSettings.value.dynamicTitle,
set: (val) => {
settingsStore.changeSetting({ key: 'dynamicTitle', value: val })
// 动态设置网页标题
useDynamicTitle()
}
})
function themeChange(val) {
settingsStore.changeSetting({ key: 'theme', value: val })
theme.value = val;
handleThemeStyle(val);
}
function handleTheme(val) {
settingsStore.changeSetting({ key: 'sideTheme', value: val })
sideTheme.value = val;
}
function saveSetting() {
proxy.$modal.loading("正在保存到本地,请稍候...");
let layoutSetting = {
"topNav": storeSettings.value.topNav,
"tagsView": storeSettings.value.tagsView,
"fixedHeader": storeSettings.value.fixedHeader,
"sidebarLogo": storeSettings.value.sidebarLogo,
"dynamicTitle": storeSettings.value.dynamicTitle,
"sideTheme": storeSettings.value.sideTheme,
"theme": storeSettings.value.theme
};
localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
setTimeout(proxy.$modal.closeLoading(), 1000)
}
function resetSetting() {
proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
localStorage.removeItem("layout-setting")
setTimeout("window.location.reload()", 1000)
}
function openSetting() {
showSettings.value = true;
}
defineExpose({
openSetting,
})
</script>
<style lang='scss' scoped>
.setting-drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, 0.85);
line-height: 22px;
font-weight: bold;
.drawer-title {
font-size: 14px;
}
}
.setting-drawer-block-checbox {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 10px;
margin-bottom: 20px;
.setting-drawer-block-checbox-item {
position: relative;
margin-right: 16px;
border-radius: 2px;
cursor: pointer;
img {
width: 48px;
height: 48px;
}
.custom-img {
width: 48px;
height: 38px;
border-radius: 5px;
box-shadow: 1px 1px 2px #898484;
}
.setting-drawer-block-checbox-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: #1890ff;
font-weight: 700;
font-size: 14px;
}
}
}
.drawer-item {
color: rgba(0, 0, 0, 0.65);
padding: 12px 0;
font-size: 14px;
.comp-style {
float: right;
margin: -3px 8px 0px 0px;
}
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<component :is="type" v-bind="linkProps()">
<slot />
</component>
</template>
<script setup>
import { isExternal } from '@/utils/validate'
const props = defineProps({
to: {
type: [String, Object],
required: true
}
})
const isExt = computed(() => {
return isExternal(props.to)
})
const type = computed(() => {
if (isExt.value) {
return 'a'
}
return 'router-link'
})
function linkProps() {
if (isExt.value) {
return {
href: props.to,
target: '_blank',
rel: 'noopener'
}
}
return {
to: props.to
}
}
</script>

View File

@@ -0,0 +1,81 @@
<template>
<div class="sidebar-logo-container" :class="{ 'collapse': collapse }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1>
</router-link>
</transition>
</div>
</template>
<script setup>
import variables from '@/assets/styles/variables.module.scss'
import logo from '@/assets/logo/logo.png'
import useSettingsStore from '@/store/modules/settings'
defineProps({
collapse: {
type: Boolean,
required: true
}
})
const title = import.meta.env.VITE_APP_TITLE;
const settingsStore = useSettingsStore();
const sideTheme = computed(() => settingsStore.sideTheme);
</script>
<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
transition: opacity 1.5s;
}
.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
opacity: 0;
}
.sidebar-logo-container {
position: relative;
width: 100%;
height: 50px;
line-height: 50px;
background: #2b2f3a;
text-align: center;
overflow: hidden;
& .sidebar-logo-link {
height: 100%;
width: 100%;
& .sidebar-logo {
width: 32px;
height: 32px;
vertical-align: middle;
margin-right: 12px;
}
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
font-weight: 600;
line-height: 50px;
font-size: 14px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
vertical-align: middle;
}
}
&.collapse {
.sidebar-logo {
margin-right: 0px;
}
}
}
</style>

View File

@@ -0,0 +1,116 @@
<template>
<div v-if="!item.hidden">
<template
v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
!item.alwaysShow
"
>
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<el-menu-item
:index="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
>
<!-- <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" /> -->
<!-- {{ onlyOneChild.meta.title }} -->
<!-- <template #title> -->
<span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">
{{ onlyOneChild.meta.title }}
</span>
<!-- </template> -->
</el-menu-item>
</app-link>
</template>
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template v-if="item.meta" #title>
<!-- <svg-icon :icon-class="item.meta && item.meta.icon" /> -->
<span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-sub-menu>
</div>
</template>
<script setup>
import { isExternal } from '@/utils/validate.js'
import AppLink from './Link.vue'
import { getNormalPath } from '@/utils/ruoyi'
const props = defineProps({
// route object
item: {
type: Object,
required: true,
},
isNest: {
type: Boolean,
default: false,
},
basePath: {
type: String,
default: '',
},
})
const onlyOneChild = ref({})
function hasOneShowingChild(children = [], parent) {
if (!children) {
children = []
}
const showingChildren = children.filter((item) => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
return true
}
return false
}
function resolvePath(routePath, routeQuery) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(props.basePath)) {
return props.basePath
}
if (routeQuery) {
let query = JSON.parse(routeQuery)
return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
}
return getNormalPath(props.basePath + '/' + routePath)
}
function hasTitle(title) {
if (title.length > 5) {
return title
} else {
return ''
}
}
</script>

View File

@@ -0,0 +1,55 @@
<template>
<div>
<!-- <logo v-if="showLogo" :collapse="isCollapse" /> -->
<el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
<transition mode="out-in">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="
sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground
"
:text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
:unique-opened="true"
:active-text-color="theme"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item
v-for="(route, index) in constantRoutes"
:key="route.path + index"
:item="route"
:base-path="route.path"
/>
</el-menu>
</transition>
</el-scrollbar>
</div>
</template>
<script setup>
import SidebarItem from './SidebarItem.vue'
import variables from '@/assets/styles/variables.module.scss'
import { useAppStore } from '@/stores/modules/app.js'
import { useSettingsStore } from '@/stores/modules/settings.js'
import router, { constantRoutes, dynamicRoutes } from '@/router'
const route = useRoute()
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const showLogo = computed(() => settingsStore.sidebarLogo)
const sideTheme = computed(() => settingsStore.sideTheme)
const theme = computed(() => settingsStore.theme)
const isCollapse = computed(() => !appStore.sidebar.opened)
const activeMenu = computed(() => {
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
})
</script>

View File

@@ -0,0 +1,105 @@
<template>
<el-scrollbar
ref="scrollContainer"
:vertical="false"
class="scroll-container"
@wheel.prevent="handleScroll"
>
<slot />
</el-scrollbar>
</template>
<script setup>
import useTagsViewStore from '@/stores/modules/tagsView.js'
const tagAndTagSpacing = ref(4);
const { proxy } = getCurrentInstance();
const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef);
onMounted(() => {
scrollWrapper.value.addEventListener('scroll', emitScroll, true)
})
onBeforeUnmount(() => {
scrollWrapper.value.removeEventListener('scroll', emitScroll)
})
function handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = scrollWrapper.value;
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
}
const emits = defineEmits()
const emitScroll = () => {
emits('scroll')
}
const tagsViewStore = useTagsViewStore()
const visitedViews = computed(() => tagsViewStore.visitedViews);
function moveToTarget(currentTag) {
const $container = proxy.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = scrollWrapper.value;
let firstTag = null
let lastTag = null
// find first tag and last tag
if (visitedViews.value.length > 0) {
firstTag = visitedViews.value[0]
lastTag = visitedViews.value[visitedViews.value.length - 1]
}
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
const tagListDom = document.getElementsByClassName('tags-view-item');
const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
let prevTag = null
let nextTag = null
for (const k in tagListDom) {
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
prevTag = tagListDom[k];
}
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
nextTag = tagListDom[k];
}
}
}
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
}
}
}
defineExpose({
moveToTarget,
})
</script>
<style lang='scss' scoped>
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
:deep(.el-scrollbar__bar) {
bottom: 0px;
}
:deep(.el-scrollbar__wrap) {
height: 39px;
}
}
</style>

View File

@@ -0,0 +1,336 @@
<template>
<div id="tags-view-container" class="tags-view-container">
<scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
<router-link
v-for="tag in visitedViews"
:key="tag.path"
:data-path="tag.path"
:class="isActive(tag) ? 'active' : ''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
class="tags-view-item"
:style="activeStyle(tag)"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)"
>
{{ tag.title }}
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
<close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
</span>
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)">
<refresh-right style="width: 1em; height: 1em;" /> 刷新页面
</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<close style="width: 1em; height: 1em;" /> 关闭当前
</li>
<li @click="closeOthersTags">
<circle-close style="width: 1em; height: 1em;" /> 关闭其他
</li>
<li v-if="!isFirstView()" @click="closeLeftTags">
<back style="width: 1em; height: 1em;" /> 关闭左侧
</li>
<li v-if="!isLastView()" @click="closeRightTags">
<right style="width: 1em; height: 1em;" /> 关闭右侧
</li>
<li @click="closeAllTags(selectedTag)">
<circle-close style="width: 1em; height: 1em;" /> 全部关闭
</li>
</ul>
</div>
</template>
<script setup>
import ScrollPane from './ScrollPane.vue'
import { getNormalPath } from '@/utils/ruoyi'
import useTagsViewStore from '@/stores/modules/tagsView.js'
import { useSettingsStore } from '@/stores/modules/settings.js'
const visible = ref(false);
const top = ref(0);
const left = ref(0);
const selectedTag = ref({});
const affixTags = ref([]);
const scrollPaneRef = ref(null);
const { proxy } = getCurrentInstance();
const route = useRoute();
const router = useRouter();
const visitedViews = computed(() => useTagsViewStore().visitedViews);
const theme = computed(() => useSettingsStore().theme);
watch(route, () => {
addTags()
moveToCurrentTag()
})
watch(visible, (value) => {
if (value) {
document.body.addEventListener('click', closeMenu)
} else {
document.body.removeEventListener('click', closeMenu)
}
})
onMounted(() => {
initTags()
addTags()
})
function isActive(r) {
return r.path === route.path
}
function activeStyle(tag) {
if (!isActive(tag)) return {};
return {
"background-color": theme.value,
"border-color": theme.value
};
}
function isAffix(tag) {
return tag.meta && tag.meta.affix
}
function isFirstView() {
try {
return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath
} catch (err) {
return false
}
}
function isLastView() {
try {
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
} catch (err) {
return false
}
}
function filterAffixTags(routes, basePath = '') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = getNormalPath(basePath + '/' + route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
})
}
if (route.children) {
const tempTags = filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
}
function initTags() {
const res = filterAffixTags(routes.value);
affixTags.value = res;
for (const tag of res) {
// Must have tag name
if (tag.name) {
useTagsViewStore().addVisitedView(tag)
}
}
}
function addTags() {
const { name } = route
if (name) {
useTagsViewStore().addView(route)
if (route.meta.link) {
useTagsViewStore().addIframeView(route);
}
}
return false
}
function moveToCurrentTag() {
nextTick(() => {
for (const r of visitedViews.value) {
if (r.path === route.path) {
scrollPaneRef.value.moveToTarget(r);
// when query is different then update
if (r.fullPath !== route.fullPath) {
useTagsViewStore().updateVisitedView(route)
}
}
}
})
}
function refreshSelectedTag(view) {
proxy.$tab.refreshPage(view);
if (route.meta.link) {
useTagsViewStore().delIframeView(route);
}
}
function closeSelectedTag(view) {
proxy.$tab.closePage(view).then(({ visitedViews }) => {
if (isActive(view)) {
toLastView(visitedViews, view)
}
})
}
function closeRightTags() {
proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
toLastView(visitedViews)
}
})
}
function closeLeftTags() {
proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
toLastView(visitedViews)
}
})
}
function closeOthersTags() {
router.push(selectedTag.value).catch(() => { });
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
moveToCurrentTag()
})
}
function closeAllTags(view) {
proxy.$tab.closeAllPage().then(({ visitedViews }) => {
if (affixTags.value.some(tag => tag.path === route.path)) {
return
}
toLastView(visitedViews, view)
})
}
function toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
router.push(latestView.fullPath)
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
router.replace({ path: '/redirect' + view.fullPath })
} else {
router.push('/')
}
}
}
function openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
const offsetWidth = proxy.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const l = e.clientX - offsetLeft + 15 // 15: margin right
if (l > maxLeft) {
left.value = maxLeft
} else {
left.value = l
}
top.value = e.clientY
visible.value = true
selectedTag.value = tag
}
function closeMenu() {
visible.value = false
}
function handleScroll() {
closeMenu()
}
</script>
<style lang='scss' scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
.tags-view-wrapper {
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
content: "";
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 5px;
}
}
}
}
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
</style>
<style lang="scss">
//reset element css of el-icon-close
.tags-view-wrapper {
.tags-view-item {
.el-icon-close {
width: 16px;
height: 16px;
vertical-align: 2px;
border-radius: 50%;
text-align: center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(0.6);
display: inline-block;
vertical-align: -3px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
width: 12px !important;
height: 12px !important;
}
}
}
}
</style>

View File

@@ -0,0 +1,5 @@
export { default as AppMain } from './AppMain.vue'
export { default as Navbar } from './Navbar.vue'
export { default as Settings } from './Settings/index.vue'
export { default as TagsView } from './TagsView/index.vue'
export { default as ResetPwd } from './ResetPwd/index.vue'

323
src/layout/index.vue Normal file
View File

@@ -0,0 +1,323 @@
<template>
<div class="wrapper-content">
<div class="content-nav">
<div class="nav-left">
<div>xSynergy远程协作后台管理系统</div>
</div>
<div class="nav-right">
<el-dropdown trigger="click" @command="handleCommand">
<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>
<!-- <el-icon><caret-bottom /></el-icon> -->
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="changePassword">
<span>修改密码</span>
</el-dropdown-item>
<el-dropdown-item command="logout" divided>
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
<div
v-if="device === 'mobile' && sidebar.opened"
class="drawer-bg"
@click="handleClickOutside"
/>
<div class="main-container" :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }">
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
<div class="sidebar-right">
<!-- :class="{ 'fixed-header': fixedHeader }" -->
<div >
<navbar ref="navbarRef"/>
</div>
<app-main />
</div>
</div>
</div>
<ResetPwd ref="resetPwdRef"/>
</div>
</template>
<script setup>
import { ElMessageBox ,ElMessage} from 'element-plus'
import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue'
import { AppMain, TagsView ,ResetPwd,Navbar} from './components/index.js'
import { useAppStore } from '@/stores/modules/app.js'
import { useSettingsStore } from '@/stores/modules/settings.js'
import { useUserStore } from '@/stores/modules/user.js'
import { removeToken } from '@/utils/auth.js'
import { onMounted ,ref} from 'vue'
const settingsStore = useSettingsStore()
const userStore = useUserStore()
const useAppStoreStore = useAppStore()
const router = useRouter()
const theme = computed(() => settingsStore.theme)
const sidebar = computed(() => useAppStoreStore.sidebar)
const device = computed(() => useAppStoreStore.device)
const needTagsView = computed(() => settingsStore.tagsView)
const nickName = computed(() => {
// 优先从 userStore 获取
if (userStore.name) {
return userStore.name
}
try {
const userData = sessionStorage.getItem('userData')
if (userData) {
const parsedData = JSON.parse(userData)
return parsedData.name || ''
}
} catch (error) {
console.error('解析 userData 失败:', error)
}
return ''
})
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,
openSidebar: sidebar.value.opened,
withoutAnimation: sidebar.value.withoutAnimation,
mobile: device.value === 'mobile',
}))
const resetPwdRef = ref(null)
const { width, height } = useWindowSize()
const WIDTH = 992
watchEffect(() => {
if (device.value === 'mobile' && sidebar.value.opened) {
useAppStoreStore.closeSideBar({ withoutAnimation: false })
}
if (width.value - 1 < WIDTH) {
useAppStoreStore.toggleDevice('mobile')
useAppStoreStore.closeSideBar({ withoutAnimation: true })
} else {
useAppStoreStore.toggleDevice('desktop')
}
})
function handleCommand(command) {
switch (command) {
case 'changePassword':
resetPwdRef.value.open()
break
case 'logout':
logout()
break
default:
break
}
}
function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
userStore.logOut().then(() => {
location.href = '/'
})
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消注销'
})
})
}
function handleClickOutside() {
useAppStoreStore.closeSideBar({ withoutAnimation: false })
}
onMounted(async () => {})
</script>
<style lang="scss" scoped>
@import '@/assets/styles/mixin.scss';
@import '@/assets/styles/variables.module.scss';
.tags-view {
opacity: 0;
top: 0;
position: fixed;
}
.main-container {
position: relative;
display: flex;
height: calc(100vh - 50px);
overflow-y: auto;
overflow: hidden;
margin: 0;
background-color: #fff;
// border-radius: 30px;
box-shadow: 0 0 10px 4px #ece9e3;
.sidebar-container {
box-shadow: rgba(0, 0, 0, 0.15) 1.95px 1.95px 2.6px;
width: 200px;
// box-shadow: 0 0 10px 4px #f6f6f6;
}
.sidebar-right {
width: calc(100% - 200px);
position: relative;
overflow: hidden;
overflow-x: hidden;
overflow-y: scroll;
// padding: 0 20px;
.right-bar {
width: 100%;
background: #f6f6f6;
overflow-x: hidden;
// border-radius: 0 30px 0 0;
}
}
/* 滚动条整体 */
.sidebar-right::-webkit-scrollbar {
height: 0px;
width: 0px;
}
}
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
background: #f6f6f6;
// padding: 20px;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
// @media screen and (max-width: 995px) {
// .wrapper-content {
// .content-nav {
// z-index: 0 !important;
// }
// }
// }
.wrapper-content {
.content-nav {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 1111;
width: 100%;
height: 50px;
-webkit-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;
background-color: #409EFF;
// background-color: #434343;
.nav-left {
display: flex;
align-items: center;
color: #fff;
box-sizing: border-box;
div {
font-size: 19px;
}
}
.nav-right {
display: flex;
flex-wrap: nowrap;
align-items: center;
height: 100%;
.screenfull {
font-size: 22px;
cursor: pointer;
color: #ffe565;
}
.avatar-wrapper {
position: relative;
display: flex;
flex-wrap: nowrap;
cursor: pointer;
align-items: center;
margin-right: 30px;
img{
width: 35px;
height: 35px;
border-radius: 50%;
}
.user-avatar {
cursor: pointer;
width: 50px;
height: 50px;
border-radius: 10px;
border-radius: 50%;
}
.username {
padding: 0 13px;
font-weight: 700;
font-size: 18px;
line-height: 1;
color: #fff;
}
.nickName {
font-weight: 700;
font-size: 18px;
color: #fff;
}
i {
cursor: pointer;
margin-left: 6px;
font-size: 20px;
color: #ffe565;
}
}
}
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
transition: width 0.28s;
}
// .hideSidebar .fixed-header {
// width: calc(100% - 54px);
// }
.sidebarHide .fixed-header {
width: 100%;
}
.mobile .fixed-header {
width: 100%;
}
</style>

View File

@@ -1,11 +1,15 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import * as Sentry from '@sentry/vue';
import {
deepClone
} from '@/utils/ruoyi'
deepClone,
parseTime,
} from '@/utils/ruoyi.js'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
@@ -17,14 +21,45 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn'
import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
const app = createApp(App)
app.config.globalProperties.parseTime = parseTime;
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
// Sentry.init({
// app,
// dsn: 'https://34a0d76174a64db09d31d13a8042560b@sentry.cnsdt.com/2', // 替换为你的 DSN
// integrations: [
// // 浏览器性能追踪集成
// Sentry.browserTracingIntegration({
// router,
// }),
// // 会话回放集成
// Sentry.replayIntegration({
// maskAllText: false,
// blockAllMedia: false,
// }),
// ],
// // 性能监控采样率
// tracesSampleRate: 1.0, // 生产环境建议设置为 0.1-0.2
// // 会话回放采样率
// replaysSessionSampleRate: 0.1,
// replaysOnErrorSampleRate: 1.0,
// // 环境配置
// environment: import.meta.env.MODE,
// // 开发环境下可禁用 Sentry
// enabled: import.meta.env.PROD,
// });
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
//全局组件
import Pagination from '@/components/Pagination/index.vue'
app.component('Pagination', Pagination)
app.use(pinia)
app.use(router)
app.use(ElementPlus, {
locale: zhCn,

60
src/plugins/auth.js Normal file
View File

@@ -0,0 +1,60 @@
import { useUserStore } from '@/stores/modules/user.js'
function authPermission(permission) {
const all_permission = "*:*:*";
const permissions = useUserStore().permissions
if (permission && permission.length > 0) {
return permissions.some(v => {
return all_permission === v || v === permission
})
} else {
return false
}
}
function authRole(role) {
const super_admin = "admin";
const roles = useUserStore().roles
if (role && role.length > 0) {
return roles.some(v => {
return super_admin === v || v === role
})
} else {
return false
}
}
export default {
// 验证用户是否具备某权限
hasPermi(permission) {
return authPermission(permission);
},
// 验证用户是否含有指定权限,只需包含其中一个
hasPermiOr(permissions) {
return permissions.some(item => {
return authPermission(item)
})
},
// 验证用户是否含有指定权限,必须全部拥有
hasPermiAnd(permissions) {
return permissions.every(item => {
return authPermission(item)
})
},
// 验证用户是否具备某角色
hasRole(role) {
return authRole(role);
},
// 验证用户是否含有指定角色,只需包含其中一个
hasRoleOr(roles) {
return roles.some(item => {
return authRole(item)
})
},
// 验证用户是否含有指定角色,必须全部拥有
hasRoleAnd(roles) {
return roles.every(item => {
return authRole(item)
})
}
}

View File

@@ -1,57 +1,199 @@
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import Layout from "@/layout/index.vue";
export const constantRoutes = [
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index.vue')
}
]
},
{
path: '/',
redirect: '/login', // 这里做重定向
hidden: true,
},
{
path: "/login",
component: () => import("@/views/login.vue"),
meta: { title: "登录" },
hidden: true,
},
{
path: "/userManagement",
redirect: "/userManagement/UserManagementPage",
component: Layout,
children: [
{
path: 'UserManagementPage',
name: "UserManagement",
component: () => import("@/views/userManagement/index.vue"),
meta: { title: "用户管理", icon: "client", affix: true },
}
]
},
{
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,
// children: [
// {
// path: 'PatrolMissionPage',
// name: "PatrolMission",
// component: () => import("@/views/patrolMission/index.vue"),
// meta: { title: "设备知识库", icon: "client", affix: true },
// },
// ]
// },
{
path: '/room-auth',
component: Layout,
hidden: true,
children: [
{
path: 'room/:roomId(\\w+)',
component: () => import('@/views/roomManagement/authRoom.vue'),
name: 'AuthRoom',
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(.*)*",
component: () => import("@/views/error/404.vue"),
hidden: true,
},
{
path: "/401",
component: () => import("@/views/error/401.vue"),
hidden: true,
meta: { title: "401未授权" }
},
]
export const dynamicRoutes = [
]
const router = createRouter({
// history: createWebHistory(import.meta.env.VITE_BASE_PATH),
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",
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"),
routes: constantRoutes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { top: 0 };
}
],
})
},
});
export default router

47
src/settings.js Normal file
View File

@@ -0,0 +1,47 @@
export default {
/**
* 网页标题
*/
title: import.meta.env.VITE_APP_TITLE,
/**
* 侧边栏主题 深色主题theme-dark浅色主题theme-light
*/
sideTheme: 'theme-dark',
/**
* 是否系统布局配置
*/
showSettings: true,
/**
* 是否显示顶部导航
*/
topNav: false,
/**
* 是否显示 tagsView
*/
tagsView: true,
/**
* 是否固定头部
*/
fixedHeader: false,
/**
* 是否显示logo
*/
sidebarLogo: false,
/**
* 是否显示动态标题
*/
dynamicTitle: false,
/**
* @type {string | array} 'production' | ['production', 'development']
* @description Need show err logs component.
* The default is only used in the production env
* If you want to also use it in dev, you can pass ['production', 'development']
*/
errorLog: 'production'
}

View File

@@ -1,12 +0,0 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

39
src/stores/modules/app.js Normal file
View File

@@ -0,0 +1,39 @@
import Cookies from 'js-cookie'
import { defineStore } from 'pinia'
export const useAppStore = defineStore(
'app',
{
state: () => ({
sidebar: {
opened: true,
withoutAnimation: false,
hide: false
},
device: 'desktop',
size: Cookies.get('size') || 'default'
}),
actions: {
toggleSideBar(withoutAnimation) {
if (this.sidebar.hide) {
return false;
}
this.sidebar.opened = !this.sidebar.opened
this.sidebar.withoutAnimation = withoutAnimation
},
closeSideBar({ withoutAnimation }) {
this.sidebar.opened = false
this.sidebar.withoutAnimation = withoutAnimation
},
toggleDevice(device) {
this.device = device
},
setSize(size) {
this.size = size;
Cookies.set('size', size)
},
toggleSideBarHide(status) {
this.sidebar.hide = status
}
}
})

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { generateUUID } from '@/utils/tools.js'
import { defineStore } from 'pinia'
export const useMeterStore = defineStore('meter', {
state: () => ({
@@ -9,7 +9,7 @@ export const useMeterStore = defineStore('meter', {
initUdid() {
var udid = window.localStorage.getItem('UDID')
if (!udid) {
udid = generateUUID();
udid = generateUUID();
window.localStorage.setItem("UDID", udid);
}
this.setUdid(udid)
@@ -30,4 +30,4 @@ export const useMeterStore = defineStore('meter', {
}
}
})
})

View File

@@ -5,7 +5,10 @@ export const useRoomStore = defineStore('room', {
roomId: '',
token: '',
userUid: '',
detailUid: '',
//邀请进入房间的用户uid
detailUid: '',
//邀请用户名称
detailName: '',
}),
actions: {
setUserUid(data) {
@@ -13,6 +16,9 @@ export const useRoomStore = defineStore('room', {
},
setDetailUid(data) {
this.detailUid = data
},
setDetailName(data) {
this.detailName = data
}
}

View File

@@ -0,0 +1,37 @@
import defaultSettings from '@/settings'
import { useDynamicTitle } from '@/utils/dynamicTitle'
import { defineStore } from 'pinia'
const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle } = defaultSettings
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
export const useSettingsStore = defineStore(
'settings',
{
state: () => ({
title: '',
theme: storageSetting.theme === undefined ? '#409EFF' : storageSetting.theme,
sideTheme: storageSetting.sideTheme === undefined ? sideTheme : storageSetting.sideTheme,
showSettings: showSettings,
topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,
sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo,
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle
}),
actions: {
// 修改布局设置
changeSetting(data) {
const { key, value } = data
if (this.hasOwnProperty(key)) {
this[key] = value
}
},
// 设置网页标题
setTitle(title) {
this.title = title
useDynamicTitle();
}
}
})

View File

@@ -0,0 +1,183 @@
import { defineStore } from 'pinia'
const useTagsViewStore = defineStore(
'tags-view',
{
state: () => ({
visitedViews: [],
cachedViews: [],
iframeViews: []
}),
actions: {
addView(view) {
this.addVisitedView(view)
this.addCachedView(view)
},
addIframeView(view) {
if (this.iframeViews.some(v => v.path === view.path)) return
this.iframeViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
)
},
addVisitedView(view) {
if (this.visitedViews.some(v => v.path === view.path)) return
this.visitedViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
)
},
addCachedView(view) {
if (this.cachedViews.includes(view.name)) return
if (!view.meta.noCache) {
this.cachedViews.push(view.name)
}
},
delView(view) {
return new Promise(resolve => {
this.delVisitedView(view)
this.delCachedView(view)
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delVisitedView(view) {
return new Promise(resolve => {
for (const [i, v] of this.visitedViews.entries()) {
if (v.path === view.path) {
this.visitedViews.splice(i, 1)
break
}
}
this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
resolve([...this.visitedViews])
})
},
delIframeView(view) {
return new Promise(resolve => {
this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
resolve([...this.iframeViews])
})
},
delCachedView(view) {
return new Promise(resolve => {
const index = this.cachedViews.indexOf(view.name)
index > -1 && this.cachedViews.splice(index, 1)
resolve([...this.cachedViews])
})
},
delOthersViews(view) {
return new Promise(resolve => {
this.delOthersVisitedViews(view)
this.delOthersCachedViews(view)
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delOthersVisitedViews(view) {
return new Promise(resolve => {
this.visitedViews = this.visitedViews.filter(v => {
return v.meta.affix || v.path === view.path
})
this.iframeViews = this.iframeViews.filter(item => item.path === view.path)
resolve([...this.visitedViews])
})
},
delOthersCachedViews(view) {
return new Promise(resolve => {
const index = this.cachedViews.indexOf(view.name)
if (index > -1) {
this.cachedViews = this.cachedViews.slice(index, index + 1)
} else {
this.cachedViews = []
}
resolve([...this.cachedViews])
})
},
delAllViews(view) {
return new Promise(resolve => {
this.delAllVisitedViews(view)
this.delAllCachedViews(view)
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delAllVisitedViews(view) {
return new Promise(resolve => {
const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
this.visitedViews = affixTags
this.iframeViews = []
resolve([...this.visitedViews])
})
},
delAllCachedViews(view) {
return new Promise(resolve => {
this.cachedViews = []
resolve([...this.cachedViews])
})
},
updateVisitedView(view) {
for (let v of this.visitedViews) {
if (v.path === view.path) {
v = Object.assign(v, view)
break
}
}
},
delRightTags(view) {
return new Promise(resolve => {
const index = this.visitedViews.findIndex(v => v.path === view.path)
if (index === -1) {
return
}
this.visitedViews = this.visitedViews.filter((item, idx) => {
if (idx <= index || (item.meta && item.meta.affix)) {
return true
}
const i = this.cachedViews.indexOf(item.name)
if (i > -1) {
this.cachedViews.splice(i, 1)
}
if(item.meta.link) {
const fi = this.iframeViews.findIndex(v => v.path === item.path)
this.iframeViews.splice(fi, 1)
}
return false
})
resolve([...this.visitedViews])
})
},
delLeftTags(view) {
return new Promise(resolve => {
const index = this.visitedViews.findIndex(v => v.path === view.path)
if (index === -1) {
return
}
this.visitedViews = this.visitedViews.filter((item, idx) => {
if (idx >= index || (item.meta && item.meta.affix)) {
return true
}
const i = this.cachedViews.indexOf(item.name)
if (i > -1) {
this.cachedViews.splice(i, 1)
}
if(item.meta.link) {
const fi = this.iframeViews.findIndex(v => v.path === item.path)
this.iframeViews.splice(fi, 1)
}
return false
})
resolve([...this.visitedViews])
})
}
}
})
export default useTagsViewStore

View File

@@ -1,27 +1,36 @@
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { login, logout, getInfo ,getAvatarsApi} from '@/api/login.js'
import { getToken, setToken, removeToken ,getUserInfo} from '@/utils/auth.js'
import { defineStore } from 'pinia'
import { ElMessage } from 'element-plus'
const useUserStore = defineStore(
export const useUserStore = defineStore(
'user',
{
state: () => ({
token: getToken(),
name: '',
avatar: '',
roles: '',
roles: '',
}),
actions: {
// 登录
async login(userInfo) {
try {
const { username, password } = userInfo;
const trimmedUsername = username.trim();
const res = await login(trimmedUsername, password);
const trimmedUsername = username.trim();
const res = await login(trimmedUsername, password);
if (res.meta.code !== 200) {
ElMessage({ message: res.meta?.message || '登录失败', type: 'error' });
return Promise.reject(res);
}
if(!res.data.user.is_admin){
ElMessage({ message: '暂无权限登录,请联系管理员', type: 'error' });
return Promise.reject(res);
}
const { token, user } = res.data;
localStorage.setItem('userData', JSON.stringify(user));
this.name = user.name;
sessionStorage.setItem('userData', JSON.stringify(user));
await this.getAvatars();
setToken(token);
this.token = token;
@@ -34,7 +43,7 @@ const useUserStore = defineStore(
getInfo() {
return new Promise((resolve, reject) => {
try {
const userData = localStorage.getItem('userData');
const userData = sessionStorage.getItem('userData');
if (!userData) {
return reject(new Error('未找到用户数据'));
@@ -49,13 +58,30 @@ const useUserStore = defineStore(
reject(error instanceof Error ? error : new Error('解析用户数据失败'));
}
});
},
// 获取用户头像
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() {
try {
await logout();
const res = await logout();
if(res.meta.code !== 200){
ElMessage({ message: res.meta?.message || '退出登录失败', type: 'error' });
return
}
this.token = '';
this.roles = '';
this.roles = '';
sessionStorage.removeItem('userData');
removeToken();
} catch (error) {
console.error('退出登录失败:', error);
@@ -67,6 +93,4 @@ const useUserStore = defineStore(
return !!getToken();
}
}
})
export default useUserStore
})

View File

@@ -1,30 +1,71 @@
import Cookies from "js-cookie";
const TokenKey = "token";
const ExpiresInKey = "Meta-Enterprise-Expires-In";
import router from '@/router';
const TokenKey = "token";
export function getToken() {
return Cookies.get(TokenKey);
// return Cookies.get(TokenKey);
return sessionStorage.getItem(TokenKey);
}
export function setToken(token) {
return Cookies.set(TokenKey, token);
// return Cookies.set(TokenKey, token);
return sessionStorage.setItem(TokenKey, token);
}
export function removeToken() {
return Cookies.remove(TokenKey);
// return Cookies.remove(TokenKey);
return sessionStorage.removeItem(TokenKey);
}
export function getExpiresIn() {
return Cookies.get(ExpiresInKey) || -1;
//获取用户信息
export function getUserInfo() {
try {
const userData = sessionStorage.getItem("userData");
// 如果userData不存在执行未授权处理
if (!userData) {
handleUnauthorized();
return null;
}
// 尝试解析JSON数据
try {
const parsedData = JSON.parse(userData);
return parsedData;
} catch (parseError) {
console.error('用户数据格式错误无法解析JSON:', parseError);
// 数据格式错误也视为未登录
sessionStorage.removeItem("userData");
handleUnauthorized();
return null;
}
} catch (error) {
console.error('获取用户信息时发生错误:', error);
handleUnauthorized();
return null;
}
}
export function setExpiresIn(time) {
return Cookies.set(ExpiresInKey, time);
}
export function removeExpiresIn() {
return Cookies.remove(ExpiresInKey);
}
function handleUnauthorized() {
removeToken();
// 使用 nextTick 确保路由状态已更新
import('vue').then(({ nextTick }) => {
nextTick(() => {
const currentPath = router.currentRoute.value.fullPath;
if (router.currentRoute.value.path !== '/login') {
router.push({
path: '/login',
query: {
redirect: currentPath !== '/login' ? currentPath : undefined
}
});
} else {
window.location.reload();
}
});
});
}

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