Merge pull request 'lxl-dev' (#2) from lxl-dev into dev

Reviewed-on: http://192.168.28.10:18088/SmartBench/CMVR-IOT-UI/pulls/2
This commit is contained in:
zhanghao 2026-08-03 16:56:47 +08:00
commit 2d82598d3b
236 changed files with 27098 additions and 18247 deletions

View File

@ -1,5 +1,5 @@
# 页面标题 # 页面标题
VITE_APP_TITLE = 招商车研AI测评机器人模拟测试平台 VITE_APP_TITLE = 招商车研物联网平台管理系统
# 开发环境配置 # 开发环境配置
VITE_APP_ENV = 'development' VITE_APP_ENV = 'development'
@ -9,8 +9,6 @@ VITE_APP_BASE_API = '/dev-api'
VITE_API_URL = http://192.168.0.222:13080 VITE_API_URL = http://192.168.0.222:13080
VITE_WS_URL = ws://192.168.0.222:13080/ws VITE_WS_URL = ws://192.168.0.222:13080/ws
VITE_PORT = 80
# explanation
VITE_INSPECTION_TYPE = "inspection" VITE_INSPECTION_TYPE = "inspection"
VITE_MINIO_URL = "http://192.168.28.10:9000/cmvr-iot/" VITE_MINIO_URL = "http://192.168.28.10:9000/cmvr-iot/"

View File

@ -1,5 +1,5 @@
# 页面标题 # 页面标题
VITE_APP_TITLE = 智能座舱测评AI机器人 VITE_APP_TITLE = 招商车研物联网平台管理系统
# 生产环境配置 # 生产环境配置
VITE_APP_ENV = 'production' VITE_APP_ENV = 'production'
@ -10,13 +10,8 @@ VITE_APP_BASE_API = '/prod-api'
# 是否在打包时开启压缩,支持 gzip 和 brotli # 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip VITE_BUILD_COMPRESS = gzip
#node-red 服务地址
VITE_NODE_RED_URL = 'http://10.148.108.59:13080' VITE_NODE_RED_URL = 'http://10.148.108.59:13080'
VITE_API_URL = http://192.168.28.10:13080 VITE_API_URL = http://192.168.28.10:13080
VITE_WS_URL = ws://192.168.0.201:13080/ws VITE_WS_URL = ws://192.168.0.201:13080/ws
# explanation
VITE_INSPECTION_TYPE = "inspection" VITE_INSPECTION_TYPE = "inspection"
VITE_MINIO_URL = "http://192.168.28.10:9000/cmvr-iot/" VITE_MINIO_URL = "http://192.168.28.10:9000/cmvr-iot/"

View File

@ -4,10 +4,13 @@ VITE_APP_TITLE = 招商车研物联网平台管理系统
# 生产环境配置 # 生产环境配置
VITE_APP_ENV = 'staging' VITE_APP_ENV = 'staging'
# 招商车研物联网平台管理系统/生产环境 # 招商车研物联网平台/测试环境
VITE_APP_BASE_API = '/stage-api' VITE_APP_BASE_API = '/stage-api'
# 是否在打包时开启压缩,支持 gzip 和 brotli # 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip VITE_BUILD_COMPRESS = gzip
VITE_API_URL = http://10.148.121.100:13080 VITE_API_URL = http://10.148.121.100:13080
VITE_WS_URL = ws://192.168.0.201:13080/ws VITE_WS_URL = ws://192.168.0.201:13080/ws
VITE_INSPECTION_TYPE = "inspection"
VITE_MINIO_URL = "http://192.168.28.10:9000/cmvr-iot/"

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
custom: http://doc.ruoyi.vip/ruoyi-vue/other/donate.html

5
.gitignore vendored
View File

@ -18,9 +18,8 @@ selenium-debug.log
*.njsproj *.njsproj
*.sln *.sln
*.local *.local
.history
package-lock.json package-lock.json
yarn.lock yarn.lock
bin/
pnpm-lock.yaml .history

125
README.md
View File

@ -1,35 +1,102 @@
# CMVR IoT 管理控制台
## 前端运行 招商车研物联网平台 Vue 3 管理端。项目以 RuoYi-Vue3 3.9.2 为新版基础框架,完整迁移原 `cmvr-iot-ui` 的业务页面、API、机器人模型和运行配置并连接 `RuoYi-Vue-master` 中的 Spring Boot 4 后端。
旧前端仅作为迁移来源,不参与本项目构建。
## 功能范围
- 系统管理:用户、角色、部门、岗位、菜单、字典、参数、通知和文件。
- 系统监控在线用户、操作日志、登录日志、缓存、服务、Quartz 和 Druid。
- 设备管理:设备注册、终端、机器人、相机、机械臂、灵巧手、机器人头部、麦克风、扬声器和电机。
- 测试平台:测试项目、任务配置、任务编排、检测项、任务实例和执行流程。
- 智能巡检:机器人、地图、点位、任务、执行实例、巡检结果、告警和驾驶舱。
- 交互评价:语音交互、触控交互、评价指标、方案、项目和语料。
- 智能座舱:车机界面、车辆配置、功能配置、流程编辑和执行评价。
- 语料与媒体:语料管理、音频上传、图片预览、视频播放和 WebSocket 实时消息。
- 开发工具:代码生成、表单构建和 Knife4j 接口文档。
## 技术栈
| 组件 | 版本 |
| --- | --- |
| Vue | 3.5.26 |
| Vite | 6.4.1 |
| Element Plus | 2.13.1 |
| Pinia | 3.0.4 |
| Vue Router | 4.6.4 |
| 可视化 | ECharts、Vue Flow、Three.js、Monaco Editor、XGPlayer |
## 环境要求
- Node.js 20 LTS
- pnpm 10
- 后端 `RuoYi-Vue-master/cmvr-iot-admin`
## 开发运行
先启动后端,确认 `http://localhost:13080` 可以访问,然后执行:
```bash ```bash
# 安装依赖 pnpm install
yarn --registry=https://registry.npmmirror.com pnpm dev
# 启动服务
yarn dev
# 构建测试环境 yarn build:stage
# 构建生产环境 yarn build:prod
# 前端访问地址 http://localhost:80
``` ```
## 内置功能 默认地址:
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 - 前端:`http://localhost:80`
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。 - 后端:`http://localhost:13080`
3. 岗位管理:配置系统用户所属担任职务。 - Knife4j`http://localhost:13080/doc.html`
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。 开发配置位于 `.env.development`
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。 ```dotenv
8. 通知公告:系统通知公告信息发布维护。 VITE_APP_TITLE=招商车研物联网平台管理系统
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 VITE_APP_BASE_API=/dev-api
10. 登录日志:系统登录日志记录查询包含登录异常。 VITE_API_URL=http://localhost:13080
11. 在线用户:当前系统中活跃用户状态监控。 VITE_WS_URL=ws://localhost:13080/ws
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。 VITE_PORT=80
13. 代码生成前后端代码的生成java、html、xml、sql支持CRUD下载 。 ```
14. 系统接口根据业务代码自动生成相关的api接口文档。
15. 服务监控监视当前系统CPU、内存、磁盘、堆栈等相关信息。 ## 构建
16. 缓存监控:对系统的缓存信息查询,命令统计等。
17. 在线构建器拖动表单元素生成相应的HTML代码。 ```bash
18. 连接池监视监视当前系统数据库连接池状态可进行分析SQL找出系统性能瓶颈。 pnpm build:dev
pnpm build:stage
pnpm build:prod
pnpm preview
```
生产构建输出到 `dist`。部署时需要由 Nginx 或网关转发 `/prod-api`,并支持 `/ws` WebSocket、文件上传和媒体请求。
## 目录说明
```text
public/models 机器人 URDF 和网格模型
public/inspection 巡检机械臂模型
src/adapters URDF/Xacro 模型适配
src/api REST API 封装
src/components 公共及业务组件
src/router 公共路由和隐藏业务路由
src/store Pinia 状态管理
src/views/device 设备管理与设备控制
src/views/test 测试业务
src/views/flow 流程编排
src/views/inspection 智能巡检
src/views/evaluate 评价管理
src/views/vi 语音交互
src/views/is 智能座舱
src/views/corpusManger 语料管理
```
业务菜单由后端 `/getRouters` 动态返回。数据库菜单中的 `component` 必须与 `src/views` 下的实际页面路径一致,权限字符必须与后端控制器保持一致。
## 配置说明
- `.env.development`:本地开发地址。
- `.env.staging`:测试环境地址。
- `.env.production`生产环境地址、Node-RED 和 MinIO。
- `vite.config.js`代理、端口、Monaco 和构建分包。
- `src/settings.js`:布局、页签和版权配置。
生产密码、访问密钥和内部网络配置应通过部署环境注入,不要提交新的敏感信息。

12
bin/build.bat Normal file
View File

@ -0,0 +1,12 @@
@echo off
echo.
echo [信息] 打包Web工程生成dist文件。
echo.
%~d0
cd %~dp0
cd ..
yarn build:prod
pause

12
bin/package.bat Normal file
View File

@ -0,0 +1,12 @@
@echo off
echo.
echo [信息] 安装Web工程生成node_modules文件。
echo.
%~d0
cd %~dp0
cd ..
yarn --registry=https://registry.npmmirror.com
pause

12
bin/run-web.bat Normal file
View File

@ -0,0 +1,12 @@
@echo off
echo.
echo [信息] 使用 Vite 命令运行 Web 工程。
echo.
%~d0
cd %~dp0
cd ..
yarn dev
pause

View File

@ -7,7 +7,7 @@
<meta name="renderer" content="webkit"> <meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<title>招商车研物联网平台管理系统</title> <title>%VITE_APP_TITLE%</title>
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]--> <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<style> <style>
html, html,

View File

@ -1,33 +1,35 @@
{ {
"name": "CMVR-IOT-UI", "name": "cmvr-iot-ui",
"version": "3.8.8", "version": "3.9.2",
"description": "CMVR-IOT平台管理系统前端UI框架", "description": "招商车研物联网平台管理系统前端",
"author": "CMVR", "author": "CMVR",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build:dev": "vite build --mode development", "build:dev": "vite build --mode development",
"build:prod": "vite build --mode production", "build:prod": "vite build",
"build:staging": "vite build --mode staging --verbose", "build:stage": "vite build --mode staging",
"build:staging": "vite build --mode staging",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "2.3.1", "@element-plus/icons-vue": "2.3.2",
"@logicflow/core": "^2.0.13", "@vue-flow/background": "^1.3.2",
"@logicflow/extension": "^2.0.18", "@vue-flow/controls": "^1.1.3",
"@vue-flow/core": "^1.48.2",
"@vueup/vue-quill": "1.2.0", "@vueup/vue-quill": "1.2.0",
"@vueuse/core": "10.11.0", "@vueuse/core": "14.1.0",
"ace-builds": "^1.43.0", "ace-builds": "^1.43.0",
"autofit.js": "^3.2.8", "autofit.js": "^3.2.8",
"axios": "0.28.1", "axios": "1.13.2",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"echarts": "5.5.1", "echarts": "5.6.0",
"element-plus": "2.9.4", "element-plus": "2.13.1",
"file-saver": "2.0.5", "file-saver": "2.0.5",
"fuse.js": "6.6.2", "fuse.js": "7.1.0",
"js-beautify": "1.14.11", "js-beautify": "1.15.4",
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"jsencrypt": "3.3.2", "jsencrypt": "3.3.2",
"json-editor-vue": "^0.18.1", "json-editor-vue": "^0.18.1",
@ -36,17 +38,17 @@
"monaco-editor": "^0.55.1", "monaco-editor": "^0.55.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"pako": "^2.1.0", "pako": "^2.1.0",
"pinia": "2.1.7", "pinia": "3.0.4",
"pnpm": "^10.23.0",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"sockjs-client": "^1.6.1", "sockjs-client": "^1.6.1",
"sortablejs": "^1.15.6",
"splitpanes": "3.1.5", "splitpanes": "3.1.5",
"three": "^0.181.2", "three": "^0.181.2",
"urdf-loader": "^0.12.6", "urdf-loader": "^0.12.6",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vue": "3.4.31", "vue": "3.5.26",
"vue-cropper": "1.1.1", "vue-cropper": "1.1.1",
"vue-router": "4.4.0", "vue-router": "4.6.4",
"vue3-ace-editor": "^2.2.4", "vue3-ace-editor": "^2.2.4",
"vue3-json-viewer": "2.3.0", "vue3-json-viewer": "2.3.0",
"vuedraggable": "4.1.0", "vuedraggable": "4.1.0",
@ -54,15 +56,20 @@
"xgplayer": "^3.0.22" "xgplayer": "^3.0.22"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "5.0.5", "@vitejs/plugin-vue": "5.2.4",
"file-loader": "^6.2.0", "sass-embedded": "1.97.2",
"sass": "1.77.5", "unplugin-auto-import": "0.18.6",
"unplugin-auto-import": "0.17.6",
"unplugin-vue-setup-extend-plus": "1.0.1", "unplugin-vue-setup-extend-plus": "1.0.1",
"vite": "5.3.2", "vite": "6.4.1",
"vite-plugin-compression": "0.5.1", "vite-plugin-compression": "0.5.1",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svg-icons": "2.0.1" "vite-plugin-svg-icons": "2.0.1"
}, },
"packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531" "overrides": {
"quill": "2.0.2"
},
"resolutions": {
"quill": "2.0.2"
},
"packageManager": "pnpm@10.24.0"
} }

5973
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,6 @@
allowBuilds:
core-js: true
es5-ext: true
esbuild: true
json-editor-vue: true
vue-demi: true

View File

@ -69,3 +69,11 @@ export function getArmStatus(params) {
params: params params: params
}) })
} }
export function getAgvStatus(params) {
return request({
url: '/api/agv/getRuntimeState',
method: 'get',
params: params
})
}

View File

@ -39,6 +39,15 @@ export function getInfo() {
}) })
} }
// 解锁屏幕
export function unlockScreen(password) {
return request({
url: '/unlockscreen',
method: 'post',
data: { password }
})
}
// 退出方法 // 退出方法
export function logout() { export function logout() {
return request({ return request({

View File

@ -43,6 +43,15 @@ export function updateDept(data) {
}) })
} }
// 保存部门排序
export function updateDeptSort(data) {
return request({
url: '/system/dept/updateSort',
method: 'put',
data: data
})
}
// 删除部门 // 删除部门
export function delDept(deptId) { export function delDept(deptId) {
return request({ return request({

View File

@ -51,6 +51,15 @@ export function updateMenu(data) {
}) })
} }
// 保存菜单排序
export function updateMenuSort(data) {
return request({
url: '/system/menu/updateSort',
method: 'put',
data: data
})
}
// 删除菜单 // 删除菜单
export function delMenu(menuId) { export function delMenu(menuId) {
return request({ return request({

View File

@ -42,3 +42,38 @@ export function delNotice(noticeId) {
method: 'delete' method: 'delete'
}) })
} }
// 首页顶部公告列表(带已读状态)
export function listNoticeTop() {
return request({
url: '/system/notice/listTop',
method: 'get'
})
}
// 标记公告已读
export function markNoticeRead(noticeId) {
return request({
url: '/system/notice/markRead',
method: 'post',
params: { noticeId }
})
}
// 批量标记已读
export function markNoticeReadAll(ids) {
return request({
url: '/system/notice/markReadAll',
method: 'post',
params: { ids }
})
}
// 查询公告已读用户列表
export function listNoticeReadUsers(query) {
return request({
url: '/system/notice/readUsers/list',
method: 'get',
params: query
})
}

View File

@ -96,7 +96,7 @@ export function updateUserPwd(oldPassword, newPassword) {
return request({ return request({
url: '/system/user/profile/updatePwd', url: '/system/user/profile/updatePwd',
method: 'put', method: 'put',
params: data data: data
}) })
} }

View File

@ -62,7 +62,7 @@ export function getInst(query) {
} }
// 查询任务实例日志 // 查询任务实例日志
export function getLogicFlow(query) { export function getFlowRuntimeLog(query) {
return request({ return request({
url: '/test/inst/log/query', url: '/test/inst/log/query',
method: 'get', method: 'get',

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1773923748724" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5930" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 212l48.8 12c101.6 24.8 176 117.6 176 220.8v254.4l18.4 18.4 24.8 25.6h-536l24.8-25.6 18.4-18.4V444.8c0-103.2 73.6-196.8 176-220.8l48.8-12M512 64c-36.8 0-64 30.4-64 68v30.4C320.8 192 223.2 307.2 223.2 444.8v228.8L136 763.2v44.8h752v-44.8l-87.2-89.6V444.8c0-137.6-97.6-252.8-224.8-283.2v-28.8c0-32-17.6-60.8-48-67.2-5.6-1.6-11.2-1.6-16-1.6z m88 808H424c0 49.6 38.4 88 88 88s88-38.4 88-88z" p-id="5931"></path></svg>

After

Width:  |  Height:  |  Size: 750 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746590936918" class="icon" viewBox="0 0 1194 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5378" xmlns:xlink="http://www.w3.org/1999/xlink" width="233.203125" height="200"><path d="M1151.9144 325.11999969V89.12a57.04000031 57.04000031 0 0 0-28.8-49.44 58.15999969 58.15999969 0 0 0-57.76000031 0 57.04000031 57.04000031 0 0 0-28.8 49.44v235.99999969c0.24 84.31999969-33.6 152.56000031-94.08 212.00000062-60.07999969 59.83999969-141.84 80.64-227.04 80.4H225.91440031L388.07439969 457.11999969a56.80000031 56.80000031 0 0 0 12.40000031-62.16 57.76000031 57.76000031 0 0 0-94.00000031-18.63999938L48.8744 631.20000031a56.88 56.88 0 0 0 0 80.79999938l264.96 262.56a58.08 58.08 0 0 0 96.55999969-25.59999938 56.80000031 56.80000031 0 0 0-14.95999969-55.2L232.07439969 731.67999969h483.44000062c116.56000031 0 226.15999969-32.08000031 308.64-113.76 82.15999969-80.80000031 128.23999969-178.15999969 127.83999938-292.87999969" p-id="5379"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1783317252138" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16006" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 64.194C264.707 64.194 64.194 264.707 64.194 512S264.707 959.806 512 959.806 959.806 759.293 959.806 512 759.293 64.194 512 64.194z m0 223.903a497.77 497.77 0 0 1-86.163-7.497C452.026 175.446 491.409 120.17 512 120.17s59.974 55.276 86.163 160.43c-27.888 4.798-56.676 7.497-86.163 7.497z m140.639-20.391c-13.594-55.676-31.786-102.556-53.077-137.34 63.572 14.594 121.247 44.681 168.927 86.063-35.285 21.79-74.268 39.082-115.85 51.277z m-281.278 0c-41.582-12.195-80.465-29.587-115.75-51.278 47.679-41.382 105.254-71.369 168.827-85.963-21.291 34.685-39.383 81.565-53.077 137.241z m308.066 216.306c-1.299-57.475-6.697-111.852-15.393-161.33 52.477-14.993 101.256-37.284 144.737-65.772 53.377 61.973 87.462 140.639 93.659 227.102H679.427z m-557.858 0c6.197-86.363 40.183-165.028 93.459-227.002 43.481 28.488 92.36 50.578 144.937 65.572-8.696 49.578-14.094 103.955-15.393 161.43H121.569z m278.979 0c1.299-55.676 6.397-105.354 13.894-148.636 31.586 5.598 64.072 8.696 97.558 8.696 33.386 0 65.971-2.999 97.558-8.596 7.497 43.281 12.495 92.96 13.894 148.536H400.548z m408.223 283.077c-43.481-28.488-92.26-50.778-144.737-65.771 8.696-49.578 14.094-103.955 15.393-161.33H902.43c-6.197 86.462-40.282 165.128-93.659 227.101z m-394.329-78.466c-7.497-43.281-12.495-92.96-13.894-148.636h222.803c-1.299 55.576-6.297 105.254-13.894 148.536-31.486-5.598-64.072-8.596-97.458-8.596s-65.97 3.099-97.557 8.696z m-199.313 78.366c-53.277-61.973-87.362-140.639-93.459-227.002h223.003c1.299 57.475 6.697 111.852 15.393 161.43-52.577 14.994-101.456 37.085-144.937 65.572zM512 903.83c-20.591 0-59.974-55.276-86.163-160.43 27.888-4.798 56.675-7.497 86.163-7.497 29.487 0 58.275 2.699 86.163 7.497C571.974 848.554 532.591 903.83 512 903.83z m87.562-10.196c21.291-34.785 39.483-81.665 53.077-137.34 41.682 12.195 80.565 29.487 115.85 51.178-47.68 41.482-105.355 71.569-168.927 86.162z m-175.124 0c-63.572-14.594-121.147-44.581-168.827-85.963 35.185-21.791 74.068-39.083 115.75-51.278 13.694 55.577 31.786 102.457 53.077 137.241z" p-id="16007" fill="currentColor"></path></svg> <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.742 36.8c2.398 7.2 5.595 12.8 11.19 18.4 4.795-4.8 7.992-11.2 10.39-18.4h-21.58zm-52.748 40h20.78l-10.39-28-10.39 28z"/><path d="M111.916 0H16.009C7.218 0 .025 7.2.025 16v96c0 8.8 7.193 16 15.984 16h95.907c8.791 0 15.984-7.2 15.984-16V16c0-8.8-6.394-16-15.984-16zM72.754 103.2c-1.598 1.6-3.197 1.6-4.795 1.6-.8 0-2.398 0-3.197-.8-.8-.8-1.599 0-1.599-.8s-.799-1.6-1.598-3.2c-.8-1.6-.8-2.4-1.599-4l-3.196-8.8H28.797L25.6 96c-1.598 3.2-2.398 5.6-3.197 7.2-.8 1.6-2.398 1.6-4.795 1.6-1.599 0-3.197-.8-4.796-1.6-1.598-1.6-2.397-2.4-2.397-4 0-.8 0-1.6.799-3.2.8-1.6.8-2.4 1.598-4l17.583-44.8c.8-1.6.8-3.2 1.599-4.8.799-1.6 1.598-3.2 2.397-4 .8-.8 1.599-2.4 3.197-3.2 1.599-.8 3.197-.8 4.796-.8 1.598 0 3.196 0 4.795.8 1.598.8 2.398 1.6 3.197 3.2.799.8 1.598 2.4 2.397 4 .8 1.6 1.599 3.2 2.398 5.6l17.583 44c1.598 3.2 2.398 5.6 2.398 7.2-.8.8-1.599 2.4-2.398 4zM116.711 72c-8.791-3.2-15.185-7.2-20.78-12-5.594 5.6-12.787 9.6-21.579 12l-2.397-4c8.791-2.4 15.984-5.6 21.579-11.2C87.939 51.2 83.144 44 81.545 36h-7.992v-3.2h21.58c-1.6-2.4-3.198-5.6-4.796-8l2.397-.8c1.599 2.4 3.997 5.6 5.595 8.8h19.98v4h-7.992c-2.397 8-6.393 15.2-11.189 20 5.595 4.8 11.988 8.8 20.78 11.2l-3.197 4z"/></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746760911144" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12537" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M395.21211 182.914448c0 62.669318 49.541323 113.472378 110.642936 113.472378 61.093427 0 110.652146-50.80306 110.65214601-113.472378 0-62.685691-49.559742-113.487727-110.65214601-113.487727C444.75241 69.426721 395.21211 120.22978 395.21211 182.914448zM395.21211 523.34693101c0 62.668295 49.541323 113.487727 110.642936 113.48772699 61.093427 0 110.652146-50.820456 110.652146-113.487727 0-62.669318-49.559742-113.472378-110.652146-113.472378C444.75241 409.874553 395.21211 460.67761301 395.21211 523.34693101zM395.21211 841.084529c0 62.686714 49.541323 113.488751 110.642936 113.488751 61.093427 0 110.652146-50.80203599 110.65214601-113.488751 0-62.669318-49.559742-113.471354-110.65214601-113.471354C444.75241 727.614198 395.21211 778.416234 395.21211 841.084529z" p-id="12538"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg> <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path fill="#5a5e66" d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>

Before

Width:  |  Height:  |  Size: 211 B

After

Width:  |  Height:  |  Size: 226 B

View File

@ -1,4 +1,4 @@
@import './variables.module.scss'; @use './variables.module.scss' as *;
@mixin colorBtn($color) { @mixin colorBtn($color) {
background: $color; background: $color;

View File

@ -1,10 +1,11 @@
@import './variables.module.scss'; @use './variables.module.scss';
@import './mixin.scss'; @use './mixin.scss';
@import './transition.scss'; @use './transition.scss';
@import './element-ui.scss'; @use './element-ui.scss';
@import './sidebar.scss'; @use './sidebar.scss';
@import './btn.scss'; @use './btn.scss';
@import './ruoyi.scss'; @use './ruoyi.scss';
@use './legacy-layout.scss';
body { body {
height: 100%; height: 100%;

View File

@ -0,0 +1,6 @@
/* Keep the migrated application visually consistent with the original CMVR UI. */
#ruoyi-git,
#ruoyi-doc,
.user-nickname {
display: none !important;
}

View File

@ -1,3 +1,5 @@
@use './variables.module.scss' as *;
#app { #app {
.main-container { .main-container {

View File

@ -78,10 +78,10 @@ watch(() => props.cron.hour, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange()) watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) { function changeRadioValue(value) {
if (props.cron.min === '*') { if (props.cron.min === '*') {
emit('update', 'min', '0', 'hour'); emit('update', 'min', '0', 'hour')
} }
if (props.cron.second === '*') { if (props.cron.second === '*') {
emit('update', 'second', '0', 'hour'); emit('update', 'second', '0', 'hour')
} }
if (value === '*') { if (value === '*') {
radioValue.value = 1 radioValue.value = 1

View File

@ -70,42 +70,46 @@
<p class="title">时间表达式</p> <p class="title">时间表达式</p>
<table> <table>
<thead> <thead>
<th v-for="item of tabTitles" :key="item">{{item}}</th> <tr>
<th>Cron 表达式</th> <th v-for="item of tabTitles" :key="item">{{item}}</th>
<th>Cron 表达式</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<td> <tr>
<span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span> <td>
<el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip> <span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
</td> <el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip>
<td> </td>
<span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span> <td>
<el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip> <span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span>
</td> <el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip>
<td> </td>
<span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span> <td>
<el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip> <span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span>
</td> <el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip>
<td> </td>
<span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span> <td>
<el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip> <span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span>
</td> <el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip>
<td> </td>
<span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span> <td>
<el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip> <span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span>
</td> <el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip>
<td> </td>
<span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span> <td>
<el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip> <span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span>
</td> <el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip>
<td> </td>
<span v-if="crontabValueObj.year.length < 10">{{crontabValueObj.year}}</span> <td>
<el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip> <span v-if="crontabValueObj.year.length < 10">{{crontabValueObj.year}}</span>
</td> <el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
<td class="result"> </td>
<span v-if="crontabValueString.length < 90">{{crontabValueString}}</span> <td class="result">
<el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip> <span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
</td> <el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -26,289 +26,289 @@ watch(() => props.ex, () => expressionChange())
// //
function expressionChange() { function expressionChange() {
// - // -
isShow.value = false; isShow.value = false
// [0123456] // [0123456]
let ruleArr = props.ex.split(' '); let ruleArr = props.ex.split(' ')
// //
let nums = 0; let nums = 0
// //
let resultArr = []; let resultArr = []
// [] // []
let nTime = new Date(); let nTime = new Date()
let nYear = nTime.getFullYear(); let nYear = nTime.getFullYear()
let nMonth = nTime.getMonth() + 1; let nMonth = nTime.getMonth() + 1
let nDay = nTime.getDate(); let nDay = nTime.getDate()
let nHour = nTime.getHours(); let nHour = nTime.getHours()
let nMin = nTime.getMinutes(); let nMin = nTime.getMinutes()
let nSecond = nTime.getSeconds(); let nSecond = nTime.getSeconds()
// 100 // 100
getSecondArr(ruleArr[0]); getSecondArr(ruleArr[0])
getMinArr(ruleArr[1]); getMinArr(ruleArr[1])
getHourArr(ruleArr[2]); getHourArr(ruleArr[2])
getDayArr(ruleArr[3]); getDayArr(ruleArr[3])
getMonthArr(ruleArr[4]); getMonthArr(ruleArr[4])
getWeekArr(ruleArr[5]); getWeekArr(ruleArr[5])
getYearArr(ruleArr[6], nYear); getYearArr(ruleArr[6], nYear)
// -便使 // -便使
let sDate = dateArr.value[0]; let sDate = dateArr.value[0]
let mDate = dateArr.value[1]; let mDate = dateArr.value[1]
let hDate = dateArr.value[2]; let hDate = dateArr.value[2]
let DDate = dateArr.value[3]; let DDate = dateArr.value[3]
let MDate = dateArr.value[4]; let MDate = dateArr.value[4]
let YDate = dateArr.value[5]; let YDate = dateArr.value[5]
// //
let sIdx = getIndex(sDate, nSecond); let sIdx = getIndex(sDate, nSecond)
let mIdx = getIndex(mDate, nMin); let mIdx = getIndex(mDate, nMin)
let hIdx = getIndex(hDate, nHour); let hIdx = getIndex(hDate, nHour)
let DIdx = getIndex(DDate, nDay); let DIdx = getIndex(DDate, nDay)
let MIdx = getIndex(MDate, nMonth); let MIdx = getIndex(MDate, nMonth)
let YIdx = getIndex(YDate, nYear); let YIdx = getIndex(YDate, nYear)
// () // ()
const resetSecond = function () { const resetSecond = function () {
sIdx = 0; sIdx = 0
nSecond = sDate[sIdx] nSecond = sDate[sIdx]
} }
const resetMin = function () { const resetMin = function () {
mIdx = 0; mIdx = 0
nMin = mDate[mIdx] nMin = mDate[mIdx]
resetSecond(); resetSecond()
} }
const resetHour = function () { const resetHour = function () {
hIdx = 0; hIdx = 0
nHour = hDate[hIdx] nHour = hDate[hIdx]
resetMin(); resetMin()
} }
const resetDay = function () { const resetDay = function () {
DIdx = 0; DIdx = 0
nDay = DDate[DIdx] nDay = DDate[DIdx]
resetHour(); resetHour()
} }
const resetMonth = function () { const resetMonth = function () {
MIdx = 0; MIdx = 0
nMonth = MDate[MIdx] nMonth = MDate[MIdx]
resetDay(); resetDay()
} }
// //
if (nYear !== YDate[YIdx]) { if (nYear !== YDate[YIdx]) {
resetMonth(); resetMonth()
} }
// //
if (nMonth !== MDate[MIdx]) { if (nMonth !== MDate[MIdx]) {
resetDay(); resetDay()
} }
// //
if (nDay !== DDate[DIdx]) { if (nDay !== DDate[DIdx]) {
resetHour(); resetHour()
} }
// //
if (nHour !== hDate[hIdx]) { if (nHour !== hDate[hIdx]) {
resetMin(); resetMin()
} }
// //
if (nMin !== mDate[mIdx]) { if (nMin !== mDate[mIdx]) {
resetSecond(); resetSecond()
} }
// //
goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) { goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
let YY = YDate[Yi]; let YY = YDate[Yi]
// //
if (nMonth > MDate[MDate.length - 1]) { if (nMonth > MDate[MDate.length - 1]) {
resetMonth(); resetMonth()
continue; continue
} }
// //
goMonth: for (let Mi = MIdx; Mi < MDate.length; Mi++) { goMonth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
// 便 // 便
let MM = MDate[Mi]; let MM = MDate[Mi];
MM = MM < 10 ? '0' + MM : MM; MM = MM < 10 ? '0' + MM : MM
// //
if (nDay > DDate[DDate.length - 1]) { if (nDay > DDate[DDate.length - 1]) {
resetDay(); resetDay()
if (Mi === MDate.length - 1) { if (Mi === MDate.length - 1) {
resetMonth(); resetMonth()
continue goYear; continue goYear
} }
continue; continue
} }
// //
goDay: for (let Di = DIdx; Di < DDate.length; Di++) { goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
// 便 // 便
let DD = DDate[Di]; let DD = DDate[Di]
let thisDD = DD < 10 ? '0' + DD : DD; let thisDD = DD < 10 ? '0' + DD : DD
// //
if (nHour > hDate[hDate.length - 1]) { if (nHour > hDate[hDate.length - 1]) {
resetHour(); resetHour()
if (Di === DDate.length - 1) { if (Di === DDate.length - 1) {
resetDay(); resetDay()
if (Mi === MDate.length - 1) { if (Mi === MDate.length - 1) {
resetMonth(); resetMonth()
continue goYear; continue goYear
} }
continue goMonth; continue goMonth
} }
continue; continue
} }
// //
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && dayRule.value !== 'workDay' && dayRule.value !== 'lastWeek' && dayRule.value !== 'lastDay') { if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && dayRule.value !== 'workDay' && dayRule.value !== 'lastWeek' && dayRule.value !== 'lastDay') {
resetDay(); resetDay()
continue goMonth; continue goMonth
} }
// //
if (dayRule.value === 'lastDay') { if (dayRule.value === 'lastDay') {
// //
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) { if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) { while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--; DD--
thisDD = DD < 10 ? '0' + DD : DD; thisDD = DD < 10 ? '0' + DD : DD
} }
} }
} else if (dayRule.value === 'workDay') { } else if (dayRule.value === 'workDay') {
// 230 // 230
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) { if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) { while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--; DD--
thisDD = DD < 10 ? '0' + DD : DD; thisDD = DD < 10 ? '0' + DD : DD
} }
} }
// X // X
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week'); let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
// //
if (thisWeek === 1) { if (thisWeek === 1) {
// //
DD++; DD++
thisDD = DD < 10 ? '0' + DD : DD; thisDD = DD < 10 ? '0' + DD : DD
// //
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) { if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD -= 3; DD -= 3
} }
} else if (thisWeek === 7) { } else if (thisWeek === 7) {
// 61 // 61
if (dayRuleSup.value !== 1) { if (dayRuleSup.value !== 1) {
DD--; DD--
} else { } else {
DD += 2; DD += 2
} }
} }
} else if (dayRule.value === 'weekDay') { } else if (dayRule.value === 'weekDay') {
// //
// //
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week'); let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
// dayRuleSup // dayRuleSup
if (dayRuleSup.value.indexOf(thisWeek) < 0) { if (dayRuleSup.value.indexOf(thisWeek) < 0) {
// //
if (Di === DDate.length - 1) { if (Di === DDate.length - 1) {
resetDay(); resetDay()
if (Mi === MDate.length - 1) { if (Mi === MDate.length - 1) {
resetMonth(); resetMonth()
continue goYear; continue goYear
} }
continue goMonth; continue goMonth
} }
continue; continue
} }
} else if (dayRule.value === 'assWeek') { } else if (dayRule.value === 'assWeek') {
// //
// 1 // 1
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week'); let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
if (dayRuleSup.value[1] >= thisWeek) { if (dayRuleSup.value[1] >= thisWeek) {
DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1; DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1
} else { } else {
DD = dayRuleSup.value[0] * 7 + dayRuleSup.value[1] - thisWeek + 1; DD = dayRuleSup.value[0] * 7 + dayRuleSup.value[1] - thisWeek + 1
} }
} else if (dayRule.value === 'lastWeek') { } else if (dayRule.value === 'lastWeek') {
// //
// 230 // 230
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) { if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) { while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--; DD--
thisDD = DD < 10 ? '0' + DD : DD; thisDD = DD < 10 ? '0' + DD : DD
} }
} }
// //
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week'); let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
// //
if (dayRuleSup.value < thisWeek) { if (dayRuleSup.value < thisWeek) {
DD -= thisWeek - dayRuleSup.value; DD -= thisWeek - dayRuleSup.value
} else if (dayRuleSup.value > thisWeek) { } else if (dayRuleSup.value > thisWeek) {
DD -= 7 - (dayRuleSup.value - thisWeek) DD -= 7 - (dayRuleSup.value - thisWeek)
} }
} }
// 1005 // 1005
DD = DD < 10 ? '0' + DD : DD; DD = DD < 10 ? '0' + DD : DD
// //
goHour: for (let hi = hIdx; hi < hDate.length; hi++) { goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi] let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
// //
if (nMin > mDate[mDate.length - 1]) { if (nMin > mDate[mDate.length - 1]) {
resetMin(); resetMin()
if (hi === hDate.length - 1) { if (hi === hDate.length - 1) {
resetHour(); resetHour()
if (Di === DDate.length - 1) { if (Di === DDate.length - 1) {
resetDay(); resetDay()
if (Mi === MDate.length - 1) { if (Mi === MDate.length - 1) {
resetMonth(); resetMonth()
continue goYear; continue goYear
} }
continue goMonth; continue goMonth
} }
continue goDay; continue goDay
} }
continue; continue
} }
// "" // ""
goMin: for (let mi = mIdx; mi < mDate.length; mi++) { goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi]; let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi]
// //
if (nSecond > sDate[sDate.length - 1]) { if (nSecond > sDate[sDate.length - 1]) {
resetSecond(); resetSecond()
if (mi === mDate.length - 1) { if (mi === mDate.length - 1) {
resetMin(); resetMin()
if (hi === hDate.length - 1) { if (hi === hDate.length - 1) {
resetHour(); resetHour()
if (Di === DDate.length - 1) { if (Di === DDate.length - 1) {
resetDay(); resetDay()
if (Mi === MDate.length - 1) { if (Mi === MDate.length - 1) {
resetMonth(); resetMonth()
continue goYear; continue goYear
} }
continue goMonth; continue goMonth
} }
continue goDay; continue goDay
} }
continue goHour; continue goHour
} }
continue; continue
} }
// "" // ""
goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) { goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si]; let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si]
// //
if (MM !== '00' && DD !== '00') { if (MM !== '00' && DD !== '00') {
resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss) resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss)
nums++; nums++
} }
// 退 // 退
if (nums === 5) break goYear; if (nums === 5) break goYear
// //
if (si === sDate.length - 1) { if (si === sDate.length - 1) {
resetSecond(); resetSecond()
if (mi === mDate.length - 1) { if (mi === mDate.length - 1) {
resetMin(); resetMin()
if (hi === hDate.length - 1) { if (hi === hDate.length - 1) {
resetHour(); resetHour()
if (Di === DDate.length - 1) { if (Di === DDate.length - 1) {
resetDay(); resetDay()
if (Mi === MDate.length - 1) { if (Mi === MDate.length - 1) {
resetMonth(); resetMonth()
continue goYear; continue goYear
} }
continue goMonth; continue goMonth
} }
continue goDay; continue goDay
} }
continue goHour; continue goHour
} }
continue goMin; continue goMin
} }
} //goSecond } //goSecond
} //goMin } //goMin
@ -318,31 +318,31 @@ function expressionChange() {
} }
// 100 // 100
if (resultArr.length === 0) { if (resultArr.length === 0) {
resultList.value = ['没有达到条件的结果!']; resultList.value = ['没有达到条件的结果!']
} else { } else {
resultList.value = resultArr; resultList.value = resultArr
if (resultArr.length !== 5) { if (resultArr.length !== 5) {
resultList.value.push('最近100年内只有上面' + resultArr.length + '条结果!') resultList.value.push('最近100年内只有上面' + resultArr.length + '条结果!')
} }
} }
// - // -
isShow.value = true; isShow.value = true
} }
// //
function getIndex(arr, value) { function getIndex(arr, value) {
if (value <= arr[0] || value > arr[arr.length - 1]) { if (value <= arr[0] || value > arr[arr.length - 1]) {
return 0; return 0
} else { } else {
for (let i = 0; i < arr.length - 1; i++) { for (let i = 0; i < arr.length - 1; i++) {
if (value > arr[i] && value <= arr[i + 1]) { if (value > arr[i] && value <= arr[i + 1]) {
return i + 1; return i + 1
} }
} }
} }
} }
// "" // ""
function getYearArr(rule, year) { function getYearArr(rule, year) {
dateArr.value[5] = getOrderArr(year, year + 100); dateArr.value[5] = getOrderArr(year, year + 100)
if (rule !== undefined) { if (rule !== undefined) {
if (rule.indexOf('-') >= 0) { if (rule.indexOf('-') >= 0) {
dateArr.value[5] = getCycleArr(rule, year + 100, false) dateArr.value[5] = getCycleArr(rule, year + 100, false)
@ -355,7 +355,7 @@ function getYearArr(rule, year) {
} }
// "" // ""
function getMonthArr(rule) { function getMonthArr(rule) {
dateArr.value[4] = getOrderArr(1, 12); dateArr.value[4] = getOrderArr(1, 12)
if (rule.indexOf('-') >= 0) { if (rule.indexOf('-') >= 0) {
dateArr.value[4] = getCycleArr(rule, 12, false) dateArr.value[4] = getCycleArr(rule, 12, false)
} else if (rule.indexOf('/') >= 0) { } else if (rule.indexOf('/') >= 0) {
@ -369,58 +369,58 @@ function getWeekArr(rule) {
// //
if (dayRule.value === '' && dayRuleSup.value === '') { if (dayRule.value === '' && dayRuleSup.value === '') {
if (rule.indexOf('-') >= 0) { if (rule.indexOf('-') >= 0) {
dayRule.value = 'weekDay'; dayRule.value = 'weekDay'
dayRuleSup.value = getCycleArr(rule, 7, false) dayRuleSup.value = getCycleArr(rule, 7, false)
} else if (rule.indexOf('#') >= 0) { } else if (rule.indexOf('#') >= 0) {
dayRule.value = 'assWeek'; dayRule.value = 'assWeek'
let matchRule = rule.match(/[0-9]{1}/g); let matchRule = rule.match(/[0-9]{1}/g)
dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])]; dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])]
dateArr.value[3] = [1]; dateArr.value[3] = [1]
if (dayRuleSup.value[1] === 7) { if (dayRuleSup.value[1] === 7) {
dayRuleSup.value[1] = 0; dayRuleSup.value[1] = 0
} }
} else if (rule.indexOf('L') >= 0) { } else if (rule.indexOf('L') >= 0) {
dayRule.value = 'lastWeek'; dayRule.value = 'lastWeek'
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0]); dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
dateArr.value[3] = [31]; dateArr.value[3] = [31]
if (dayRuleSup.value === 7) { if (dayRuleSup.value === 7) {
dayRuleSup.value = 0; dayRuleSup.value = 0
} }
} else if (rule !== '*' && rule !== '?') { } else if (rule !== '*' && rule !== '?') {
dayRule.value = 'weekDay'; dayRule.value = 'weekDay'
dayRuleSup.value = getAssignArr(rule) dayRuleSup.value = getAssignArr(rule)
} }
} }
} }
// ""- // ""-
function getDayArr(rule) { function getDayArr(rule) {
dateArr.value[3] = getOrderArr(1, 31); dateArr.value[3] = getOrderArr(1, 31)
dayRule.value = ''; dayRule.value = ''
dayRuleSup.value = ''; dayRuleSup.value = ''
if (rule.indexOf('-') >= 0) { if (rule.indexOf('-') >= 0) {
dateArr.value[3] = getCycleArr(rule, 31, false) dateArr.value[3] = getCycleArr(rule, 31, false)
dayRuleSup.value = 'null'; dayRuleSup.value = 'null'
} else if (rule.indexOf('/') >= 0) { } else if (rule.indexOf('/') >= 0) {
dateArr.value[3] = getAverageArr(rule, 31) dateArr.value[3] = getAverageArr(rule, 31)
dayRuleSup.value = 'null'; dayRuleSup.value = 'null'
} else if (rule.indexOf('W') >= 0) { } else if (rule.indexOf('W') >= 0) {
dayRule.value = 'workDay'; dayRule.value = 'workDay'
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0]); dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
dateArr.value[3] = [dayRuleSup.value]; dateArr.value[3] = [dayRuleSup.value]
} else if (rule.indexOf('L') >= 0) { } else if (rule.indexOf('L') >= 0) {
dayRule.value = 'lastDay'; dayRule.value = 'lastDay'
dayRuleSup.value = 'null'; dayRuleSup.value = 'null'
dateArr.value[3] = [31]; dateArr.value[3] = [31]
} else if (rule !== '*' && rule !== '?') { } else if (rule !== '*' && rule !== '?') {
dateArr.value[3] = getAssignArr(rule) dateArr.value[3] = getAssignArr(rule)
dayRuleSup.value = 'null'; dayRuleSup.value = 'null'
} else if (rule === '*') { } else if (rule === '*') {
dayRuleSup.value = 'null'; dayRuleSup.value = 'null'
} }
} }
// "" // ""
function getHourArr(rule) { function getHourArr(rule) {
dateArr.value[2] = getOrderArr(0, 23); dateArr.value[2] = getOrderArr(0, 23)
if (rule.indexOf('-') >= 0) { if (rule.indexOf('-') >= 0) {
dateArr.value[2] = getCycleArr(rule, 24, true) dateArr.value[2] = getCycleArr(rule, 24, true)
} else if (rule.indexOf('/') >= 0) { } else if (rule.indexOf('/') >= 0) {
@ -431,7 +431,7 @@ function getHourArr(rule) {
} }
// "" // ""
function getMinArr(rule) { function getMinArr(rule) {
dateArr.value[1] = getOrderArr(0, 59); dateArr.value[1] = getOrderArr(0, 59)
if (rule.indexOf('-') >= 0) { if (rule.indexOf('-') >= 0) {
dateArr.value[1] = getCycleArr(rule, 60, true) dateArr.value[1] = getCycleArr(rule, 60, true)
} else if (rule.indexOf('/') >= 0) { } else if (rule.indexOf('/') >= 0) {
@ -442,7 +442,7 @@ function getMinArr(rule) {
} }
// "" // ""
function getSecondArr(rule) { function getSecondArr(rule) {
dateArr.value[0] = getOrderArr(0, 59); dateArr.value[0] = getOrderArr(0, 59)
if (rule.indexOf('-') >= 0) { if (rule.indexOf('-') >= 0) {
dateArr.value[0] = getCycleArr(rule, 60, true) dateArr.value[0] = getCycleArr(rule, 60, true)
} else if (rule.indexOf('/') >= 0) { } else if (rule.indexOf('/') >= 0) {
@ -453,86 +453,86 @@ function getSecondArr(rule) {
} }
// min-max // min-max
function getOrderArr(min, max) { function getOrderArr(min, max) {
let arr = []; let arr = []
for (let i = min; i <= max; i++) { for (let i = min; i <= max; i++) {
arr.push(i); arr.push(i)
} }
return arr; return arr
} }
// //
function getAssignArr(rule) { function getAssignArr(rule) {
let arr = []; let arr = []
let assiginArr = rule.split(','); let assiginArr = rule.split(',')
for (let i = 0; i < assiginArr.length; i++) { for (let i = 0; i < assiginArr.length; i++) {
arr[i] = Number(assiginArr[i]) arr[i] = Number(assiginArr[i])
} }
arr.sort(compare) arr.sort(compare)
return arr; return arr
} }
// //
function getAverageArr(rule, limit) { function getAverageArr(rule, limit) {
let arr = []; let arr = []
let agArr = rule.split('/'); let agArr = rule.split('/')
let min = Number(agArr[0]); let min = Number(agArr[0])
let step = Number(agArr[1]); let step = Number(agArr[1])
while (min <= limit) { while (min <= limit) {
arr.push(min); arr.push(min)
min += step; min += step
} }
return arr; return arr
} }
// //
function getCycleArr(rule, limit, status) { function getCycleArr(rule, limit, status) {
// status--01 // status--01
let arr = []; let arr = []
let cycleArr = rule.split('-'); let cycleArr = rule.split('-')
let min = Number(cycleArr[0]); let min = Number(cycleArr[0])
let max = Number(cycleArr[1]); let max = Number(cycleArr[1])
if (min > max) { if (min > max) {
max += limit; max += limit
} }
for (let i = min; i <= max; i++) { for (let i = min; i <= max; i++) {
let add = 0; let add = 0
if (status === false && i % limit === 0) { if (status === false && i % limit === 0) {
add = limit; add = limit
} }
arr.push(Math.round(i % limit + add)) arr.push(Math.round(i % limit + add))
} }
arr.sort(compare) arr.sort(compare)
return arr; return arr
} }
// Array.sort // Array.sort
function compare(value1, value2) { function compare(value1, value2) {
if (value2 - value1 > 0) { if (value2 - value1 > 0) {
return -1; return -1
} else { } else {
return 1; return 1
} }
} }
// 2017-9-19 18:04:33 // 2017-9-19 18:04:33
function formatDate(value, type) { function formatDate(value, type) {
// //
let time = typeof value == 'number' ? new Date(value) : value; let time = typeof value == 'number' ? new Date(value) : value
let Y = time.getFullYear(); let Y = time.getFullYear()
let M = time.getMonth() + 1; let M = time.getMonth() + 1
let D = time.getDate(); let D = time.getDate()
let h = time.getHours(); let h = time.getHours()
let m = time.getMinutes(); let m = time.getMinutes()
let s = time.getSeconds(); let s = time.getSeconds()
let week = time.getDay(); let week = time.getDay()
// type // type
if (type === undefined) { if (type === undefined) {
return Y + '-' + (M < 10 ? '0' + M : M) + '-' + (D < 10 ? '0' + D : D) + ' ' + (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s); return Y + '-' + (M < 10 ? '0' + M : M) + '-' + (D < 10 ? '0' + D : D) + ' ' + (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
} else if (type === 'week') { } else if (type === 'week') {
// quartz 1 // quartz 1
return week + 1; return week + 1
} }
} }
// //
function checkDate(value) { function checkDate(value) {
let time = new Date(value); let time = new Date(value)
let format = formatDate(time) let format = formatDate(time)
return value === format; return value === format
} }
onMounted(() => { onMounted(() => {
expressionChange() expressionChange()

View File

@ -61,22 +61,24 @@ const props = defineProps({
} }
} }
}) })
const fullYear = ref(0)
const maxFullYear = ref(0) const fullYear = Number(new Date().getFullYear())
const maxFullYear = fullYear + 10
const radioValue = ref(1) const radioValue = ref(1)
const cycle01 = ref(0) const cycle01 = ref(fullYear)
const cycle02 = ref(0) const cycle02 = ref(fullYear + 1)
const average01 = ref(0) const average01 = ref(fullYear)
const average02 = ref(1) const average02 = ref(1)
const checkboxList = ref([]) const checkboxList = ref([])
const checkCopy = ref([]) const checkCopy = ref([fullYear])
const cycleTotal = computed(() => { const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, fullYear.value, maxFullYear.value - 1) cycle01.value = props.check(cycle01.value, fullYear, maxFullYear - 1)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear.value) cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear)
return cycle01.value + '-' + cycle02.value return cycle01.value + '-' + cycle02.value
}) })
const averageTotal = computed(() => { const averageTotal = computed(() => {
average01.value = props.check(average01.value, fullYear.value, maxFullYear.value - 1) average01.value = props.check(average01.value, fullYear, maxFullYear - 1)
average02.value = props.check(average02.value, 1, 10) average02.value = props.check(average02.value, 1, 10)
return average01.value + '/' + average02.value return average01.value + '/' + average02.value
}) })
@ -97,8 +99,8 @@ function changeRadioValue(value) {
radioValue.value = 3 radioValue.value = 3
} else if (value.indexOf("/") > -1) { } else if (value.indexOf("/") > -1) {
const indexArr = value.split('/') const indexArr = value.split('/')
average01.value = Number(indexArr[1]) average01.value = Number(indexArr[0])
average02.value = Number(indexArr[0]) average02.value = Number(indexArr[1])
radioValue.value = 4 radioValue.value = 4
} else { } else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))] checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
@ -129,14 +131,6 @@ function onRadioChange() {
break break
} }
} }
onMounted(() => {
fullYear.value = Number(new Date().getFullYear())
maxFullYear.value = fullYear.value + 10
cycle01.value = fullYear.value
cycle02.value = cycle01.value + 1
average01.value = fullYear.value
checkCopy.value = [fullYear.value]
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<template v-for="(item, index) in options"> <template v-for="(item, index) in options">
<template v-if="values.includes(item.value)"> <template v-if="isValueMatch(item.value)">
<span <span
v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)" v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)"
:key="item.value" :key="item.value"
@ -26,7 +26,7 @@
<script setup> <script setup>
// //
const unmatchArray = ref([]); const unmatchArray = ref([])
const props = defineProps({ const props = defineProps({
// //
@ -45,33 +45,38 @@ const props = defineProps({
type: String, type: String,
default: ",", default: ",",
} }
}); })
const values = computed(() => { const values = computed(() => {
if (props.value === null || typeof props.value === 'undefined' || props.value === '') return []; if (props.value === null || typeof props.value === 'undefined' || props.value === '') return []
return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator); if (typeof props.value === 'number' || typeof props.value === 'boolean') return [props.value]
}); return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator)
})
const unmatch = computed(() => { const unmatch = computed(() => {
unmatchArray.value = []; unmatchArray.value = []
// value // value
if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false
// //
let unmatch = false // let unmatch = false //
values.value.forEach(item => { values.value.forEach(item => {
if (!props.options.some(v => v.value === item)) { if (!props.options.some(v => v.value == item)) {
unmatchArray.value.push(item) unmatchArray.value.push(item)
unmatch = true // true unmatch = true // true
} }
}) })
return unmatch // return unmatch //
}); })
function handleArray(array) { function handleArray(array) {
if (array.length === 0) return ""; if (array.length === 0) return ""
return array.reduce((pre, cur) => { return array.reduce((pre, cur) => {
return pre + " " + cur; return pre + " " + cur
}); })
}
function isValueMatch(itemValue) {
return values.value.some(val => val == itemValue)
} }
</script> </script>

View File

@ -27,17 +27,18 @@
</template> </template>
<script setup> <script setup>
import { QuillEditor } from "@vueup/vue-quill"; import axios from 'axios'
import "@vueup/vue-quill/dist/vue-quill.snow.css"; import { QuillEditor } from "@vueup/vue-quill"
import { getToken } from "@/utils/auth"; import "@vueup/vue-quill/dist/vue-quill.snow.css"
import { getToken } from "@/utils/auth"
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance()
const quillEditorRef = ref(); const quillEditorRef = ref()
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload") //
const headers = ref({ const headers = ref({
Authorization: "Bearer " + getToken() Authorization: "Bearer " + getToken()
}); })
const props = defineProps({ const props = defineProps({
/* 编辑器的内容 */ /* 编辑器的内容 */
@ -69,7 +70,7 @@ const props = defineProps({
type: String, type: String,
default: "url", default: "url",
} }
}); })
const options = ref({ const options = ref({
theme: "snow", theme: "snow",
@ -92,59 +93,60 @@ const options = ref({
}, },
placeholder: "请输入内容", placeholder: "请输入内容",
readOnly: props.readOnly readOnly: props.readOnly
}); })
const styles = computed(() => { const styles = computed(() => {
let style = {}; let style = {}
if (props.minHeight) { if (props.minHeight) {
style.minHeight = `${props.minHeight}px`; style.minHeight = `${props.minHeight}px`
} }
if (props.height) { if (props.height) {
style.height = `${props.height}px`; style.height = `${props.height}px`
} }
return style; return style
}); })
const content = ref(""); const content = ref("")
watch(() => props.modelValue, (v) => { watch(() => props.modelValue, (v) => {
if (v !== content.value) { if (v !== content.value) {
content.value = v == undefined ? "<p></p>" : v; content.value = v == undefined ? "<p></p>" : v
} }
}, { immediate: true }); }, { immediate: true })
// //
onMounted(() => { onMounted(() => {
if (props.type == 'url') { if (props.type == 'url') {
let quill = quillEditorRef.value.getQuill(); let quill = quillEditorRef.value.getQuill()
let toolbar = quill.getModule("toolbar"); let toolbar = quill.getModule("toolbar")
toolbar.addHandler("image", (value) => { toolbar.addHandler("image", (value) => {
if (value) { if (value) {
proxy.$refs.uploadRef.click(); proxy.$refs.uploadRef.click()
} else { } else {
quill.format("image", false); quill.format("image", false)
} }
}); })
quill.root.addEventListener('paste', handlePasteCapture, true)
} }
}); })
// //
function handleBeforeUpload(file) { function handleBeforeUpload(file) {
const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"]; const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"]
const isJPG = type.includes(file.type); const isJPG = type.includes(file.type)
// //
if (!isJPG) { if (!isJPG) {
proxy.$modal.msgError(`图片格式错误!`); proxy.$modal.msgError(`图片格式错误!`)
return false; return false
} }
// //
if (props.fileSize) { if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize; const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) { if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`); proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
return false; return false
} }
} }
return true; return true
} }
// //
@ -152,21 +154,44 @@ function handleUploadSuccess(res, file) {
// //
if (res.code == 200) { if (res.code == 200) {
// //
let quill = toRaw(quillEditorRef.value).getQuill(); let quill = toRaw(quillEditorRef.value).getQuill()
// //
let length = quill.selection.savedRange.index; let length = quill.selection.savedRange.index
// res.url // res.url
quill.insertEmbed(length, "image", import.meta.env.VITE_APP_BASE_API + res.fileName); quill.insertEmbed(length, "image", import.meta.env.VITE_APP_BASE_API + res.fileName)
// //
quill.setSelection(length + 1); quill.setSelection(length + 1)
} else { } else {
proxy.$modal.msgError("图片插入失败"); proxy.$modal.msgError("图片插入失败")
} }
} }
// //
function handleUploadError() { function handleUploadError() {
proxy.$modal.msgError("图片插入失败"); proxy.$modal.msgError("图片插入失败")
}
//
function handlePasteCapture(e) {
const clipboard = e.clipboardData || window.clipboardData
if (clipboard && clipboard.items) {
for (let i = 0; i < clipboard.items.length; i++) {
const item = clipboard.items[i]
if (item.type.indexOf('image') !== -1) {
e.preventDefault()
const file = item.getAsFile()
insertImage(file)
}
}
}
}
function insertImage(file) {
const formData = new FormData()
formData.append("file", file)
axios.post(uploadUrl.value, formData, { headers: { "Content-Type": "multipart/form-data", Authorization: headers.value.Authorization } }).then(res => {
handleUploadSuccess(res.data)
})
} }
</script> </script>

View File

@ -0,0 +1,137 @@
<template>
<el-dialog :title="title" v-model="visible" :width="width" append-to-body @close="handleClose">
<el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="headers" :action="uploadUrl" :disabled="isUploading" :on-progress="handleProgress" :on-change="handleFileChange" :on-remove="handleFileRemove" :on-success="handleSuccess" :auto-upload="false" drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="updateSupport"> {{ updateSupportLabel }} </el-checkbox>
</div>
<span>仅允许导入xlsxlsx格式文件</span>
<el-link v-if="templateUrl" type="primary" underline="never" style="font-size: 12px; vertical-align: baseline" @click="handleDownloadTemplate">下载模板</el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit"> </el-button>
<el-button @click="visible = false"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { getToken } from '@/utils/auth'
const { proxy } = getCurrentInstance()
const props = defineProps({
//
title: {
type: String,
default: '数据导入'
},
//
width: {
type: String,
default: '400px'
},
//
action: {
type: String,
required: true
},
//
templateAction: {
type: String,
default: ''
},
//
templateFileName: {
type: String,
default: 'template'
},
//
updateSupportLabel: {
type: String,
default: '是否更新已经存在的数据'
}
})
const emit = defineEmits(['success'])
const uploadRef = ref(null)
const visible = ref(false)
const selectedFile = ref(null)
const isUploading = ref(false)
const updateSupport = ref(false)
const headers = { Authorization: 'Bearer ' + getToken() }
const uploadUrl = computed(() => {
return import.meta.env.VITE_APP_BASE_API + props.action + '?updateSupport=' + (updateSupport.value ? 1 : 0)
})
const templateUrl = computed(() => !!props.templateAction)
// ref
function open() {
updateSupport.value = false
isUploading.value = false
visible.value = true
nextTick(() => {
selectedFile.value = null
uploadRef.value?.clearFiles()
})
}
//
function handleClose() {
isUploading.value = false
selectedFile.value = null
uploadRef.value?.clearFiles()
}
//
function handleDownloadTemplate() {
proxy.download(props.templateAction, {}, `${props.templateFileName}_${new Date().getTime()}.xlsx`)
}
//
function handleProgress() {
isUploading.value = true
}
/** 文件选择处理 */
const handleFileChange = (file, fileList) => {
selectedFile.value = file
}
/** 文件删除处理 */
const handleFileRemove = (file, fileList) => {
selectedFile.value = null
}
//
function handleSuccess(response) {
visible.value = false
isUploading.value = false
selectedFile.value = null
uploadRef.value?.clearFiles()
proxy.$alert("<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>" + response.msg + '</div>', '导入结果', { dangerouslyUseHTMLString: true })
emit('success')
}
//
function handleSubmit() {
const file = selectedFile.value
if (!file || file.length === 0 || !file.name.toLowerCase().endsWith('.xls') && !file.name.toLowerCase().endsWith('.xlsx')) {
proxy.$modal.msgError("请选择后缀为 “xls”或“xlsx”的文件。")
return
}
uploadRef.value.submit()
}
defineExpose({ open })
</script>

View File

@ -5,6 +5,7 @@
:action="uploadFileUrl" :action="uploadFileUrl"
:before-upload="handleBeforeUpload" :before-upload="handleBeforeUpload"
:file-list="fileList" :file-list="fileList"
:data="data"
:limit="limit" :limit="limit"
:on-error="handleUploadError" :on-error="handleUploadError"
:on-exceed="handleExceed" :on-exceed="handleExceed"
@ -13,25 +14,26 @@
:headers="headers" :headers="headers"
class="upload-file-uploader" class="upload-file-uploader"
ref="fileUpload" ref="fileUpload"
v-if="!disabled"
> >
<!-- 上传按钮 --> <!-- 上传按钮 -->
<el-button type="primary">选取文件</el-button> <el-button type="primary">选取文件</el-button>
</el-upload> </el-upload>
<!-- 上传提示 --> <!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip"> <div class="el-upload__tip" v-if="showTip && !disabled">
请上传 请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template> <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template> <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件 的文件
</div> </div>
<!-- 文件列表 --> <!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul"> <transition-group ref="uploadFileList" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList"> <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank"> <el-link :href="`${baseUrl}${file.url}`" underline="never" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span> <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link> </el-link>
<div class="ele-upload-list__item-content-action"> <div class="ele-upload-list__item-content-action">
<el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link> <el-link underline="never" @click="handleDelete(index)" type="danger" v-if="!disabled">&nbsp;删除</el-link>
</div> </div>
</li> </li>
</transition-group> </transition-group>
@ -39,132 +41,152 @@
</template> </template>
<script setup> <script setup>
import { getToken } from "@/utils/auth"; import { getToken } from "@/utils/auth"
import Sortable from 'sortablejs'
const props = defineProps({ const props = defineProps({
modelValue: [String, Object, Array], modelValue: [String, Object, Array],
//
action: {
type: String,
default: "/common/upload"
},
//
data: {
type: Object
},
// //
limit: { limit: {
type: Number, type: Number,
default: 5, default: 5
}, },
// (MB) // (MB)
fileSize: { fileSize: {
type: Number, type: Number,
default: 5, default: 5
}, },
// , ['png', 'jpg', 'jpeg'] // , ['png', 'jpg', 'jpeg']
fileType: { fileType: {
type: Array, type: Array,
default: () => ["doc", "xls", "ppt", "txt", "pdf"], default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"]
}, },
// //
isShowTip: { isShowTip: {
type: Boolean, type: Boolean,
default: true default: true
},
//
disabled: {
type: Boolean,
default: false
},
//
drag: {
type: Boolean,
default: true
} }
}); })
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance()
const emit = defineEmits(); const emit = defineEmits()
const number = ref(0); const number = ref(0)
const uploadList = ref([]); const uploadList = ref([])
const baseUrl = import.meta.env.VITE_APP_BASE_API; const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) //
const headers = ref({ Authorization: "Bearer " + getToken() }); const headers = ref({ Authorization: "Bearer " + getToken() })
const fileList = ref([]); const fileList = ref([])
const showTip = computed( const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize) () => props.isShowTip && (props.fileType || props.fileSize)
); )
watch(() => props.modelValue, val => { watch(() => props.modelValue, val => {
if (val) { if (val) {
let temp = 1; let temp = 1
// //
const list = Array.isArray(val) ? val : props.modelValue.split(','); const list = Array.isArray(val) ? val : props.modelValue.split(',')
// //
fileList.value = list.map(item => { fileList.value = list.map(item => {
if (typeof item === "string") { if (typeof item === "string") {
item = { name: item, url: item }; item = { name: item, url: item }
} }
item.uid = item.uid || new Date().getTime() + temp++; item.uid = item.uid || new Date().getTime() + temp++
return item; return item
}); })
} else { } else {
fileList.value = []; fileList.value = []
return []; return []
} }
},{ deep: true, immediate: true }); },{ deep: true, immediate: true })
// //
function handleBeforeUpload(file) { function handleBeforeUpload(file) {
// //
if (props.fileType.length) { if (props.fileType.length) {
const fileName = file.name.split('.'); const fileName = file.name.split('.')
const fileExt = fileName[fileName.length - 1]; const fileExt = fileName[fileName.length - 1]
const isTypeOk = props.fileType.indexOf(fileExt) >= 0; const isTypeOk = props.fileType.indexOf(fileExt) >= 0
if (!isTypeOk) { if (!isTypeOk) {
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}格式文件!`); proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}格式文件!`)
return false; return false
} }
} }
// //
if (file.name.includes(',')) { if (file.name.includes(',')) {
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!'); proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
return false; return false
} }
// //
if (props.fileSize) { if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize; const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) { if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`); proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
return false; return false
} }
} }
proxy.$modal.loading("正在上传文件,请稍候..."); proxy.$modal.loading("正在上传文件,请稍候...")
number.value++; number.value++
return true; return true
} }
// //
function handleExceed() { function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`); proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
} }
// //
function handleUploadError(err) { function handleUploadError(err) {
proxy.$modal.msgError("上传文件失败"); proxy.$modal.msgError("上传文件失败")
proxy.$modal.closeLoading()
} }
// //
function handleUploadSuccess(res, file) { function handleUploadSuccess(res, file) {
if (res.code === 200) { if (res.code === 200) {
uploadList.value.push({ name: res.fileName, url: res.fileName }); uploadList.value.push({ name: res.fileName, url: res.fileName })
uploadedSuccessfully(); uploadedSuccessfully()
} else { } else {
number.value--; number.value--
proxy.$modal.closeLoading(); proxy.$modal.closeLoading()
proxy.$modal.msgError(res.msg); proxy.$modal.msgError(res.msg)
proxy.$refs.fileUpload.handleRemove(file); proxy.$refs.fileUpload.handleRemove(file)
uploadedSuccessfully(); uploadedSuccessfully()
} }
} }
// //
function handleDelete(index) { function handleDelete(index) {
fileList.value.splice(index, 1); fileList.value.splice(index, 1)
emit("update:modelValue", listToString(fileList.value)); emit("update:modelValue", listToString(fileList.value))
} }
// //
function uploadedSuccessfully() { function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) { if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value); fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
uploadList.value = []; uploadList.value = []
number.value = 0; number.value = 0
console.log('fileList.value', fileList.value) emit("update:modelValue", listToString(fileList.value))
emit("update:modelValue", listToString(fileList.value)); proxy.$modal.closeLoading()
proxy.$modal.closeLoading();
} }
} }
@ -172,25 +194,41 @@ function uploadedSuccessfully() {
function getFileName(name) { function getFileName(name) {
// url // url
if (name.lastIndexOf("/") > -1) { if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1); return name.slice(name.lastIndexOf("/") + 1)
} else { } else {
return name; return name
} }
} }
// //
function listToString(list, separator) { function listToString(list, separator) {
let strs = ""; let strs = ""
separator = separator || ","; separator = separator || ","
for (let i in list) { for (let i in list) {
if (list[i].url) { if (list[i].url) {
strs += list[i].url + separator; strs += list[i].url + separator
} }
} }
return strs != '' ? strs.substr(0, strs.length - 1) : ''; return strs != '' ? strs.substr(0, strs.length - 1) : ''
} }
</script>
//
onMounted(() => {
if (props.drag && !props.disabled) {
nextTick(() => {
const element = proxy.$refs.uploadFileList?.$el || proxy.$refs.uploadFileList
Sortable.create(element, {
ghostClass: 'file-upload-darg',
onEnd: (evt) => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
fileList.value.splice(evt.newIndex, 0, movedItem)
emit('update:modelValue', listToString(fileList.value))
}
})
})
}
})
</script>
<style scoped lang="scss"> <style scoped lang="scss">
.upload-file-uploader { .upload-file-uploader {
margin-bottom: 5px; margin-bottom: 5px;

View File

@ -24,7 +24,7 @@ defineProps({
const emit = defineEmits() const emit = defineEmits()
const toggleClick = () => { const toggleClick = () => {
emit('toggleClick'); emit('toggleClick')
} }
</script> </script>

View File

@ -1,19 +1,78 @@
<template> <template>
<div :class="{ 'show': show }" class="header-search"> <div class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" /> <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-select <el-dialog
ref="headerSearchSelectRef" v-model="show"
v-model="search" width="600"
:remote-method="querySearch" @close="close"
filterable @opened="onDialogOpened"
default-first-option :show-close="false"
remote append-to-body
placeholder="Search"
class="header-search-select"
@change="change"
> >
<el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" /> <el-input
</el-select> v-model="search"
ref="headerSearchSelectRef"
size="large"
@input="querySearch"
prefix-icon="Search"
placeholder="菜单搜索支持标题、URL模糊查询"
clearable
@keyup.enter="selectActiveResult"
@keydown.up.prevent="navigateResult('up')"
@keydown.down.prevent="navigateResult('down')"
>
</el-input>
<div class="result-count" v-if="search && options.length > 0">
找到 <strong>{{ options.length }}</strong> 个结果
</div>
<div class="result-wrap">
<el-scrollbar>
<template v-if="options.length > 0">
<div
class="search-item"
tabindex="1"
v-for="(item, index) in options"
:key="item.path"
:class="{ 'is-active': index === activeIndex }"
:style="activeStyle(index)"
@mouseenter="activeIndex = index"
@mouseleave="activeIndex = -1"
>
<div class="left">
<svg-icon class="menu-icon" :icon-class="item.icon" />
</div>
<div class="search-info" @click="change(item)">
<div class="menu-title" v-html="highlightText(item.title.join(' / '))"></div>
<div class="menu-path" v-html="highlightText(item.path)"></div>
</div>
<svg-icon icon-class="enter" v-show="index === activeIndex" />
</div>
</template>
<div class="empty-state" v-else-if="search && options.length === 0">
<el-icon class="empty-icon"><Search /></el-icon>
<p class="empty-text">未找到 "<strong>{{ search }}</strong>" 相关菜单</p>
<p class="empty-tip">试试其他关键词或路径</p>
</div>
</el-scrollbar>
</div>
<div class="search-footer">
<span class="shortcut-item">
<kbd></kbd><kbd></kbd> 切换
</span>
<span class="shortcut-item">
<kbd></kbd> 选择
</span>
<span class="shortcut-item">
<kbd>Esc</kbd> 关闭
</span>
</div>
</el-dialog>
</div> </div>
</template> </template>
@ -21,54 +80,66 @@
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { getNormalPath } from '@/utils/ruoyi' import { getNormalPath } from '@/utils/ruoyi'
import { isHttp } from '@/utils/validate' import { isHttp } from '@/utils/validate'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission' import usePermissionStore from '@/store/modules/permission'
const search = ref(''); const search = ref('')
const options = ref([]); const options = ref([])
const searchPool = ref([]); const searchPool = ref([])
const show = ref(false); const activeIndex = ref(-1)
const fuse = ref(undefined); const show = ref(false)
const headerSearchSelectRef = ref(null); const fuse = ref(undefined)
const router = useRouter(); const headerSearchSelectRef = ref(null)
const routes = computed(() => usePermissionStore().routes); const router = useRouter()
const theme = computed(() => useSettingsStore().theme)
const routes = computed(() => usePermissionStore().defaultRoutes)
function click() { function click() {
show.value = !show.value show.value = !show.value
if (show.value) { if (show.value) {
headerSearchSelectRef.value && headerSearchSelectRef.value.focus() options.value = searchPool.value
} }
}; }
function onDialogOpened() {
nextTick(() => {
headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
})
}
function close() { function close() {
headerSearchSelectRef.value && headerSearchSelectRef.value.blur() headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
options.value = [] search.value = ''
options.value = searchPool.value
show.value = false show.value = false
activeIndex.value = -1
} }
function change(val) { function change(val) {
const path = val.path; const p = val.path
const query = val.query; const query = val.query
if (isHttp(path)) { if (isHttp(p)) {
// http(s):// // http(s)://
const pindex = path.indexOf("http"); const pindex = p.indexOf("http")
window.open(path.substr(pindex, path.length), "_blank"); window.open(p.substr(pindex, p.length), "_blank")
} else { } else {
if (query) { if (query) {
router.push({ path: path, query: JSON.parse(query) }); router.push({ path: p, query: JSON.parse(query) })
} else { } else {
router.push(path) router.push(p)
} }
} }
search.value = '' search.value = ''
options.value = [] options.value = searchPool.value
nextTick(() => { nextTick(() => {
show.value = false show.value = false
}) })
} }
function initFuse(list) { function initFuse(list) {
fuse.value = new Fuse(list, { fuse.value = new Fuse(list, {
shouldSort: true, shouldSort: true,
threshold: 0.4, threshold: 0.2,
location: 0,
distance: 100, distance: 100,
minMatchCharLength: 1, minMatchCharLength: 1,
keys: [{ keys: [{
@ -80,34 +151,27 @@ function initFuse(list) {
}] }]
}) })
} }
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
function generateRoutes(routes, basePath = '', prefixTitle = []) { function generateRoutes(routes, basePath = '', prefixTitle = []) {
let res = [] let res = []
for (const r of routes) { for (const r of routes) {
// skip hidden router
if (r.hidden) { continue } if (r.hidden) { continue }
const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path; const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path
const data = { const data = {
path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path, path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
title: [...prefixTitle] title: [...prefixTitle],
icon: ''
} }
if (r.meta && r.meta.title) { if (r.meta && r.meta.title) {
data.title = [...data.title, r.meta.title] data.title = [...data.title, r.meta.title]
data.icon = r.meta.icon
if (r.redirect !== 'noRedirect') { if (r.redirect !== "noRedirect") {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data) res.push(data)
} }
} }
if (r.query) { if (r.query) {
data.query = r.query data.query = r.query
} }
// recursive child routes
if (r.children) { if (r.children) {
const tempRoutes = generateRoutes(r.children, data.path, data.title) const tempRoutes = generateRoutes(r.children, data.path, data.title)
if (tempRoutes.length >= 1) { if (tempRoutes.length >= 1) {
@ -117,28 +181,63 @@ function generateRoutes(routes, basePath = '', prefixTitle = []) {
} }
return res return res
} }
function querySearch(query) { function querySearch(query) {
activeIndex.value = -1
if (query !== '') { if (query !== '') {
options.value = fuse.value.search(query) const q = query.toLowerCase()
const pathMatches = searchPool.value.filter(item =>
item.path.toLowerCase().includes(q)
)
const fuseMatches = fuse.value.search(query).map(item => item.item)
const merged = [...pathMatches]
fuseMatches.forEach(item => {
if (!merged.find(m => m.path === item.path)) {
merged.push(item)
}
})
options.value = merged
} else { } else {
options.value = [] options.value = searchPool.value
} }
} }
onMounted(() => { function activeStyle(index) {
searchPool.value = generateRoutes(routes.value); if (index !== activeIndex.value) return {}
}) return {
"background-color": theme.value,
watchEffect(() => { "color": "#fff"
searchPool.value = generateRoutes(routes.value)
})
watch(show, (value) => {
if (value) {
document.body.addEventListener('click', close)
} else {
document.body.removeEventListener('click', close)
} }
}
function navigateResult(direction) {
if (direction === "up") {
activeIndex.value = activeIndex.value <= 0 ? options.value.length - 1 : activeIndex.value - 1
} else if (direction === "down") {
activeIndex.value = activeIndex.value >= options.value.length - 1 ? 0 : activeIndex.value + 1
}
}
function selectActiveResult() {
if (options.value.length > 0 && activeIndex.value >= 0) {
change(options.value[activeIndex.value])
}
}
function highlightText(text) {
if (!text) return ''
if (!search.value) return text
const keyword = escapeRegExp(search.value)
const reg = new RegExp(`(${keyword})`, 'gi')
return text.replace(reg, '<span class="highlight">$1</span>')
}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
onMounted(() => {
searchPool.value = generateRoutes(routes.value)
}) })
watch(searchPool, (list) => { watch(searchPool, (list) => {

View File

@ -30,11 +30,11 @@ const props = defineProps({
activeIcon: { activeIcon: {
type: String type: String
} }
}); })
const iconName = ref(''); const iconName = ref('')
const iconList = ref(icons); const iconList = ref(icons)
const emit = defineEmits(['selected']); const emit = defineEmits(['selected'])
function filterIcons() { function filterIcons() {
iconList.value = icons iconList.value = icons

View File

@ -1,8 +1,8 @@
let icons = [] let icons = []
const modules = import.meta.glob('./../../assets/icons/svg/*.svg'); const modules = import.meta.glob('./../../assets/icons/svg/*.svg')
for (const path in modules) { for (const path in modules) {
const p = path.split('assets/icons/svg/')[1].split('.svg')[0]; const p = path.split('assets/icons/svg/')[1].split('.svg')[0]
icons.push(p); icons.push(p)
} }
export default icons export default icons

View File

@ -15,7 +15,7 @@
</template> </template>
<script setup> <script setup>
import { isExternal } from "@/utils/validate"; import { isExternal } from "@/utils/validate"
const props = defineProps({ const props = defineProps({
src: { src: {
@ -30,41 +30,41 @@ const props = defineProps({
type: [Number, String], type: [Number, String],
default: "" default: ""
} }
}); })
const realSrc = computed(() => { const realSrc = computed(() => {
if (!props.src) { if (!props.src) {
return; return
} }
let real_src = props.src.split(",")[0]; let real_src = props.src.split(",")[0]
if (isExternal(real_src)) { if (isExternal(real_src)) {
return real_src; return real_src
} }
return import.meta.env.VITE_APP_BASE_API + real_src; return import.meta.env.VITE_APP_BASE_API + real_src
}); })
const realSrcList = computed(() => { const realSrcList = computed(() => {
if (!props.src) { if (!props.src) {
return; return
} }
let real_src_list = props.src.split(","); let real_src_list = props.src.split(",")
let srcList = []; let srcList = []
real_src_list.forEach(item => { real_src_list.forEach(item => {
if (isExternal(item)) { if (isExternal(item)) {
return srcList.push(item); return srcList.push(item)
} }
return srcList.push(import.meta.env.VITE_APP_BASE_API + item); return srcList.push(import.meta.env.VITE_APP_BASE_API + item)
}); })
return srcList; return srcList
}); })
const realWidth = computed(() => const realWidth = computed(() =>
typeof props.width == "string" ? props.width : `${props.width}px` typeof props.width == "string" ? props.width : `${props.width}px`
); )
const realHeight = computed(() => const realHeight = computed(() =>
typeof props.height == "string" ? props.height : `${props.height}px` typeof props.height == "string" ? props.height : `${props.height}px`
); )
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -2,10 +2,12 @@
<div class="component-upload-image"> <div class="component-upload-image">
<el-upload <el-upload
multiple multiple
:disabled="disabled"
:action="uploadImgUrl" :action="uploadImgUrl"
list-type="picture-card" list-type="picture-card"
:on-success="handleUploadSuccess" :on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload" :before-upload="handleBeforeUpload"
:data="data"
:limit="limit" :limit="limit"
:on-error="handleUploadError" :on-error="handleUploadError"
:on-exceed="handleExceed" :on-exceed="handleExceed"
@ -20,7 +22,7 @@
<el-icon class="avatar-uploader-icon"><plus /></el-icon> <el-icon class="avatar-uploader-icon"><plus /></el-icon>
</el-upload> </el-upload>
<!-- 上传提示 --> <!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip"> <div class="el-upload__tip" v-if="showTip && !disabled">
请上传 请上传
<template v-if="fileSize"> <template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
@ -46,166 +48,202 @@
</template> </template>
<script setup> <script setup>
import { getToken } from "@/utils/auth"; import { getToken } from "@/utils/auth"
import { isExternal } from "@/utils/validate"; import { isExternal } from "@/utils/validate"
import Sortable from 'sortablejs'
const props = defineProps({ const props = defineProps({
modelValue: [String, Object, Array], modelValue: [String, Object, Array],
//
action: {
type: String,
default: "/common/upload"
},
//
data: {
type: Object
},
// //
limit: { limit: {
type: Number, type: Number,
default: 5, default: 5
}, },
// (MB) // (MB)
fileSize: { fileSize: {
type: Number, type: Number,
default: 5, default: 5
}, },
// , ['png', 'jpg', 'jpeg'] // , ['png', 'jpg', 'jpeg']
fileType: { fileType: {
type: Array, type: Array,
default: () => ["png", "jpg", "jpeg"], default: () => ["png", "jpg", "jpeg"]
}, },
// //
isShowTip: { isShowTip: {
type: Boolean, type: Boolean,
default: true default: true
}, },
}); //
disabled: {
type: Boolean,
default: false
},
//
drag: {
type: Boolean,
default: true
}
})
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance()
const emit = defineEmits(); const emit = defineEmits()
const number = ref(0); const number = ref(0)
const uploadList = ref([]); const uploadList = ref([])
const dialogImageUrl = ref(""); const dialogImageUrl = ref("")
const dialogVisible = ref(false); const dialogVisible = ref(false)
const baseUrl = import.meta.env.VITE_APP_BASE_API; const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) //
const headers = ref({ Authorization: "Bearer " + getToken() }); const headers = ref({ Authorization: "Bearer " + getToken() })
const fileList = ref([]); const fileList = ref([])
const showTip = computed( const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize) () => props.isShowTip && (props.fileType || props.fileSize)
); )
watch(() => props.modelValue, val => { watch(() => props.modelValue, val => {
if (val) { if (val) {
// //
const list = Array.isArray(val) ? val : props.modelValue.split(","); const list = Array.isArray(val) ? val : props.modelValue.split(",")
// //
fileList.value = list.map(item => { fileList.value = list.map(item => {
if (typeof item === "string") { if (typeof item === "string") {
if (item.indexOf(baseUrl) === -1 && !isExternal(item)) { if (item.indexOf(baseUrl) === -1 && !isExternal(item)) {
item = { name: baseUrl + item, url: baseUrl + item }; item = { name: baseUrl + item, url: baseUrl + item }
} else { } else {
item = { name: item, url: item }; item = { name: item, url: item }
} }
} }
return item; return item
}); })
} else { } else {
fileList.value = []; fileList.value = []
return []; return []
} }
},{ deep: true, immediate: true }); },{ deep: true, immediate: true })
// loading // loading
function handleBeforeUpload(file) { function handleBeforeUpload(file) {
let isImg = false; let isImg = false
if (props.fileType.length) { if (props.fileType.length) {
let fileExtension = ""; let fileExtension = ""
if (file.name.lastIndexOf(".") > -1) { if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1); fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1)
} }
isImg = props.fileType.some(type => { isImg = props.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true; if (file.type.indexOf(type) > -1) return true
if (fileExtension && fileExtension.indexOf(type) > -1) return true; if (fileExtension && fileExtension.indexOf(type) > -1) return true
return false; return false
}); })
} else { } else {
isImg = file.type.indexOf("image") > -1; isImg = file.type.indexOf("image") > -1
} }
if (!isImg) { if (!isImg) {
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`); proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`)
return false; return false
} }
if (file.name.includes(',')) { if (file.name.includes(',')) {
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!'); proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
return false; return false
} }
if (props.fileSize) { if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize; const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) { if (!isLt) {
proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`); proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`)
return false; return false
} }
} }
proxy.$modal.loading("正在上传图片,请稍候..."); proxy.$modal.loading("正在上传图片,请稍候...")
number.value++; number.value++
} }
// //
function handleExceed() { function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`); proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
} }
// //
function handleUploadSuccess(res, file) { function handleUploadSuccess(res, file) {
if (res.code === 200) { if (res.code === 200) {
uploadList.value.push({ name: res.fileName, url: res.fileName }); uploadList.value.push({ name: res.fileName, url: res.fileName })
uploadedSuccessfully(); uploadedSuccessfully()
} else { } else {
number.value--; number.value--
proxy.$modal.closeLoading(); proxy.$modal.closeLoading()
proxy.$modal.msgError(res.msg); proxy.$modal.msgError(res.msg)
proxy.$refs.imageUpload.handleRemove(file); proxy.$refs.imageUpload.handleRemove(file)
uploadedSuccessfully(); uploadedSuccessfully()
} }
} }
// //
function handleDelete(file) { function handleDelete(file) {
const findex = fileList.value.map(f => f.name).indexOf(file.name); const findex = fileList.value.map(f => f.name).indexOf(file.name)
if (findex > -1 && uploadList.value.length === number.value) { if (findex > -1 && uploadList.value.length === number.value) {
fileList.value.splice(findex, 1); fileList.value.splice(findex, 1)
emit("update:modelValue", listToString(fileList.value)); emit("update:modelValue", listToString(fileList.value))
return false; return false
} }
} }
// //
function uploadedSuccessfully() { function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) { if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value); fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
uploadList.value = []; uploadList.value = []
number.value = 0; number.value = 0
emit("update:modelValue", listToString(fileList.value)); emit("update:modelValue", listToString(fileList.value))
proxy.$modal.closeLoading(); proxy.$modal.closeLoading()
} }
} }
// //
function handleUploadError() { function handleUploadError() {
proxy.$modal.msgError("上传图片失败"); proxy.$modal.msgError("上传图片失败")
proxy.$modal.closeLoading(); proxy.$modal.closeLoading()
} }
// //
function handlePictureCardPreview(file) { function handlePictureCardPreview(file) {
dialogImageUrl.value = file.url; dialogImageUrl.value = file.url
dialogVisible.value = true; dialogVisible.value = true
} }
// //
function listToString(list, separator) { function listToString(list, separator) {
let strs = ""; let strs = ""
separator = separator || ","; separator = separator || ","
for (let i in list) { for (let i in list) {
if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) { if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
strs += list[i].url.replace(baseUrl, "") + separator; strs += list[i].url.replace(baseUrl, "") + separator
} }
} }
return strs != "" ? strs.substr(0, strs.length - 1) : ""; return strs != "" ? strs.substr(0, strs.length - 1) : ""
} }
//
onMounted(() => {
if (props.drag && !props.disabled) {
nextTick(() => {
const element = proxy.$refs.imageUpload?.$el?.querySelector('.el-upload-list')
Sortable.create(element, {
onEnd: (evt) => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
fileList.value.splice(evt.newIndex, 0, movedItem)
emit('update:modelValue', listToString(fileList.value))
}
})
})
}
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -59,7 +59,7 @@ const props = defineProps({
} }
}) })
const emit = defineEmits(); const emit = defineEmits()
const currentPage = computed({ const currentPage = computed({
get() { get() {
return props.page return props.page
@ -76,6 +76,7 @@ const pageSize = computed({
emit('update:limit', val) emit('update:limit', val)
} }
}) })
function handleSizeChange(val) { function handleSizeChange(val) {
if (currentPage.value * val > props.total) { if (currentPage.value * val > props.total) {
currentPage.value = 1 currentPage.value = 1
@ -85,13 +86,13 @@ function handleSizeChange(val) {
scrollTo(0, 800) scrollTo(0, 800)
} }
} }
function handleCurrentChange(val) { function handleCurrentChange(val) {
emit('pagination', { page: val, limit: pageSize.value }) emit('pagination', { page: val, limit: pageSize.value })
if (props.autoScroll) { if (props.autoScroll) {
scrollTo(0, 800) scrollTo(0, 800)
} }
} }
</script> </script>
<style scoped> <style scoped>

View File

@ -1,21 +1,26 @@
<template> <template>
<div class="top-right-btn" :style="style"> <div class="top-right-btn" :style="style">
<el-row> <el-row>
<!-- <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">--> <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
<!-- <el-button circle icon="Search" @click="toggleSearch()" />--> <el-button circle icon="Search" @click="toggleSearch()" />
<!-- </el-tooltip>--> </el-tooltip>
<el-tooltip class="item" effect="dark" content="刷新" placement="top"> <el-tooltip class="item" effect="dark" content="刷新" placement="top">
<el-button circle icon="Refresh" @click="refresh()" /> <el-button circle icon="Refresh" @click="refresh()" />
</el-tooltip> </el-tooltip>
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns"> <el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="Object.keys(columns).length > 0">
<el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/> <el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
<el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'"> <el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
<el-button circle icon="Menu" /> <el-button circle icon="Menu" />
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<template v-for="item in columns" :key="item.key"> <!-- 全选/反选 按钮 -->
<el-dropdown-item>
<el-checkbox :indeterminate="isIndeterminate" v-model="isChecked" @change="toggleCheckAll"> 列展示 </el-checkbox>
</el-dropdown-item>
<div class="check-line"></div>
<template v-for="(item, key) in columns" :key="item.key">
<el-dropdown-item> <el-dropdown-item>
<el-checkbox :checked="item.visible" @change="checkboxChange($event, item.label)" :label="item.label" /> <el-checkbox v-model="item.visible" @change="checkboxChange($event, key)" :label="item.label" />
</el-dropdown-item> </el-dropdown-item>
</template> </template>
</el-dropdown-menu> </el-dropdown-menu>
@ -27,7 +32,7 @@
<el-transfer <el-transfer
:titles="['显示', '隐藏']" :titles="['显示', '隐藏']"
v-model="value" v-model="value"
:data="columns" :data="transferData"
@change="dataChange" @change="dataChange"
></el-transfer> ></el-transfer>
</el-dialog> </el-dialog>
@ -35,87 +40,191 @@
</template> </template>
<script setup> <script setup>
import cache from '@/plugins/cache'
const props = defineProps({ const props = defineProps({
/* 是否显示检索条件 */ /* 是否显示检索条件 */
showSearch: { showSearch: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
/* 显隐列信息 */ /* 显隐列信息(数组格式、对象格式) */
columns: { columns: {
type: Array, type: [Array, Object],
default: () => ({})
}, },
/* 是否显示检索图标 */ /* 是否显示检索图标 */
search: { search: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
/* 显隐列类型transfer穿梭框、checkbox复选框 */ /* 显隐列类型transfer穿梭框、checkbox复选框 */
showColumnsType: { showColumnsType: {
type: String, type: String,
default: "checkbox", default: "checkbox"
}, },
/* 右外边距 */ /* 右外边距 */
gutter: { gutter: {
type: Number, type: Number,
default: 10, default: 10
}, },
/* 列显隐状态记忆的 localStorage key传入则启用记忆不传则不记忆 */
storageKey: {
type: String,
default: ""
}
}) })
const emits = defineEmits(['update:showSearch', 'queryTable']); const emits = defineEmits(['update:showSearch', 'queryTable'])
// //
const value = ref([]); const value = ref([])
// //
const title = ref("显示/隐藏"); const title = ref("显示/隐藏")
// //
const open = ref(false); const open = ref(false)
const style = computed(() => { const style = computed(() => {
const ret = {}; const ret = {}
if (props.gutter) { if (props.gutter) {
ret.marginRight = `${props.gutter / 2}px`; ret.marginRight = `${props.gutter / 2}px`
} }
return ret; return ret
}); })
// /
const isChecked = computed({
get: () => Array.isArray(props.columns) ? props.columns.every(col => col.visible) : Object.values(props.columns).every((col) => col.visible),
set: () => {}
})
const isIndeterminate = computed(() => Array.isArray(props.columns) ? props.columns.some((col) => col.visible) && !isChecked.value : Object.values(props.columns).some((col) => col.visible) && !isChecked.value)
const transferData = computed(() => Array.isArray(props.columns) ? props.columns.map((item, index) => ({ key: index, label: item.label })) : Object.keys(props.columns).map((key, index) => ({ key: index, label: props.columns[key].label })))
// //
const { proxy } = getCurrentInstance()
function toggleSearch() { function toggleSearch() {
emits("update:showSearch", !props.showSearch); let el = proxy.$el
let formEl = null
while ((el = el.parentElement) && el !== document.body) {
if ((formEl = el.querySelector('.el-form'))) break
}
if (!formEl) return emits('update:showSearch', !props.showSearch)
animateSearch(formEl, props.showSearch)
}
function animateSearch(el, isHide) {
const DURATION = 260
const TRANSITION = 'max-height 0.25s ease, opacity 0.2s ease'
const clear = () => Object.assign(el.style, { transition: '', maxHeight: '', opacity: '', overflow: '' })
Object.assign(el.style, { overflow: 'hidden', transition: '' })
if (isHide) {
Object.assign(el.style, { maxHeight: el.scrollHeight + 'px', opacity: '1', transition: TRANSITION })
requestAnimationFrame(() => Object.assign(el.style, { maxHeight: '0', opacity: '0' }))
setTimeout(() => { emits('update:showSearch', false); clear() }, DURATION)
} else {
emits('update:showSearch', true)
nextTick(() => {
Object.assign(el.style, { maxHeight: '0', opacity: '0' })
requestAnimationFrame(() => requestAnimationFrame(() => {
Object.assign(el.style, { transition: TRANSITION, maxHeight: el.scrollHeight + 'px', opacity: '1' })
}))
setTimeout(clear, DURATION)
})
}
} }
// //
function refresh() { function refresh() {
emits("queryTable"); emits("queryTable")
} }
// //
function dataChange(data) { function dataChange(data) {
for (let item in props.columns) { if (Array.isArray(props.columns)) {
const key = props.columns[item].key; for (let item in props.columns) {
props.columns[item].visible = !data.includes(key); const key = props.columns[item].key
props.columns[item].visible = !data.includes(key)
}
} else {
Object.keys(props.columns).forEach((key, index) => {
props.columns[key].visible = !data.includes(index)
})
} }
saveStorage()
} }
// dialog // dialog
function showColumn() { function showColumn() {
open.value = true; open.value = true
} }
if (props.showColumnsType == 'transfer') { // storageKey localStorage
// if (props.storageKey) {
for (let item in props.columns) { try {
if (props.columns[item].visible === false) { const saved = cache.local.getJSON(props.storageKey)
value.value.push(parseInt(item)); if (saved && typeof saved === 'object') {
if (Array.isArray(props.columns)) {
props.columns.forEach((col, index) => {
if (saved[index] !== undefined) col.visible = saved[index]
})
} else {
Object.keys(props.columns).forEach(key => {
if (saved[key] !== undefined) props.columns[key].visible = saved[key]
})
}
} }
} catch (e) {}
}
if (props.showColumnsType == "transfer") {
// transfer穿
if (Array.isArray(props.columns)) {
for (let item in props.columns) {
if (props.columns[item].visible === false) {
value.value.push(parseInt(item))
}
}
} else {
Object.keys(props.columns).forEach((key, index) => {
if (props.columns[key].visible === false) {
value.value.push(index)
}
})
} }
} }
// //
function checkboxChange(event, label) { function checkboxChange(event, key) {
props.columns.filter(item => item.label == label)[0].visible = event; if (Array.isArray(props.columns)) {
props.columns.filter(item => item.key == key)[0].visible = event
} else {
props.columns[key].visible = event
}
saveStorage()
} }
// /
function toggleCheckAll() {
const newValue = !isChecked.value
if (Array.isArray(props.columns)) {
props.columns.forEach((col) => (col.visible = newValue))
} else {
Object.values(props.columns).forEach((col) => (col.visible = newValue))
}
saveStorage()
}
// localStorage
function saveStorage() {
if (!props.storageKey) return
try {
let state = {}
if (Array.isArray(props.columns)) {
props.columns.forEach((col, index) => { state[index] = col.visible })
} else {
Object.keys(props.columns).forEach(key => { state[key] = props.columns[key].visible })
}
cache.local.setJSON(props.storageKey, state)
} catch (e) {}
}
</script> </script>
<style lang='scss' scoped> <style lang='scss' scoped>

View File

@ -5,7 +5,7 @@
</template> </template>
<script setup> <script setup>
const url = ref('http://doc.ruoyi.vip/ruoyi-vue'); const url = ref(import.meta.env.VITE_APP_BASE_API + '/doc.html')
function goto() { function goto() {
window.open(url.value) window.open(url.value)

View File

@ -5,7 +5,7 @@
</template> </template>
<script setup> <script setup>
const url = ref('https://gitee.com/y_project/RuoYi-Vue'); const url = ref(import.meta.env.VITE_APP_BASE_API + '/doc.html')
function goto() { function goto() {
window.open(url.value) window.open(url.value)

View File

@ -7,7 +7,7 @@
<script setup> <script setup>
import { useFullscreen } from '@vueuse/core' import { useFullscreen } from '@vueuse/core'
const { isFullscreen, enter, exit, toggle } = useFullscreen(); const { isFullscreen, toggle } = useFullscreen()
</script> </script>
<style lang='scss' scoped> <style lang='scss' scoped>

View File

@ -16,23 +16,21 @@
</template> </template>
<script setup> <script setup>
import useAppStore from "@/store/modules/app"; import useAppStore from "@/store/modules/app"
const appStore = useAppStore(); const appStore = useAppStore()
const size = computed(() => appStore.size); const size = computed(() => appStore.size)
const route = useRoute(); const { proxy } = getCurrentInstance()
const router = useRouter();
const { proxy } = getCurrentInstance();
const sizeOptions = ref([ const sizeOptions = ref([
{ label: "较大", value: "large" }, { label: "较大", value: "large" },
{ label: "默认", value: "default" }, { label: "默认", value: "default" },
{ label: "稍小", value: "small" }, { label: "稍小", value: "small" },
]); ])
function handleSetSize(size) { function handleSetSize(size) {
proxy.$modal.loading("正在设置布局大小,请稍候..."); proxy.$modal.loading("正在设置布局大小,请稍候...")
appStore.setSize(size); appStore.setSize(size)
setTimeout("window.location.reload()", 1000); setTimeout("window.location.reload()", 1000)
} }
</script> </script>

View File

@ -1,30 +1,36 @@
<template> <template>
<svg <svg
:width="size" :class="svgClass"
:height="size" :width="resolvedSize"
:height="resolvedSize"
:style="svgStyle" :style="svgStyle"
class="svg-icon"
aria-hidden="true" aria-hidden="true"
> >
<use :xlink:href="symbolId" /> <use :xlink:href="symbolId" :fill="color || 'currentColor'" />
</svg> </svg>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'
const props = defineProps({ const props = defineProps({
iconClass: {
type: String,
default: ''
},
name: { name: {
type: String, type: String,
required: true default: ''
},
className: {
type: String,
default: ''
}, },
size: { size: {
type: [String, Number], type: [String, Number],
default: 24 default: undefined
}, },
color: { color: {
type: String, type: String,
default: 'currentColor' default: ''
}, },
customStyle: { customStyle: {
type: Object, type: Object,
@ -32,11 +38,22 @@ const props = defineProps({
} }
}) })
const symbolId = computed(() => `#icon-${props.name}`) const icon = computed(() => props.name || props.iconClass)
const symbolId = computed(() => `#icon-${icon.value}`)
const resolvedSize = computed(() => props.size ?? (props.name ? 24 : undefined))
const svgClass = computed(() => props.className ? `svg-icon ${props.className}` : 'svg-icon')
const svgStyle = computed(() => ({ const svgStyle = computed(() => ({
verticalAlign: 'middle', color: props.color || undefined,
color: props.color, // color fill
...props.customStyle ...props.customStyle
})) }))
</script> </script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>

View File

@ -1,10 +1,10 @@
import * as components from '@element-plus/icons-vue' import * as components from '@element-plus/icons-vue'
export default { export default {
install: (app) => { install: (app) => {
for (const key in components) { for (const key in components) {
const componentConfig = components[key]; const componentConfig = components[key]
app.component(componentConfig.name, componentConfig); app.component(componentConfig.name, componentConfig)
} }
}, }
}; }

View File

@ -0,0 +1,756 @@
<template>
<div class="tree-sidebar" :class="{ collapsed: collapsed, resizing: isResizing, 'no-initial-transition': isLoadingFromStorage}" :style="{ width: sidebarWidth + 'px' }">
<!-- 右侧拖动条 -->
<div v-if="!collapsed" class="resize-handle" @mousedown="startResize" @touchstart="startResize" :class="{ active: isResizing }" />
<div class="tree-header">
<span class="tree-title" v-show="!collapsed">
<el-icon><component :is="titleIcon" /></el-icon> {{ title }}
</span>
<div class="tree-actions" v-show="!collapsed">
<el-tooltip :content="isExpandedAll ? '收起全部' : '展开全部'" placement="right">
<el-icon class="tree-action-icon" @click="toggleExpandAll">
<ArrowDown v-if="isExpandedAll" />
<ArrowUp v-else />
</el-icon>
</el-tooltip>
<el-tooltip content="刷新" placement="right">
<el-icon class="tree-action-icon" @click="handleRefresh"><Refresh /></el-icon>
</el-tooltip>
<slot name="actions"></slot>
</div>
</div>
<!-- 侧边栏展开/收起按钮 -->
<div class="collapse-button-container">
<el-tooltip :content="collapsed ? '展开' : '收起'" placement="right">
<el-icon class="collapse-button" @click="toggleCollapsed">
<DArrowRight v-if="collapsed" />
<DArrowLeft v-else />
</el-icon>
</el-tooltip>
</div>
<div class="tree-search" v-show="!collapsed" v-if="showSearch">
<el-input v-model="searchKeyword" :placeholder="searchPlaceholder" clearable>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<div class="tree-wrap" v-show="!collapsed">
<el-tree
ref="treeRef"
:data="treeData"
:props="treeProps"
:expand-on-click-node="expandOnClickNode"
:filter-node-method="filterNodeMethod"
:default-expand-all="defaultExpandAll"
:default-expanded-keys="defaultExpandedKeys"
:node-key="nodeKey"
:check-strictly="checkStrictly"
:show-checkbox="showCheckbox"
@node-click="onNodeClick"
@check="onCheck"
@node-expand="onNodeExpand"
@node-collapse="onNodeCollapse"
>
<template #default="{ node, data }">
<slot name="node" :node="node" :data="data">
<span class="tree-node">
<el-icon class="node-icon">
<Folder v-if="data.children && data.children.length" />
<Document v-else />
</el-icon>
<span class="node-label" :title="node.label">{{ node.label }}</span>
</span>
</slot>
</template>
</el-tree>
</div>
</div>
</template>
<script setup>
const props = defineProps({
//
treeData: {
type: Array,
default: () => []
},
//
title: {
type: String,
default: '树形结构'
},
//
titleIcon: {
type: [String, Object],
default: 'OfficeBuilding'
},
//
showSearch: {
type: Boolean,
default: true
},
//
searchPlaceholder: {
type: String,
default: '请输入名称'
},
//
defaultCollapsed: {
type: Boolean,
default: false
},
//
treeProps: {
type: Object,
default: () => ({
children: "children",
label: "label"
})
},
//
nodeKey: {
type: String,
default: 'id'
},
//
expandOnClickNode: {
type: Boolean,
default: false
},
//
showCheckbox: {
type: Boolean,
default: false
},
//
checkStrictly: {
type: Boolean,
default: false
},
//
defaultExpandAll: {
type: Boolean,
default: false
},
// key
defaultExpandedKeys: {
type: Array,
default: () => []
},
//
defaultWidth: {
type: Number,
default: 220
},
//
collapsedWidth: {
type: Number,
default: 20
},
//
minWidth: {
type: Number,
default: 180
},
//
maxWidth: {
type: Number,
default: 400
},
// key
storageKey: {
type: String,
default: 'tree-sidebar-width'
},
//
enableStorage: {
type: Boolean,
default: true
},
//
filterMethod: {
type: Function,
default: null
}
})
const emit = defineEmits([
'collapsed-change',
'expanded-all-change',
'refresh',
'node-click',
'check',
'node-expand',
'node-collapse',
'search'
])
const treeRef = ref(null)
//
const searchKeyword = ref('')
const collapsed = ref(props.defaultCollapsed)
const sidebarWidth = ref(props.defaultCollapsed ? props.collapsedWidth : props.defaultWidth)
const isResizing = ref(false)
const startX = ref(0)
const startWidth = ref(0)
const saveWidthTimer = ref(null)
const rafId = ref(null)
const isLoadingFromStorage = ref(false)
const expandedAll = ref(props.defaultExpandAll)
//
const isExpandedAll = computed({
get: () => expandedAll.value,
set: (val) => {
expandedAll.value = val
}
})
//
const filterNodeMethod = (value, data) => {
if (props.filterMethod) {
return props.filterMethod(value, data)
}
if (!value) return true
return data.label && data.label.indexOf(value) !== -1
}
//
watch(collapsed, (newVal, oldVal) => {
if (newVal !== oldVal) {
handleCollapseChange(newVal)
emit('collapsed-change', newVal)
}
})
// /
watch(expandedAll, (newVal) => {
nextTick(() => {
if (newVal) {
expandAllNodes()
} else {
collapseAllNodes()
}
})
emit('expanded-all-change', newVal)
})
//
watch(searchKeyword, (val) => {
if (treeRef.value) {
treeRef.value.filter(val)
emit('search', val)
}
})
//
const cleanup = () => {
if (rafId.value) {
cancelAnimationFrame(rafId.value)
rafId.value = null
}
if (saveWidthTimer.value) {
clearTimeout(saveWidthTimer.value)
saveWidthTimer.value = null
}
}
// /
const handleCollapseChange = (isCollapsed) => {
if (isCollapsed) {
saveWidthToStorage()
sidebarWidth.value = props.collapsedWidth
} else {
const savedWidth = getSavedWidth()
sidebarWidth.value = savedWidth !== null ? savedWidth : props.defaultWidth
}
}
//
const getSavedWidth = () => {
if (!props.enableStorage) {
return null
}
try {
const savedWidth = localStorage.getItem(props.storageKey)
if (savedWidth) {
const width = parseInt(savedWidth, 10)
if (!isNaN(width) && width >= props.minWidth && width <= props.maxWidth) {
return width
}
}
} catch (error) {
console.warn(`Failed to load sidebar width from storage with key ${props.storageKey}:`, error)
}
return null
}
//
const saveWidthToStorage = () => {
if (collapsed.value || !props.enableStorage) return
try {
localStorage.setItem(props.storageKey, sidebarWidth.value.toString())
} catch (error) {
console.warn(`Failed to save sidebar width to storage with key ${props.storageKey}:`, error)
}
}
// /
const toggleCollapsed = () => {
collapsed.value = !collapsed.value
}
// /
const toggleExpandAll = () => {
expandedAll.value = !expandedAll.value
}
//
const expandAllNodes = () => {
if (!treeRef.value) return
const allNodes = getAllNodes(treeRef.value.root)
allNodes.forEach(node => {
if (node.expanded !== undefined && !node.expanded) {
node.expanded = true
}
})
}
//
const getAllNodes = (rootNode) => {
const nodes = []
const traverse = (node) => {
if (!node) return
nodes.push(node)
if (node.childNodes && node.childNodes.length) {
node.childNodes.forEach(child => traverse(child))
}
}
traverse(rootNode)
return nodes
}
//
const collapseAllNodes = () => {
if (!treeRef.value) return
const allNodes = getAllNodes(treeRef.value.root)
allNodes.forEach(node => {
if (node.expanded !== undefined && node.expanded) {
node.expanded = false
}
})
}
//
const handleRefresh = () => {
emit('refresh')
}
//
const onNodeClick = (data, node, e) => {
emit('node-click', data, node, e)
}
//
const onCheck = (data, checkedInfo) => {
emit('check', data, checkedInfo)
}
//
const onNodeExpand = (data, node, e) => {
emit('node-expand', data, node, e)
}
//
const onNodeCollapse = (data, node, e) => {
emit('node-collapse', data, node, e)
}
const setCurrentKey = (key) => {
if (treeRef.value) {
treeRef.value.setCurrentKey(key)
}
}
const getCurrentNode = () => {
if (treeRef.value) {
return treeRef.value.getCurrentNode()
}
return null
}
const getCurrentKey = () => {
if (treeRef.value) {
return treeRef.value.getCurrentKey()
}
return null
}
const setCheckedKeys = (keys) => {
if (treeRef.value && props.showCheckbox) {
treeRef.value.setCheckedKeys(keys)
}
}
const getCheckedKeys = () => {
if (treeRef.value && props.showCheckbox) {
return treeRef.value.getCheckedKeys()
}
return []
}
const getCheckedNodes = () => {
if (treeRef.value && props.showCheckbox) {
return treeRef.value.getCheckedNodes()
}
return []
}
const clearSearch = () => {
searchKeyword.value = ""
if (treeRef.value) {
treeRef.value.filter("")
}
}
const filter = (value) => {
searchKeyword.value = value
}
const startResize = (e) => {
e.preventDefault()
e.stopPropagation()
isResizing.value = true
startX.value = e.type === 'mousedown' ? e.clientX : e.touches[0].clientX
startWidth.value = sidebarWidth.value
if (e.type === 'mousedown') {
document.addEventListener('mousemove', handleResizeMove)
document.addEventListener('mouseup', stopResize)
} else {
document.addEventListener('touchmove', handleResizeMove, { passive: false })
document.addEventListener('touchend', stopResize)
}
disableUserSelect()
}
const handleResizeMove = (e) => {
if (!isResizing.value) return
if (rafId.value) {
cancelAnimationFrame(rafId.value)
}
rafId.value = requestAnimationFrame(() => {
e.preventDefault()
e.stopPropagation()
const clientX = e.type === 'mousemove' ? e.clientX : e.touches[0].clientX
const deltaX = clientX - startX.value
const newWidth = startWidth.value + deltaX
const clampedWidth = Math.max(props.minWidth, Math.min(props.maxWidth, newWidth))
if (Math.abs(clampedWidth - sidebarWidth.value) >= 1) {
sidebarWidth.value = clampedWidth
}
})
}
const stopResize = () => {
if (!isResizing.value) return
isResizing.value = false
if (rafId.value) {
cancelAnimationFrame(rafId.value)
rafId.value = null
}
startX.value = 0
startWidth.value = 0
document.removeEventListener('mousemove', handleResizeMove)
document.removeEventListener('mouseup', stopResize)
document.removeEventListener('touchmove', handleResizeMove)
document.removeEventListener('touchend', stopResize)
enableUserSelect()
saveWidthToStorage()
}
const disableUserSelect = () => {
document.body.style.userSelect = 'none'
document.body.style.webkitUserSelect = 'none'
document.body.style.mozUserSelect = 'none'
document.body.style.msUserSelect = 'none'
}
const enableUserSelect = () => {
document.body.style.userSelect = ''
document.body.style.webkitUserSelect = ''
document.body.style.mozUserSelect = ''
document.body.style.msUserSelect = ''
}
const resetWidth = () => {
sidebarWidth.value = props.defaultWidth
saveWidthToStorage()
}
const getCurrentWidth = () => {
return sidebarWidth.value
}
const setWidth = (width) => {
if (typeof width === 'number' && width >= props.minWidth && width <= props.maxWidth) {
sidebarWidth.value = width
if (!collapsed.value) {
saveWidthToStorage()
}
}
}
defineExpose({
setCurrentKey,
getCurrentNode,
getCurrentKey,
setCheckedKeys,
getCheckedKeys,
getCheckedNodes,
clearSearch,
filter,
resetWidth,
getCurrentWidth,
setWidth,
expandAllNodes,
collapseAllNodes,
toggleCollapsed,
treeRef
})
onMounted(() => {
isLoadingFromStorage.value = true
if (!collapsed.value && props.enableStorage) {
const savedWidth = getSavedWidth()
if (savedWidth !== null) {
sidebarWidth.value = savedWidth
}
}
nextTick(() => {
isLoadingFromStorage.value = false
})
if (expandedAll.value) {
nextTick(() => {
expandAllNodes()
})
}
})
onBeforeUnmount(() => {
cleanup()
})
</script>
<style lang="scss" scoped>
.tree-sidebar {
flex-shrink: 0;
width: 220px;
background: #fff;
border-right: 1px solid #e8eaed;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
transition: width 0.25s ease;
&.collapsed {
width: 42px;
}
&.resizing {
transition: none;
will-change: width;
* {
pointer-events: none !important;
}
}
&.no-initial-transition {
transition: none;
}
}
.resize-handle {
position: absolute;
top: 0;
right: 0;
width: 6px;
height: 100%;
cursor: col-resize;
z-index: 20;
background: transparent;
transition: background 0.2s;
&:hover {
background: rgba(64, 158, 255, 0.3);
}
&.active {
background: rgba(64, 158, 255, 0.5);
}
}
.collapse-button-container {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
width: 15px;
height: 20px;
background: #fff;
border-radius: 0 4px 4px 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
.tree-sidebar.collapsed & {
right: 0;
background: #f7f8fa;
border-radius: 0 4px 4px 0;
}
.tree-sidebar.resizing & {
pointer-events: none;
}
}
.collapse-button {
font-size: 20px;
color: #909399;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
&:hover {
color: #409eff;
background: #ecf5ff;
}
}
.tree-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
height: 40px;
border-bottom: 1px solid #e8eaed;
background: #f7f8fa;
flex-shrink: 0;
.tree-title {
font-size: 13px;
font-weight: 600;
color: #303133;
white-space: nowrap;
overflow: hidden;
display: flex;
align-items: center;
gap: 5px;
.el-icon {
color: #409eff;
font-size: 16px;
}
}
.tree-actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
}
.tree-action-icon {
font-size: 20px;
color: #909399;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
&:hover {
color: #409eff;
background: #ecf5ff;
}
}
.tree-search {
padding: 10px 10px 4px;
flex-shrink: 0;
}
.tree-wrap {
flex: 1;
overflow-y: auto;
padding: 6px 6px 12px;
.tree-sidebar.resizing & {
overflow: hidden;
}
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: #dcdfe6;
border-radius: 4px;
&:hover {
background: #c0c4cc;
}
}
:deep(.el-tree-node__content) {
height: 32px;
border-radius: 4px;
margin-bottom: 1px;
&:hover {
background: #f0f7ff;
}
}
:deep(.el-tree-node.is-current > .el-tree-node__content) {
background: #e6f0fd;
color: #409eff;
font-weight: 600;
.node-icon {
color: #409eff !important;
}
}
}
.tree-node {
display: flex;
align-items: center;
gap: 5px;
font-size: 13px;
overflow: hidden;
.node-icon {
font-size: 14px;
color: #f5a623;
flex-shrink: 0;
}
.node-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>

View File

@ -22,10 +22,10 @@ const url = computed(() => props.src)
onMounted(() => { onMounted(() => {
setTimeout(() => { setTimeout(() => {
loading.value = false; loading.value = false
}, 300); }, 300)
window.onresize = function temp() { window.onresize = function temp() {
height.value = document.documentElement.clientHeight - 94.5 + "px;"; height.value = document.documentElement.clientHeight - 94.5 + "px;"
}; }
}) })
</script> </script>

View File

@ -2,65 +2,64 @@
* v-copyText 复制文本内容 * v-copyText 复制文本内容
* Copyright (c) 2022 ruoyi * Copyright (c) 2022 ruoyi
*/ */
export default { export default {
beforeMount(el, { value, arg }) { beforeMount(el, { value, arg }) {
if (arg === "callback") { if (arg === "callback") {
el.$copyCallback = value; el.$copyCallback = value
} else { } else {
el.$copyValue = value; el.$copyValue = value
const handler = () => { const handler = () => {
copyTextToClipboard(el.$copyValue); copyTextToClipboard(el.$copyValue)
if (el.$copyCallback) { if (el.$copyCallback) {
el.$copyCallback(el.$copyValue); el.$copyCallback(el.$copyValue)
} }
}; }
el.addEventListener("click", handler); el.addEventListener("click", handler)
el.$destroyCopy = () => el.removeEventListener("click", handler); el.$destroyCopy = () => el.removeEventListener("click", handler)
} }
} }
} }
function copyTextToClipboard(input, { target = document.body } = {}) { function copyTextToClipboard(input, { target = document.body } = {}) {
const element = document.createElement('textarea'); const element = document.createElement('textarea')
const previouslyFocusedElement = document.activeElement; const previouslyFocusedElement = document.activeElement
element.value = input; element.value = input
// Prevent keyboard from showing on mobile // Prevent keyboard from showing on mobile
element.setAttribute('readonly', ''); element.setAttribute('readonly', '')
element.style.contain = 'strict'; element.style.contain = 'strict'
element.style.position = 'absolute'; element.style.position = 'absolute'
element.style.left = '-9999px'; element.style.left = '-9999px'
element.style.fontSize = '12pt'; // Prevent zooming on iOS element.style.fontSize = '12pt' // Prevent zooming on iOS
const selection = document.getSelection(); const selection = document.getSelection()
const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0); const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0)
target.append(element); target.append(element)
element.select(); element.select()
// Explicit selection workaround for iOS // Explicit selection workaround for iOS
element.selectionStart = 0; element.selectionStart = 0
element.selectionEnd = input.length; element.selectionEnd = input.length
let isSuccess = false; let isSuccess = false
try { try {
isSuccess = document.execCommand('copy'); isSuccess = document.execCommand('copy')
} catch { } } catch { }
element.remove(); element.remove()
if (originalRange) { if (originalRange) {
selection.removeAllRanges(); selection.removeAllRanges()
selection.addRange(originalRange); selection.addRange(originalRange)
} }
// Get the focus back on the previously focused element, if any // Get the focus back on the previously focused element, if any
if (previouslyFocusedElement) { if (previouslyFocusedElement) {
previouslyFocusedElement.focus(); previouslyFocusedElement.focus()
} }
return isSuccess; return isSuccess
} }

View File

@ -2,13 +2,12 @@
* v-hasPermi 操作权限处理 * v-hasPermi 操作权限处理
* Copyright (c) 2019 ruoyi * Copyright (c) 2019 ruoyi
*/ */
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
export default { export default {
mounted(el, binding, vnode) { mounted(el, binding, vnode) {
const { value } = binding const { value } = binding
const all_permission = "*:*:*"; const all_permission = "*:*:*"
const permissions = useUserStore().permissions const permissions = useUserStore().permissions
if (value && value instanceof Array && value.length > 0) { if (value && value instanceof Array && value.length > 0) {

View File

@ -2,13 +2,12 @@
* v-hasRole 角色权限处理 * v-hasRole 角色权限处理
* Copyright (c) 2019 ruoyi * Copyright (c) 2019 ruoyi
*/ */
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
export default { export default {
mounted(el, binding, vnode) { mounted(el, binding, vnode) {
const { value } = binding const { value } = binding
const super_admin = "admin"; const super_admin = "admin"
const roles = useUserStore().roles const roles = useUserStore().roles
if (value && value instanceof Array && value.length > 0) { if (value && value instanceof Array && value.length > 0) {

View File

@ -8,10 +8,12 @@
</transition> </transition>
</router-view> </router-view>
<iframe-toggle /> <iframe-toggle />
<copyright />
</section> </section>
</template> </template>
<script setup> <script setup>
import copyright from "./Copyright/index"
import iframeToggle from "./IframeToggle/index" import iframeToggle from "./IframeToggle/index"
import useTagsViewStore from '@/store/modules/tagsView' import useTagsViewStore from '@/store/modules/tagsView'
@ -22,7 +24,7 @@ onMounted(() => {
addIframe() addIframe()
}) })
watch((route) => { watchEffect(() => {
addIframe() addIframe()
}) })
@ -34,7 +36,7 @@ function addIframe() {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/assets/styles/variables.module"; @use "@/assets/styles/variables.module" as *;
.app-main { .app-main {
/* 50= navbar 50 */ /* 50= navbar 50 */
height: calc(100% - 84px); height: calc(100% - 84px);
@ -85,4 +87,3 @@ function addIframe() {
border-radius: 3px; border-radius: 3px;
} }
</style> </style>

View File

@ -0,0 +1,31 @@
<template>
<footer v-if="visible" class="copyright">
<span>{{ content }}</span>
</footer>
</template>
<script setup>
import useSettingsStore from '@/store/modules/settings'
const settingsStore = useSettingsStore()
const visible = computed(() => settingsStore.footerVisible)
const content = computed(() => settingsStore.footerContent)
</script>
<style scoped>
.copyright {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 36px;
padding: 10px 20px;
text-align: right;
background-color: #f8f8f8;
color: #666;
font-size: 14px;
border-top: 1px solid #e7e7e7;
z-index: 999;
}
</style>

View File

@ -0,0 +1,326 @@
<template>
<el-drawer v-model="visible" title="公告详情" direction="rtl" size="50%" append-to-body :before-close="handleClose" class="notice-detail-drawer">
<div v-loading="loading" class="notice-detail-drawer__body">
<div v-if="!detail" class="notice-empty">
<el-icon><Document /></el-icon>
<span>暂无数据</span>
</div>
<div v-else class="notice-page">
<div class="notice-type-wrap">
<span v-if="detail.noticeType === '1'" class="notice-type-tag type-notify">
<el-icon><Bell /></el-icon>
</span>
<span v-else-if="detail.noticeType === '2'" class="notice-type-tag type-announce">
<el-icon><Message /></el-icon>
</span>
<span v-else class="notice-type-tag type-notify">
<el-icon><Document /></el-icon>
</span>
</div>
<h1 class="notice-title">{{ detail.noticeTitle }}</h1>
<div class="notice-meta">
<span class="meta-item">
<el-icon><User /></el-icon>
<span>{{ detail.createBy || '—' }}</span>
</span>
<span class="meta-item">
<el-icon><Clock /></el-icon>
<span>{{ detail.createTime || '—' }}</span>
</span>
<span class="meta-item">
<span :class="['status-dot', isStatusNormal ? 'status-ok' : 'status-off']"></span>
<span>{{ isStatusNormal ? '正常' : '已关闭' }}</span>
</span>
</div>
<div class="notice-divider">
<span class="notice-divider-dot"></span>
<span class="notice-divider-dot"></span>
<span class="notice-divider-dot"></span>
</div>
<div class="notice-body">
<div v-if="hasContent" class="notice-content" v-html="detail.noticeContent" />
<div v-else class="notice-empty notice-empty--inner">
<el-icon><Document /></el-icon>
</div>
</div>
</div>
</div>
</el-drawer>
</template>
<script setup>
import { getNotice } from '@/api/system/notice'
const visible = ref(false)
const loading = ref(false)
const detail = ref(null)
const isStatusNormal = computed(() => {
const status = detail.value && detail.value.status
return status === '0' || status === 0
})
const hasContent = computed(() => {
const content = detail.value && detail.value.noticeContent
return content != null && String(content).trim() !== ''
})
function open(payload) {
let id = null
let preset = null
if (payload != null && typeof payload === 'object') {
id = payload.noticeId
if (payload.noticeContent != null) {
preset = payload
}
} else {
id = payload
}
visible.value = true
if (preset) {
detail.value = preset
return
}
if (id == null || id === '') {
detail.value = null
return
}
loading.value = true
detail.value = null
getNotice(id).then(res => {
detail.value = res.data
}).catch(() => {
detail.value = null
}).finally(() => {
loading.value = false
})
}
function handleClose() {
visible.value = false
detail.value = null
loading.value = false
}
defineExpose({
open
})
</script>
<style lang="scss" scoped>
.notice-page {
max-width: 760px;
margin: 0 auto;
padding: 8px 8px 20px;
animation: notice-fade-up 0.28s ease both;
}
@keyframes notice-fade-up {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
.notice-type-tag {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 12px;
border-radius: 2px;
font-size: 11px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
margin-bottom: 14px;
}
.type-notify {
background: var(--notice-tag-notify-bg, #fff8e6);
color: var(--notice-tag-notify-color, #b7791f);
border-left: 3px solid #d97706;
}
.type-announce {
background: var(--notice-tag-announce-bg, #e8f5e9);
color: var(--notice-tag-announce-color, #276749);
border-left: 3px solid #38a169;
}
.notice-title {
font-size: 22px;
font-weight: 700;
color: var(--notice-title-color, #1a202c);
line-height: 1.45;
margin: 0 0 16px;
letter-spacing: -0.2px;
}
.notice-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16px;
padding: 12px 0;
border-top: 1px solid var(--notice-border-color, #e9ecef);
border-bottom: 1px solid var(--notice-border-color, #e9ecef);
margin-bottom: 28px;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: var(--notice-meta-color, #718096);
}
.meta-item .el-icon {
font-size: 12px;
color: var(--notice-meta-icon-color, #a0aec0);
}
.status-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
margin-right: 4px;
}
.status-ok { background: #38a169; }
.status-off { background: #e53e3e; }
.notice-divider {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.notice-divider::before,
.notice-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--notice-divider-gradient, linear-gradient(to right, transparent, #dee2e6, transparent));
}
.notice-divider-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--notice-divider-dot-color, #cbd5e0);
}
.notice-body {
background: var(--notice-body-bg, #fff);
border-radius: 6px;
padding: 28px 32px;
box-shadow: var(--notice-body-shadow, 0 1px 4px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.04));
min-height: 120px;
}
.notice-content {
font-size: 14px;
line-height: 1.85;
color: var(--notice-content-color, #2d3748);
word-break: break-word;
}
.notice-content :deep(p) { margin: 0 0 1em; }
.notice-content :deep(h1),
.notice-content :deep(h2),
.notice-content :deep(h3) {
font-weight: 700;
color: var(--notice-title-color, #1a202c);
margin: 1.4em 0 0.6em;
}
.notice-content :deep(h1) { font-size: 18px; }
.notice-content :deep(h2) { font-size: 16px; }
.notice-content :deep(h3) { font-size: 14px; }
.notice-content :deep(a) {
color: #3182ce;
text-decoration: underline;
}
.notice-content :deep(a:hover) { color: #2b6cb0; }
.notice-content :deep(img) {
max-width: 100%;
border-radius: 4px;
margin: 8px 0;
}
.notice-content :deep(ul),
.notice-content :deep(ol) {
padding-left: 20px;
margin: 0 0 1em;
}
.notice-content :deep(li) { margin-bottom: 4px; }
.notice-content :deep(blockquote) {
border-left: 3px solid var(--notice-blockquote-border, #cbd5e0);
margin: 1em 0;
padding: 6px 16px;
color: var(--notice-meta-color, #718096);
background: var(--notice-blockquote-bg, #f7fafc);
}
.notice-content :deep(table) {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
font-size: 13px;
}
.notice-content :deep(table th),
.notice-content :deep(table td) {
border: 1px solid var(--notice-table-border, #e2e8f0);
padding: 7px 12px;
}
.notice-content :deep(table th) {
background: var(--notice-table-header-bg, #f7fafc);
font-weight: 600;
}
.notice-empty {
text-align: center;
padding: 40px 0;
color: var(--notice-meta-icon-color, #a0aec0);
font-size: 13px;
}
.notice-empty .el-icon {
font-size: 28px;
display: inline-flex;
margin-bottom: 10px;
}
.notice-empty--inner { padding: 32px 0; }
.notice-detail-drawer__body {
height: 100%;
overflow: auto;
padding: 10px 16px 22px;
}
</style>
<style lang="scss">
.notice-detail-drawer {
.el-drawer__header {
margin-bottom: 0;
padding: 16px 20px;
border-bottom: 1px solid var(--notice-border-color, #ebeef5);
font-size: 16px;
font-weight: 600;
color: var(--notice-title-color, #303133);
}
.el-drawer__body {
background: var(--notice-drawer-body-bg, #f5f6f8);
padding: 0;
}
}
</style>

View File

@ -0,0 +1,184 @@
<template>
<div>
<el-popover ref="noticePopover" placement="bottom-end" :width="320" trigger="manual" v-model:visible="noticeVisible" popper-class="notice-popover">
<!-- 弹出内容 -->
<div class="notice-header">
<span class="notice-title">通知公告</span>
<span class="notice-mark-all" @click="markAllRead">全部已读</span>
</div>
<div v-if="noticeLoading" class="notice-loading">
<el-icon class="is-loading"><Loading /></el-icon> ...
</div>
<div v-else-if="noticeList.length === 0" class="notice-empty">
<el-icon style="font-size:24px;display:block;margin-bottom:6px;"><Postcard /></el-icon>
暂无公告
</div>
<div v-else>
<div v-for="item in noticeList" :key="item.noticeId" class="notice-item" :class="{ 'is-read': item.isRead }" @click="previewNotice(item)">
<el-tag size="small" :type="item.noticeType === '1' ? 'warning' : 'success'" class="notice-tag">
{{ item.noticeType === '1' ? '通知' : '公告' }}
</el-tag>
<span class="notice-item-title">{{ item.noticeTitle }}</span>
<span class="notice-item-date">{{ item.createTime }}</span>
</div>
</div>
<!-- 触发器 -->
<template #reference>
<div class="right-menu-item hover-effect notice-trigger" @mouseenter="onNoticeEnter" @mouseleave="onNoticeLeave">
<svg-icon icon-class="bell" />
<span v-if="unreadCount > 0" class="notice-badge">{{ unreadCount }}</span>
</div>
</template>
</el-popover>
<!-- 预览弹窗 -->
<notice-detail-view ref="noticeViewRef" />
</div>
</template>
<script setup>
import NoticeDetailView from './DetailView'
import { listNoticeTop, markNoticeRead, markNoticeReadAll } from '@/api/system/notice'
const noticePopover = ref(null)
const noticeList = ref([])
const unreadCount = ref(0)
const noticeLoading = ref(false)
const noticeVisible = ref(false)
const noticeLeaveTimer = ref(null)
const { proxy } = getCurrentInstance()
//
function loadNoticeTop() {
noticeLoading.value = true
listNoticeTop().then(res => {
noticeList.value = res.data || []
unreadCount.value = res.unreadCount !== undefined ? res.unreadCount : noticeList.value.filter(n => !n.isRead).length
}).finally(() => {
noticeLoading.value = false
})
}
onMounted(() => loadNoticeTop())
//
function onNoticeEnter() {
clearTimeout(noticeLeaveTimer.value)
noticeVisible.value = true
nextTick(() => {
const popper = noticePopover.value?.popperRef?.contentRef
if (popper && !popper._noticeBound) {
popper._noticeBound = true
popper.addEventListener('mouseenter', () => clearTimeout(noticeLeaveTimer.value))
popper.addEventListener('mouseleave', () => {
noticeLeaveTimer.value = setTimeout(() => { noticeVisible.value = false }, 100)
})
}
})
}
//
function onNoticeLeave() {
noticeLeaveTimer.value = setTimeout(() => { noticeVisible.value = false }, 150)
}
//
function previewNotice(item) {
if (!item.isRead) {
markNoticeRead(item.noticeId).catch(() => {})
const idx = noticeList.value.indexOf(item)
if (idx !== -1) noticeList.value[idx] = { ...item, isRead: true }
unreadCount.value = Math.max(0, unreadCount.value - 1)
}
proxy.$refs["noticeViewRef"].open(item.noticeId)
}
//
function markAllRead() {
const ids = noticeList.value.map(n => n.noticeId).join(',')
if (!ids) return
markNoticeReadAll(ids).catch(() => {})
noticeList.value = noticeList.value.map(n => ({ ...n, isRead: true }))
unreadCount.value = 0
}
</script>
<style lang="scss" scoped>
.notice-trigger {
position: relative;
transform: translateX(-6px);
.svg-icon { width: 1.2em; height: 1.2em; vertical-align: -0.2em; }
.notice-badge {
position: absolute;
top: 7px;
right: -3px;
background: #f56c6c;
color: #fff;
border-radius: 10px;
font-size: 10px;
height: 16px;
line-height: 16px;
padding: 0 4px;
min-width: 16px;
text-align: center;
white-space: nowrap;
pointer-events: none;
}
}
.notice-popover { padding: 0 !important; }
.notice-popover .notice-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: #f7f9fb;
border-bottom: 1px solid #eee;
font-size: 13px;
font-weight: 600;
color: #333;
}
.notice-popover .notice-mark-all {
font-size: 12px;
color: var(--el-color-primary);
font-weight: normal;
cursor: pointer;
}
.notice-popover .notice-mark-all:hover { color: #2b7cc1; }
.notice-popover .notice-loading,
.notice-popover .notice-empty {
padding: 24px;
text-align: center;
color: #bbb;
font-size: 12px;
line-height: 1.8;
}
.notice-popover .notice-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
transition: background 0.15s;
}
.notice-popover .notice-item:last-child { border-bottom: none; }
.notice-popover .notice-item:hover { background: #f7f9fb; }
.notice-popover .notice-item.is-read .notice-tag,
.notice-popover .notice-item.is-read .notice-item-title,
.notice-popover .notice-item.is-read .notice-item-date { opacity: 0.45; filter: grayscale(1); color: #999; }
.notice-popover .notice-tag { flex-shrink: 0; }
.notice-popover .notice-item-title {
flex: 1;
font-size: 12px;
color: #333;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.notice-popover .notice-item-date {
flex-shrink: 0;
font-size: 11px;
color: #bbb;
}
</style>

View File

@ -9,17 +9,17 @@
</template> </template>
<script setup> <script setup>
import InnerLink from "../InnerLink/index"; import InnerLink from "../InnerLink/index"
import useTagsViewStore from "@/store/modules/tagsView"; import useTagsViewStore from "@/store/modules/tagsView"
const route = useRoute(); const route = useRoute()
const tagsViewStore = useTagsViewStore(); const tagsViewStore = useTagsViewStore()
function iframeUrl(url, query) { function iframeUrl(url, query) {
if (Object.keys(query).length > 0) { if (Object.keys(query).length > 0) {
let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&"); let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&")
return url + "?" + params; return url + "?" + params
} }
return url; return url
} }
</script> </script>

View File

@ -1,9 +1,10 @@
<template> <template>
<div :style="'height:' + height"> <div :style="'height:' + height" v-loading="loading" element-loading-text="正在加载页面,请稍候!">
<iframe <iframe
:id="iframeId" :id="iframeId"
style="width: 100%; height: 100%" style="width: 100%; height: 100%"
:src="src" :src="src"
ref="iframeRef"
frameborder="no" frameborder="no"
></iframe> ></iframe>
</div> </div>
@ -18,7 +19,17 @@ const props = defineProps({
iframeId: { iframeId: {
type: String type: String
} }
}); })
const height = ref(document.documentElement.clientHeight - 94.5 + "px"); const loading = ref(true)
const height = ref(document.documentElement.clientHeight - 94.5 + 'px')
const iframeRef = ref(null)
onMounted(() => {
if (iframeRef.value) {
iframeRef.value.onload = () => {
loading.value = false
}
}
})
</script> </script>

View File

@ -1,13 +1,25 @@
<template> <template>
<div class="navbar"> <div class="navbar" :class="'nav' + settingsStore.navType">
<hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> <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" /> <breadcrumb v-if="settingsStore.navType == 1" id="breadcrumb-container" class="breadcrumb-container" />
<top-nav id="topmenu-container" class="topmenu-container" v-if="settingsStore.topNav" /> <top-nav v-if="settingsStore.navType == 2" id="topmenu-container" class="topmenu-container" />
<template v-if="settingsStore.navType == 3">
<logo v-show="settingsStore.sidebarLogo" :collapse="false"></logo>
<top-bar id="topbar-container" class="topbar-container" />
</template>
<div class="right-menu"> <div class="right-menu">
<template v-if="appStore.device !== 'mobile'"> <template v-if="appStore.device !== 'mobile'">
<header-search id="header-search" class="right-menu-item" /> <header-search id="header-search" class="right-menu-item" />
<el-tooltip content="源码地址" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip content="文档地址" effect="dark" placement="bottom">
<ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
</el-tooltip>
<screenfull id="screenfull" class="right-menu-item hover-effect" /> <screenfull id="screenfull" class="right-menu-item hover-effect" />
<el-tooltip content="主题模式" effect="dark" placement="bottom"> <el-tooltip content="主题模式" effect="dark" placement="bottom">
@ -20,28 +32,34 @@
<el-tooltip content="布局大小" effect="dark" placement="bottom"> <el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" /> <size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip> </el-tooltip>
<el-tooltip content="消息通知" effect="dark" placement="bottom">
<header-notice id="header-notice" class="right-menu-item hover-effect" />
</el-tooltip>
</template> </template>
<div class="avatar-container">
<el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click"> <el-dropdown @command="handleCommand" class="avatar-container right-menu-item hover-effect" trigger="hover">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
<img :src="userStore.avatar" class="user-avatar" /> <img :src="userStore.avatar" class="user-avatar" />
<el-icon><caret-bottom /></el-icon> <span class="user-nickname"> {{ userStore.nickName }} </span>
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<router-link to="/zhtest/profilettt"> <router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item> <el-dropdown-item>个人中心</el-dropdown-item>
</router-link> </router-link>
<el-dropdown-item command="setLayout" v-if="settingsStore.showSettings"> <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
<span>布局设置</span> <span>布局设置</span>
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item divided command="logout"> <el-dropdown-item command="lockScreen">
<span>退出登录</span> <span>锁定屏幕</span>
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> <el-dropdown-item divided command="logout">
</template> <span>退出登录</span>
</el-dropdown> </el-dropdown-item>
</div> </el-dropdown-menu>
</template>
</el-dropdown>
</div> </div>
</div> </div>
</template> </template>
@ -49,17 +67,26 @@
<script setup> <script setup>
import { ElMessageBox } from 'element-plus' import { ElMessageBox } from 'element-plus'
import Breadcrumb from '@/components/Breadcrumb' import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav' import TopNav from './TopNav'
import TopBar from './TopBar'
import Logo from './Sidebar/Logo'
import Hamburger from '@/components/Hamburger' import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull' import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect' import SizeSelect from '@/components/SizeSelect'
import HeaderSearch from '@/components/HeaderSearch' import HeaderSearch from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
import useAppStore from '@/store/modules/app' import useAppStore from '@/store/modules/app'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import useLockStore from '@/store/modules/lock'
import useSettingsStore from '@/store/modules/settings' import useSettingsStore from '@/store/modules/settings'
import HeaderNotice from './HeaderNotice'
const route = useRoute()
const router = useRouter()
const appStore = useAppStore() const appStore = useAppStore()
const userStore = useUserStore() const userStore = useUserStore()
const lockStore = useLockStore()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
function toggleSideBar() { function toggleSideBar() {
@ -69,13 +96,16 @@ function toggleSideBar() {
function handleCommand(command) { function handleCommand(command) {
switch (command) { switch (command) {
case "setLayout": case "setLayout":
setLayout(); setLayout()
break; break
case "lockScreen":
lockScreen()
break
case "logout": case "logout":
logout(); logout()
break; break
default: default:
break; break
} }
} }
@ -86,18 +116,60 @@ function logout() {
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
userStore.logOut().then(() => { userStore.logOut().then(() => {
location.href = '/index'; location.href = '/index'
}) })
}).catch(() => { }); }).catch(() => { })
} }
const emits = defineEmits(['setLayout']) const emits = defineEmits(['setLayout'])
function setLayout() { function setLayout() {
emits('setLayout'); emits('setLayout')
} }
function toggleTheme() { function lockScreen() {
settingsStore.toggleTheme() const currentPath = route.fullPath
lockStore.lockScreen(currentPath)
router.push('/lock')
}
async function toggleTheme(event) {
const x = event?.clientX || window.innerWidth / 2
const y = event?.clientY || window.innerHeight / 2
const wasDark = settingsStore.isDark
const isReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
const isSupported = document.startViewTransition && !isReducedMotion
if (!isSupported) {
settingsStore.toggleTheme()
return
}
try {
const transition = document.startViewTransition(async () => {
await new Promise((resolve) => setTimeout(resolve, 10))
settingsStore.toggleTheme()
await nextTick()
})
await transition.ready
const endRadius = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y))
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`]
document.documentElement.animate(
{
clipPath: !wasDark ? [...clipPath].reverse() : clipPath
}, {
duration: 650,
easing: "cubic-bezier(0.4, 0, 0.2, 1)",
fill: "forwards",
pseudoElement: !wasDark ? "::view-transition-old(root)" : "::view-transition-new(root)"
}
)
await transition.finished
} catch (error) {
console.warn("View transition failed, falling back to immediate toggle:", error)
settingsStore.toggleTheme()
}
} }
</script> </script>

View File

@ -1,5 +1,26 @@
<template> <template>
<el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px"> <el-drawer v-model="showSettings" :withHeader="false" :lock-scroll="false" direction="rtl" size="300px">
<div class="setting-drawer-title">
<h3 class="drawer-title">菜单导航设置</h3>
</div>
<div class="nav-wrap">
<el-tooltip content="左侧菜单" placement="bottom">
<div class="item left" @click="handleNavType(1)" :class="{ activeItem: navType == 1 }">
<b></b><b></b>
</div>
</el-tooltip>
<el-tooltip content="混合菜单" placement="bottom">
<div class="item mix" @click="handleNavType(2)" :class="{ activeItem: navType == 2 }">
<b></b><b></b>
</div>
</el-tooltip>
<el-tooltip content="顶部菜单" placement="bottom">
<div class="item top" @click="handleNavType(3)" :class="{ activeItem: navType == 3 }">
<b></b><b></b>
</div>
</el-tooltip>
</div>
<div class="setting-drawer-title"> <div class="setting-drawer-title">
<h3 class="drawer-title">主题风格设置</h3> <h3 class="drawer-title">主题风格设置</h3>
</div> </div>
@ -36,16 +57,33 @@
<h3 class="drawer-title">系统布局配置</h3> <h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item"> <div class="drawer-item">
<span>开启 TopNav</span> <span>开启页签</span>
<span class="comp-style"> <span class="comp-style">
<el-switch v-model="settingsStore.topNav" @change="topNavChange" class="drawer-switch" /> <el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
</span> </span>
</div> </div>
<div class="drawer-item"> <div class="drawer-item">
<span>开启 Tags-Views</span> <span>持久化标签页</span>
<span class="comp-style"> <span class="comp-style">
<el-switch v-model="settingsStore.tagsView" class="drawer-switch" /> <el-switch v-model="settingsStore.tagsViewPersist" :disabled="!settingsStore.tagsView" @change="tagsViewPersistChange" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>显示页签图标</span>
<span class="comp-style">
<el-switch v-model="settingsStore.tagsIcon" :disabled="!settingsStore.tagsView" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>标签页样式</span>
<span class="comp-style">
<el-radio-group v-model="settingsStore.tagsViewStyle" :disabled="!settingsStore.tagsView" size="small">
<el-radio-button label="card">卡片</el-radio-button>
<el-radio-button label="chrome">谷歌</el-radio-button>
</el-radio-group>
</span> </span>
</div> </div>
@ -66,7 +104,14 @@
<div class="drawer-item"> <div class="drawer-item">
<span>动态标题</span> <span>动态标题</span>
<span class="comp-style"> <span class="comp-style">
<el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" /> <el-switch v-model="settingsStore.dynamicTitle" @change="dynamicTitleChange" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>底部版权</span>
<span class="comp-style">
<el-switch v-model="settingsStore.footerVisible" class="drawer-switch" />
</span> </span>
</div> </div>
@ -79,70 +124,102 @@
</template> </template>
<script setup> <script setup>
import variables from '@/assets/styles/variables.module.scss'
import axios from 'axios'
import { ElLoading, ElMessage } from 'element-plus'
import { useDynamicTitle } from '@/utils/dynamicTitle'
import useAppStore from '@/store/modules/app' import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings' import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission' import usePermissionStore from '@/store/modules/permission'
import { handleThemeStyle } from '@/utils/theme' import { handleThemeStyle } from '@/utils/theme'
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance()
const appStore = useAppStore() const appStore = useAppStore()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
const showSettings = ref(false); const showSettings = ref(false)
const theme = ref(settingsStore.theme); const navType = ref(settingsStore.navType)
const sideTheme = ref(settingsStore.sideTheme); const theme = ref(settingsStore.theme)
const storeSettings = computed(() => settingsStore); const sideTheme = ref(settingsStore.sideTheme)
const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"]); const tagsViewPersist = ref(settingsStore.tagsViewPersist)
const storeSettings = computed(() => settingsStore)
const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"])
/** 是否需要topnav */ /** 是否需要dynamicTitle */
function topNavChange(val) { function dynamicTitleChange() {
if (!val) { useSettingsStore().setTitle(useSettingsStore().title)
appStore.toggleSideBarHide(false); }
permissionStore.setSidebarRouters(permissionStore.defaultRoutes);
} function tagsViewPersistChange(val) {
settingsStore.tagsViewPersist = val
tagsViewPersist.value = val
} }
function themeChange(val) { function themeChange(val) {
settingsStore.theme = val; settingsStore.theme = val
handleThemeStyle(val); handleThemeStyle(val)
} }
function handleTheme(val) { function handleTheme(val) {
settingsStore.sideTheme = val; settingsStore.sideTheme = val
sideTheme.value = val; sideTheme.value = val
} }
function handleNavType(val) {
settingsStore.navType = val
navType.value = val
}
/** 菜单导航设置 */
watch(() => navType, val => {
if (val.value == 1) {
appStore.sidebar.opened = true
appStore.toggleSideBarHide(false)
}
if (val.value == 2) {
appStore.sidebar.opened = true
}
if (val.value == 3) {
appStore.sidebar.opened = false
appStore.toggleSideBarHide(true)
}
if ([1, 3].includes(val.value)) {
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
}
}, { immediate: true, deep: true }
)
function saveSetting() { function saveSetting() {
proxy.$modal.loading("正在保存到本地,请稍候..."); proxy.$modal.loading("正在保存到本地,请稍候...")
if (!tagsViewPersist.value) {
proxy.$cache.local.remove('tags-view-visited')
}
let layoutSetting = { let layoutSetting = {
"topNav": storeSettings.value.topNav, "navType": storeSettings.value.navType,
"tagsView": storeSettings.value.tagsView, "tagsView": storeSettings.value.tagsView,
"tagsIcon": storeSettings.value.tagsIcon,
"tagsViewStyle": storeSettings.value.tagsViewStyle,
"tagsViewPersist": storeSettings.value.tagsViewPersist,
"fixedHeader": storeSettings.value.fixedHeader, "fixedHeader": storeSettings.value.fixedHeader,
"sidebarLogo": storeSettings.value.sidebarLogo, "sidebarLogo": storeSettings.value.sidebarLogo,
"dynamicTitle": storeSettings.value.dynamicTitle, "dynamicTitle": storeSettings.value.dynamicTitle,
"footerVisible": storeSettings.value.footerVisible,
"sideTheme": storeSettings.value.sideTheme, "sideTheme": storeSettings.value.sideTheme,
"theme": storeSettings.value.theme "theme": storeSettings.value.theme
}; }
localStorage.setItem("layout-setting", JSON.stringify(layoutSetting)); localStorage.setItem("layout-setting", JSON.stringify(layoutSetting))
setTimeout(proxy.$modal.closeLoading(), 1000) setTimeout(proxy.$modal.closeLoading(), 1000)
} }
function resetSetting() { function resetSetting() {
proxy.$modal.loading("正在清除设置缓存并刷新,请稍候..."); proxy.$cache.local.remove('tags-view-visited')
proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...")
localStorage.removeItem("layout-setting") localStorage.removeItem("layout-setting")
setTimeout("window.location.reload()", 1000) setTimeout("window.location.reload()", 1000)
} }
function openSetting() { function openSetting() {
showSettings.value = true; showSettings.value = true
} }
defineExpose({ defineExpose({
openSetting, openSetting
}) })
</script> </script>

View File

@ -1,11 +1,11 @@
<template> <template>
<component :is="type" v-bind="linkProps()"> <component :is="type" v-bind="linkProps()">
<slot/> <slot />
</component> </component>
</template> </template>
<script setup> <script setup>
import {isExternal} from '@/utils/validate' import { isExternal } from '@/utils/validate'
const props = defineProps({ const props = defineProps({
to: { to: {
@ -14,16 +14,6 @@ const props = defineProps({
} }
}) })
//
const needNewWindow = computed(() => {
//
const path = typeof props.to === 'string' ? props.to : props.to.path
// /new-window/
const list = ['/inspection/cockpit']
return list.includes(path)
})
const isExt = computed(() => { const isExt = computed(() => {
return isExternal(props.to) return isExternal(props.to)
}) })
@ -43,19 +33,6 @@ function linkProps() {
rel: 'noopener' rel: 'noopener'
} }
} }
if (needNewWindow.value) {
//
const fullPath = typeof props.to === 'string'
? props.to
: props.to.path + (props.to.query ? '?' + new URLSearchParams(props.to.query).toString() : '')
return {
href: fullPath,
target: '_blank',
rel: 'noopener'
}
}
return { return {
to: props.to to: props.to
} }

View File

@ -25,29 +25,35 @@ defineProps({
} }
}) })
const title = import.meta.env.VITE_APP_TITLE; const title = import.meta.env.VITE_APP_TITLE
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore()
const sideTheme = computed(() => settingsStore.sideTheme); const sideTheme = computed(() => settingsStore.sideTheme)
// Logo // Logo
const getLogoBackground = computed(() => { const getLogoBackground = computed(() => {
if (settingsStore.isDark) { if (settingsStore.isDark) {
return 'var(--sidebar-bg)'; return 'var(--sidebar-bg)'
} }
return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg; if (settingsStore.navType == 3) {
}); return variables.menuLightBg
}
return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg
})
// Logo // Logo
const getLogoTextColor = computed(() => { const getLogoTextColor = computed(() => {
if (settingsStore.isDark) { if (settingsStore.isDark) {
return 'var(--sidebar-text)'; return 'var(--sidebar-logo-text)'
} }
return sideTheme.value === 'theme-dark' ? '#fff' : variables.menuLightText; if (settingsStore.navType == 3) {
}); return variables.menuLightText
}
return sideTheme.value === 'theme-dark' ? '#fff' : variables.menuLightText
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/assets/styles/variables.module.scss'; @use '@/assets/styles/variables.module.scss' as *;
.sidebarLogoFade-enter-active { .sidebarLogoFade-enter-active {
transition: opacity 1.5s; transition: opacity 1.5s;

View File

@ -48,11 +48,11 @@ const props = defineProps({
} }
}) })
const onlyOneChild = ref({}); const onlyOneChild = ref({})
function hasOneShowingChild(children = [], parent) { function hasOneShowingChild(children = [], parent) {
if (!children) { if (!children) {
children = []; children = []
} }
const showingChildren = children.filter(item => { const showingChildren = children.filter(item => {
if (item.hidden) { if (item.hidden) {
@ -74,7 +74,7 @@ function hasOneShowingChild(children = [], parent) {
} }
return false return false
}; }
function resolvePath(routePath, routeQuery) { function resolvePath(routePath, routeQuery) {
if (isExternal(routePath)) { if (isExternal(routePath)) {
@ -84,7 +84,7 @@ function resolvePath(routePath, routeQuery) {
return props.basePath return props.basePath
} }
if (routeQuery) { if (routeQuery) {
let query = JSON.parse(routeQuery); let query = JSON.parse(routeQuery)
return { path: getNormalPath(props.basePath + '/' + routePath), query: query } return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
} }
return getNormalPath(props.basePath + '/' + routePath) return getNormalPath(props.basePath + '/' + routePath)
@ -92,9 +92,9 @@ function resolvePath(routePath, routeQuery) {
function hasTitle(title){ function hasTitle(title){
if (title.length > 5) { if (title.length > 5) {
return title; return title
} else { } else {
return ""; return ""
} }
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div :class="{ 'has-logo': showLogo }" class="sidebar-container"> <div :class="['sidebar-theme-wrapper', {'has-logo':showLogo}, sideTheme]" class="sidebar-container">
<logo v-if="showLogo" :collapse="isCollapse" /> <logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper"> <el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu <el-menu
@ -32,40 +32,40 @@ import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings' import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission' import usePermissionStore from '@/store/modules/permission'
const route = useRoute(); const route = useRoute()
const appStore = useAppStore() const appStore = useAppStore()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
const sidebarRouters = computed(() => permissionStore.sidebarRouters); const sidebarRouters = computed(() => permissionStore.sidebarRouters)
const showLogo = computed(() => settingsStore.sidebarLogo); const showLogo = computed(() => settingsStore.sidebarLogo)
const sideTheme = computed(() => settingsStore.sideTheme); const sideTheme = computed(() => settingsStore.sideTheme)
const theme = computed(() => settingsStore.theme); const theme = computed(() => settingsStore.theme)
const isCollapse = computed(() => !appStore.sidebar.opened); const isCollapse = computed(() => !appStore.sidebar.opened)
// //
const getMenuBackground = computed(() => { const getMenuBackground = computed(() => {
if (settingsStore.isDark) { if (settingsStore.isDark) {
return 'var(--sidebar-bg)'; return 'var(--sidebar-bg)'
} }
return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg; return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg
}); })
// //
const getMenuTextColor = computed(() => { const getMenuTextColor = computed(() => {
if (settingsStore.isDark) { if (settingsStore.isDark) {
return 'var(--sidebar-text)'; return 'var(--sidebar-text)'
} }
return sideTheme.value === 'theme-dark' ? variables.menuText : variables.menuLightText; return sideTheme.value === 'theme-dark' ? variables.menuText : variables.menuLightText
}); })
const activeMenu = computed(() => { const activeMenu = computed(() => {
const { meta, path } = route; const { meta, path } = route
if (meta.activeMenu) { if (meta.activeMenu) {
return meta.activeMenu; return meta.activeMenu
} }
return path; return path
}); })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -221,7 +221,7 @@ function toLastView(visitedViews, view) {
} else { } else {
// now the default is to redirect to the home page if there is no tags-view, // now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs. // you can adjust it according to your needs.
if (view.name === 'Dashboard') { if (view && view.name === 'Dashboard') {
// to reload home page // to reload home page
router.replace({ path: '/redirect' + view.fullPath }) router.replace({ path: '/redirect' + view.fullPath })
} else { } else {

View File

@ -0,0 +1,99 @@
<template>
<el-menu class="topbar-menu" :ellipsis="false" :default-active="activeMenu" :active-text-color="theme" mode="horizontal">
<sidebar-item :key="route.path + index" v-for="(route, index) in topMenus" :item="route" :base-path="route.path" />
<el-sub-menu index="more" class="el-sub-menu__hide-arrow" v-if="moreRoutes.length > 0">
<template #title>
<span>更多菜单</span>
</template>
<sidebar-item :key="route.path + index" v-for="(route, index) in moreRoutes" :item="route" :base-path="route.path" />
</el-sub-menu>
</el-menu>
</template>
<script setup>
import SidebarItem from '../Sidebar/SidebarItem'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
const route = useRoute()
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const sidebarRouters = computed(() => permissionStore.sidebarRouters)
const theme = computed(() => settingsStore.theme)
const device = computed(() => appStore.device)
const activeMenu = computed(() => {
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
})
const visibleNumber = ref(5)
const topMenus = computed(() => {
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value)
})
const moreRoutes = computed(() => {
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(visibleNumber.value)
})
function setVisibleNumber() {
const width = document.body.getBoundingClientRect().width / 3
visibleNumber.value = Math.max(1, parseInt(width / 85))
}
onMounted(() => {
window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber)
})
onMounted(() => {
setVisibleNumber()
})
</script>
<style lang="scss">
/* menu item */
.topbar-menu.el-menu--horizontal .el-submenu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
padding: 0 10px !important;
}
.topbar-menu.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #303133 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
.el-sub-menu.is-active .svg-icon, .el-menu-item.is-active .svg-icon + span, .el-sub-menu.is-active .svg-icon + span, .el-sub-menu.is-active .el-sub-menu__title span {
color: v-bind(theme);
}
/* sub-menu item */
.topbar-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
float: left;
line-height: 50px !important;
color: #303133 !important;
margin: 0 15px -3px!important;
}
/* topbar more arrow */
.topbar-menu .el-sub-menu .el-sub-menu__icon-arrow {
position: static;
margin-left: 8px;
margin-top: 0px;
display: block !important;
}
/* menu__title el-menu-item */
.topbar-menu.el-menu--horizontal .el-sub-menu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
height: 60px;
}
</style>

View File

@ -0,0 +1,220 @@
<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
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>
</el-menu>
</template>
<script setup>
import { constantRoutes } from "@/router"
import { isHttp } from '@/utils/validate'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
//
const visibleNumber = ref(null)
// index
const currentIndex = ref(null)
//
const hideList = ['/index', '/user/profile']
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const route = useRoute()
const router = useRouter()
//
const theme = computed(() => settingsStore.theme)
//
const routers = computed(() => permissionStore.topbarRouters)
//
const topMenus = computed(() => {
let topMenus = []
routers.value.map((menu) => {
if (menu.hidden !== true) {
//
if (menu.path === '/' && menu.children) {
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)
if (!route.meta.link) {
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"))
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 = Math.max(1, 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) {
permissionStore.setSidebarRouters(routes)
} 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 {
height: 50px !important;
border-bottom: none;
}
.topmenu-container.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #303133 !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: #303133 !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;
}
/* 图标右间距 */
.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,5 +1,5 @@
<template> <template>
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }"> <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme, '--current-color-light': theme + '1a', '--current-color-dark-bg': theme + '33' }">
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/> <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar v-if="!sidebar.hide" class="sidebar-container" /> <sidebar v-if="!sidebar.hide" class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container"> <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
@ -17,18 +17,15 @@
import { useWindowSize } from '@vueuse/core' import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue' import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components' import { AppMain, Navbar, Settings, TagsView } from './components'
import defaultSettings from '@/settings'
import useAppStore from '@/store/modules/app' import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings' import useSettingsStore from '@/store/modules/settings'
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const theme = computed(() => settingsStore.theme); const theme = computed(() => settingsStore.theme)
const sideTheme = computed(() => settingsStore.sideTheme); const sidebar = computed(() => useAppStore().sidebar)
const sidebar = computed(() => useAppStore().sidebar); const device = computed(() => useAppStore().device)
const device = computed(() => useAppStore().device); const needTagsView = computed(() => settingsStore.tagsView)
const needTagsView = computed(() => settingsStore.tagsView); const fixedHeader = computed(() => settingsStore.fixedHeader)
const fixedHeader = computed(() => settingsStore.fixedHeader);
const classObj = computed(() => ({ const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened, hideSidebar: !sidebar.value.opened,
@ -37,8 +34,8 @@ const classObj = computed(() => ({
mobile: device.value === 'mobile' mobile: device.value === 'mobile'
})) }))
const { width, height } = useWindowSize(); const { width, height } = useWindowSize()
const WIDTH = 992; // refer to Bootstrap's responsive design const WIDTH = 992 // refer to Bootstrap's responsive design
watch(() => device.value, () => { watch(() => device.value, () => {
if (device.value === 'mobile' && sidebar.value.opened) { if (device.value === 'mobile' && sidebar.value.opened) {
@ -59,15 +56,15 @@ function handleClickOutside() {
useAppStore().closeSideBar({ withoutAnimation: false }) useAppStore().closeSideBar({ withoutAnimation: false })
} }
const settingRef = ref(null); const settingRef = ref(null)
function setLayout() { function setLayout() {
settingRef.value.openSetting(); settingRef.value.openSetting()
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/assets/styles/mixin.scss"; @use "@/assets/styles/mixin.scss" as *;
@import "@/assets/styles/variables.module.scss"; @use "@/assets/styles/variables.module.scss" as *;
.app-wrapper { .app-wrapper {
@include clearfix; @include clearfix;

View File

@ -13,7 +13,7 @@ import App from './App'
import store from './store' import store from './store'
import router from './router' import router from './router'
import directive from './directive' // directive import directive from './directive' // directive
import WebSocketPlugin from './plugins/websocket'; import WebSocketPlugin from './plugins/websocket'
// 注册指令 // 注册指令
import plugins from './plugins' // plugins import plugins from './plugins' // plugins
@ -27,6 +27,7 @@ import elementIcons from '@/components/SvgIcon/svgicon'
import './permission' // permission control import './permission' // permission control
import { useDict } from '@/utils/dict' import { useDict } from '@/utils/dict'
import { getConfigKey } from "@/api/system/config"
import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi' import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
// 分页组件 // 分页组件
@ -43,21 +44,24 @@ import ImageUpload from "@/components/ImageUpload"
import ImagePreview from "@/components/ImagePreview" import ImagePreview from "@/components/ImagePreview"
// 字典标签组件 // 字典标签组件
import DictTag from '@/components/DictTag' import DictTag from '@/components/DictTag'
import { JsonViewer } from 'vue3-json-viewer'
import 'vue3-json-viewer/dist/index.css'
// ace.config.js独立配置文件 import ace from 'ace-builds'
import ace from 'ace-builds'; import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url';
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl); // 动态绑定语法模块路径 ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
const app = createApp(App) const app = createApp(App)
// 全局方法挂载 // 全局方法挂载
app.config.globalProperties.useDict = useDict app.config.globalProperties.useDict = useDict
app.config.globalProperties.download = download app.config.globalProperties.download = download
app.config.globalProperties.parseTime = parseTime app.config.globalProperties.parseTime = parseTime
app.config.globalProperties.resetForm = resetForm app.config.globalProperties.resetForm = resetForm
app.config.globalProperties.handleTree = handleTree app.config.globalProperties.handleTree = handleTree
app.config.globalProperties.addDateRange = addDateRange app.config.globalProperties.addDateRange = addDateRange
app.config.globalProperties.getConfigKey = getConfigKey
app.config.globalProperties.selectDictLabel = selectDictLabel app.config.globalProperties.selectDictLabel = selectDictLabel
app.config.globalProperties.selectDictLabels = selectDictLabels app.config.globalProperties.selectDictLabels = selectDictLabels
@ -69,11 +73,16 @@ app.component('ImageUpload', ImageUpload)
app.component('ImagePreview', ImagePreview) app.component('ImagePreview', ImagePreview)
app.component('RightToolbar', RightToolbar) app.component('RightToolbar', RightToolbar)
app.component('Editor', Editor) app.component('Editor', Editor)
app.component('JsonViewer', JsonViewer)
app.use(router) app.use(router)
app.use(store) app.use(store)
app.use(plugins) app.use(plugins)
app.use(elementIcons) app.use(elementIcons)
app.use(WebSocketPlugin, {
url: import.meta.env.VITE_WS_URL,
userId: 'your_user_id'
})
app.component('svg-icon', SvgIcon) app.component('svg-icon', SvgIcon)
directive(app) directive(app)
@ -85,11 +94,4 @@ app.use(ElementPlus, {
size: Cookies.get('size') || 'default' size: Cookies.get('size') || 'default'
}) })
app.use(WebSocketPlugin, {
url: import.meta.env.VITE_WS_URL,
// url: 'ws://192.168.0.201:13080/ws',
userId: 'your_user_id'
});
app.mount('#app') app.mount('#app')

View File

@ -6,6 +6,7 @@ import { getToken } from '@/utils/auth'
import { isHttp, isPathMatch } from '@/utils/validate' import { isHttp, isPathMatch } from '@/utils/validate'
import { isRelogin } from '@/utils/request' import { isRelogin } from '@/utils/request'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import useLockStore from '@/store/modules/lock'
import useSettingsStore from '@/store/modules/settings' import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission' import usePermissionStore from '@/store/modules/permission'
@ -17,56 +18,59 @@ const isWhiteList = (path) => {
return whiteList.some(pattern => isPathMatch(pattern, path)) return whiteList.some(pattern => isPathMatch(pattern, path))
} }
router.beforeEach((to, from, next) => { router.beforeEach(async (to, from) => {
if (to.meta?.noLogin) { if (to.meta?.noLogin) {
next() return true
return
} }
NProgress.start() NProgress.start()
if (getToken()) { if (getToken()) {
to.meta.title && useSettingsStore().setTitle(to.meta.title) to.meta.title && useSettingsStore().setTitle(to.meta.title)
/* has token*/ const isLock = useLockStore().isLock
if (to.path === '/login') { if (to.path === '/login') {
next({ path: '/' })
NProgress.done() NProgress.done()
} else if (isWhiteList(to.path)) { return { path: '/' }
next() }
} else { if (isWhiteList(to.path)) {
if (useUserStore().roles.length === 0) { return true
isRelogin.show = true }
// 判断当前用户是否已拉取完user_info信息 if (isLock && to.path !== '/lock') {
useUserStore().getInfo().then(() => { NProgress.done()
isRelogin.show = false return { path: '/lock' }
usePermissionStore().generateRoutes().then(accessRoutes => { }
console.log('accessRoutes', accessRoutes) if (!isLock && to.path === '/lock') {
// 根据roles权限生成可访问的路由表 NProgress.done()
accessRoutes.forEach(route => { return { path: '/' }
if (!isHttp(route.path)) { }
router.addRoute(route) // 动态添加可访问路由表 if (useUserStore().roles.length === 0) {
} isRelogin.show = true
}) try {
console.log('router', router) // 拉取user_info信息
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 await useUserStore().getInfo()
}) isRelogin.show = false
}).catch(err => { // 根据roles权限生成可访问的路由
useUserStore().logOut().then(() => { const accessRoutes = await usePermissionStore().generateRoutes()
ElMessage.error(err) accessRoutes.forEach(route => {
next({ path: '/' }) if (!isHttp(route.path)) {
}) router.addRoute(route)
}
}) })
} else { // 重新导航到目标路由,确保动态路由已注册
next() return { ...to, replace: true }
} catch (err) {
await useUserStore().logOut()
ElMessage.error(err)
return { path: '/' }
} }
} }
return true
} else { } else {
// 没有token // 没有token
if (isWhiteList(to.path)) { if (isWhiteList(to.path)) {
// 在免登录白名单,直接进入 // 在免登录白名单,直接进入
next() return true
} else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
NProgress.done()
} }
NProgress.done()
return `/login?redirect=${to.fullPath}` // 否则全部重定向到登录页
} }
}) })

View File

@ -1,7 +1,7 @@
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
function authPermission(permission) { function authPermission(permission) {
const all_permission = "*:*:*"; const all_permission = "*:*:*"
const permissions = useUserStore().permissions const permissions = useUserStore().permissions
if (permission && permission.length > 0) { if (permission && permission.length > 0) {
return permissions.some(v => { return permissions.some(v => {
@ -13,7 +13,7 @@ function authPermission(permission) {
} }
function authRole(role) { function authRole(role) {
const super_admin = "admin"; const super_admin = "admin"
const roles = useUserStore().roles const roles = useUserStore().roles
if (role && role.length > 0) { if (role && role.length > 0) {
return roles.some(v => { return roles.some(v => {
@ -27,7 +27,7 @@ function authRole(role) {
export default { export default {
// 验证用户是否具备某权限 // 验证用户是否具备某权限
hasPermi(permission) { hasPermi(permission) {
return authPermission(permission); return authPermission(permission)
}, },
// 验证用户是否含有指定权限,只需包含其中一个 // 验证用户是否含有指定权限,只需包含其中一个
hasPermiOr(permissions) { hasPermiOr(permissions) {
@ -43,7 +43,7 @@ export default {
}, },
// 验证用户是否具备某角色 // 验证用户是否具备某角色
hasRole(role) { hasRole(role) {
return authRole(role); return authRole(role)
}, },
// 验证用户是否含有指定角色,只需包含其中一个 // 验证用户是否含有指定角色,只需包含其中一个
hasRoleOr(roles) { hasRoleOr(roles) {

View File

@ -29,7 +29,7 @@ const sessionCache = {
return null return null
}, },
remove (key) { remove (key) {
sessionStorage.removeItem(key); sessionStorage.removeItem(key)
} }
} }
const localCache = { const localCache = {
@ -63,7 +63,7 @@ const localCache = {
return null return null
}, },
remove (key) { remove (key) {
localStorage.removeItem(key); localStorage.removeItem(key)
} }
} }

View File

@ -6,7 +6,7 @@ import errorCode from '@/utils/errorCode'
import { blobValidate } from '@/utils/ruoyi' import { blobValidate } from '@/utils/ruoyi'
const baseURL = import.meta.env.VITE_APP_BASE_API const baseURL = import.meta.env.VITE_APP_BASE_API
let downloadLoadingInstance; let downloadLoadingInstance
export default { export default {
name(name, isDelete = true) { name(name, isDelete = true) {
@ -17,29 +17,29 @@ export default {
responseType: 'blob', responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() } headers: { 'Authorization': 'Bearer ' + getToken() }
}).then((res) => { }).then((res) => {
const isBlob = blobValidate(res.data); const isBlob = blobValidate(res.data)
if (isBlob) { if (isBlob) {
const blob = new Blob([res.data]) const blob = new Blob([res.data])
this.saveAs(blob, decodeURIComponent(res.headers['download-filename'])) this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
} else { } else {
this.printErrMsg(res.data); this.printErrMsg(res.data)
} }
}) })
}, },
resource(resource) { resource(resource) {
var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource); var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource)
axios({ axios({
method: 'get', method: 'get',
url: url, url: url,
responseType: 'blob', responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() } headers: { 'Authorization': 'Bearer ' + getToken() }
}).then((res) => { }).then((res) => {
const isBlob = blobValidate(res.data); const isBlob = blobValidate(res.data)
if (isBlob) { if (isBlob) {
const blob = new Blob([res.data]) const blob = new Blob([res.data])
this.saveAs(blob, decodeURIComponent(res.headers['download-filename'])) this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
} else { } else {
this.printErrMsg(res.data); this.printErrMsg(res.data)
} }
}) })
}, },
@ -52,28 +52,28 @@ export default {
responseType: 'blob', responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() } headers: { 'Authorization': 'Bearer ' + getToken() }
}).then((res) => { }).then((res) => {
const isBlob = blobValidate(res.data); const isBlob = blobValidate(res.data)
if (isBlob) { if (isBlob) {
const blob = new Blob([res.data], { type: 'application/zip' }) const blob = new Blob([res.data], { type: 'application/zip' })
this.saveAs(blob, name) this.saveAs(blob, name)
} else { } else {
this.printErrMsg(res.data); this.printErrMsg(res.data)
} }
downloadLoadingInstance.close(); downloadLoadingInstance.close()
}).catch((r) => { }).catch((r) => {
console.error(r) console.error(r)
ElMessage.error('下载文件出现错误,请联系管理员!') ElMessage.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close(); downloadLoadingInstance.close()
}) })
}, },
saveAs(text, name, opts) { saveAs(text, name, opts) {
saveAs(text, name, opts); saveAs(text, name, opts)
}, },
async printErrMsg(data) { async printErrMsg(data) {
const resText = await data.text(); const resText = await data.text()
const rspObj = JSON.parse(resText); const rspObj = JSON.parse(resText)
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
ElMessage.error(errMsg); ElMessage.error(errMsg)
} }
} }

View File

@ -1,6 +1,6 @@
import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus' import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus'
let loadingInstance; let loadingInstance
export default { export default {
// 消息提示 // 消息提示
@ -41,7 +41,7 @@ export default {
}, },
// 错误通知 // 错误通知
notifyError(content) { notifyError(content) {
ElNotification.error(content); ElNotification.error(content)
}, },
// 成功通知 // 成功通知
notifySuccess(content) { notifySuccess(content) {
@ -77,6 +77,6 @@ export default {
}, },
// 关闭遮罩层 // 关闭遮罩层
closeLoading() { closeLoading() {
loadingInstance.close(); loadingInstance.close()
} }
} }

View File

@ -4,15 +4,19 @@ import router from '@/router'
export default { export default {
// 刷新当前tab页签 // 刷新当前tab页签
refreshPage(obj) { refreshPage(obj) {
const { path, query, matched } = router.currentRoute.value; const { path, query, matched } = router.currentRoute.value
// 防止在重定向过程中重复刷新
if (path.startsWith('/redirect/')) {
return Promise.resolve()
}
if (obj === undefined) { if (obj === undefined) {
matched.forEach((m) => { matched.forEach((m) => {
if (m.components && m.components.default && m.components.default.name) { if (m.components && m.components.default && m.components.default.name) {
if (!['Layout', 'ParentView'].includes(m.components.default.name)) { if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
obj = { name: m.components.default.name, path: path, query: query }; obj = { name: m.components.default.name, path: path, query: query }
} }
} }
}); })
} }
return useTagsViewStore().delCachedView(obj).then(() => { return useTagsViewStore().delCachedView(obj).then(() => {
const { path, query } = obj const { path, query } = obj
@ -24,9 +28,9 @@ export default {
}, },
// 关闭当前tab页签打开新页签 // 关闭当前tab页签打开新页签
closeOpenPage(obj) { closeOpenPage(obj) {
useTagsViewStore().delView(router.currentRoute.value); useTagsViewStore().delView(router.currentRoute.value)
if (obj !== undefined) { if (obj !== undefined) {
return router.push(obj); return router.push(obj)
} }
}, },
// 关闭指定tab页签 // 关闭指定tab页签
@ -37,33 +41,35 @@ export default {
if (latestView) { if (latestView) {
return router.push(latestView.fullPath) return router.push(latestView.fullPath)
} }
return router.push('/'); return router.push('/')
}); })
} }
return useTagsViewStore().delView(obj); return useTagsViewStore().delView(obj)
}, },
// 关闭所有tab页签 // 关闭所有tab页签
closeAllPage() { closeAllPage() {
return useTagsViewStore().delAllViews(); return useTagsViewStore().delAllViews()
}, },
// 关闭左侧tab页签 // 关闭左侧tab页签
closeLeftPage(obj) { closeLeftPage(obj) {
return useTagsViewStore().delLeftTags(obj || router.currentRoute.value); return useTagsViewStore().delLeftTags(obj || router.currentRoute.value)
}, },
// 关闭右侧tab页签 // 关闭右侧tab页签
closeRightPage(obj) { closeRightPage(obj) {
return useTagsViewStore().delRightTags(obj || router.currentRoute.value); return useTagsViewStore().delRightTags(obj || router.currentRoute.value)
}, },
// 关闭其他tab页签 // 关闭其他tab页签
closeOtherPage(obj) { closeOtherPage(obj) {
return useTagsViewStore().delOthersViews(obj || router.currentRoute.value); return useTagsViewStore().delOthersViews(obj || router.currentRoute.value)
}, },
// 打开tab页签 // 打开tab页签
openPage(url) { openPage(title, url, params) {
return router.push(url); const obj = { path: url, meta: { title: title } }
useTagsViewStore().addView(obj)
return router.push({ path: url, query: params })
}, },
// 修改tab页签 // 修改tab页签
updatePage(obj) { updatePage(obj) {
return useTagsViewStore().updateVisitedView(obj); return useTagsViewStore().updateVisitedView(obj)
} }
} }

View File

@ -1,5 +1,8 @@
import { fa } from 'element-plus/es/locale/index.mjs'; const MEDIA_HEADER_SIZE = 26;
import { throttle } from 'lodash'; const MEDIA_MAGIC = [0x43, 0x4d, 0x56, 0x46]; // CMVF
const MEDIA_MIME_TYPES = {
1: 'video/mp4; codecs="avc1.42C01E"'
};
class WebSocketManager { class WebSocketManager {
constructor(url, userId) { constructor(url, userId) {
@ -11,7 +14,8 @@ class WebSocketManager {
this.heartbeatTimer = null; this.heartbeatTimer = null;
this.isManualClose = false; this.isManualClose = false;
this.messageQueue = []; this.messageQueue = [];
this.maxQueueSize = 1; this.processingMessages = false;
this.maxQueueSize = 100;
this.status = false; this.status = false;
} }
@ -29,21 +33,28 @@ class WebSocketManager {
this.status = true; this.status = true;
this.startHeartbeat(); this.startHeartbeat();
this.processMessageQueue(); this.processMessageQueue();
this.dispatch('open');
}; };
this.ws.onmessage = (event) => { this.ws.onmessage = async (event) => {
if (this.messageQueue.length < this.maxQueueSize) { const data = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data;
this.messageQueue.push(event); if (data instanceof ArrayBuffer && this.isMediaPacket(data)) {
this.processMessageQueue(); this.dispatchMediaPacket(data);
} else { return;
console.warn('消息队列已满,丢弃消息');
} }
if (this.messageQueue.length >= this.maxQueueSize) {
console.warn('WebSocket控制消息队列已满丢弃最旧消息');
this.messageQueue.shift();
}
this.messageQueue.push(data);
this.processMessageQueue();
}; };
this.ws.onclose = () => { this.ws.onclose = () => {
console.log('WebSocket 断开'); console.log('WebSocket 断开');
this.status = false; this.status = false;
this.stopHeartbeat(); this.stopHeartbeat();
this.dispatch('close');
if (!this.isManualClose) { if (!this.isManualClose) {
this.reconnect(); this.reconnect();
} }
@ -54,65 +65,80 @@ class WebSocketManager {
}; };
} }
async processMessageQueue() { isMediaPacket(buffer) {
if (!this.ws || this.messageQueue.length === 0) return; if (buffer.byteLength < MEDIA_HEADER_SIZE) return false;
const bytes = new Uint8Array(buffer, 0, MEDIA_MAGIC.length);
return MEDIA_MAGIC.every((value, index) => bytes[index] === value);
}
const event = this.messageQueue.shift(); dispatchMediaPacket(buffer) {
try { try {
// 处理消息数据 const view = new DataView(buffer);
console.log('收到消息数据类型:', event.data.constructor.name); const version = view.getUint8(4);
let compressedData; const mediaType = view.getUint8(5);
const timestamp = view.getUint32(8) * 0x100000000 + view.getUint32(12);
// 处理 Blob 或 ArrayBuffer const sequence = view.getUint32(16);
if (event.data instanceof Blob) { const channelLength = view.getUint16(20);
console.log('处理 Blob 数据,大小:', event.data.size); const payloadLength = view.getUint32(22);
compressedData = new Uint8Array(await event.data.arrayBuffer()); const payloadOffset = MEDIA_HEADER_SIZE + channelLength;
} else if (event.data instanceof ArrayBuffer) { if (version !== 1 || payloadOffset + payloadLength !== buffer.byteLength) {
console.log('处理 ArrayBuffer 数据,大小:', event.data.byteLength); throw new Error('媒体数据包头无效');
compressedData = new Uint8Array(event.data);
} else {
throw new Error('不支持的数据类型: ' + event.data.constructor.name);
} }
const channel = new TextDecoder().decode(new Uint8Array(buffer, MEDIA_HEADER_SIZE, channelLength));
// 使用 DecompressionStream 解压 GZIP 数据 const mimeType = MEDIA_MIME_TYPES[mediaType];
const decompressionStream = new DecompressionStream('gzip'); if (!mimeType) {
const writer = decompressionStream.writable.getWriter(); throw new Error(`不支持的媒体类型: ${mediaType}`);
writer.write(compressedData);
writer.close();
const decompressedStream = decompressionStream.readable;
const reader = decompressedStream.getReader();
let decompressedData = new Uint8Array(0);
while (true) {
const { done, value } = await reader.read();
if (done) break;
const newData = new Uint8Array(decompressedData.length + value.length);
newData.set(decompressedData);
newData.set(value, decompressedData.length);
decompressedData = newData;
} }
this.dispatch(channel, {
media: true,
mimeType,
sequence,
timestamp,
data: buffer.slice(payloadOffset, payloadOffset + payloadLength)
});
} catch (error) {
console.error('WebSocket媒体包解析失败:', error);
}
}
// 将解压后的数据转换为字符串 async processMessageQueue() {
const textDecoder = new TextDecoder(); if (this.processingMessages || !this.ws) return;
const decompressedString = textDecoder.decode(decompressedData); this.processingMessages = true;
console.log('解压后的数据:', JSON.parse(decompressedString)); try {
while (this.messageQueue.length > 0) {
// 解析 JSON const data = this.messageQueue.shift();
const { type, channel, payload,timestamp } = JSON.parse(decompressedString); let message;
console.log('后端到前端延迟:', ((new Date()).getTime() - timestamp)); if (typeof data === 'string') {
message = JSON.parse(data);
if (channel && this.events[channel]) { } else if (data instanceof ArrayBuffer) {
this.events[channel].forEach(callback => callback(payload)); const decompressedStream = new Blob([data])
} else if (type && this.events[type]) { .stream()
this.events[type].forEach(callback => callback(payload)); .pipeThrough(new DecompressionStream('gzip'));
const decompressedData = await new Response(decompressedStream).arrayBuffer();
message = JSON.parse(new TextDecoder().decode(decompressedData));
} else {
throw new Error(`不支持的WebSocket数据类型: ${typeof data}`);
}
if (message.channel) {
this.dispatch(message.channel, message.payload);
} else if (message.type) {
this.dispatch(message.type, message.payload ?? message.content ?? message);
}
} }
setTimeout(() => this.processMessageQueue(), 0);
} catch (err) { } catch (err) {
console.error('WebSocket 消息解析失败:', err); console.error('WebSocket消息解析失败:', err);
console.error('错误堆栈:', err.stack); } finally {
console.error('原始数据类型:', event.data.constructor.name); this.processingMessages = false;
console.error('原始数据大小:', event.data instanceof Blob ? event.data.size : event.data.byteLength || '未知'); if (this.messageQueue.length > 0) {
queueMicrotask(() => this.processMessageQueue());
}
}
}
dispatch(eventName, payload) {
const callbacks = this.events[eventName];
if (callbacks) {
callbacks.slice().forEach(callback => callback(payload));
} }
} }

View File

@ -1,7 +1,6 @@
import { createWebHistory, createRouter } from "vue-router"; import { createWebHistory, createRouter } from "vue-router";
/* Layout */ /* Layout */
import Layout from "@/layout"; import Layout from "@/layout";
import path from "path";
/** /**
* Note: 路由配置项 * Note: 路由配置项
@ -136,18 +135,18 @@ export const constantRoutes = [
hidden: true, hidden: true,
}, },
{ {
//logicFlow // 流程运行状态
path: "/logic_flow", path: "/logic_flow",
component: () => component: () =>
import("@/views/device/register/components/LogicFlow/index.vue"), import("@/views/device/register/components/VueFlow/index.vue"),
meta: { title: "logicFlow", noLogin: true }, meta: { title: "流程运行状态", noLogin: true },
hidden: true, hidden: true,
}, },
{ {
//logicFlow // 流程编排
path: "/flow", path: "/flow",
component: () => import("@/views/flow/index.vue"), component: () => import("@/views/flow/index.vue"),
meta: { title: "flow", noLogin: true }, meta: { title: "流程编排", noLogin: true },
hidden: true, hidden: true,
}, },
{ {
@ -170,6 +169,26 @@ export const constantRoutes = [
}, },
], ],
}, },
{
path: "/lock",
component: () => import("@/views/lock"),
hidden: true,
meta: { title: "锁定屏幕" },
},
{
path: "/user",
component: Layout,
hidden: true,
redirect: "noredirect",
children: [
{
path: "profile/:activeTab?",
component: () => import("@/views/system/user/profile/index"),
name: "Profile",
meta: { title: "个人中心", icon: "user" },
},
],
},
]; ];
// 动态路由,基于用户权限动态去加载 // 动态路由,基于用户权限动态去加载

View File

@ -3,25 +3,42 @@ export default {
* 网页标题 * 网页标题
*/ */
title: import.meta.env.VITE_APP_TITLE, title: import.meta.env.VITE_APP_TITLE,
/** /**
* 侧边栏主题 深色主题theme-dark浅色主题theme-light * 侧边栏主题 深色主题theme-dark浅色主题theme-light
*/ */
sideTheme: 'theme-dark', sideTheme: 'theme-dark',
/** /**
* 是否系统布局配置 * 是否系统布局配置
*/ */
showSettings: true, showSettings: true,
/** /**
* 是否显示顶部导航 * 菜单导航模式 1纯左侧 2混合左侧+顶部 3纯顶部
*/ */
topNav: false, navType: 1,
/** /**
* 是否显示 tagsView * 是否显示 tagsView
*/ */
tagsView: true, tagsView: true,
/**
* 持久化标签页
*/
tagsViewPersist: false,
/**
* 显示页签图标
*/
tagsIcon: false,
/**
* 标签页样式card 卡片默认chrome 谷歌浏览器风格
*/
tagsViewStyle: 'card',
/** /**
* 是否固定头部 * 是否固定头部
*/ */
@ -38,10 +55,13 @@ export default {
dynamicTitle: 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' footerVisible: false,
/**
* 底部版权文本内容
*/
footerContent: 'Copyright © 2025 CMVR. All Rights Reserved.'
} }

View File

@ -15,7 +15,7 @@ const useAppStore = defineStore(
actions: { actions: {
toggleSideBar(withoutAnimation) { toggleSideBar(withoutAnimation) {
if (this.sidebar.hide) { if (this.sidebar.hide) {
return false; return false
} }
this.sidebar.opened = !this.sidebar.opened this.sidebar.opened = !this.sidebar.opened
this.sidebar.withoutAnimation = withoutAnimation this.sidebar.withoutAnimation = withoutAnimation
@ -34,7 +34,7 @@ const useAppStore = defineStore(
this.device = device this.device = device
}, },
setSize(size) { setSize(size) {
this.size = size; this.size = size
Cookies.set('size', size) Cookies.set('size', size)
}, },
toggleSideBarHide(status) { toggleSideBarHide(status) {

View File

@ -8,16 +8,16 @@ const useDictStore = defineStore(
// 获取字典 // 获取字典
getDict(_key) { getDict(_key) {
if (_key == null && _key == "") { if (_key == null && _key == "") {
return null; return null
} }
try { try {
for (let i = 0; i < this.dict.length; i++) { for (let i = 0; i < this.dict.length; i++) {
if (this.dict[i].key == _key) { if (this.dict[i].key == _key) {
return this.dict[i].value; return this.dict[i].value
} }
} }
} catch (e) { } catch (e) {
return null; return null
} }
}, },
// 设置字典 // 设置字典
@ -26,27 +26,27 @@ const useDictStore = defineStore(
this.dict.push({ this.dict.push({
key: _key, key: _key,
value: value value: value
}); })
} }
}, },
// 删除字典 // 删除字典
removeDict(_key) { removeDict(_key) {
var bln = false; var bln = false
try { try {
for (let i = 0; i < this.dict.length; i++) { for (let i = 0; i < this.dict.length; i++) {
if (this.dict[i].key == _key) { if (this.dict[i].key == _key) {
this.dict.splice(i, 1); this.dict.splice(i, 1)
return true; return true
} }
} }
} catch (e) { } catch (e) {
bln = false; bln = false
} }
return bln; return bln
}, },
// 清空字典 // 清空字典
cleanDict() { cleanDict() {
this.dict = new Array(); this.dict = new Array()
}, },
// 初始字典 // 初始字典
initDict() { initDict() {

27
src/store/modules/lock.js Normal file
View File

@ -0,0 +1,27 @@
const LOCK_KEY = 'screen-lock'
const LOCK_PATH_KEY = 'screen-lock-path'
export const useLockStore = defineStore('lock', {
state: () => ({
isLock: JSON.parse(localStorage.getItem(LOCK_KEY) || 'false'),
lockPath: localStorage.getItem(LOCK_PATH_KEY) || '/index'
}),
actions: {
// 锁定屏幕,同时记录当前路径
lockScreen(currentPath) {
this.lockPath = currentPath || '/index'
localStorage.setItem(LOCK_PATH_KEY, this.lockPath)
this.isLock = true
localStorage.setItem(LOCK_KEY, 'true')
},
// 解锁屏幕,清除路径
unlockScreen() {
this.isLock = false
localStorage.setItem(LOCK_KEY, 'false')
this.lockPath = '/index'
localStorage.setItem(LOCK_PATH_KEY, '/index')
}
}
})
export default useLockStore

View File

@ -85,28 +85,13 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
function filterChildren(childrenMap, lastRouter = false) { function filterChildren(childrenMap, lastRouter = false) {
var children = [] var children = []
childrenMap.forEach((el, index) => { childrenMap.forEach(el => {
if (el.children && el.children.length) { el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path
if (el.component === 'ParentView' && !lastRouter) { if (el.children && el.children.length && el.component === 'ParentView') {
el.children.forEach(c => { children = children.concat(filterChildren(el.children, el))
c.path = el.path + '/' + c.path } else {
if (c.children && c.children.length) { children.push(el)
children = children.concat(filterChildren(c.children, c))
return
}
children.push(c)
})
return
}
} }
if (lastRouter) {
el.path = lastRouter.path + '/' + el.path
if (el.children && el.children.length) {
children = children.concat(filterChildren(el.children, el))
return
}
}
children = children.concat(el)
}) })
return children return children
} }
@ -129,14 +114,14 @@ export function filterDynamicRoutes(routes) {
} }
export const loadView = (view) => { export const loadView = (view) => {
let res; let res
for (const path in modules) { for (const path in modules) {
const dir = path.split('views/')[1].split('.vue')[0]; const dir = path.split('views/')[1].split('.vue')[0]
if (dir === view) { if (dir === view) {
res = () => modules[path](); res = () => modules[path]()
} }
} }
return res; return res
} }
export default usePermissionStore export default usePermissionStore

View File

@ -1,11 +1,12 @@
import defaultSettings from '@/settings' import defaultSettings from '@/settings'
import { useDark, useToggle } from '@vueuse/core' import { useDark, useToggle } from '@vueuse/core'
import { useDynamicTitle } from '@/utils/dynamicTitle' import { useDynamicTitle } from '@/utils/dynamicTitle'
import { handleThemeStyle } from '@/utils/theme'
const isDark = useDark() const isDark = useDark()
const toggleDark = useToggle(isDark) const toggleDark = useToggle(isDark)
const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle } = defaultSettings const { sideTheme, showSettings, navType, tagsView, tagsViewPersist, tagsIcon, tagsViewStyle, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = defaultSettings
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || '' const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
@ -17,11 +18,16 @@ const useSettingsStore = defineStore(
theme: storageSetting.theme || '#409EFF', theme: storageSetting.theme || '#409EFF',
sideTheme: storageSetting.sideTheme || sideTheme, sideTheme: storageSetting.sideTheme || sideTheme,
showSettings: showSettings, showSettings: showSettings,
topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav, navType: storageSetting.navType === undefined ? navType : storageSetting.navType,
tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView, tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
tagsViewPersist: storageSetting.tagsViewPersist === undefined ? tagsViewPersist : storageSetting.tagsViewPersist,
tagsIcon: storageSetting.tagsIcon === undefined ? tagsIcon : storageSetting.tagsIcon,
tagsViewStyle: storageSetting.tagsViewStyle === undefined ? tagsViewStyle : storageSetting.tagsViewStyle,
fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader, fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,
sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo, sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo,
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle, dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle,
footerVisible: storageSetting.footerVisible === undefined ? footerVisible : storageSetting.footerVisible,
footerContent: footerContent,
isDark: isDark.value isDark: isDark.value
}), }),
actions: { actions: {
@ -41,6 +47,9 @@ const useSettingsStore = defineStore(
toggleTheme() { toggleTheme() {
this.isDark = !this.isDark this.isDark = !this.isDark
toggleDark() toggleDark()
nextTick(() => {
handleThemeStyle(this.theme)
})
} }
} }
}) })

View File

@ -1,3 +1,26 @@
import cache from '@/plugins/cache'
import useSettingsStore from '@/store/modules/settings'
const PERSIST_KEY = 'tags-view-visited'
function isPersistEnabled() {
return useSettingsStore().tagsViewPersist
}
function saveVisitedViews(views) {
if (!isPersistEnabled()) return
const toSave = views.filter(v => !(v.meta && v.meta.affix)).map(v => ({ path: v.path, fullPath: v.fullPath, name: v.name, title: v.title, query: v.query, meta: v.meta }))
cache.local.setJSON(PERSIST_KEY, toSave)
}
function loadVisitedViews() {
return cache.local.getJSON(PERSIST_KEY) || []
}
function clearVisitedViews() {
cache.local.remove(PERSIST_KEY)
}
const useTagsViewStore = defineStore( const useTagsViewStore = defineStore(
'tags-view', 'tags-view',
{ {
@ -26,6 +49,15 @@ const useTagsViewStore = defineStore(
title: view.meta.title || 'no-name' title: view.meta.title || 'no-name'
}) })
) )
saveVisitedViews(this.visitedViews)
},
addAffixView(view) {
if (this.visitedViews.some(v => v.path === view.path)) return
this.visitedViews.unshift(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
)
}, },
addCachedView(view) { addCachedView(view) {
if (this.cachedViews.includes(view.name)) return if (this.cachedViews.includes(view.name)) return
@ -52,6 +84,7 @@ const useTagsViewStore = defineStore(
} }
} }
this.iframeViews = this.iframeViews.filter(item => item.path !== view.path) this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
saveVisitedViews(this.visitedViews)
resolve([...this.visitedViews]) resolve([...this.visitedViews])
}) })
}, },
@ -84,6 +117,7 @@ const useTagsViewStore = defineStore(
return v.meta.affix || v.path === view.path return v.meta.affix || v.path === view.path
}) })
this.iframeViews = this.iframeViews.filter(item => item.path === view.path) this.iframeViews = this.iframeViews.filter(item => item.path === view.path)
saveVisitedViews(this.visitedViews)
resolve([...this.visitedViews]) resolve([...this.visitedViews])
}) })
}, },
@ -113,6 +147,7 @@ const useTagsViewStore = defineStore(
const affixTags = this.visitedViews.filter(tag => tag.meta.affix) const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
this.visitedViews = affixTags this.visitedViews = affixTags
this.iframeViews = [] this.iframeViews = []
clearVisitedViews()
resolve([...this.visitedViews]) resolve([...this.visitedViews])
}) })
}, },
@ -150,6 +185,7 @@ const useTagsViewStore = defineStore(
} }
return false return false
}) })
saveVisitedViews(this.visitedViews)
resolve([...this.visitedViews]) resolve([...this.visitedViews])
}) })
}, },
@ -173,8 +209,16 @@ const useTagsViewStore = defineStore(
} }
return false return false
}) })
saveVisitedViews(this.visitedViews)
resolve([...this.visitedViews]) resolve([...this.visitedViews])
}) })
},
// 恢复持久化的 tags
loadPersistedViews() {
const views = loadVisitedViews()
views.forEach(view => {
this.addVisitedView(view)
})
} }
} }
}) })

View File

@ -1,6 +1,10 @@
import router from '@/router'
import cache from '@/plugins/cache'
import { ElMessageBox, } from 'element-plus'
import { login, logout, getInfo } from '@/api/login' import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth' import { getToken, setToken, removeToken } from '@/utils/auth'
import { isHttp, isEmpty } from "@/utils/validate" import { isHttp, isEmpty } from "@/utils/validate"
import useLockStore from '@/store/modules/lock'
import defAva from '@/assets/images/profile.jpg' import defAva from '@/assets/images/profile.jpg'
const useUserStore = defineStore( const useUserStore = defineStore(
@ -10,6 +14,7 @@ const useUserStore = defineStore(
token: getToken(), token: getToken(),
id: '', id: '',
name: '', name: '',
nickName: '',
avatar: '', avatar: '',
roles: [], roles: [],
permissions: [] permissions: []
@ -25,6 +30,7 @@ const useUserStore = defineStore(
login(username, password, code, uuid).then(res => { login(username, password, code, uuid).then(res => {
setToken(res.token) setToken(res.token)
this.token = res.token this.token = res.token
useLockStore().unlockScreen()
resolve() resolve()
}).catch(error => { }).catch(error => {
reject(error) reject(error)
@ -48,7 +54,21 @@ const useUserStore = defineStore(
} }
this.id = user.userId this.id = user.userId
this.name = user.userName this.name = user.userName
this.nickName = user.nickName
this.avatar = avatar this.avatar = avatar
cache.session.set('pwrChrtype', res.pwdChrtype)
/* 初始密码提示 */
if(res.isDefaultModifyPwd) {
ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {})
}
/* 过期密码提示 */
if(!res.isDefaultModifyPwd && res.isPasswordExpired) {
ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {})
}
resolve(res) resolve(res)
}).catch(error => { }).catch(error => {
reject(error) reject(error)

View File

@ -1,105 +0,0 @@
import { getFlowById } from '@/api/device/register'
const filterNodeRedData = async (id) => {
let nodeRedData = []
const data = await getFlowById(id)
if (data.nodes) {
nodeRedData = data.nodes
} else {
console.error('请求失败')
}
return nodeRedData
}
const mapNodeType = (nodeRedType) => {
const typeMap = {
'group': 'customGroup', // Node-RED的inject节点转为圆形
// 'inject': 'circle', // Node-RED的inject节点转为圆形
// 'debug': 'diamond', // debug节点转为菱形
// 'function': 'rect' // 函数节点转为矩形
};
return typeMap[nodeRedType] || 'CustomNode'; // 默认矩形
}
export const convertNodeRedToLogicFlow = async (id) => {
const nodeRedData = await filterNodeRedData(id)
// 节点数据
const nodes = nodeRedData.map((node) => {
return {
id: node.id, // 或生成新ID`node-${index}`
type: mapNodeType(node.type), // 映射节点类型
x: node.x * 2,
y: node.y * 2,
text: node.name || node.type, // 使用节点名称或类型作为文本
children: node.type === 'group' ? node.nodes : null,
properties: {
...node,
state: 'unexecuted',
}
};
});
const edges = []
// 连线数据
nodeRedData.forEach((item) => {
if (item.wires && item.wires.length > 0) {
item.wires.forEach((w, index) => {
w.forEach((id) => {
const obj = {
id: `edge-${item.id}-${id}`, // 或使用唯一标识生成方式
type: 'bezier',
sourceNodeId: item.id,
targetNodeId: id,
sourceAnchorId: item.id + '_1',
targetAnchorId: id + '_3'
}
edges.push(obj)
})
})
}
})
return { nodes, edges }
}
export const recursiveFilter = (data, id, result = [], type = 'customGroup') => {
data.forEach((item) => {
if (item.type === type && item.properties.nodes && item.properties.nodes.includes(id)) {
result.push(item)
recursiveFilter(data, item.id, result)
}
});
return result
}
export const changeOtherNodeState = (nodes, lf) => {
const filterArr = ['inject', 'cmvrLoop', 'startCamera', 'getImage', 'stopCamera', 'startVideo', 'stopVideo', 'end']
const loopNodes = nodes.filter(item => {
return !filterArr.includes(item.properties.type)
})
loopNodes.forEach(l => {
nodes.forEach(n => {
if (l.properties.wires[0].includes(n.id)) {
const { state } = lf.getProperties(n.id)
if (state !== 'unexecuted') {
lf.setProperties(l.id, {
state: 'success'
})
}
}
if (l.properties?.wires[1]?.includes(n.id)) {
const { state } = lf.getProperties(n.id)
if (state !== 'failed') {
lf.setProperties(l.id, {
state: 'running'
})
} else {
lf.setProperties(l.id, {
state: 'failed'
})
}
}
})
})
}

View File

@ -0,0 +1,76 @@
import { getFlowById } from "@/api/device/register";
const loadNodeRedData = async (id) => {
const data = await getFlowById(id);
return Array.isArray(data?.nodes) ? data.nodes : [];
};
export const convertNodeRedToVueFlow = async (id) => {
const source = await loadNodeRedData(id);
const nodes = source.map((node) => {
const isGroup = node.type === "group";
const width = isGroup ? Number(node.w || 300) + 80 : 200;
const height = isGroup ? Number(node.h || 180) + 80 : 100;
const center = { x: Number(node.x || 0) * 2, y: Number(node.y || 0) * 2 };
return {
id: node.id,
type: isGroup ? "runtime-group" : "runtime-node",
position: { x: center.x - width / 2, y: center.y - height / 2 },
width,
height,
draggable: false,
selectable: true,
connectable: false,
deletable: false,
zIndex: isGroup ? -1 : 1,
data: {
text: node.name || node.type,
properties: { ...node, state: "unexecuted" },
children: isGroup ? node.nodes || [] : [],
},
};
});
const edges = [];
source.forEach((node) => {
(node.wires || []).forEach((targets) => {
(targets || []).forEach((targetId) => {
edges.push({
id: `edge-${node.id}-${targetId}`,
source: node.id,
target: targetId,
sourceHandle: `${node.id}_1`,
targetHandle: `${targetId}_3`,
type: "bezier",
animated: true,
selectable: true,
focusable: true,
interactionWidth: 32,
markerEnd: {
type: "arrowclosed",
color: "#2f80ed",
width: 20,
height: 20,
},
style: { stroke: "#2f80ed", strokeWidth: 2 },
});
});
});
});
return { nodes, edges };
};
export const updateDerivedNodeStates = (nodes, getProperties, setProperties) => {
const ignored = ["inject", "cmvrLoop", "startCamera", "getImage", "stopCamera", "startVideo", "stopVideo", "end"];
nodes.filter((node) => !ignored.includes(node.data.properties.type)).forEach((current) => {
nodes.forEach((target) => {
if (current.data.properties.wires?.[0]?.includes(target.id) && getProperties(target.id).state !== "unexecuted") {
setProperties(current.id, { state: "success" });
}
if (current.data.properties.wires?.[1]?.includes(target.id)) {
setProperties(current.id, { state: getProperties(target.id).state === "failed" ? "failed" : "running" });
}
});
});
};

View File

@ -5,20 +5,20 @@ import { getDicts } from '@/api/system/dict/data'
* 获取字典数据 * 获取字典数据
*/ */
export function useDict(...args) { export function useDict(...args) {
const res = ref({}); const res = ref({})
return (() => { return (() => {
args.forEach((dictType, index) => { args.forEach((dictType, index) => {
res.value[dictType] = []; res.value[dictType] = []
const dicts = useDictStore().getDict(dictType); const dicts = useDictStore().getDict(dictType)
if (dicts) { if (dicts) {
res.value[dictType] = dicts; res.value[dictType] = dicts
} else { } else {
getDicts(dictType).then(resp => { getDicts(dictType).then(resp => {
res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass })) res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
useDictStore().setDict(dictType, res.value[dictType]); useDictStore().setDict(dictType, res.value[dictType])
}) })
} }
}) })
return toRefs(res.value); return toRefs(res.value)
})() })()
} }

View File

@ -1,4 +1,3 @@
import store from '@/store'
import defaultSettings from '@/settings' import defaultSettings from '@/settings'
import useSettingsStore from '@/store/modules/settings' import useSettingsStore from '@/store/modules/settings'
@ -6,10 +5,10 @@ import useSettingsStore from '@/store/modules/settings'
* 动态修改标题 * 动态修改标题
*/ */
export function useDynamicTitle() { export function useDynamicTitle() {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore()
if (settingsStore.dynamicTitle) { if (settingsStore.dynamicTitle) {
document.title = settingsStore.title + ' - ' + defaultSettings.title; document.title = settingsStore.title + ' - ' + defaultSettings.title
} else { } else {
document.title = defaultSettings.title; document.title = defaultSettings.title
} }
} }

View File

@ -214,13 +214,11 @@ export const getInputNumber = (nodeId) => {
export const removeSwitchEdge = (nodeId, sourceAnchorId) => { export const removeSwitchEdge = (nodeId, sourceAnchorId) => {
const { edges } = lf.getGraphData(); const { edges } = lf.getGraphData();
const edge = edges.find(item => { const matchedEdges = edges.filter(item => {
return item.sourceNodeId === nodeId && item.sourceAnchorId === sourceAnchorId return item.sourceNodeId === nodeId && item.sourceAnchorId === sourceAnchorId
}) })
if (edge) { matchedEdges.forEach(edge => lf.deleteEdge(edge.id))
lf.deleteEdge(edge.id);
}
} }
export const convertToTree = (data) => { export const convertToTree = (data) => {

View File

@ -0,0 +1,37 @@
export const drawingDefaultValue = []
export function initDrawingDefaultValue() {
if (drawingDefaultValue.length === 0) {
drawingDefaultValue.push({
layout: 'colFormItem',
tagIcon: 'input',
label: '手机号',
vModel: 'mobile',
formId: 6,
tag: 'el-input',
placeholder: '请输入手机号',
defaultValue: '',
span: 24,
style: {width: '100%'},
clearable: true,
prepend: '',
append: '',
'prefix-icon': 'Cellphone',
'suffix-icon': '',
maxlength: 11,
'show-word-limit': true,
readonly: false,
disabled: false,
required: true,
changeTag: true,
regList: [{
pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
message: '手机号格式错误'
}]
})
}
}
export function cleanDrawingDefaultValue() {
drawingDefaultValue.splice(0, drawingDefaultValue.length)
}

View File

@ -318,7 +318,7 @@ function buildElRadioGroupChild(conf) {
if (conf.options && conf.options.length) { if (conf.options && conf.options.length) {
const tag = conf.optionType === 'button' ? 'el-radio-button' : 'el-radio' const tag = conf.optionType === 'button' ? 'el-radio-button' : 'el-radio'
const border = conf.border ? 'border' : '' const border = conf.border ? 'border' : ''
children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`) children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :value="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
} }
return children.join('\n') return children.join('\n')
} }
@ -328,7 +328,7 @@ function buildElCheckboxGroupChild(conf) {
if (conf.options && conf.options.length) { if (conf.options && conf.options.length) {
const tag = conf.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox' const tag = conf.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox'
const border = conf.border ? 'border' : '' const border = conf.border ? 'border' : ''
children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`) children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :value="item.label" :disabled="item.disabled" ${border} />`)
} }
return children.join('\n') return children.join('\n')
} }

View File

@ -4,14 +4,14 @@ import { parseTime } from './ruoyi'
* 表格时间格式化 * 表格时间格式化
*/ */
export function formatDate(cellValue) { export function formatDate(cellValue) {
if (cellValue == null || cellValue == "") return ""; if (cellValue == null || cellValue == "") return ""
var date = new Date(cellValue) const date = new Date(cellValue)
var year = date.getFullYear() const year = date.getFullYear()
var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1 const month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() const day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() const hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() const minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() const seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
} }
@ -84,7 +84,7 @@ export function getQueryObject(url) {
export function byteLength(str) { export function byteLength(str) {
// returns the byte length of an utf8 string // returns the byte length of an utf8 string
let s = str.length let s = str.length
for (var i = str.length - 1; i >= 0; i--) { for (let i = str.length - 1; i >= 0; i--) {
const code = str.charCodeAt(i) const code = str.charCodeAt(i)
if (code > 0x7f && code <= 0x7ff) s++ if (code > 0x7f && code <= 0x7ff) s++
else if (code > 0x7ff && code <= 0xffff) s += 2 else if (code > 0x7ff && code <= 0xffff) s += 2
@ -390,18 +390,18 @@ export function isNumberStr(str) {
export function isAudioByMimeType(mimeType) { export function isAudioByMimeType(mimeType) {
const audioMimeTypes = [ const audioMimeTypes = [
'audio/mpeg', 'audio/mpeg',
'audio/wav', 'audio/wav',
'audio/x-wav', 'audio/x-wav',
'audio/flac', 'audio/flac',
'audio/aac', 'audio/aac',
'audio/ogg', 'audio/ogg',
'audio/mp4', 'audio/mp4',
'audio/webm', 'audio/webm',
'audio/x-m4a', 'audio/x-m4a',
'audio/amr', 'audio/amr',
'audio/midi' 'audio/midi'
]; ]
return typeof mimeType === 'string' && audioMimeTypes.includes(mimeType.toLowerCase())
return audioMimeTypes.includes(mimeType.toLowerCase());
} }

73
src/utils/passwordRule.js Normal file
View File

@ -0,0 +1,73 @@
/**
* 密码强度规则
* 根据参数 chrtype 动态生成校验规则
*
* chrtype 说明
* 0 - 任意字符默认
* 1 - 纯数字0-9
* 2 - 纯字母a-z / A-Z
* 3 - 字母 + 数字必须同时包含
* 4 - 字母 + 数字 + 特殊字符必须同时包含特殊字符~!@#$%^&*()-=_+
*/
import cache from '@/plugins/cache'
// 密码限制类型
const pwdChrType = ref(cache.session.get('pwrChrtype') || '0')
// 各类型对应的正则、错误提示
const PWD_RULES = {
'0': { pattern: /^[^<>"'|\\]+$/, message: '密码不能包含非法字符:< > " \' \\ |' },
'1': { pattern: /^[0-9]+$/, message: '密码只能为数字0-9' },
'2': { pattern: /^[a-zA-Z]+$/, message: '密码只能为英文字母a-z、A-Z' },
'3': { pattern: /^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]+$/, message: '密码必须同时包含字母和数字' },
'4': { pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[~!@#$%^&*()\-=_+])[A-Za-z\d~!@#$%^&*()\-=_+]+$/, message: '密码必须同时包含字母、数字和特殊字符(~!@#$%^&*()-=_+' }
}
export function usePasswordRule() {
// 默认密码校验
const pwdValidator = computed(() => {
const rule = PWD_RULES[pwdChrType.value] || PWD_RULES['0']
return [
{ required: true, message: '密码不能为空', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度必须介于 6 和 20 之间', trigger: 'blur' },
{ pattern: rule.pattern, message: rule.message, trigger: 'blur' }
]
})
// 校验prompt的inputValidator函数
const pwdPromptValidator = (value) => {
const rule = PWD_RULES['0']
if (!value || value.length < 6 || value.length > 20) {
return '密码长度必须介于 6 和 20 之间'
}
if (!rule.pattern.test(value)) {
return rule.message
}
}
// 个人中心密码校验
const infoPwdValidator = computed(() => {
const rule = PWD_RULES[pwdChrType.value] || PWD_RULES['0']
return [
{ required: true, message: '新密码不能为空', trigger: 'blur' },
{ min: 6, max: 20, message: '新密码长度必须介于 6 和 20 之间', trigger: 'blur' },
{ pattern: rule.pattern, message: rule.message, trigger: 'blur' }
]
})
// 注册页面密码校验
const registerPwdValidator = computed(() => {
const rule = PWD_RULES['0']
return [
{ required: true, message: '请输入您的密码', trigger: 'blur' },
{ min: 6, max: 20, message: '用户密码长度必须介于 6 和 20 之间', trigger: 'blur' },
{ pattern: rule.pattern, message: rule.message, trigger: 'blur' }
]
})
return {
pwdChrType,
pwdValidator,
infoPwdValidator,
pwdPromptValidator,
registerPwdValidator
}
}

View File

@ -9,7 +9,7 @@ export function checkPermi(value) {
if (value && value instanceof Array && value.length > 0) { if (value && value instanceof Array && value.length > 0) {
const permissions = useUserStore().permissions const permissions = useUserStore().permissions
const permissionDatas = value const permissionDatas = value
const all_permission = "*:*:*"; const all_permission = "*:*:*"
const hasPermission = permissions.some(permission => { const hasPermission = permissions.some(permission => {
return all_permission === permission || permissionDatas.includes(permission) return all_permission === permission || permissionDatas.includes(permission)
@ -34,7 +34,7 @@ export function checkRole(value) {
if (value && value instanceof Array && value.length > 0) { if (value && value instanceof Array && value.length > 0) {
const roles = useUserStore().roles const roles = useUserStore().roles
const permissionRoles = value const permissionRoles = value
const super_admin = "admin"; const super_admin = "admin"
const hasRole = roles.some(role => { const hasRole = roles.some(role => {
return super_admin === role || permissionRoles.includes(role) return super_admin === role || permissionRoles.includes(role)

View File

@ -7,9 +7,9 @@ import cache from '@/plugins/cache'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
let downloadLoadingInstance; let downloadLoadingInstance
// 是否显示重新登录 // 是否显示重新登录
export let isRelogin = { show: false }; export let isRelogin = { show: false }
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例 // 创建axios实例
@ -26,15 +26,17 @@ service.interceptors.request.use(config => {
const isToken = (config.headers || {}).isToken === false const isToken = (config.headers || {}).isToken === false
// 是否需要防止数据重复提交 // 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
// 间隔时间(ms),小于此时间视为重复提交
const interval = (config.headers || {}).interval || 1000
if (getToken() && !isToken) { if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
} }
// get请求映射params参数 // get请求映射params参数
if (config.method === 'get' && config.params) { if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params); let url = config.url + '?' + tansParams(config.params)
url = url.slice(0, -1); url = url.slice(0, -1)
config.params = {}; config.params = {}
config.url = url; config.url = url
} }
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) { if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
const requestObj = { const requestObj = {
@ -42,22 +44,21 @@ service.interceptors.request.use(config => {
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime() time: new Date().getTime()
} }
const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小 const requestSize = Object.keys(JSON.stringify(requestObj)).length // 请求数据大小
const limitSize = 5 * 1024 * 1024; // 限制存放数据5M const limitSize = 5 * 1024 * 1024 // 限制存放数据5M
if (requestSize >= limitSize) { if (requestSize >= limitSize) {
console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制无法进行防重复提交验证。') console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制无法进行防重复提交验证。')
return config; return config
} }
const sessionObj = cache.session.getJSON('sessionObj') const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') { if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj) cache.session.setJSON('sessionObj', requestObj)
} else { } else {
const s_url = sessionObj.url; // 请求地址 const s_url = sessionObj.url // 请求地址
const s_data = sessionObj.data; // 请求数据 const s_data = sessionObj.data // 请求数据
const s_time = sessionObj.time; // 请求时间 const s_time = sessionObj.time // 请求时间
const interval = 500; // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交'; const message = '数据正在处理,请勿重复提交'
console.warn(`[${s_url}]: ` + message) console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message)) return Promise.reject(new Error(message))
} else { } else {
@ -74,7 +75,7 @@ service.interceptors.request.use(config => {
// 响应拦截器 // 响应拦截器
service.interceptors.response.use(res => { service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态 // 未设置状态码则默认成功状态
const code = res.data.code || 200; const code = res.data.code || 200
// 获取错误信息 // 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default'] const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回 // 二进制数据则直接返回
@ -83,19 +84,19 @@ service.interceptors.response.use(res => {
} }
if (code === 401) { if (code === 401) {
if (!isRelogin.show) { if (!isRelogin.show) {
isRelogin.show = true; isRelogin.show = true
ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false; isRelogin.show = false
useUserStore().logOut().then(() => { useUserStore().logOut().then(() => {
location.href = '/index'; location.href = '/index'
}) })
}).catch(() => { }).catch(() => {
isRelogin.show = false; isRelogin.show = false
}); })
} }
return Promise.reject('无效的会话,或者会话已过期,请重新登录。') return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) { } else if (code === 500) {
ElMessage({ message: msg, grouping: true, type: 'error' }) ElMessage({ message: msg, type: 'error' })
return Promise.reject(new Error(msg)) return Promise.reject(new Error(msg))
} else if (code === 601) { } else if (code === 601) {
ElMessage({ message: msg, type: 'warning' }) ElMessage({ message: msg, type: 'warning' })
@ -109,15 +110,15 @@ service.interceptors.response.use(res => {
}, },
error => { error => {
console.log('err' + error) console.log('err' + error)
let { message } = error; let { message } = error
if (message == "Network Error") { if (message == "Network Error") {
message = "后端接口连接异常"; message = "后端接口连接异常"
} else if (message.includes("timeout")) { } else if (message.includes("timeout")) {
message = "系统接口请求超时"; message = "系统接口请求超时"
} else if (message.includes("Request failed with status code")) { } else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常"; message = "系统接口" + message.slice(-3) + "异常"
} }
ElMessage({ message: message, type: 'error', grouping: true, duration: 5 * 1000 }) ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error) return Promise.reject(error)
} }
) )
@ -131,21 +132,21 @@ export function download(url, params, filename, config) {
responseType: 'blob', responseType: 'blob',
...config ...config
}).then(async (data) => { }).then(async (data) => {
const isBlob = blobValidate(data); const isBlob = blobValidate(data)
if (isBlob) { if (isBlob) {
const blob = new Blob([data]) const blob = new Blob([data])
saveAs(blob, filename) saveAs(blob, filename)
} else { } else {
const resText = await data.text(); const resText = await data.text()
const rspObj = JSON.parse(resText); const rspObj = JSON.parse(resText)
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
ElMessage.error(errMsg); ElMessage.error(errMsg)
} }
downloadLoadingInstance.close(); downloadLoadingInstance.close()
}).catch((r) => { }).catch((r) => {
console.error(r) console.error(r)
ElMessage.error('下载文件出现错误,请联系管理员!') ElMessage.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close(); downloadLoadingInstance.close()
}) })
} }

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