Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d82598d3b | |||
| 1a26063ce2 | |||
| e501d80953 | |||
| 758d5f55e6 | |||
| 31d6458fd9 | |||
| c9a6256465 | |||
| 31027439a4 | |||
| a30903dd70 | |||
| 320d1a9d92 | |||
| 1b0f414ba1 | |||
| 6cefb1a636 | |||
| 998d7d3826 | |||
| f5711a1212 | |||
| c7ad9e3e11 | |||
| 5d8974672e | |||
| 9ff9194e41 | |||
| 587ba3482e | |||
| 9d37c56b8d | |||
| a24733f6ea | |||
| af2f9398b7 | |||
| 36fad9c1ca | |||
| ecd13e2494 | |||
| eebd0dc6b7 | |||
| 6af1118a3c | |||
| 780665fa9a | |||
| 64d6cad887 | |||
| 2d2854814b | |||
| 7590d5a986 | |||
| 8e53faad52 | |||
| e9717c85e2 | |||
| bd2014edf0 | |||
| a0b397fc40 | |||
| d198189d2c | |||
| 4e8998216f | |||
| dcae537302 | |||
| 7c449c52d9 | |||
| 4de5e21929 | |||
| 5b4cdb7140 | |||
| 77223344b5 | |||
| d2299381ca | |||
| be4eef6fd1 | |||
| ec6e238356 |
@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 招商车研AI测评机器人模拟测试平台
|
||||
VITE_APP_TITLE = 招商车研物联网平台管理系统
|
||||
|
||||
# 开发环境配置
|
||||
VITE_APP_ENV = 'development'
|
||||
@ -7,8 +7,8 @@ VITE_APP_ENV = 'development'
|
||||
# 招商车研物联网平台/开发环境
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
|
||||
VITE_API_URL = http://192.168.0.10:13080
|
||||
VITE_WS_URL = ws://192.168.0.10:13080/ws
|
||||
# VITE_API_URL = http://10.148.108.95:13080 //杨溪IP
|
||||
# VITE_API_URL = http://10.148.108.58:13080 //赵培利IP
|
||||
|
||||
VITE_API_URL = http://192.168.0.222:13080
|
||||
VITE_WS_URL = ws://192.168.0.222:13080/ws
|
||||
VITE_PORT = 80
|
||||
VITE_INSPECTION_TYPE = "inspection"
|
||||
VITE_MINIO_URL = "http://192.168.28.10:9000/cmvr-iot/"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 智能座舱测评AI机器人
|
||||
VITE_APP_TITLE = 招商车研物联网平台管理系统
|
||||
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'production'
|
||||
@ -10,8 +10,8 @@ VITE_APP_BASE_API = '/prod-api'
|
||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||
VITE_BUILD_COMPRESS = gzip
|
||||
|
||||
#node-red 服务地址
|
||||
VITE_NODE_RED_URL = 'http://10.148.108.59:13080'
|
||||
|
||||
VITE_API_URL = http://192.168.0.100:13080
|
||||
VITE_WS_URL = ws://192.168.0.100:13080/ws
|
||||
VITE_API_URL = http://192.168.28.10:13080
|
||||
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/"
|
||||
|
||||
@ -4,8 +4,13 @@ VITE_APP_TITLE = 招商车研物联网平台管理系统
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'staging'
|
||||
|
||||
# 招商车研物联网平台管理系统/生产环境
|
||||
# 招商车研物联网平台/测试环境
|
||||
VITE_APP_BASE_API = '/stage-api'
|
||||
|
||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||
VITE_BUILD_COMPRESS = gzip
|
||||
|
||||
VITE_API_URL = http://10.148.121.100:13080
|
||||
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
@ -0,0 +1 @@
|
||||
custom: http://doc.ruoyi.vip/ruoyi-vue/other/donate.html
|
||||
5
.gitignore
vendored
@ -18,9 +18,8 @@ selenium-debug.log
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.local
|
||||
.history
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
bin/
|
||||
pnpm-lock.yaml
|
||||
|
||||
.history
|
||||
|
||||
10
Dockerfile
Normal file
@ -0,0 +1,10 @@
|
||||
FROM node:18
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
|
||||
COPY . .
|
||||
17
Dockerfile.frontend
Normal file
@ -0,0 +1,17 @@
|
||||
# 使用官方的 Node.js 镜像作为基础镜像
|
||||
FROM node:18
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制 package.json 和 package-lock.json(如果存在)到工作目录
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装项目依赖
|
||||
RUN npm install
|
||||
|
||||
# 复制项目文件到工作目录
|
||||
COPY . .
|
||||
|
||||
# 构建项目
|
||||
RUN npm run build
|
||||
125
README.md
@ -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
|
||||
# 安装依赖
|
||||
yarn --registry=https://registry.npmmirror.com
|
||||
|
||||
# 启动服务
|
||||
yarn dev
|
||||
|
||||
# 构建测试环境 yarn build:stage
|
||||
# 构建生产环境 yarn build:prod
|
||||
# 前端访问地址 http://localhost:80
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 内置功能
|
||||
默认地址:
|
||||
|
||||
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
|
||||
3. 岗位管理:配置系统用户所属担任职务。
|
||||
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
|
||||
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
|
||||
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
7. 参数管理:对系统动态配置常用参数。
|
||||
8. 通知公告:系统通知公告信息发布维护。
|
||||
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
10. 登录日志:系统登录日志记录查询包含登录异常。
|
||||
11. 在线用户:当前系统中活跃用户状态监控。
|
||||
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
|
||||
13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
|
||||
14. 系统接口:根据业务代码自动生成相关的api接口文档。
|
||||
15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
|
||||
16. 缓存监控:对系统的缓存信息查询,命令统计等。
|
||||
17. 在线构建器:拖动表单元素生成相应的HTML代码。
|
||||
18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。
|
||||
- 前端:`http://localhost:80`
|
||||
- 后端:`http://localhost:13080`
|
||||
- Knife4j:`http://localhost:13080/doc.html`
|
||||
|
||||
开发配置位于 `.env.development`:
|
||||
|
||||
```dotenv
|
||||
VITE_APP_TITLE=招商车研物联网平台管理系统
|
||||
VITE_APP_BASE_API=/dev-api
|
||||
VITE_API_URL=http://localhost:13080
|
||||
VITE_WS_URL=ws://localhost:13080/ws
|
||||
VITE_PORT=80
|
||||
```
|
||||
|
||||
## 构建
|
||||
|
||||
```bash
|
||||
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
@ -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
@ -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
@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
echo.
|
||||
echo [信息] 使用 Vite 命令运行 Web 工程。
|
||||
echo.
|
||||
|
||||
%~d0
|
||||
cd %~dp0
|
||||
|
||||
cd ..
|
||||
yarn dev
|
||||
|
||||
pause
|
||||
@ -7,7 +7,7 @@
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<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]-->
|
||||
<style>
|
||||
html,
|
||||
|
||||
58
package.json
@ -1,32 +1,35 @@
|
||||
{
|
||||
"name": "CMVR-IOT-UI",
|
||||
"version": "3.8.8",
|
||||
"description": "CMVR-IOT平台管理系统前端UI框架",
|
||||
"name": "cmvr-iot-ui",
|
||||
"version": "3.9.2",
|
||||
"description": "招商车研物联网平台管理系统前端",
|
||||
"author": "CMVR",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:dev": "vite build --mode development",
|
||||
"build:prod": "vite build --mode production",
|
||||
"build:test": "vite build --mode test",
|
||||
"build:prod": "vite build",
|
||||
"build:stage": "vite build --mode staging",
|
||||
"build:staging": "vite build --mode staging",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "2.3.1",
|
||||
"@logicflow/core": "^2.0.13",
|
||||
"@logicflow/extension": "^2.0.18",
|
||||
"@element-plus/icons-vue": "2.3.2",
|
||||
"@vue-flow/background": "^1.3.2",
|
||||
"@vue-flow/controls": "^1.1.3",
|
||||
"@vue-flow/core": "^1.48.2",
|
||||
"@vueup/vue-quill": "1.2.0",
|
||||
"@vueuse/core": "10.11.0",
|
||||
"@vueuse/core": "14.1.0",
|
||||
"ace-builds": "^1.43.0",
|
||||
"axios": "0.28.1",
|
||||
"autofit.js": "^3.2.8",
|
||||
"axios": "1.13.2",
|
||||
"clipboard": "2.0.11",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "5.5.1",
|
||||
"element-plus": "2.9.4",
|
||||
"echarts": "5.6.0",
|
||||
"element-plus": "2.13.1",
|
||||
"file-saver": "2.0.5",
|
||||
"fuse.js": "6.6.2",
|
||||
"js-beautify": "1.14.11",
|
||||
"fuse.js": "7.1.0",
|
||||
"js-beautify": "1.15.4",
|
||||
"js-cookie": "3.0.5",
|
||||
"jsencrypt": "3.3.2",
|
||||
"json-editor-vue": "^0.18.1",
|
||||
@ -35,31 +38,38 @@
|
||||
"monaco-editor": "^0.55.1",
|
||||
"nprogress": "0.2.0",
|
||||
"pako": "^2.1.0",
|
||||
"pinia": "2.1.7",
|
||||
"pnpm": "^10.23.0",
|
||||
"pinia": "3.0.4",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"sockjs-client": "^1.6.1",
|
||||
"sortablejs": "^1.15.6",
|
||||
"splitpanes": "3.1.5",
|
||||
"three": "^0.181.2",
|
||||
"urdf-loader": "^0.12.6",
|
||||
"uuid": "^11.1.0",
|
||||
"vue": "3.4.31",
|
||||
"vue": "3.5.26",
|
||||
"vue-cropper": "1.1.1",
|
||||
"vue-router": "4.4.0",
|
||||
"vue-router": "4.6.4",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
"vue3-json-viewer": "2.3.0",
|
||||
"vuedraggable": "4.1.0",
|
||||
"xacro-parser": "^0.3.11",
|
||||
"xgplayer": "^3.0.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "5.0.5",
|
||||
"file-loader": "^6.2.0",
|
||||
"sass": "1.77.5",
|
||||
"unplugin-auto-import": "0.17.6",
|
||||
"@vitejs/plugin-vue": "5.2.4",
|
||||
"sass-embedded": "1.97.2",
|
||||
"unplugin-auto-import": "0.18.6",
|
||||
"unplugin-vue-setup-extend-plus": "1.0.1",
|
||||
"vite": "5.3.2",
|
||||
"vite": "6.4.1",
|
||||
"vite-plugin-compression": "0.5.1",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svg-icons": "2.0.1"
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"quill": "2.0.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"quill": "2.0.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.24.0"
|
||||
}
|
||||
|
||||
5973
pnpm-lock.yaml
generated
Normal file
6
pnpm-workspace.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
allowBuilds:
|
||||
core-js: true
|
||||
es5-ext: true
|
||||
esbuild: true
|
||||
json-editor-vue: true
|
||||
vue-demi: true
|
||||
54
public/bot.svg
Normal file
@ -0,0 +1,54 @@
|
||||
<svg
|
||||
viewBox="0 0 22 22"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
>
|
||||
<rect width="22" height="22" x="0" y="0" />
|
||||
<path
|
||||
d="M11 7.33335L11 3.66669L7.33331 3.66669"
|
||||
fill-rule="nonzero"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<rect
|
||||
width="14.666667"
|
||||
height="11.000000"
|
||||
x="3.666687"
|
||||
y="7.333344"
|
||||
rx="1.833333"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<path
|
||||
d="M1.83331 12.8333L3.66665 12.8333"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<path
|
||||
d="M18.3333 12.8333L20.1666 12.8333"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<path
|
||||
d="M13.75 11.9167L13.75 13.75"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<path
|
||||
d="M8.25 11.9167L8.25 13.75"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
465
public/inspection/elfin10/elfin10.urdf.xacro
Normal file
@ -0,0 +1,465 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<robot name="elfin10" xmlns:xacro="http://wiki.ros.org/xacro">
|
||||
|
||||
<xacro:arg name="use_real_hardware" default="true" />
|
||||
<xacro:arg name="use_fake_hardware" default="false" />
|
||||
|
||||
<!-- Load TRANSMISSION file: -->
|
||||
<xacro:include filename="file://$(find elfin_description)/elfin10_transmission.xacro" />
|
||||
<!-- Load GAZEBO/ROS2_CONTROL file: -->
|
||||
<xacro:include filename="file://$(find elfin_description)/elfin10_gazebo.xacro" />
|
||||
|
||||
<xacro:property name="PI" value="3.1415926535897931"/>
|
||||
|
||||
<xacro:property name="density" value="3.09"/>
|
||||
<!-- <xacro:macro name="elfin10"> -->
|
||||
|
||||
<!-- TRANSMISSION: -->
|
||||
<xacro:elfin10_transmission />
|
||||
<!-- GAZEBO - ROS2_CONTROL: -->
|
||||
<xacro:elfin10_gazebo
|
||||
use_real_hardware="$(arg use_real_hardware)"
|
||||
use_fake_hardware="$(arg use_fake_hardware)"/>
|
||||
|
||||
<link name="world"/>
|
||||
|
||||
<joint name="world_base" type="fixed">
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 -1.5708" />
|
||||
<parent
|
||||
link="world" />
|
||||
<child
|
||||
link="elfin_base_link" />
|
||||
</joint>
|
||||
|
||||
<link name="elfin_base_link"/>
|
||||
|
||||
<joint name="elfin_base_joint" type="fixed">
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 ${PI/2}" />
|
||||
<parent
|
||||
link="elfin_base_link" />
|
||||
<child
|
||||
link="elfin_base" />
|
||||
</joint>
|
||||
|
||||
<link
|
||||
name="elfin_base">
|
||||
<inertial>
|
||||
<origin
|
||||
xyz="-0.00226511349377665 4.35718066723588E-06 0.0325128562537015"
|
||||
rpy="0 0 0" />
|
||||
<mass
|
||||
value="1.66727740473724" />
|
||||
<inertia
|
||||
ixx="0.004269288474386"
|
||||
ixy="6.02518130623876E-08"
|
||||
ixz="-6.92087963593712E-05"
|
||||
iyy="0.00385585336301628"
|
||||
iyz="-5.51423740998761E-08"
|
||||
izz="0.0068204473327682" />
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_base.STL" />
|
||||
</geometry>
|
||||
<material
|
||||
name="">
|
||||
<color
|
||||
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||
</material>
|
||||
</visual>
|
||||
<collision>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_base.STL" />
|
||||
</geometry>
|
||||
</collision>
|
||||
</link>
|
||||
<link
|
||||
name="elfin_link1">
|
||||
<inertial>
|
||||
<origin
|
||||
xyz="7.61162863471406E-07 -0.00300107309756385 -0.0432743915921636"
|
||||
rpy="0 0 0" />
|
||||
<mass
|
||||
value="6.3589066662676" />
|
||||
<inertia
|
||||
ixx="0.0391771076676677"
|
||||
ixy="5.46819690041203E-08"
|
||||
ixz="4.18972540516346E-07"
|
||||
iyy="0.0381555138034643"
|
||||
iyz="0.00661206821297882"
|
||||
izz="0.0211340101406971" />
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link1.STL" />
|
||||
</geometry>
|
||||
<material
|
||||
name="">
|
||||
<color
|
||||
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||
</material>
|
||||
</visual>
|
||||
<collision>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link1.STL" />
|
||||
</geometry>
|
||||
</collision>
|
||||
</link>
|
||||
<joint
|
||||
name="elfin_joint1"
|
||||
type="revolute">
|
||||
<origin
|
||||
xyz="0 0 0.26"
|
||||
rpy="0 0 0" />
|
||||
<parent
|
||||
link="elfin_base" />
|
||||
<child
|
||||
link="elfin_link1" />
|
||||
<axis
|
||||
xyz="0 0 1" />
|
||||
<limit
|
||||
lower="-3.14"
|
||||
upper="3.14"
|
||||
effort="420"
|
||||
velocity="1.57" />
|
||||
</joint>
|
||||
<link
|
||||
name="elfin_link2">
|
||||
<inertial>
|
||||
<origin
|
||||
xyz="-1.29217317440473E-07 0.204406209458582 0.145205671136993"
|
||||
rpy="0 0 0" />
|
||||
<mass
|
||||
value="5.51072381215317" />
|
||||
<inertia
|
||||
ixx="0.140886684844275"
|
||||
ixy="-1.98882818733046E-05"
|
||||
ixz="1.28793862225363E-06"
|
||||
iyy="0.00904523320332792"
|
||||
iyz="0.00556623571013066"
|
||||
izz="0.143882970137024" />
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link2.STL" />
|
||||
</geometry>
|
||||
<material
|
||||
name="">
|
||||
<color
|
||||
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||
</material>
|
||||
</visual>
|
||||
<collision>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link2.STL" />
|
||||
</geometry>
|
||||
</collision>
|
||||
</link>
|
||||
<joint
|
||||
name="elfin_joint2"
|
||||
type="revolute">
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="1.5708 0 0" />
|
||||
<parent
|
||||
link="elfin_link1" />
|
||||
<child
|
||||
link="elfin_link2" />
|
||||
<axis
|
||||
xyz="0 0 1" />
|
||||
<limit
|
||||
lower="-2.35"
|
||||
upper="2.35"
|
||||
effort="420"
|
||||
velocity="1.57" />
|
||||
</joint>
|
||||
<link
|
||||
name="elfin_link3">
|
||||
<inertial>
|
||||
<origin
|
||||
xyz="0.0357493010718184 -2.74303992683623E-06 -0.00441107040289549"
|
||||
rpy="0 0 0" />
|
||||
<mass
|
||||
value="3.25128088148268" />
|
||||
<inertia
|
||||
ixx="0.00712273850158261"
|
||||
ixy="4.14573354600121E-07"
|
||||
ixz="-0.00240614658407673"
|
||||
iyy="0.0131139013038093"
|
||||
iyz="-1.53858261863385E-07"
|
||||
izz="0.0123861174875263" />
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link3.STL" />
|
||||
</geometry>
|
||||
<material
|
||||
name="">
|
||||
<color
|
||||
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||
</material>
|
||||
</visual>
|
||||
<collision>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link3.STL" />
|
||||
</geometry>
|
||||
</collision>
|
||||
</link>
|
||||
<joint
|
||||
name="elfin_joint3"
|
||||
type="revolute">
|
||||
<origin
|
||||
xyz="0 0.48 0"
|
||||
rpy="-3.1416 0 1.5708" />
|
||||
<parent
|
||||
link="elfin_link2" />
|
||||
<child
|
||||
link="elfin_link3" />
|
||||
<axis
|
||||
xyz="0 0 1" />
|
||||
<limit
|
||||
lower="-2.61"
|
||||
upper="2.61"
|
||||
effort="200"
|
||||
velocity="1.57" />
|
||||
</joint>
|
||||
<link
|
||||
name="elfin_link4">
|
||||
<inertial>
|
||||
<origin
|
||||
xyz="3.8972274666868E-06 -0.065883038527668 0.300149807420176"
|
||||
rpy="0 0 0" />
|
||||
<mass
|
||||
value="2.45076028364132" />
|
||||
<inertia
|
||||
ixx="0.0366043963749035"
|
||||
ixy="2.14481800816891E-07"
|
||||
ixz="1.74370308297691E-06"
|
||||
iyy="0.0331005868546169"
|
||||
iyz="0.00876542795872678"
|
||||
izz="0.00689303506560623" />
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link4.STL" />
|
||||
</geometry>
|
||||
<material
|
||||
name="">
|
||||
<color
|
||||
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||
</material>
|
||||
</visual>
|
||||
<collision>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link4.STL" />
|
||||
</geometry>
|
||||
</collision>
|
||||
</link>
|
||||
<joint
|
||||
name="elfin_joint4"
|
||||
type="revolute">
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="1.5708 0 1.5708" />
|
||||
<parent
|
||||
link="elfin_link3" />
|
||||
<child
|
||||
link="elfin_link4" />
|
||||
<axis
|
||||
xyz="0 0 1" />
|
||||
<limit
|
||||
lower="-3.14"
|
||||
upper="3.14"
|
||||
effort="200"
|
||||
velocity="1.57" />
|
||||
</joint>
|
||||
<link
|
||||
name="elfin_link5">
|
||||
<inertial>
|
||||
<origin
|
||||
xyz="5.37084413168812E-06 -0.0262437975997385 -0.00353971828905618"
|
||||
rpy="0 0 0" />
|
||||
<mass
|
||||
value="1.39234539037872" />
|
||||
<inertia
|
||||
ixx="0.0033625821599978"
|
||||
ixy="2.21090591006111E-07"
|
||||
ixz="-5.5969383984273E-07"
|
||||
iyy="0.00182229285631246"
|
||||
iyz="0.000639319386824375"
|
||||
izz="0.00292731816323284" />
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link5.STL" />
|
||||
</geometry>
|
||||
<material
|
||||
name="">
|
||||
<color
|
||||
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||
</material>
|
||||
</visual>
|
||||
<collision>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link5.STL" />
|
||||
</geometry>
|
||||
</collision>
|
||||
</link>
|
||||
<joint
|
||||
name="elfin_joint5"
|
||||
type="revolute">
|
||||
<origin
|
||||
xyz="0 0 0.52"
|
||||
rpy="-1.5708 0 0" />
|
||||
<parent
|
||||
link="elfin_link4" />
|
||||
<child
|
||||
link="elfin_link5" />
|
||||
<axis
|
||||
xyz="0 0 1" />
|
||||
<limit
|
||||
lower="-2.56"
|
||||
upper="2.56"
|
||||
effort="69"
|
||||
velocity="1.57" />
|
||||
</joint>
|
||||
<link
|
||||
name="elfin_link6">
|
||||
<inertial>
|
||||
<origin
|
||||
xyz="-8.45231478862452E-06 -0.000388392932234965 0.144636177508013"
|
||||
rpy="0 0 0" />
|
||||
<mass
|
||||
value="0.297803050744826" />
|
||||
<inertia
|
||||
ixx="0.000151919382929214"
|
||||
ixy="-1.8598001023495E-09"
|
||||
ixz="6.95653480101813E-08"
|
||||
iyy="0.000155217204136347"
|
||||
iyz="-6.63866631888914E-08"
|
||||
izz="0.00027009779924127" />
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link6.STL" />
|
||||
</geometry>
|
||||
<material
|
||||
name="">
|
||||
<color
|
||||
rgba="0.752941176470588 0.752941176470588 0.752941176470588 1" />
|
||||
</material>
|
||||
</visual>
|
||||
<collision>
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="0 0 0" />
|
||||
<geometry>
|
||||
<mesh
|
||||
filename="file://$(find elfin_description)/meshes/elfin10/elfin_link6.STL" />
|
||||
</geometry>
|
||||
</collision>
|
||||
</link>
|
||||
<joint
|
||||
name="elfin_joint6"
|
||||
type="revolute">
|
||||
<origin
|
||||
xyz="0 0 0"
|
||||
rpy="1.5708 0 0" />
|
||||
<parent
|
||||
link="elfin_link5" />
|
||||
<child
|
||||
link="elfin_link6" />
|
||||
<axis
|
||||
xyz="0 0 1" />
|
||||
<limit
|
||||
lower="-3.14"
|
||||
upper="3.14"
|
||||
effort="69"
|
||||
velocity="1.57" />
|
||||
</joint>
|
||||
<link
|
||||
name="elfin_end_link"/>
|
||||
<joint
|
||||
name="elfin_end_joint"
|
||||
type="fixed">
|
||||
<origin
|
||||
xyz="0 0.0 0.1735"
|
||||
rpy="0 0 0" />
|
||||
<parent
|
||||
link="elfin_link6" />
|
||||
<child
|
||||
link="elfin_end_link" />
|
||||
<axis
|
||||
xyz="0 0 0" />
|
||||
</joint>
|
||||
|
||||
<link name="elfin_dummy_gripper"/>
|
||||
|
||||
<joint name="elfin_dummy_joint" type="fixed">
|
||||
<origin
|
||||
xyz="0 0.109 0"
|
||||
rpy="${-PI/2} ${-PI/2} 0" />
|
||||
<parent
|
||||
link="elfin_link6" />
|
||||
<child
|
||||
link="elfin_dummy_gripper" />
|
||||
</joint>
|
||||
|
||||
</robot>
|
||||
134
public/inspection/elfin10/elfin10_gazebo.xacro
Normal file
@ -0,0 +1,134 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
|
||||
|
||||
<xacro:macro name="elfin10_gazebo"
|
||||
params="use_fake_hardware:=false
|
||||
use_real_hardware:=true">
|
||||
|
||||
<gazebo>
|
||||
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
|
||||
<robot_sim_type>gazebo_ros2_control/GazeboSystem</robot_sim_type>
|
||||
<parameters>$(find elfin10_ros2_gazebo)/config/elfin_arm_controller.yaml</parameters>
|
||||
</plugin>
|
||||
</gazebo>
|
||||
|
||||
<ros2_control name="GazeboSystem" type="system">
|
||||
|
||||
<hardware>
|
||||
<xacro:if value="${use_real_hardware}">
|
||||
<plugin>elfin_hardware_interface/ElfinHWInterface</plugin>
|
||||
</xacro:if>
|
||||
|
||||
<xacro:unless value="${use_real_hardware}">
|
||||
<xacro:if value="${use_fake_hardware}">
|
||||
<plugin>fake_components/GenericSystem</plugin>
|
||||
</xacro:if>
|
||||
|
||||
<xacro:unless value="${use_fake_hardware}">
|
||||
<plugin>gazebo_ros2_control/GazeboSystem</plugin>
|
||||
</xacro:unless>
|
||||
</xacro:unless>
|
||||
</hardware>
|
||||
|
||||
|
||||
<joint name="elfin_joint1">
|
||||
<command_interface name="position">
|
||||
<param name="min">-3.14</param>
|
||||
<param name="max">3.14</param>
|
||||
</command_interface>
|
||||
<command_interface name="velocity">
|
||||
<param name="min">-1.57</param>
|
||||
<param name="max">1.57</param>
|
||||
</command_interface>
|
||||
<state_interface name="position"/>
|
||||
<state_interface name="velocity"/>
|
||||
</joint>
|
||||
<joint name="elfin_joint2">
|
||||
<command_interface name="position">
|
||||
<param name="min">-2.35</param>
|
||||
<param name="max">2.35</param>
|
||||
</command_interface>
|
||||
<command_interface name="velocity">
|
||||
<param name="min">-1.57</param>
|
||||
<param name="max">1.57</param>
|
||||
</command_interface>
|
||||
<state_interface name="position"/>
|
||||
<state_interface name="velocity"/>
|
||||
</joint>
|
||||
<joint name="elfin_joint3">
|
||||
<command_interface name="position">
|
||||
<param name="min">-2.61</param>
|
||||
<param name="max">2.61</param>
|
||||
</command_interface>
|
||||
<command_interface name="velocity">
|
||||
<param name="min">-1.57</param>
|
||||
<param name="max">1.57</param>
|
||||
</command_interface>
|
||||
<state_interface name="position"/>
|
||||
<state_interface name="velocity"/>
|
||||
</joint>
|
||||
<joint name="elfin_joint4">
|
||||
<command_interface name="position">
|
||||
<param name="min">-3.14</param>
|
||||
<param name="max">3.14</param>
|
||||
</command_interface>
|
||||
<command_interface name="velocity">
|
||||
<param name="min">-1.57</param>
|
||||
<param name="max">1.57</param>
|
||||
</command_interface>
|
||||
<state_interface name="position"/>
|
||||
<state_interface name="velocity"/>
|
||||
</joint>
|
||||
<joint name="elfin_joint5">
|
||||
<command_interface name="position">
|
||||
<param name="min">-2.56</param>
|
||||
<param name="max">2.56</param>
|
||||
</command_interface>
|
||||
<command_interface name="velocity">
|
||||
<param name="min">-1.57</param>
|
||||
<param name="max">1.57</param>
|
||||
</command_interface>
|
||||
<state_interface name="position"/>
|
||||
<state_interface name="velocity"/>
|
||||
</joint>
|
||||
<joint name="elfin_joint6">
|
||||
<command_interface name="position">
|
||||
<param name="min">-3.14</param>
|
||||
<param name="max">3.14</param>
|
||||
</command_interface>
|
||||
<command_interface name="velocity">
|
||||
<param name="min">-1.57</param>
|
||||
<param name="max">1.57</param>
|
||||
</command_interface>
|
||||
<state_interface name="position"/>
|
||||
<state_interface name="velocity"/>
|
||||
</joint>
|
||||
|
||||
</ros2_control>
|
||||
|
||||
<gazebo reference="elfin_link2">
|
||||
<visual name='visual'>
|
||||
<material>
|
||||
<script>
|
||||
<uri>file://media/materials/scripts/gazebo.material</uri>
|
||||
<name>Gazebo/Grey</name>
|
||||
</script>
|
||||
</material>
|
||||
</visual>
|
||||
</gazebo>
|
||||
|
||||
<gazebo reference="elfin_link4">
|
||||
<visual name='visual'>
|
||||
<material>
|
||||
<script>
|
||||
<uri>file://media/materials/scripts/gazebo.material</uri>
|
||||
<name>Gazebo/Grey</name>
|
||||
</script>
|
||||
</material>
|
||||
</visual>
|
||||
</gazebo>
|
||||
|
||||
</xacro:macro>
|
||||
|
||||
</robot>
|
||||
69
public/inspection/elfin10/elfin10_transmission.xacro
Normal file
@ -0,0 +1,69 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<robot xmlns:xacro="http://wiki.ros.org/xacro">
|
||||
|
||||
<xacro:macro name="elfin10_transmission" >
|
||||
|
||||
<transmission name="elfin_joint1_trans">
|
||||
<type>transmission_interface/SimpleTransmission</type>
|
||||
<joint name="elfin_joint1">
|
||||
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
||||
</joint>
|
||||
<actuator name="elfin_joint1_motor">
|
||||
<mechanicalReduction>1</mechanicalReduction>
|
||||
</actuator>
|
||||
</transmission>
|
||||
|
||||
<transmission name="elfin_joint2_trans">
|
||||
<type>transmission_interface/SimpleTransmission</type>
|
||||
<joint name="elfin_joint2">
|
||||
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
||||
</joint>
|
||||
<actuator name="elfin_joint2_motor">
|
||||
<mechanicalReduction>1</mechanicalReduction>
|
||||
</actuator>
|
||||
</transmission>
|
||||
|
||||
<transmission name="elfin_joint3_trans">
|
||||
<type>transmission_interface/SimpleTransmission</type>
|
||||
<joint name="elfin_joint3">
|
||||
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
||||
</joint>
|
||||
<actuator name="elfin_joint3_motor">
|
||||
<mechanicalReduction>1</mechanicalReduction>
|
||||
</actuator>
|
||||
</transmission>
|
||||
|
||||
<transmission name="elfin_joint4_trans">
|
||||
<type>transmission_interface/SimpleTransmission</type>
|
||||
<joint name="elfin_joint4">
|
||||
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
||||
</joint>
|
||||
<actuator name="elfin_joint4_motor">
|
||||
<mechanicalReduction>1</mechanicalReduction>
|
||||
</actuator>
|
||||
</transmission>
|
||||
|
||||
<transmission name="elfin_joint5_trans">
|
||||
<type>transmission_interface/SimpleTransmission</type>
|
||||
<joint name="elfin_joint5">
|
||||
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
||||
</joint>
|
||||
<actuator name="elfin_joint5_motor">
|
||||
<mechanicalReduction>1</mechanicalReduction>
|
||||
</actuator>
|
||||
</transmission>
|
||||
|
||||
<transmission name="elfin_joint6_trans">
|
||||
<type>transmission_interface/SimpleTransmission</type>
|
||||
<joint name="elfin_joint6">
|
||||
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
||||
</joint>
|
||||
<actuator name="elfin_joint6_motor">
|
||||
<mechanicalReduction>1</mechanicalReduction>
|
||||
</actuator>
|
||||
</transmission>
|
||||
|
||||
</xacro:macro>
|
||||
|
||||
</robot>
|
||||
BIN
public/inspection/elfin10/meshes/elfin10/elfin_base.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_end_link.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link1.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link2.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link3.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link4.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link5.STL
Normal file
BIN
public/inspection/elfin10/meshes/elfin10/elfin_link6.STL
Normal file
461
src/adapters/URDFAdapter.js
Normal file
@ -0,0 +1,461 @@
|
||||
/**
|
||||
* URDF Adapter
|
||||
* Converts urdf-loaders result to unified model
|
||||
*/
|
||||
import * as THREE from 'three';
|
||||
import { UnifiedRobotModel, Link, Joint, JointLimits, VisualGeometry, CollisionGeometry, InertialProperties } from '@/models/UnifiedRobotModel.js';
|
||||
|
||||
export class URDFAdapter {
|
||||
/**
|
||||
* Convert urdf-loaders robot object to unified model
|
||||
* @param {THREE.Group} robot - Robot object returned by urdf-loaders
|
||||
* @param {string} urdfXML - Original URDF XML content (optional, for extracting inertial data)
|
||||
* @returns {UnifiedRobotModel}
|
||||
*/
|
||||
static convert(robot, urdfXML = null) {
|
||||
const model = new UnifiedRobotModel();
|
||||
model.name = robot.name || 'robot';
|
||||
model.threeObject = robot;
|
||||
|
||||
// Mark model type as URDF
|
||||
if (!robot.userData) robot.userData = {};
|
||||
robot.userData.type = 'urdf';
|
||||
|
||||
if (!robot.links || !robot.joints) {
|
||||
console.warn('URDF model missing links or joints information');
|
||||
return model;
|
||||
}
|
||||
|
||||
// If XML provided, parse inertial data
|
||||
let inertialData = {};
|
||||
if (urdfXML) {
|
||||
inertialData = this.parseInertialFromXML(urdfXML);
|
||||
}
|
||||
|
||||
// Convert links
|
||||
Object.values(robot.links).forEach(urdfLink => {
|
||||
const link = this.convertLink(urdfLink);
|
||||
|
||||
// If urdf-loader didn't parse inertial, get from XML
|
||||
if (!link.inertial && inertialData[urdfLink.name]) {
|
||||
link.inertial = inertialData[urdfLink.name];
|
||||
}
|
||||
|
||||
model.addLink(link);
|
||||
});
|
||||
|
||||
// Convert joints
|
||||
Object.values(robot.joints).forEach(urdfJoint => {
|
||||
const joint = this.convertJoint(urdfJoint);
|
||||
model.addJoint(joint);
|
||||
});
|
||||
|
||||
// If XML provided, supplement effort and velocity from XML (urdf-loaders may not have parsed)
|
||||
if (urdfXML) {
|
||||
this.supplementJointLimitsFromXML(model, urdfXML);
|
||||
}
|
||||
|
||||
// Find root link (link that is not a child of any joint)
|
||||
const allChildren = new Set(
|
||||
Array.from(model.joints.values()).map(j => j.child).filter(c => c)
|
||||
);
|
||||
const rootLinks = Array.from(model.links.keys()).filter(
|
||||
name => !allChildren.has(name)
|
||||
);
|
||||
if (rootLinks.length > 0) {
|
||||
model.rootLink = rootLinks[0];
|
||||
}
|
||||
|
||||
// Enhance materials for better lighting (MuJoCo style)
|
||||
this.enhanceMaterials(robot);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance materials for better lighting (MuJoCo style)
|
||||
* Applies consistent shininess and specular properties to all materials
|
||||
* Directly modifies materials in place to ensure changes persist
|
||||
*/
|
||||
static enhanceMaterials(robotObject) {
|
||||
robotObject.traverse((child) => {
|
||||
if (child.isMesh && child.material) {
|
||||
// Handle material arrays
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material = child.material.map(mat => {
|
||||
return this.enhanceSingleMaterial(mat);
|
||||
});
|
||||
} else {
|
||||
child.material = this.enhanceSingleMaterial(child.material);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance a single material with better lighting properties
|
||||
* Returns enhanced material (may be cloned or modified in place)
|
||||
* Saves original properties for lighting toggle
|
||||
*/
|
||||
static enhanceSingleMaterial(material) {
|
||||
if (material.isMeshPhongMaterial || material.isMeshStandardMaterial) {
|
||||
// Save original properties if not already saved (for lighting toggle)
|
||||
if (material.userData.originalShininess === undefined) {
|
||||
material.userData.originalShininess = material.shininess !== undefined ? material.shininess : 30;
|
||||
// IMPORTANT: Save original color to preserve URDF material colors
|
||||
if (!material.userData.originalColor && material.color) {
|
||||
material.userData.originalColor = material.color.clone();
|
||||
}
|
||||
// Save original specular - if material had no specular, save null
|
||||
if (!material.specular) {
|
||||
material.userData.originalSpecular = null;
|
||||
} else if (material.specular.isColor) {
|
||||
const spec = material.specular;
|
||||
if (spec.r < 0.1 && spec.g < 0.1 && spec.b < 0.1) {
|
||||
material.userData.originalSpecular = null; // Likely default
|
||||
} else {
|
||||
material.userData.originalSpecular = spec.clone();
|
||||
}
|
||||
} else if (typeof material.specular === 'number') {
|
||||
if (material.specular === 0x111111 || material.specular < 0x111111) {
|
||||
material.userData.originalSpecular = null;
|
||||
} else {
|
||||
material.userData.originalSpecular = new THREE.Color(material.specular);
|
||||
}
|
||||
} else {
|
||||
material.userData.originalSpecular = null;
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT: Preserve original color from URDF material definitions
|
||||
// Do not modify material.color - urdf-loaders already sets the correct color
|
||||
|
||||
// Apply enhanced lighting (default enabled)
|
||||
// Increase shininess for better highlights
|
||||
if (material.shininess === undefined || material.shininess < 50) {
|
||||
material.shininess = 50;
|
||||
}
|
||||
|
||||
// Enhance specular reflection - ensure it's a Color object with proper values
|
||||
if (!material.specular) {
|
||||
material.specular = new THREE.Color(0.3, 0.3, 0.3);
|
||||
} else if (material.specular.isColor) {
|
||||
// If it's already a Color object, update values
|
||||
if (material.specular.r < 0.2 || material.specular.g < 0.2 || material.specular.b < 0.2) {
|
||||
material.specular.setRGB(0.3, 0.3, 0.3);
|
||||
}
|
||||
} else if (typeof material.specular === 'number') {
|
||||
// Convert number to Color object
|
||||
if (material.specular < 0x333333) {
|
||||
material.specular = new THREE.Color(0.3, 0.3, 0.3);
|
||||
} else {
|
||||
// Convert hex to Color
|
||||
material.specular = new THREE.Color(material.specular);
|
||||
if (material.specular.r < 0.2) {
|
||||
material.specular.setRGB(0.3, 0.3, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark material as needing update
|
||||
material.needsUpdate = true;
|
||||
return material;
|
||||
} else if (material.type === 'MeshBasicMaterial') {
|
||||
// Convert MeshBasicMaterial to MeshPhongMaterial
|
||||
const oldMaterial = material;
|
||||
const envMap = typeof window !== 'undefined' && window.app?.sceneManager?.environmentManager?.getEnvironmentMap();
|
||||
const newMaterial = new THREE.MeshPhongMaterial({
|
||||
color: oldMaterial.color,
|
||||
map: oldMaterial.map,
|
||||
transparent: oldMaterial.transparent,
|
||||
opacity: oldMaterial.opacity,
|
||||
side: oldMaterial.side,
|
||||
shininess: 50,
|
||||
specular: new THREE.Color(0.3, 0.3, 0.3),
|
||||
envMap: envMap || null,
|
||||
reflectivity: envMap ? 0.3 : 0
|
||||
});
|
||||
// Save original properties for lighting toggle
|
||||
newMaterial.userData.originalShininess = 30;
|
||||
newMaterial.userData.originalSpecular = null; // MeshBasicMaterial had no specular
|
||||
return newMaterial;
|
||||
}
|
||||
|
||||
return material;
|
||||
}
|
||||
|
||||
static convertLink(urdfLink) {
|
||||
const link = new Link(urdfLink.name);
|
||||
link.threeObject = urdfLink;
|
||||
|
||||
// Convert inertial properties
|
||||
if (urdfLink.inertial) {
|
||||
link.inertial = this.convertInertial(urdfLink.inertial);
|
||||
}
|
||||
|
||||
// Note: urdf-loaders has already converted visual and collision to Three.js objects
|
||||
// We mainly extract metadata, actual meshes are in threeObject
|
||||
|
||||
return link;
|
||||
}
|
||||
|
||||
static convertJoint(urdfJoint) {
|
||||
// URDF joint object has jointType property (not type)
|
||||
const jointType = urdfJoint.jointType || urdfJoint.type || 'fixed';
|
||||
const joint = new Joint(urdfJoint.name, jointType);
|
||||
|
||||
// Extract parent name (urdf-loader returns Three.js object reference)
|
||||
joint.parent = urdfJoint.parent?.name || null;
|
||||
|
||||
// child may not be in urdfJoint.child, but in Three.js children array
|
||||
// In Three.js scene graph, Joint is parent node, Child Link is Joint's child node
|
||||
if (urdfJoint.child && urdfJoint.child.name) {
|
||||
joint.child = urdfJoint.child.name;
|
||||
} else if (urdfJoint.children && urdfJoint.children.length > 0) {
|
||||
// Find Link object from children array
|
||||
const childLink = urdfJoint.children.find(child =>
|
||||
child.isURDFLink || child.type === 'URDFLink'
|
||||
);
|
||||
if (childLink) {
|
||||
joint.child = childLink.name;
|
||||
}
|
||||
}
|
||||
|
||||
joint.threeObject = urdfJoint;
|
||||
|
||||
// Convert origin
|
||||
if (urdfJoint.origin) {
|
||||
joint.origin = {
|
||||
xyz: urdfJoint.origin.xyz || [0, 0, 0],
|
||||
rpy: urdfJoint.origin.rpy || [0, 0, 0]
|
||||
};
|
||||
}
|
||||
|
||||
// Convert axis
|
||||
if (urdfJoint.axis) {
|
||||
joint.axis = { xyz: urdfJoint.axis.xyz || [0, 0, 1] };
|
||||
}
|
||||
|
||||
// Convert limits
|
||||
if (urdfJoint.limit) {
|
||||
joint.limits = this.convertLimits(urdfJoint.limit);
|
||||
}
|
||||
|
||||
// Get current value
|
||||
if (urdfJoint.angle !== undefined) {
|
||||
joint.currentValue = urdfJoint.angle;
|
||||
} else if (urdfJoint.jointValue !== undefined) {
|
||||
joint.currentValue = urdfJoint.jointValue;
|
||||
}
|
||||
|
||||
return joint;
|
||||
}
|
||||
|
||||
static convertLimits(limit) {
|
||||
const limits = new JointLimits();
|
||||
if (limit.lower !== undefined) limits.lower = limit.lower;
|
||||
if (limit.upper !== undefined) limits.upper = limit.upper;
|
||||
if (limit.effort !== undefined) limits.effort = limit.effort;
|
||||
if (limit.velocity !== undefined) limits.velocity = limit.velocity;
|
||||
return limits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplement joint effort and velocity information from URDF XML
|
||||
* (because urdf-loaders may not have parsed these attributes)
|
||||
*/
|
||||
static supplementJointLimitsFromXML(model, urdfXML) {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(urdfXML, 'text/xml');
|
||||
|
||||
const jointElements = doc.querySelectorAll('joint');
|
||||
jointElements.forEach(jointEl => {
|
||||
const jointName = jointEl.getAttribute('name');
|
||||
const joint = model.joints.get(jointName);
|
||||
|
||||
if (joint) {
|
||||
const limitEl = jointEl.querySelector('limit');
|
||||
if (limitEl) {
|
||||
if (!joint.limits) {
|
||||
joint.limits = new JointLimits();
|
||||
}
|
||||
|
||||
// Read effort and velocity
|
||||
const effort = limitEl.getAttribute('effort');
|
||||
const velocity = limitEl.getAttribute('velocity');
|
||||
|
||||
if (effort !== null) {
|
||||
joint.limits.effort = parseFloat(effort);
|
||||
}
|
||||
if (velocity !== null) {
|
||||
joint.limits.velocity = parseFloat(velocity);
|
||||
}
|
||||
|
||||
// Also ensure lower and upper are correctly set
|
||||
const lower = limitEl.getAttribute('lower');
|
||||
const upper = limitEl.getAttribute('upper');
|
||||
if (lower !== null) {
|
||||
joint.limits.lower = parseFloat(lower);
|
||||
}
|
||||
if (upper !== null) {
|
||||
joint.limits.upper = parseFloat(upper);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to supplement joint limit information from XML:', error);
|
||||
}
|
||||
}
|
||||
|
||||
static convertInertial(inertial) {
|
||||
const props = new InertialProperties();
|
||||
if (inertial.mass !== undefined) props.mass = inertial.mass;
|
||||
if (inertial.origin) {
|
||||
props.origin = {
|
||||
xyz: inertial.origin.xyz || [0, 0, 0],
|
||||
rpy: inertial.origin.rpy || [0, 0, 0]
|
||||
};
|
||||
}
|
||||
if (inertial.ixx !== undefined) props.ixx = inertial.ixx;
|
||||
if (inertial.iyy !== undefined) props.iyy = inertial.iyy;
|
||||
if (inertial.izz !== undefined) props.izz = inertial.izz;
|
||||
if (inertial.ixy !== undefined) props.ixy = inertial.ixy;
|
||||
if (inertial.ixz !== undefined) props.ixz = inertial.ixz;
|
||||
if (inertial.iyz !== undefined) props.iyz = inertial.iyz;
|
||||
return props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse inertial data from URDF XML
|
||||
* @param {string} xmlContent - URDF XML content
|
||||
* @returns {Object} Mapping from link names to inertial data
|
||||
*/
|
||||
static parseInertialFromXML(xmlContent) {
|
||||
const inertialData = {};
|
||||
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlContent, 'text/xml');
|
||||
|
||||
// Get all link elements
|
||||
const links = xmlDoc.getElementsByTagName('link');
|
||||
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
const linkElement = links[i];
|
||||
const linkName = linkElement.getAttribute('name');
|
||||
|
||||
// Find inertial element
|
||||
const inertialElement = linkElement.getElementsByTagName('inertial')[0];
|
||||
if (!inertialElement) continue;
|
||||
|
||||
const inertial = new InertialProperties();
|
||||
|
||||
// Parse mass
|
||||
const massElement = inertialElement.getElementsByTagName('mass')[0];
|
||||
if (massElement) {
|
||||
inertial.mass = parseFloat(massElement.getAttribute('value'));
|
||||
}
|
||||
|
||||
// Parse origin
|
||||
const originElement = inertialElement.getElementsByTagName('origin')[0];
|
||||
if (originElement) {
|
||||
const xyz = originElement.getAttribute('xyz');
|
||||
const rpy = originElement.getAttribute('rpy');
|
||||
inertial.origin = {
|
||||
xyz: xyz ? xyz.split(' ').map(parseFloat) : [0, 0, 0],
|
||||
rpy: rpy ? rpy.split(' ').map(parseFloat) : [0, 0, 0]
|
||||
};
|
||||
} else {
|
||||
inertial.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
|
||||
}
|
||||
|
||||
// Parse inertia
|
||||
const inertiaElement = inertialElement.getElementsByTagName('inertia')[0];
|
||||
if (inertiaElement) {
|
||||
inertial.ixx = parseFloat(inertiaElement.getAttribute('ixx')) || 0;
|
||||
inertial.iyy = parseFloat(inertiaElement.getAttribute('iyy')) || 0;
|
||||
inertial.izz = parseFloat(inertiaElement.getAttribute('izz')) || 0;
|
||||
inertial.ixy = parseFloat(inertiaElement.getAttribute('ixy')) || 0;
|
||||
inertial.ixz = parseFloat(inertiaElement.getAttribute('ixz')) || 0;
|
||||
inertial.iyz = parseFloat(inertiaElement.getAttribute('iyz')) || 0;
|
||||
}
|
||||
|
||||
inertialData[linkName] = inertial;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse URDF XML inertial data:', error);
|
||||
}
|
||||
|
||||
return inertialData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set joint angle (using urdf-loaders' setJointValue method)
|
||||
* Reference URDFClasses.js setJointValue implementation
|
||||
*/
|
||||
static setJointAngle(joint, angle, ignoreLimits = false) {
|
||||
joint.currentValue = angle;
|
||||
|
||||
// URDF format: use urdf-loader's setJointValue method
|
||||
if (joint.threeObject) {
|
||||
// If ignoring limits, temporarily modify URDF joint object's limit values
|
||||
let originalLimits = null;
|
||||
if (ignoreLimits && joint.threeObject.limit) {
|
||||
originalLimits = {
|
||||
lower: joint.threeObject.limit.lower,
|
||||
upper: joint.threeObject.limit.upper
|
||||
};
|
||||
joint.threeObject.limit.lower = -Math.PI * 2;
|
||||
joint.threeObject.limit.upper = Math.PI * 2;
|
||||
}
|
||||
|
||||
// Prefer setJointValue method (urdf-loader's standard method)
|
||||
if (typeof joint.threeObject.setJointValue === 'function') {
|
||||
joint.threeObject.setJointValue(angle);
|
||||
|
||||
// Restore original limits
|
||||
if (originalLimits && joint.threeObject.limit) {
|
||||
joint.threeObject.limit.lower = originalLimits.lower;
|
||||
joint.threeObject.limit.upper = originalLimits.upper;
|
||||
}
|
||||
return;
|
||||
} else if (typeof joint.threeObject.setAngle === 'function') {
|
||||
joint.threeObject.setAngle(angle);
|
||||
|
||||
// Restore original limits
|
||||
if (originalLimits && joint.threeObject.limit) {
|
||||
joint.threeObject.limit.lower = originalLimits.lower;
|
||||
joint.threeObject.limit.upper = originalLimits.upper;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore original limits (if none of the above methods executed)
|
||||
if (originalLimits && joint.threeObject.limit) {
|
||||
joint.threeObject.limit.lower = originalLimits.lower;
|
||||
joint.threeObject.limit.upper = originalLimits.upper;
|
||||
}
|
||||
|
||||
// If none available, manually set rotation
|
||||
// Note: For URDF, we usually shouldn't reach here, as urdf-loader should provide setJointValue
|
||||
console.warn(`Joint ${joint.name} has no setJointValue or setAngle method, attempting manual setup`);
|
||||
if (joint.type === 'revolute' || joint.type === 'continuous') {
|
||||
const axis = joint.axis ? new THREE.Vector3(...joint.axis.xyz).normalize() : new THREE.Vector3(0, 0, 1);
|
||||
// Note: joint.threeObject may not be a Three.js object, cannot directly set rotation
|
||||
if (joint.threeObject.rotation) {
|
||||
joint.threeObject.rotation.setFromAxisAngle(axis, angle);
|
||||
}
|
||||
} else if (joint.type === 'prismatic') {
|
||||
const axis = joint.axis ? new THREE.Vector3(...joint.axis.xyz).normalize() : new THREE.Vector3(1, 0, 0);
|
||||
if (joint.threeObject.position) {
|
||||
joint.threeObject.position.copy(axis.multiplyScalar(angle));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn(`Joint ${joint.name} has no threeObject`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
616
src/adapters/XacroAdapter.js
Normal file
@ -0,0 +1,616 @@
|
||||
/**
|
||||
* Xacro Adapter
|
||||
*
|
||||
* Parses ROS Xacro files and converts them to URDF format using xacro-parser.
|
||||
*
|
||||
* Supported Features:
|
||||
* - Property definitions and substitutions (xacro:property)
|
||||
* - Macro definitions with parameters (xacro:macro)
|
||||
* - Conditional blocks (xacro:if, xacro:unless)
|
||||
* - File inclusions (xacro:include) with package:// and relative paths
|
||||
* - Python-style boolean constants (True/False)
|
||||
* - Arithmetic expressions and property evaluation
|
||||
*
|
||||
* Compatibility:
|
||||
* - ROS Jade and later (inOrder=true, requirePrefix=true, localProperties=true)
|
||||
* - Automatically injects True/False constants for compatibility with ROS xacro files
|
||||
*
|
||||
* Usage:
|
||||
* - Upload all related xacro files (main file and included files)
|
||||
* - The adapter automatically resolves file inclusions from the uploaded file map
|
||||
* - Mesh files are resolved using package:// paths or relative paths
|
||||
*/
|
||||
import { XacroParser } from 'xacro-parser';
|
||||
import { URDFAdapter } from './URDFAdapter.js';
|
||||
|
||||
export class XacroAdapter {
|
||||
/**
|
||||
* Parse xacro content and convert to unified model
|
||||
* @param {string} xacroContent - Xacro file content
|
||||
* @param {string} fileName - Xacro file name (for working path)
|
||||
* @param {Map} fileMap - File map (path -> File object)
|
||||
* @param {File} file - Original file object (optional)
|
||||
* @returns {Promise<UnifiedRobotModel>}
|
||||
*/
|
||||
static async parse(xacroContent, fileName, fileMap = null, file = null) {
|
||||
try {
|
||||
// Create xacro parser
|
||||
const parser = new XacroParser();
|
||||
|
||||
// Configure parser for ROS Jade and later (default settings)
|
||||
parser.inOrder = true;
|
||||
parser.requirePrefix = true;
|
||||
parser.localProperties = true;
|
||||
|
||||
// Set working path (directory where xacro file is located)
|
||||
const workingPath = fileName.includes('/')
|
||||
? fileName.substring(0, fileName.lastIndexOf('/') + 1)
|
||||
: '';
|
||||
parser.workingPath = workingPath;
|
||||
|
||||
// Inject Python-style boolean constants as xacro properties
|
||||
// Some xacro files use True/False (capitalized) in conditions
|
||||
xacroContent = this.injectBooleanConstants(xacroContent);
|
||||
|
||||
// Extract and set xacro arguments with their default values
|
||||
// Also add common ROS arguments that might be used without definition
|
||||
const xacroArgs = this.extractXacroArguments(xacroContent);
|
||||
|
||||
// Add common ROS arguments with sensible defaults if not already defined
|
||||
if (!xacroArgs.hasOwnProperty('DEBUG')) {
|
||||
xacroArgs.DEBUG = 'false';
|
||||
}
|
||||
if (!xacroArgs.hasOwnProperty('SELF_COLLIDE')) {
|
||||
xacroArgs.SELF_COLLIDE = 'false';
|
||||
}
|
||||
|
||||
parser.arguments = xacroArgs;
|
||||
|
||||
// If fileMap provided, setup custom file loader
|
||||
if (fileMap) {
|
||||
parser.getFileContents = async (path) => {
|
||||
return await this.loadFileFromMap(path, fileMap, workingPath);
|
||||
};
|
||||
}
|
||||
|
||||
// Parse xacro to URDF XML
|
||||
const urdfXML = await parser.parse(xacroContent);
|
||||
|
||||
|
||||
// Convert XMLDocument to string
|
||||
const serializer = new XMLSerializer();
|
||||
let urdfString = serializer.serializeToString(urdfXML);
|
||||
|
||||
// Clean up the XML string - remove empty xmlns attributes that might cause issues
|
||||
urdfString = urdfString.replace(/\sxmlns=""/g, '');
|
||||
|
||||
// Re-parse the URDF string with DOMParser to create a clean XMLDocument
|
||||
// This removes any xacro-specific nodes that urdf-loader might not handle
|
||||
const domParser = new DOMParser();
|
||||
const cleanUrdfXML = domParser.parseFromString(urdfString, 'text/xml');
|
||||
|
||||
// Check for parsing errors
|
||||
const parseError = cleanUrdfXML.querySelector('parsererror');
|
||||
if (parseError) {
|
||||
throw new Error('Generated URDF has invalid XML: ' + parseError.textContent);
|
||||
}
|
||||
|
||||
// Remove problematic elements that urdf-loader might not handle well
|
||||
// Remove gazebo elements (plugins, sensors, etc.)
|
||||
const gazeboElements = cleanUrdfXML.querySelectorAll('gazebo');
|
||||
gazeboElements.forEach(el => el.parentNode?.removeChild(el));
|
||||
|
||||
// Remove transmission elements (these are for Gazebo/ROS control)
|
||||
const transmissionElements = cleanUrdfXML.querySelectorAll('transmission');
|
||||
transmissionElements.forEach(el => el.parentNode?.removeChild(el));
|
||||
|
||||
// Remove visual/collision elements with empty geometry tags
|
||||
this.removeEmptyGeometry(cleanUrdfXML);
|
||||
|
||||
// Clean up empty text nodes and comments that might cause issues
|
||||
this.cleanXMLNodes(cleanUrdfXML.documentElement);
|
||||
|
||||
// Convert the clean XMLDocument back to string for URDFLoader
|
||||
const finalUrdfString = serializer.serializeToString(cleanUrdfXML);
|
||||
|
||||
// Now use existing URDF loading infrastructure
|
||||
// Import URDFLoader dynamically
|
||||
const urdfModule = await import('urdf-loader');
|
||||
const URDFLoader = urdfModule.URDFLoader || urdfModule.default || urdfModule;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const loader = new URDFLoader();
|
||||
loader.parseCollision = true;
|
||||
|
||||
// Extract directory where URDF file is located
|
||||
const urdfDir = workingPath;
|
||||
|
||||
// If file map provided, setup resource loader (same as URDF loading)
|
||||
if (fileMap) {
|
||||
// Parse URDF content, find all used package names
|
||||
const packages = this.extractPackagesFromURDF(finalUrdfString);
|
||||
|
||||
// Build package map
|
||||
const packageMap = {};
|
||||
packages.forEach(pkg => {
|
||||
packageMap[pkg] = pkg;
|
||||
});
|
||||
packageMap[''] = '';
|
||||
|
||||
loader.packages = packageMap;
|
||||
|
||||
// Set URL Modifier (same as URDF loading)
|
||||
const urlModifier = (url) => {
|
||||
// Handle blob URLs
|
||||
if (url.startsWith('blob:')) {
|
||||
const blobMatch = url.match(/^blob:https?:\/\/[^\/]+\/(.+)$/);
|
||||
if (blobMatch && blobMatch[1]) {
|
||||
const fileName = blobMatch[1];
|
||||
if (/\.(jpg|jpeg|png|gif|bmp|tga|tiff|webp|dae|stl|obj|gltf|glb)$/i.test(fileName)) {
|
||||
url = fileName;
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
const isTextureFile = /\.(jpg|jpeg|png|gif|bmp|tga|tiff|webp)$/i.test(url);
|
||||
const isMeshFile = /\.(dae|stl|obj|gltf|glb)$/i.test(url);
|
||||
|
||||
let meshPath = url;
|
||||
|
||||
// Remove http:// or https:// prefix
|
||||
if (meshPath.startsWith('http://') || meshPath.startsWith('https://')) {
|
||||
try {
|
||||
const urlObj = new URL(meshPath);
|
||||
meshPath = urlObj.pathname;
|
||||
if (meshPath.startsWith('/')) {
|
||||
meshPath = meshPath.substring(1);
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid URL, use as-is
|
||||
}
|
||||
}
|
||||
|
||||
// Remove package:// prefix
|
||||
if (meshPath.startsWith('package://')) {
|
||||
meshPath = meshPath.replace(/^package:\/\//, '');
|
||||
const parts = meshPath.split('/');
|
||||
if (parts.length > 1) {
|
||||
meshPath = parts.slice(1).join('/');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leading ./
|
||||
meshPath = meshPath.replace(/^\.\//, '');
|
||||
|
||||
// Handle relative paths
|
||||
let normalizedPath = meshPath;
|
||||
if (meshPath.includes('../')) {
|
||||
const parts = meshPath.split('/');
|
||||
const resolvedParts = [];
|
||||
for (const part of parts) {
|
||||
if (part === '..') {
|
||||
resolvedParts.pop();
|
||||
} else if (part !== '.' && part !== '') {
|
||||
resolvedParts.push(part);
|
||||
}
|
||||
}
|
||||
normalizedPath = resolvedParts.join('/');
|
||||
}
|
||||
|
||||
// Build full path
|
||||
const fullPath = urdfDir + normalizedPath;
|
||||
const altPath = urdfDir + meshPath;
|
||||
|
||||
// Find file in fileMap
|
||||
let matchedFile = fileMap.get(fullPath);
|
||||
|
||||
if (!matchedFile && altPath !== fullPath) {
|
||||
matchedFile = fileMap.get(altPath);
|
||||
}
|
||||
|
||||
if (!matchedFile) {
|
||||
matchedFile = fileMap.get(normalizedPath);
|
||||
}
|
||||
|
||||
if (!matchedFile) {
|
||||
matchedFile = fileMap.get(meshPath);
|
||||
}
|
||||
|
||||
if (!matchedFile) {
|
||||
const targetFileName = normalizedPath.split('/').pop() || meshPath.split('/').pop();
|
||||
for (const [key, file] of fileMap.entries()) {
|
||||
const keyFileName = key.split('/').pop();
|
||||
if (keyFileName === targetFileName) {
|
||||
matchedFile = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedFile) {
|
||||
const bloburl = URL.createObjectURL(matchedFile);
|
||||
return bloburl;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
loader.manager.setURLModifier(urlModifier);
|
||||
|
||||
// Custom loadMeshCb (same as URDF loading)
|
||||
const originalLoadMeshCb = loader.loadMeshCb || loader.defaultMeshLoader.bind(loader);
|
||||
loader.loadMeshCb = (path, manager, done) => {
|
||||
this.findFileInMapByPath(path, fileMap, urdfDir).then(file => {
|
||||
if (file) {
|
||||
const ext = (file.name || path).toLowerCase().split('.').pop();
|
||||
this.loadMeshFileAsync(file, ext, manager).then(meshObject => {
|
||||
if (meshObject) {
|
||||
done(meshObject, null);
|
||||
} else {
|
||||
done(null, new Error(`Failed to load mesh file: ${path}`));
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(`Failed to load mesh: ${path}`, err);
|
||||
done(null, err);
|
||||
});
|
||||
} else {
|
||||
let stlPath = path
|
||||
stlPath = stlPath.replace('file:///', 'inspection/elfin10')
|
||||
originalLoadMeshCb(path, manager, done);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error(`Failed to find file: ${path}`, error);
|
||||
originalLoadMeshCb(path, manager, done);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Create temporary URL for URDF content
|
||||
const blob = new Blob([finalUrdfString], { type: 'text/xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
loader.load(url, (robot) => {
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Convert to unified model using URDFAdapter
|
||||
try {
|
||||
const model = URDFAdapter.convert(robot, finalUrdfString);
|
||||
resolve(model);
|
||||
} catch (error) {
|
||||
console.error('[XacroAdapter] URDF conversion error:', error);
|
||||
reject(new Error('URDF conversion failed: ' + error.message));
|
||||
}
|
||||
}, undefined, (error) => {
|
||||
URL.revokeObjectURL(url);
|
||||
console.error('[XacroAdapter] URDF loading error:', error);
|
||||
reject(new Error('URDF loading failed: ' + (error.message || error)));
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[XacroAdapter] Xacro parsing error:', error);
|
||||
throw new Error('Xacro parsing failed: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load file from fileMap for xacro includes
|
||||
* @param {string} path - File path
|
||||
* @param {Map} fileMap - File map
|
||||
* @param {string} workingPath - Working directory
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
static async loadFileFromMap(path, fileMap, workingPath) {
|
||||
// Clean path - remove leading slash if present
|
||||
let cleanPath = path;
|
||||
// if (cleanPath.startsWith('/')) {
|
||||
// cleanPath = cleanPath.substring(1);
|
||||
// }
|
||||
|
||||
// // Remove leading ./
|
||||
// cleanPath = cleanPath.replace(/^\.\//, '');
|
||||
cleanPath = cleanPath.split('///')[1];
|
||||
// Try different path combinations
|
||||
const possiblePaths = [
|
||||
cleanPath,
|
||||
workingPath + cleanPath,
|
||||
path,
|
||||
workingPath + path.replace(/^\/+/, ''),
|
||||
// Also try without the first directory component
|
||||
cleanPath.includes('/') ? cleanPath.substring(cleanPath.indexOf('/') + 1) : cleanPath,
|
||||
];
|
||||
|
||||
// Try each path
|
||||
for (const tryPath of possiblePaths) {
|
||||
const file = fileMap.get(tryPath);
|
||||
if (file) {
|
||||
const content = await file.text();
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
// Try filename only match
|
||||
const fileName = cleanPath.split('/').pop();
|
||||
for (const [key, file] of fileMap.entries()) {
|
||||
const keyFileName = key.split('/').pop();
|
||||
if (keyFileName === fileName) {
|
||||
const content = await file.text();
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
console.error('[XacroAdapter] Cannot find included file:', path);
|
||||
throw new Error(`Cannot find included file: ${path}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove visual/collision elements with empty geometry tags
|
||||
* xacro-parser may not fully expand conditional geometry (xacro:if)
|
||||
* We'll just remove these empty visual elements rather than try to fix them
|
||||
* @param {XMLDocument} xmlDoc - XML document to clean
|
||||
*/
|
||||
static removeEmptyGeometry(xmlDoc) {
|
||||
// Find all geometry elements
|
||||
const geometryElements = xmlDoc.querySelectorAll('geometry');
|
||||
|
||||
geometryElements.forEach(geom => {
|
||||
// Check if geometry is empty (no child elements)
|
||||
if (!geom.children || geom.children.length === 0) {
|
||||
// Find the parent visual or collision element
|
||||
let parent = geom.parentNode;
|
||||
if (parent) {
|
||||
const grandParent = parent.parentNode;
|
||||
if (grandParent) {
|
||||
if (parent.nodeName === 'visual' || parent.nodeName === 'collision') {
|
||||
// Remove empty visual/collision elements
|
||||
grandParent.removeChild(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively clean XML nodes - remove empty text nodes and comments
|
||||
* @param {Element} element - Element to clean
|
||||
*/
|
||||
static cleanXMLNodes(element) {
|
||||
if (!element || !element.childNodes) return;
|
||||
|
||||
const nodesToRemove = [];
|
||||
|
||||
// Collect nodes to remove
|
||||
for (let i = 0; i < element.childNodes.length; i++) {
|
||||
const node = element.childNodes[i];
|
||||
|
||||
// Remove comments
|
||||
if (node.nodeType === Node.COMMENT_NODE) {
|
||||
nodesToRemove.push(node);
|
||||
}
|
||||
// Remove empty or whitespace-only text nodes
|
||||
else if (node.nodeType === Node.TEXT_NODE) {
|
||||
if (!node.nodeValue || node.nodeValue.trim() === '') {
|
||||
nodesToRemove.push(node);
|
||||
}
|
||||
}
|
||||
// Recursively clean element nodes
|
||||
else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
this.cleanXMLNodes(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove collected nodes
|
||||
nodesToRemove.forEach(node => {
|
||||
element.removeChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract package names from URDF content
|
||||
* @param {string} urdfContent - URDF content
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
static extractPackagesFromURDF(urdfContent) {
|
||||
const packages = new Set();
|
||||
const packageRegex = /package:\/\/([^\/]+)/g;
|
||||
let match;
|
||||
while ((match = packageRegex.exec(urdfContent)) !== null) {
|
||||
packages.add(match[1]);
|
||||
}
|
||||
return packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find file in fileMap by path
|
||||
* @param {string} path - File path
|
||||
* @param {Map} fileMap - File map
|
||||
* @param {string} urdfDir - URDF directory
|
||||
* @returns {Promise<File|null>}
|
||||
*/
|
||||
static async findFileInMapByPath(path, fileMap, urdfDir) {
|
||||
let meshPath = path;
|
||||
|
||||
// Remove blob: prefix
|
||||
meshPath = meshPath.replace(/^blob:[^\/]+\//, '');
|
||||
|
||||
// Remove package:// prefix
|
||||
if (meshPath.startsWith('package://')) {
|
||||
meshPath = meshPath.replace(/^package:\/\//, '');
|
||||
const parts = meshPath.split('/');
|
||||
if (parts.length > 1) {
|
||||
meshPath = parts.slice(1).join('/');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leading ./
|
||||
meshPath = meshPath.replace(/^\.\//, '');
|
||||
|
||||
// Build full path
|
||||
const fullPath = urdfDir + meshPath;
|
||||
|
||||
// Try full path
|
||||
let file = fileMap.get(fullPath);
|
||||
if (file) return file;
|
||||
|
||||
// Try path without directory
|
||||
file = fileMap.get(meshPath);
|
||||
if (file) return file;
|
||||
|
||||
// Try filename match
|
||||
const targetFileName = meshPath.split('/').pop();
|
||||
for (const [key, f] of fileMap.entries()) {
|
||||
const keyFileName = key.split('/').pop();
|
||||
if (keyFileName === targetFileName) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load mesh file asynchronously
|
||||
* @param {File} file - File object
|
||||
* @param {string} ext - File extension
|
||||
* @param {THREE.LoadingManager} manager - Loading manager
|
||||
* @returns {Promise<THREE.Object3D>}
|
||||
*/
|
||||
static async loadMeshFileAsync(file, ext, manager) {
|
||||
const THREE = await import('three');
|
||||
const blobUrl = URL.createObjectURL(file);
|
||||
|
||||
try {
|
||||
let meshObject = null;
|
||||
|
||||
switch (ext) {
|
||||
case 'stl': {
|
||||
const { STLLoader } = await import('three/examples/jsm/loaders/STLLoader.js');
|
||||
const stlLoader = new STLLoader(manager);
|
||||
const stlGeometry = await new Promise((resolve, reject) => {
|
||||
stlLoader.load(blobUrl, resolve, undefined, reject);
|
||||
});
|
||||
const stlMaterial = new THREE.MeshPhongMaterial();
|
||||
meshObject = new THREE.Mesh(stlGeometry, stlMaterial);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'dae': {
|
||||
const { ColladaLoader } = await import('three/examples/jsm/loaders/ColladaLoader.js');
|
||||
const colladaLoader = new ColladaLoader(manager);
|
||||
const colladaModel = await new Promise((resolve, reject) => {
|
||||
colladaLoader.load(blobUrl, resolve, undefined, reject);
|
||||
});
|
||||
meshObject = colladaModel.scene;
|
||||
|
||||
// Remove lights
|
||||
if (meshObject && meshObject.traverse) {
|
||||
const lightsToRemove = [];
|
||||
meshObject.traverse(child => {
|
||||
if (child.isLight) {
|
||||
lightsToRemove.push(child);
|
||||
}
|
||||
});
|
||||
lightsToRemove.forEach(light => {
|
||||
if (light.parent) {
|
||||
light.parent.remove(light);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'obj': {
|
||||
const { OBJLoader } = await import('three/examples/jsm/loaders/OBJLoader.js');
|
||||
const objLoader = new OBJLoader(manager);
|
||||
meshObject = await new Promise((resolve, reject) => {
|
||||
objLoader.load(blobUrl, resolve, undefined, reject);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'gltf':
|
||||
case 'glb': {
|
||||
const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js');
|
||||
const gltfLoader = new GLTFLoader(manager);
|
||||
const gltfModel = await new Promise((resolve, reject) => {
|
||||
gltfLoader.load(blobUrl, resolve, undefined, reject);
|
||||
});
|
||||
meshObject = gltfModel.scene;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn(`Unsupported file format: ${ext}`);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
return null;
|
||||
}
|
||||
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
return meshObject;
|
||||
} catch (error) {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
console.error(`Failed to load mesh file: ${file.name}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject Python-style boolean constants (True/False) into xacro content
|
||||
* Some xacro files use capitalized True/False in conditional expressions
|
||||
* The parameters are passed as strings ("True", "False") but compared to constants
|
||||
* We define the constants to match the string values for comparison to work
|
||||
* @param {string} xacroContent - Original xacro content
|
||||
* @returns {string} - Modified xacro content with boolean constants defined
|
||||
*/
|
||||
static injectBooleanConstants(xacroContent) {
|
||||
// Find the robot tag opening
|
||||
const robotTagMatch = xacroContent.match(/(<robot[^>]*>)/);
|
||||
if (!robotTagMatch) {
|
||||
return xacroContent;
|
||||
}
|
||||
|
||||
// Define True/False as string properties for comparison
|
||||
// When mirror_dae="True" is passed, it becomes the string "True"
|
||||
// We want ${mirror_dae == True} to work, so True must also be "True"
|
||||
const booleanProperties = `
|
||||
<!-- Injected by XacroAdapter: Python-style boolean constants -->
|
||||
<xacro:property name="True" value="True"/>
|
||||
<xacro:property name="False" value="False"/>
|
||||
`;
|
||||
|
||||
const insertPosition = robotTagMatch.index + robotTagMatch[0].length;
|
||||
return xacroContent.substring(0, insertPosition) +
|
||||
booleanProperties +
|
||||
xacroContent.substring(insertPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract xacro arguments and their default values from xacro content
|
||||
* Xacro files can define arguments using <xacro:arg name="..." default="..."/>
|
||||
* These need to be provided to the parser via parser.arguments
|
||||
* @param {string} xacroContent - Xacro file content
|
||||
* @returns {Object} - Object with argument names as keys and default values
|
||||
*/
|
||||
static extractXacroArguments(xacroContent) {
|
||||
const args = {};
|
||||
|
||||
// Match <xacro:arg name="NAME" default="VALUE"/>
|
||||
// Also match <arg name="NAME" default="VALUE"/> (without xacro: prefix)
|
||||
const argPattern = /<(?:xacro:)?arg\s+name=["']([^"']+)["'](?:\s+default=["']([^"']*)["'])?/g;
|
||||
|
||||
let match;
|
||||
while ((match = argPattern.exec(xacroContent)) !== null) {
|
||||
const argName = match[1];
|
||||
const defaultValue = match[2] || 'false'; // Default to 'false' if no default specified
|
||||
args[argName] = defaultValue;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
46
src/api/corpusManger/corpus.js
Normal file
@ -0,0 +1,46 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function addCategory(data) {
|
||||
return request({
|
||||
url: '/tts/corpus/category',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getCategoryTreeApi() {
|
||||
return request({
|
||||
url: '/tts/corpus/category/tree',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function updateCategoryTreeApi(data) {
|
||||
return request({
|
||||
url: '/tts/corpus/category',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteCategory(id) {
|
||||
return request({
|
||||
url: `/tts/corpus/category/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
||||
export function getCorpusListApi(params) {
|
||||
return request({
|
||||
url: '/tts/corpus/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getDashboardApi() {
|
||||
return request({
|
||||
url: '/tts/corpus/dashboard/stats',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
69
src/api/corpusManger/speech_synthesis.js
Normal file
@ -0,0 +1,69 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function taskApi(data) {
|
||||
return request({
|
||||
url: '/tts/task',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function taskListApi(params) {
|
||||
return request({
|
||||
url: '/tts/task/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getDashboardApi(params) {
|
||||
return request({
|
||||
url: '/tts/task/dashboard/stats',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function downloadWavApi(id) {
|
||||
return request({
|
||||
url: `/tts/task/audio/${id}`,
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTask(ids) {
|
||||
return request({
|
||||
url: `/tts/task/${ids}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function regenerateApi(id) {
|
||||
return request({
|
||||
url: `/tts/task/regenerate/${id}`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function generalizationApi(data) {
|
||||
return request({
|
||||
url: '/tts/task/llm/query',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function batchTaskApi(data) {
|
||||
return request({
|
||||
url: '/tts/task/batch',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function saveTaskApi(data) {
|
||||
return request({
|
||||
url: `/tts/task/save-to-corpus/${data.taskId}/${data.categoryId}`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
@ -61,3 +61,19 @@ export function flowStop(instId) {
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function getArmStatus(params) {
|
||||
return request({
|
||||
url: '/api/arm/getPose',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getAgvStatus(params) {
|
||||
return request({
|
||||
url: '/api/agv/getRuntimeState',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
@ -5,7 +5,7 @@ export function startMicrophoneApi(data) {
|
||||
return request({
|
||||
url: '/api/Microphone/start',
|
||||
method: 'post',
|
||||
params: data
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ export function pauseMicrophoneApi(data) {
|
||||
return request({
|
||||
url: '/api/Microphone/pause',
|
||||
method: 'post',
|
||||
params: data
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ export function resumeMicrophoneApi(data) {
|
||||
return request({
|
||||
url: '/api/Microphone/resume',
|
||||
method: 'post',
|
||||
params: data
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ export function stopMicrophoneApi(data) {
|
||||
return request({
|
||||
url: '/api/Microphone/stop',
|
||||
method: 'post',
|
||||
params: data
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
@ -41,6 +41,6 @@ export function playSpeaker(data) {
|
||||
return request({
|
||||
url: '/api/Speaker/play',
|
||||
method: 'post',
|
||||
params: data
|
||||
data: data
|
||||
})
|
||||
}
|
||||
@ -5,7 +5,7 @@ export function pause(data) {
|
||||
return request({
|
||||
url: '/api/Speaker/pause',
|
||||
method: 'post',
|
||||
params: data
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ export function play(data) {
|
||||
return request({
|
||||
url: '/api/Speaker/play',
|
||||
method: 'post',
|
||||
params: data
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ export function resume(data) {
|
||||
return request({
|
||||
url: '/api/Speaker/resume',
|
||||
method: 'post',
|
||||
params: data
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ export function stop(data) {
|
||||
return request({
|
||||
url: '/api/Speaker/stop',
|
||||
method: 'post',
|
||||
params: data
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ export function setVolume(data) {
|
||||
return request({
|
||||
url: '/api/Speaker/volume',
|
||||
method: 'post',
|
||||
params: data
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
97
src/api/inspection/cockpit.js
Normal file
@ -0,0 +1,97 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getPoseApi(params) {
|
||||
return request({
|
||||
url: '/api/arm/getPose',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getJointStateApi(params) {
|
||||
return request({
|
||||
url: '/api/arm/getJointState',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function speedLApi(data) {
|
||||
return request({
|
||||
url: '/api/arm/speedL',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function moveJApi(data) {
|
||||
return request({
|
||||
url: '/api/arm/moveJ',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function stopMotionApi(params) {
|
||||
return request({
|
||||
url: '/api/arm/stopMotion',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function torqueOnApi(params) {
|
||||
return request({
|
||||
url: '/api/arm/torqueOn',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function speedJApi(data) {
|
||||
return request({
|
||||
url: '/api/arm/speedJ',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function clearFault(params) {
|
||||
return request({
|
||||
url: '/api/arm/clearFault',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getMicVolume(params) {
|
||||
return request({
|
||||
url: '/api/Microphone/volume',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function setMicVolume(data) {
|
||||
return request({
|
||||
url: '/api/Microphone/volume',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getSpeakerVolume(params) {
|
||||
return request({
|
||||
url: '/api/Speaker/volume',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function setSpeakerVolume(data) {
|
||||
return request({
|
||||
url: '/api/Speaker/volume',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
33
src/api/inspection/map.js
Normal file
@ -0,0 +1,33 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
|
||||
export function getMapList(params) {
|
||||
return request({
|
||||
url: '/inspection/map/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function addMap(data) {
|
||||
return request({
|
||||
url: '/inspection/map',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateMap(data) {
|
||||
return request({
|
||||
url: '/inspection/map',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteMap(id) {
|
||||
return request({
|
||||
url: `/inspection/map/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
32
src/api/inspection/point.js
Normal file
@ -0,0 +1,32 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getPointList(params) {
|
||||
return request({
|
||||
url: '/inspection/waypoint/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function addPoint(data) {
|
||||
return request({
|
||||
url: '/inspection/waypoint',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updatePoint(data) {
|
||||
return request({
|
||||
url: '/inspection/waypoint',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deletePoint(id) {
|
||||
return request({
|
||||
url: `/inspection/waypoint/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
23
src/api/inspection/result.js
Normal file
@ -0,0 +1,23 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 查询巡检结果列表。
|
||||
*/
|
||||
export function getInspectionResultList(params) {
|
||||
return request({
|
||||
url: '/inspection/result/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交人工复核结果。
|
||||
*/
|
||||
export function reviewInspectionResult(id, data) {
|
||||
return request({
|
||||
url: `/inspection/result/${id}/review`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
107
src/api/inspection/robot.js
Normal file
@ -0,0 +1,107 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getRobotList(params) {
|
||||
return request({
|
||||
url: '/inspection/robot/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function addRobot(data) {
|
||||
return request({
|
||||
url: '/inspection/robot',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRobot(data) {
|
||||
return request({
|
||||
url: '/inspection/robot',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRobot(id) {
|
||||
return request({
|
||||
url: `/inspection/robot/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器人地图
|
||||
* @param {*} robotId
|
||||
* @returns
|
||||
*/
|
||||
export function getMapByRobot(robotId) {
|
||||
return request({
|
||||
url: `/inspection/robot/${robotId}/maps`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地图JSON数据
|
||||
* @param {*} params
|
||||
* @returns
|
||||
*/
|
||||
export function getMapJson(params) {
|
||||
return request({
|
||||
url: `/inspection/robot/${params.robotId}/download-map`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器人位置
|
||||
* @param {*} robotId
|
||||
* @returns
|
||||
*/
|
||||
export function getRobotLocation(robotId) {
|
||||
return request({
|
||||
url: `/inspection/robot/${robotId}/location`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器人状态
|
||||
* @param {*} robotId
|
||||
* @returns
|
||||
*/
|
||||
export function getRobotStatus(robotId) {
|
||||
return request({
|
||||
url: `/inspection/robot/{robotId}/sync-status`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人运动控制
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
export function moveRobot(data) {
|
||||
return request({
|
||||
url: '/api/agv/setVelocity',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人停止运动
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
export function stopRobot(data) {
|
||||
return request({
|
||||
url: '/api/agv/stopVelocityControl',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
75
src/api/inspection/runTask.js
Normal file
@ -0,0 +1,75 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getRunTaskList(params) {
|
||||
return request({
|
||||
url: '/inspection/taskInstance/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getRunTaskDetail(id) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addRunTask(data) {
|
||||
return request({
|
||||
url: '/inspection/taskInstance',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRunTask(data) {
|
||||
return request({
|
||||
url: '/inspection/taskInstance',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRunTask(id) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function startRunTask(id) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/start/${id}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function stopRunTask(id) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/stop/${id}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function pauseRunTask(id) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/pause/${id}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function resumeRunTask(id) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/resume/${id}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function getTaskLogListApi(params) {
|
||||
return request({
|
||||
url: '/inspection/taskLog/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
47
src/api/inspection/task.js
Normal file
@ -0,0 +1,47 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getTaskList(params) {
|
||||
return request({
|
||||
url: '/inspection/task/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function addTask(data) {
|
||||
return request({
|
||||
url: '/inspection/task',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTask(data) {
|
||||
return request({
|
||||
url: '/inspection/task',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTask(id) {
|
||||
return request({
|
||||
url: `/inspection/task/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function getWaypointByTaskId(taskId) {
|
||||
return request({
|
||||
url: `/inspection/task/${taskId}/waypoints`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function bindWaypointsToTask(taskId, waypointIds) {
|
||||
return request({
|
||||
url: `/inspection/task/${taskId}/waypoints`,
|
||||
method: 'post',
|
||||
data: waypointIds
|
||||
})
|
||||
}
|
||||
32
src/api/intelligenceTest/car.js
Normal file
@ -0,0 +1,32 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function addVehicle(data) {
|
||||
return request({
|
||||
url: '/aima/vehicle',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getVehicleList(params) {
|
||||
return request({
|
||||
url: '/aima/vehicle/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function updateVehicle(data) {
|
||||
return request({
|
||||
url: '/aima/vehicle',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteVehicle(ids) {
|
||||
return request({
|
||||
url: `/aima/vehicle/${ids}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
32
src/api/intelligenceTest/mobilePhone.js
Normal file
@ -0,0 +1,32 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function addPhone(data) {
|
||||
return request({
|
||||
url: '/aima/phone',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getPhoneList(params) {
|
||||
return request({
|
||||
url: '/aima/phone/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function updatePhone(data) {
|
||||
return request({
|
||||
url: '/aima/phone',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deletePhone(ids) {
|
||||
return request({
|
||||
url: `/aima/phone/${ids}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
69
src/api/intelligenceTest/runningTask.js
Normal file
@ -0,0 +1,69 @@
|
||||
import request from '@/utils/request'
|
||||
import { method } from 'lodash'
|
||||
|
||||
export function addTaskinstance(data) {
|
||||
return request({
|
||||
url: '/aima/taskinstance',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getTaskinstanceList(params) {
|
||||
return request({
|
||||
url: '/aima/taskinstance/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTaskinstance(data) {
|
||||
return request({
|
||||
url: '/aima/taskinstance',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTaskinstance(ids) {
|
||||
return request({
|
||||
url: `/aima/taskinstance/{ids}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function startTaskinstance(id) {
|
||||
return request({
|
||||
url: `/aima/taskinstance/start/${id}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function pauseTaskinstance(id) {
|
||||
return request({
|
||||
url: `/aima/taskinstance/pause/${id}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function stopTaskinstance(id) {
|
||||
return request({
|
||||
url: `/aima/taskinstance/stop/${id}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function resumeTaskinstance(id) {
|
||||
return request({
|
||||
url: `/aima/taskinstance/resume/${id}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function getTaskLogListApi(params) {
|
||||
return request({
|
||||
url: '/aima/testlog/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
47
src/api/intelligenceTest/taskManage.js
Normal file
@ -0,0 +1,47 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function addTask(data) {
|
||||
return request({
|
||||
url: '/aima/task',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getTaskList(params) {
|
||||
return request({
|
||||
url: '/aima/task/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTask(data) {
|
||||
return request({
|
||||
url: '/aima/task',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTask(ids) {
|
||||
return request({
|
||||
url: `/aima/task/${ids}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function getTestCaseByTaskId(taskId) {
|
||||
return request({
|
||||
url: `/aima/task/testCases/${taskId}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function bindTastCase(taskId, ids) {
|
||||
return request({
|
||||
url: `/aima/task/bindTestCases/${taskId}`,
|
||||
method: 'post',
|
||||
data: ids
|
||||
})
|
||||
}
|
||||
32
src/api/intelligenceTest/testCase.js
Normal file
@ -0,0 +1,32 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function addTestCase(data) {
|
||||
return request({
|
||||
url: '/aima/testcase',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getTestCaseList(params) {
|
||||
return request({
|
||||
url: '/aima/testcase/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTestCase(data) {
|
||||
return request({
|
||||
url: '/aima/testcase',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTestCase(ids) {
|
||||
return request({
|
||||
url: `/aima/testcase/${ids}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@ -39,6 +39,15 @@ export function getInfo() {
|
||||
})
|
||||
}
|
||||
|
||||
// 解锁屏幕
|
||||
export function unlockScreen(password) {
|
||||
return request({
|
||||
url: '/unlockscreen',
|
||||
method: 'post',
|
||||
data: { password }
|
||||
})
|
||||
}
|
||||
|
||||
// 退出方法
|
||||
export function logout() {
|
||||
return request({
|
||||
|
||||
@ -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) {
|
||||
return request({
|
||||
|
||||
@ -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) {
|
||||
return request({
|
||||
|
||||
@ -42,3 +42,38 @@ export function delNotice(noticeId) {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ export function updateUserPwd(oldPassword, newPassword) {
|
||||
return request({
|
||||
url: '/system/user/profile/updatePwd',
|
||||
method: 'put',
|
||||
params: data
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -62,7 +62,7 @@ export function getInst(query) {
|
||||
}
|
||||
|
||||
// 查询任务实例日志
|
||||
export function getLogicFlow(query) {
|
||||
export function getFlowRuntimeLog(query) {
|
||||
return request({
|
||||
url: '/test/inst/log/query',
|
||||
method: 'get',
|
||||
|
||||
4
src/assets/icons/svg/add.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?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="1783317138800" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12777" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
|
||||
<path d="M898.004474 894.186428 798.735272 894.186428 798.735272 993.45563C798.735272 1009.90301 785.401891 1023.236391 768.954512 1023.236391 752.507132 1023.236391 739.173751 1009.90301 739.173751 993.45563L739.173751 894.186428 639.904549 894.186428C623.457169 894.186428 610.123788 880.853047 610.123788 864.405667 610.123788 847.958288 623.457169 834.624907 639.904549 834.624907L739.173751 834.624907 739.173751 735.355705C739.173751 718.908325 752.507132 705.574944 768.954512 705.574944 785.401891 705.574944 798.735272 718.908325 798.735272 735.355705L798.735272 834.624907 898.004474 834.624907C914.451854 834.624907 927.785235 847.958288 927.785235 864.405667 927.785235 880.853047 914.451854 894.186428 898.004474 894.186428ZM768.954512 641.049963C752.507132 641.049963 739.173751 627.716582 739.173751 611.269202L739.173751 60.32513 415.012081 60.32513C415.013608 60.452653 415.021626 60.579031 415.021626 60.706935L415.021626 291.316928C415.021626 307.764307 401.688245 321.097688 385.240865 321.097688L155.776286 321.097688 155.776286 836.152125 515.054437 836.152125C531.501817 836.152125 544.835198 849.485506 544.835198 865.932886 544.835198 882.380265 531.501817 895.713647 515.054437 895.713647L125.995526 895.713647C109.548146 895.713647 96.214765 882.380265 96.214765 865.932886L96.214765 242.064131C96.214765 237.412987 97.282291 233.011544 99.18406 229.089647 99.90567 226.826309 101.078574 224.786327 102.737515 223.086151L309.85087 10.837906C310.512919 10.088805 311.210476 9.372158 311.943541 8.693309L314.300802 6.277632C317.094849 3.414097 320.879296 1.89146 325.120764 1.610452 327.381429 1.061417 329.7406 0.763609 332.170022 0.763609L768.954512 0.763609C785.401891 0.763609 798.735272 14.09699 798.735272 30.54437L798.735272 611.269202C798.735272 627.716582 785.401891 641.049963 768.954512 641.049963ZM344.780647 60.32513 155.776286 254.015761 155.776286 261.536167 355.460104 261.536167 355.460104 60.706935C355.460104 60.579031 355.468122 60.452653 355.46965 60.32513L344.780647 60.32513Z" p-id="12778" fill="currentColor"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
1
src/assets/icons/svg/bell.svg
Normal 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 |
1
src/assets/icons/svg/consuming.svg
Normal 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="1783317313942" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17054" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M672.881778 502.215111a225.28 225.28 0 0 1-77.141334 50.688A225.450667 225.450667 0 0 1 512 568.888889a225.450667 225.450667 0 0 1-83.740444-15.928889 225.28 225.28 0 0 1-77.141334-50.744889 225.223111 225.223111 0 0 1-50.688-77.141333A225.564444 225.564444 0 0 1 284.444444 341.333333c0-28.672 5.347556-57.116444 15.928889-83.740444 11.605333-29.240889 28.501333-54.954667 50.744889-77.141333a225.28 225.28 0 0 1 77.141334-50.688A225.564444 225.564444 0 0 1 512 113.777778c28.672 0 57.116444 5.347556 83.740444 15.928889 29.240889 11.605333 54.954667 28.501333 77.141334 50.744889 22.186667 22.186667 39.139556 47.900444 50.688 77.141333 10.638222 26.624 16.042667 55.068444 15.985778 83.740444 0 28.672-5.404444 57.116444-15.928889 83.740445a225.28 225.28 0 0 1-50.744889 77.141333z m-50.801778-270.961778a154.168889 154.168889 0 0 0-52.792889-34.702222A154.339556 154.339556 0 0 0 512 185.628444c-19.626667 0-39.082667 3.697778-57.287111 10.922667a154.112 154.112 0 0 0-52.792889 34.702222c-15.075556 15.018667-26.908444 32.995556-34.702222 52.792889-7.224889 18.204444-10.922667 37.660444-10.922667 57.287111 0 19.797333 3.640889 38.912 10.922667 57.287111 7.793778 19.797333 19.626667 37.774222 34.702222 52.792889 15.018667 15.075556 32.995556 26.908444 52.792889 34.702223 18.204444 7.224889 37.660444 10.979556 57.287111 10.922666 19.626667 0 39.082667-3.697778 57.287111-10.922666 19.797333-7.793778 37.774222-19.626667 52.792889-34.702223 15.075556-15.018667 26.908444-32.995556 34.702222-52.792889 7.224889-18.204444 10.922667-37.660444 10.922667-57.287111 0-19.626667-3.697778-39.082667-10.922667-57.287111a153.998222 153.998222 0 0 0-34.702222-52.792889z" p-id="17055" fill="currentColor"></path><path d="M228.750222 616.220444a388.152889 388.152889 0 0 1 122.481778-88.177777 368.64 368.64 0 0 1 134.371556-33.052445 33.678222 33.678222 0 0 1 25.144888 9.671111c6.883556 6.599111 10.979556 15.928889 11.434667 25.770667a38.912 38.912 0 0 1-8.931555 26.851556 34.133333 34.133333 0 0 1-24.177778 12.288 302.819556 302.819556 0 0 0-110.364445 27.192888 318.691556 318.691556 0 0 0-100.579555 72.362667 342.357333 342.357333 0 0 0-68.380445 106.951111 359.992889 359.992889 0 0 0-26.055111 117.703111c-1.024 20.593778-17.578667 36.408889-36.864 35.271111a34.133333 34.133333 0 0 1-24.120889-12.401777 38.912 38.912 0 0 1-8.874666-26.965334 438.385778 438.385778 0 0 1 31.687111-143.246222 416.938667 416.938667 0 0 1 83.228444-130.275555z m426.894222 333.824c-19.342222 0-34.986667-16.725333-34.986666-37.319111v-231.537777c0-20.650667 15.644444-37.319111 34.986666-37.319112 19.342222 0 34.986667 16.668444 34.986667 37.319112v231.537777c0 20.593778-15.644444 37.319111-34.986667 37.319111z m-111.957333-61.610666c-19.342222 0-34.986667-16.725333-34.986667-37.319111V744.675556c0-20.650667 15.644444-37.376 34.986667-37.376 19.342222 0 34.986667 16.725333 34.986667 37.376v106.382222c0 20.650667-15.701333 37.376-34.986667 37.376z m223.971556 0c-19.342222 0-34.986667-16.725333-34.986667-37.319111V744.675556c0-20.650667 15.644444-37.376 34.986667-37.376 19.342222 0 34.986667 16.725333 34.986666 37.376v106.382222c0 20.650667-15.701333 37.376-34.986666 37.376zM967.111111 797.013333c0 20.593778-15.644444 37.319111-34.986667 37.319111h-45.511111c-19.342222 0-34.986667-16.725333-34.986666-37.319111 0-20.650667 15.644444-37.376 34.986666-37.376h45.511111c19.342222 0 34.986667 16.725333 34.986667 37.376z" p-id="17056" fill="currentColor"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
1
src/assets/icons/svg/enter.svg
Normal 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 |
1
src/assets/icons/svg/more-up.svg
Normal 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 |
6
src/assets/icons/svg/pause.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?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="1780372291375" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5453" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
|
||||
<path d="M624.941176 360.929882c-12.408471 0-22.528 13.432471-22.528 30.117647v301.176471c0 16.624941 10.119529 30.117647 22.528 30.117647h45.296942c12.408471 0 22.528-13.492706 22.528-30.117647v-301.176471c0-16.685176-10.119529-30.117647-22.528-30.117647h-45.296942z m-271.058823 0c-12.408471 0-22.528 13.432471-22.528 30.117647v301.176471c0 16.624941 10.119529 30.117647 22.528 30.117647h45.296941c12.408471 0 22.528-13.492706 22.528-30.117647v-301.176471c0-16.685176-10.119529-30.117647-22.528-30.117647h-45.296941z" fill="currentColor" p-id="5454"></path>
|
||||
<path d="M250.036706 138.059294a141.673412 141.673412 0 0 0-141.552941 141.552941v524.047059a141.673412 141.673412 0 0 0 141.552941 141.552941h524.047059a141.673412 141.673412 0 0 0 141.552941-141.552941v-524.047059a141.673412 141.673412 0 0 0-141.552941-141.552941h-524.047059z m524.047059 885.458824h-524.047059a220.099765 220.099765 0 0 1-219.858824-219.858824v-524.047059a220.099765 220.099765 0 0 1 219.858824-219.858823h524.047059a220.099765 220.099765 0 0 1 219.858823 219.858823v524.047059a220.099765 220.099765 0 0 1-219.858823 219.858824z" fill="currentColor" p-id="5455"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
src/assets/icons/svg/remove.svg
Normal 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="1780472431337" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19258" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M814.29 136.567H207.664c-55.752 0-101.274 13.3-101.274 56.776v26.086h808.663v-26.086c0.511-42.965-45.011-56.776-100.763-56.776" p-id="19259" fill="#2c2c2c"></path><path d="M723.245 191.808l-19.948-140.66c-3.58-27.62-29.667-50.125-57.287-50.125H376.456c-28.132 0-53.707 22.505-57.799 50.126l-19.948 141.17c-3.58 27.621 15.856 22.506 43.477 22.506h337.07c28.133-0.511 47.57 4.604 43.989-23.017z m-360.6-28.643L377.99 51.66h265.463l15.344 111.505H362.645zM831.17 282.342H190.785c-36.827 0-64.959 30.177-61.378 67.005l55.24 607.648c3.58 36.827 36.316 67.005 73.655 67.005h505.35c36.828 0 70.074-30.178 73.655-67.005l55.24-607.137c3.58-36.827-24.04-67.516-61.378-67.516zM376.456 953.415H245.514l-43.476-592.816h174.418v592.816z m221.474 0H423.512V360.599H597.93v592.816z m177.487 0H646.01V360.599h172.883l-43.476 592.816z" p-id="19260" fill="#2c2c2c"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -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 |
6
src/assets/icons/svg/start.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?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="1780371806721" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1692" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
|
||||
<path d="M703.146667 479.146667L469.333333 285.866667a42.666667 42.666667 0 0 0-69.973333 32.853333v386.56a42.666667 42.666667 0 0 0 69.973333 32.853333l232.106667-193.28a42.666667 42.666667 0 0 0 1.706667-65.706666z" fill="currentColor" p-id="1693"></path>
|
||||
<path d="M250.036706 138.059294a141.673412 141.673412 0 0 0-141.552941 141.552941v524.047059a141.673412 141.673412 0 0 0 141.552941 141.552941h524.047059a141.673412 141.673412 0 0 0 141.552941-141.552941v-524.047059a141.673412 141.673412 0 0 0-141.552941-141.552941h-524.047059z m524.047059 885.458824h-524.047059a220.099765 220.099765 0 0 1-219.858824-219.858824v-524.047059a220.099765 220.099765 0 0 1 219.858824-219.858823h524.047059a220.099765 220.099765 0 0 1 219.858823 219.858823v524.047059a220.099765 220.099765 0 0 1-219.858823 219.858824z" fill="currentColor" p-id="5455"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
6
src/assets/icons/svg/stop.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?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="1780378438689" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15665" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
|
||||
<path d="M877.696 0H146.24A146.24 146.24 0 0 0 0 146.24v731.456A146.24 146.24 0 0 0 146.24 1024h731.456A146.24 146.24 0 0 0 1024 877.696V146.24A146.24 146.24 0 0 0 877.696 0z m73.152 877.696c0 40.448-32.768 73.216-73.216 73.216H146.24a73.216 73.216 0 0 1-73.152-73.216V146.24c0-40.384 32.768-73.152 73.152-73.152h731.456c40.448 0 73.216 32.768 73.216 73.152v731.456z" fill="currentColor" p-id="15666"></path>
|
||||
<path d="M688.512 644.224a40.32 40.32 0 0 1-40.384 40.32H371.072a40.32 40.32 0 0 1-40.32-40.32V367.168a40.32 40.32 0 0 1 40.32-40.32h277.056a40.32 40.32 0 0 1 40.384 40.32v277.056z" fill="currentColor" p-id="15667"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 966 B |
5
src/assets/icons/svg/task.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?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="1783317214101" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14894" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M799.197483 1024H224.802517A130.013483 130.013483 0 0 1 94.881079 894.067056V227.374022A130.013483 130.013483 0 0 1 224.802517 97.452584h47.65627v64.960719h-47.65627a65.018247 65.018247 0 0 0-64.972225 64.960719v666.693034a65.018247 65.018247 0 0 0 64.960719 64.960719h574.406472a65.018247 65.018247 0 0 0 64.960719-64.960719V227.374022a65.018247 65.018247 0 0 0-64.960719-64.960719h-42.099056V97.452584h42.099056a130.013483 130.013483 0 0 1 129.921438 129.921438v666.693034A130.013483 130.013483 0 0 1 799.197483 1024z" fill="currentColor" p-id="14895"></path><path d="M644.308854 259.854382h-264.629214a129.932944 129.932944 0 0 1 0-259.854382h264.629214a129.932944 129.932944 0 0 1 0 259.854382z m-264.629214-194.893663a64.972225 64.972225 0 0 0 0 129.932944h264.629214a64.972225 64.972225 0 0 0 0-129.932944z" fill="currentColor" p-id="14896"></path>
|
||||
<path d="M241.796315 374.910562m32.480359 0l475.446652 0q32.48036 0 32.480359 32.480359l0 0q0 32.48036-32.480359 32.48036l-475.446652 0q-32.48036 0-32.480359-32.48036l0 0q0-32.48036 32.480359-32.480359Z" fill="currentColor" p-id="14897"></path><path d="M241.796315 541.845573m32.480359 0l475.446652 0q32.48036 0 32.480359 32.48036l0 0q0 32.48036-32.480359 32.480359l-475.446652 0q-32.48036 0-32.480359-32.480359l0 0q0-32.48036 32.480359-32.48036Z" fill="currentColor" p-id="14898"></path><path d="M241.796315 708.780584m32.480359 0l475.446652 0q32.48036 0 32.480359 32.48036l0 0q0 32.48036-32.480359 32.480359l-475.446652 0q-32.48036 0-32.480359-32.480359l0 0q0-32.48036 32.480359-32.48036Z" fill="currentColor" p-id="14899"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
4
src/assets/icons/svg/timbre.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?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="1783317427036" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19916" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
|
||||
<path d="M874.18 149.92C777.456 53.254 648.91 0 512.102 0 375.343 0 246.78 53.255 150.097 149.93 53.38 246.635 0.118 375.225 0.118 512.003 0.094 648.796 53.314 777.357 150.02 874.122 246.69 970.736 375.266 1024 512.007 1024c136.807 0 265.425-53.298 362.172-149.979 199.588-199.608 199.619-524.417 0-724.102z m-47.186 676.907C742.864 910.946 631 957.32 512.008 957.32c-118.907 0-230.744-46.35-314.82-130.392-84.077-84.093-130.368-195.934-130.35-314.924 0-118.973 46.314-230.826 130.42-314.899C281.354 113.03 393.154 66.731 512.104 66.731c118.955 0 230.779 46.297 314.891 130.38 173.59 173.631 173.59 456.144 0 629.716z m-93.643-189.351L556.37 535.125V256c0-23.557-19.111-42.667-42.674-42.667-23.545 0-42.656 19.11-42.656 42.667v298.67c0 15.826 19.671 29.096 32.468 36.437 3.538 5.33 13.268 10.153 19.2 13.565l176 106.673c20.388 11.77 42.495 4.775 54.27-15.62 11.776-20.4 0.812-46.469-19.63-58.244v-0.005z m0 0" p-id="19917" fill="currentColor"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -1,4 +1,4 @@
|
||||
@import './variables.module.scss';
|
||||
@use './variables.module.scss' as *;
|
||||
|
||||
@mixin colorBtn($color) {
|
||||
background: $color;
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
@import './variables.module.scss';
|
||||
@import './mixin.scss';
|
||||
@import './transition.scss';
|
||||
@import './element-ui.scss';
|
||||
@import './sidebar.scss';
|
||||
@import './btn.scss';
|
||||
@import './ruoyi.scss';
|
||||
@use './variables.module.scss';
|
||||
@use './mixin.scss';
|
||||
@use './transition.scss';
|
||||
@use './element-ui.scss';
|
||||
@use './sidebar.scss';
|
||||
@use './btn.scss';
|
||||
@use './ruoyi.scss';
|
||||
@use './legacy-layout.scss';
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
|
||||
6
src/assets/styles/legacy-layout.scss
Normal 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;
|
||||
}
|
||||
@ -1,7 +1,10 @@
|
||||
@use './variables.module.scss' as *;
|
||||
|
||||
#app {
|
||||
|
||||
.main-container {
|
||||
min-height: 100%;
|
||||
// min-height: 100%;
|
||||
height: 100%;
|
||||
transition: margin-left .28s;
|
||||
margin-left: $base-sidebar-width;
|
||||
position: relative;
|
||||
|
||||
@ -78,10 +78,10 @@ watch(() => props.cron.hour, value => changeRadioValue(value))
|
||||
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
|
||||
function changeRadioValue(value) {
|
||||
if (props.cron.min === '*') {
|
||||
emit('update', 'min', '0', 'hour');
|
||||
emit('update', 'min', '0', 'hour')
|
||||
}
|
||||
if (props.cron.second === '*') {
|
||||
emit('update', 'second', '0', 'hour');
|
||||
emit('update', 'second', '0', 'hour')
|
||||
}
|
||||
if (value === '*') {
|
||||
radioValue.value = 1
|
||||
|
||||
@ -70,42 +70,46 @@
|
||||
<p class="title">时间表达式</p>
|
||||
<table>
|
||||
<thead>
|
||||
<th v-for="item of tabTitles" :key="item">{{item}}</th>
|
||||
<th>Cron 表达式</th>
|
||||
<tr>
|
||||
<th v-for="item of tabTitles" :key="item">{{item}}</th>
|
||||
<th>Cron 表达式</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
|
||||
</td>
|
||||
<td class="result">
|
||||
<span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
|
||||
<el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
|
||||
</td>
|
||||
<tr>
|
||||
<td>
|
||||
<span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
|
||||
</td>
|
||||
<td class="result">
|
||||
<span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
|
||||
<el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@ -26,289 +26,289 @@ watch(() => props.ex, () => expressionChange())
|
||||
// 表达式值变化时,开始去计算结果
|
||||
function expressionChange() {
|
||||
// 计算开始-隐藏结果
|
||||
isShow.value = false;
|
||||
isShow.value = false
|
||||
// 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
|
||||
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 nYear = nTime.getFullYear();
|
||||
let nMonth = nTime.getMonth() + 1;
|
||||
let nDay = nTime.getDate();
|
||||
let nHour = nTime.getHours();
|
||||
let nMin = nTime.getMinutes();
|
||||
let nSecond = nTime.getSeconds();
|
||||
let nTime = new Date()
|
||||
let nYear = nTime.getFullYear()
|
||||
let nMonth = nTime.getMonth() + 1
|
||||
let nDay = nTime.getDate()
|
||||
let nHour = nTime.getHours()
|
||||
let nMin = nTime.getMinutes()
|
||||
let nSecond = nTime.getSeconds()
|
||||
// 根据规则获取到近100年可能年数组、月数组等等
|
||||
getSecondArr(ruleArr[0]);
|
||||
getMinArr(ruleArr[1]);
|
||||
getHourArr(ruleArr[2]);
|
||||
getDayArr(ruleArr[3]);
|
||||
getMonthArr(ruleArr[4]);
|
||||
getWeekArr(ruleArr[5]);
|
||||
getYearArr(ruleArr[6], nYear);
|
||||
getSecondArr(ruleArr[0])
|
||||
getMinArr(ruleArr[1])
|
||||
getHourArr(ruleArr[2])
|
||||
getDayArr(ruleArr[3])
|
||||
getMonthArr(ruleArr[4])
|
||||
getWeekArr(ruleArr[5])
|
||||
getYearArr(ruleArr[6], nYear)
|
||||
// 将获取到的数组赋值-方便使用
|
||||
let sDate = dateArr.value[0];
|
||||
let mDate = dateArr.value[1];
|
||||
let hDate = dateArr.value[2];
|
||||
let DDate = dateArr.value[3];
|
||||
let MDate = dateArr.value[4];
|
||||
let YDate = dateArr.value[5];
|
||||
let sDate = dateArr.value[0]
|
||||
let mDate = dateArr.value[1]
|
||||
let hDate = dateArr.value[2]
|
||||
let DDate = dateArr.value[3]
|
||||
let MDate = dateArr.value[4]
|
||||
let YDate = dateArr.value[5]
|
||||
// 获取当前时间在数组中的索引
|
||||
let sIdx = getIndex(sDate, nSecond);
|
||||
let mIdx = getIndex(mDate, nMin);
|
||||
let hIdx = getIndex(hDate, nHour);
|
||||
let DIdx = getIndex(DDate, nDay);
|
||||
let MIdx = getIndex(MDate, nMonth);
|
||||
let YIdx = getIndex(YDate, nYear);
|
||||
let sIdx = getIndex(sDate, nSecond)
|
||||
let mIdx = getIndex(mDate, nMin)
|
||||
let hIdx = getIndex(hDate, nHour)
|
||||
let DIdx = getIndex(DDate, nDay)
|
||||
let MIdx = getIndex(MDate, nMonth)
|
||||
let YIdx = getIndex(YDate, nYear)
|
||||
// 重置月日时分秒的函数(后面用的比较多)
|
||||
const resetSecond = function () {
|
||||
sIdx = 0;
|
||||
sIdx = 0
|
||||
nSecond = sDate[sIdx]
|
||||
}
|
||||
const resetMin = function () {
|
||||
mIdx = 0;
|
||||
mIdx = 0
|
||||
nMin = mDate[mIdx]
|
||||
resetSecond();
|
||||
resetSecond()
|
||||
}
|
||||
const resetHour = function () {
|
||||
hIdx = 0;
|
||||
hIdx = 0
|
||||
nHour = hDate[hIdx]
|
||||
resetMin();
|
||||
resetMin()
|
||||
}
|
||||
const resetDay = function () {
|
||||
DIdx = 0;
|
||||
DIdx = 0
|
||||
nDay = DDate[DIdx]
|
||||
resetHour();
|
||||
resetHour()
|
||||
}
|
||||
const resetMonth = function () {
|
||||
MIdx = 0;
|
||||
MIdx = 0
|
||||
nMonth = MDate[MIdx]
|
||||
resetDay();
|
||||
resetDay()
|
||||
}
|
||||
// 如果当前年份不为数组中当前值
|
||||
if (nYear !== YDate[YIdx]) {
|
||||
resetMonth();
|
||||
resetMonth()
|
||||
}
|
||||
// 如果当前月份不为数组中当前值
|
||||
if (nMonth !== MDate[MIdx]) {
|
||||
resetDay();
|
||||
resetDay()
|
||||
}
|
||||
// 如果当前“日”不为数组中当前值
|
||||
if (nDay !== DDate[DIdx]) {
|
||||
resetHour();
|
||||
resetHour()
|
||||
}
|
||||
// 如果当前“时”不为数组中当前值
|
||||
if (nHour !== hDate[hIdx]) {
|
||||
resetMin();
|
||||
resetMin()
|
||||
}
|
||||
// 如果当前“分”不为数组中当前值
|
||||
if (nMin !== mDate[mIdx]) {
|
||||
resetSecond();
|
||||
resetSecond()
|
||||
}
|
||||
// 循环年份数组
|
||||
goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
|
||||
let YY = YDate[Yi];
|
||||
let YY = YDate[Yi]
|
||||
// 如果到达最大值时
|
||||
if (nMonth > MDate[MDate.length - 1]) {
|
||||
resetMonth();
|
||||
continue;
|
||||
resetMonth()
|
||||
continue
|
||||
}
|
||||
// 循环月份数组
|
||||
goMonth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
|
||||
// 赋值、方便后面运算
|
||||
let MM = MDate[Mi];
|
||||
MM = MM < 10 ? '0' + MM : MM;
|
||||
MM = MM < 10 ? '0' + MM : MM
|
||||
// 如果到达最大值时
|
||||
if (nDay > DDate[DDate.length - 1]) {
|
||||
resetDay();
|
||||
resetDay()
|
||||
if (Mi === MDate.length - 1) {
|
||||
resetMonth();
|
||||
continue goYear;
|
||||
resetMonth()
|
||||
continue goYear
|
||||
}
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
// 循环日期数组
|
||||
goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
|
||||
// 赋值、方便后面运算
|
||||
let DD = DDate[Di];
|
||||
let thisDD = DD < 10 ? '0' + DD : DD;
|
||||
let DD = DDate[Di]
|
||||
let thisDD = DD < 10 ? '0' + DD : DD
|
||||
// 如果到达最大值时
|
||||
if (nHour > hDate[hDate.length - 1]) {
|
||||
resetHour();
|
||||
resetHour()
|
||||
if (Di === DDate.length - 1) {
|
||||
resetDay();
|
||||
resetDay()
|
||||
if (Mi === MDate.length - 1) {
|
||||
resetMonth();
|
||||
continue goYear;
|
||||
resetMonth()
|
||||
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') {
|
||||
resetDay();
|
||||
continue goMonth;
|
||||
resetDay()
|
||||
continue goMonth
|
||||
}
|
||||
// 如果日期规则中有值时
|
||||
if (dayRule.value === 'lastDay') {
|
||||
// 如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
|
||||
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
DD--;
|
||||
thisDD = DD < 10 ? '0' + DD : DD;
|
||||
DD--
|
||||
thisDD = DD < 10 ? '0' + DD : DD
|
||||
}
|
||||
}
|
||||
} else if (dayRule.value === 'workDay') {
|
||||
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
|
||||
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
DD--;
|
||||
thisDD = DD < 10 ? '0' + DD : DD;
|
||||
DD--
|
||||
thisDD = DD < 10 ? '0' + DD : DD
|
||||
}
|
||||
}
|
||||
// 获取达到条件的日期是星期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) {
|
||||
// 先找下一个日,并判断是否为月底
|
||||
DD++;
|
||||
thisDD = DD < 10 ? '0' + DD : DD;
|
||||
DD++
|
||||
thisDD = DD < 10 ? '0' + DD : DD
|
||||
// 判断下一日已经不是合法日期
|
||||
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
DD -= 3;
|
||||
DD -= 3
|
||||
}
|
||||
} else if (thisWeek === 7) {
|
||||
// 当星期6时只需判断不是1号就可进行操作
|
||||
if (dayRuleSup.value !== 1) {
|
||||
DD--;
|
||||
DD--
|
||||
} else {
|
||||
DD += 2;
|
||||
DD += 2
|
||||
}
|
||||
}
|
||||
} 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)中
|
||||
if (dayRuleSup.value.indexOf(thisWeek) < 0) {
|
||||
// 如果到达最大值时
|
||||
if (Di === DDate.length - 1) {
|
||||
resetDay();
|
||||
resetDay()
|
||||
if (Mi === MDate.length - 1) {
|
||||
resetMonth();
|
||||
continue goYear;
|
||||
resetMonth()
|
||||
continue goYear
|
||||
}
|
||||
continue goMonth;
|
||||
continue goMonth
|
||||
}
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
} else if (dayRule.value === 'assWeek') {
|
||||
// 如果指定了是第几周的星期几
|
||||
// 获取每月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) {
|
||||
DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1;
|
||||
DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1
|
||||
} 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') {
|
||||
// 如果指定了每月最后一个星期几
|
||||
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
|
||||
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
DD--;
|
||||
thisDD = DD < 10 ? '0' + DD : 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) {
|
||||
DD -= thisWeek - dayRuleSup.value;
|
||||
DD -= thisWeek - dayRuleSup.value
|
||||
} else if (dayRuleSup.value > thisWeek) {
|
||||
DD -= 7 - (dayRuleSup.value - thisWeek)
|
||||
}
|
||||
}
|
||||
// 判断时间值是否小于10置换成“05”这种格式
|
||||
DD = DD < 10 ? '0' + DD : DD;
|
||||
DD = DD < 10 ? '0' + DD : DD
|
||||
// 循环“时”数组
|
||||
goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
|
||||
let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
|
||||
// 如果到达最大值时
|
||||
if (nMin > mDate[mDate.length - 1]) {
|
||||
resetMin();
|
||||
resetMin()
|
||||
if (hi === hDate.length - 1) {
|
||||
resetHour();
|
||||
resetHour()
|
||||
if (Di === DDate.length - 1) {
|
||||
resetDay();
|
||||
resetDay()
|
||||
if (Mi === MDate.length - 1) {
|
||||
resetMonth();
|
||||
continue goYear;
|
||||
resetMonth()
|
||||
continue goYear
|
||||
}
|
||||
continue goMonth;
|
||||
continue goMonth
|
||||
}
|
||||
continue goDay;
|
||||
continue goDay
|
||||
}
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
// 循环"分"数组
|
||||
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]) {
|
||||
resetSecond();
|
||||
resetSecond()
|
||||
if (mi === mDate.length - 1) {
|
||||
resetMin();
|
||||
resetMin()
|
||||
if (hi === hDate.length - 1) {
|
||||
resetHour();
|
||||
resetHour()
|
||||
if (Di === DDate.length - 1) {
|
||||
resetDay();
|
||||
resetDay()
|
||||
if (Mi === MDate.length - 1) {
|
||||
resetMonth();
|
||||
continue goYear;
|
||||
resetMonth()
|
||||
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++) {
|
||||
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') {
|
||||
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) {
|
||||
resetSecond();
|
||||
resetSecond()
|
||||
if (mi === mDate.length - 1) {
|
||||
resetMin();
|
||||
resetMin()
|
||||
if (hi === hDate.length - 1) {
|
||||
resetHour();
|
||||
resetHour()
|
||||
if (Di === DDate.length - 1) {
|
||||
resetDay();
|
||||
resetDay()
|
||||
if (Mi === MDate.length - 1) {
|
||||
resetMonth();
|
||||
continue goYear;
|
||||
resetMonth()
|
||||
continue goYear
|
||||
}
|
||||
continue goMonth;
|
||||
continue goMonth
|
||||
}
|
||||
continue goDay;
|
||||
continue goDay
|
||||
}
|
||||
continue goHour;
|
||||
continue goHour
|
||||
}
|
||||
continue goMin;
|
||||
continue goMin
|
||||
}
|
||||
} //goSecond
|
||||
} //goMin
|
||||
@ -318,31 +318,31 @@ function expressionChange() {
|
||||
}
|
||||
// 判断100年内的结果条数
|
||||
if (resultArr.length === 0) {
|
||||
resultList.value = ['没有达到条件的结果!'];
|
||||
resultList.value = ['没有达到条件的结果!']
|
||||
} else {
|
||||
resultList.value = resultArr;
|
||||
resultList.value = resultArr
|
||||
if (resultArr.length !== 5) {
|
||||
resultList.value.push('最近100年内只有上面' + resultArr.length + '条结果!')
|
||||
}
|
||||
}
|
||||
// 计算完成-显示结果
|
||||
isShow.value = true;
|
||||
isShow.value = true
|
||||
}
|
||||
// 用于计算某位数字在数组中的索引
|
||||
function getIndex(arr, value) {
|
||||
if (value <= arr[0] || value > arr[arr.length - 1]) {
|
||||
return 0;
|
||||
return 0
|
||||
} else {
|
||||
for (let i = 0; i < arr.length - 1; i++) {
|
||||
if (value > arr[i] && value <= arr[i + 1]) {
|
||||
return i + 1;
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取"年"数组
|
||||
function getYearArr(rule, year) {
|
||||
dateArr.value[5] = getOrderArr(year, year + 100);
|
||||
dateArr.value[5] = getOrderArr(year, year + 100)
|
||||
if (rule !== undefined) {
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dateArr.value[5] = getCycleArr(rule, year + 100, false)
|
||||
@ -355,7 +355,7 @@ function getYearArr(rule, year) {
|
||||
}
|
||||
// 获取"月"数组
|
||||
function getMonthArr(rule) {
|
||||
dateArr.value[4] = getOrderArr(1, 12);
|
||||
dateArr.value[4] = getOrderArr(1, 12)
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dateArr.value[4] = getCycleArr(rule, 12, false)
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
@ -369,58 +369,58 @@ function getWeekArr(rule) {
|
||||
// 只有当日期规则的两个值均为“”时则表达日期是有选项的
|
||||
if (dayRule.value === '' && dayRuleSup.value === '') {
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dayRule.value = 'weekDay';
|
||||
dayRule.value = 'weekDay'
|
||||
dayRuleSup.value = getCycleArr(rule, 7, false)
|
||||
} else if (rule.indexOf('#') >= 0) {
|
||||
dayRule.value = 'assWeek';
|
||||
let matchRule = rule.match(/[0-9]{1}/g);
|
||||
dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])];
|
||||
dateArr.value[3] = [1];
|
||||
dayRule.value = 'assWeek'
|
||||
let matchRule = rule.match(/[0-9]{1}/g)
|
||||
dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])]
|
||||
dateArr.value[3] = [1]
|
||||
if (dayRuleSup.value[1] === 7) {
|
||||
dayRuleSup.value[1] = 0;
|
||||
dayRuleSup.value[1] = 0
|
||||
}
|
||||
} else if (rule.indexOf('L') >= 0) {
|
||||
dayRule.value = 'lastWeek';
|
||||
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0]);
|
||||
dateArr.value[3] = [31];
|
||||
dayRule.value = 'lastWeek'
|
||||
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
|
||||
dateArr.value[3] = [31]
|
||||
if (dayRuleSup.value === 7) {
|
||||
dayRuleSup.value = 0;
|
||||
dayRuleSup.value = 0
|
||||
}
|
||||
} else if (rule !== '*' && rule !== '?') {
|
||||
dayRule.value = 'weekDay';
|
||||
dayRule.value = 'weekDay'
|
||||
dayRuleSup.value = getAssignArr(rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取"日"数组-少量为日期规则
|
||||
function getDayArr(rule) {
|
||||
dateArr.value[3] = getOrderArr(1, 31);
|
||||
dayRule.value = '';
|
||||
dayRuleSup.value = '';
|
||||
dateArr.value[3] = getOrderArr(1, 31)
|
||||
dayRule.value = ''
|
||||
dayRuleSup.value = ''
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dateArr.value[3] = getCycleArr(rule, 31, false)
|
||||
dayRuleSup.value = 'null';
|
||||
dayRuleSup.value = 'null'
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
dateArr.value[3] = getAverageArr(rule, 31)
|
||||
dayRuleSup.value = 'null';
|
||||
dayRuleSup.value = 'null'
|
||||
} else if (rule.indexOf('W') >= 0) {
|
||||
dayRule.value = 'workDay';
|
||||
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0]);
|
||||
dateArr.value[3] = [dayRuleSup.value];
|
||||
dayRule.value = 'workDay'
|
||||
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
|
||||
dateArr.value[3] = [dayRuleSup.value]
|
||||
} else if (rule.indexOf('L') >= 0) {
|
||||
dayRule.value = 'lastDay';
|
||||
dayRuleSup.value = 'null';
|
||||
dateArr.value[3] = [31];
|
||||
dayRule.value = 'lastDay'
|
||||
dayRuleSup.value = 'null'
|
||||
dateArr.value[3] = [31]
|
||||
} else if (rule !== '*' && rule !== '?') {
|
||||
dateArr.value[3] = getAssignArr(rule)
|
||||
dayRuleSup.value = 'null';
|
||||
dayRuleSup.value = 'null'
|
||||
} else if (rule === '*') {
|
||||
dayRuleSup.value = 'null';
|
||||
dayRuleSup.value = 'null'
|
||||
}
|
||||
}
|
||||
// 获取"时"数组
|
||||
function getHourArr(rule) {
|
||||
dateArr.value[2] = getOrderArr(0, 23);
|
||||
dateArr.value[2] = getOrderArr(0, 23)
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dateArr.value[2] = getCycleArr(rule, 24, true)
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
@ -431,7 +431,7 @@ function getHourArr(rule) {
|
||||
}
|
||||
// 获取"分"数组
|
||||
function getMinArr(rule) {
|
||||
dateArr.value[1] = getOrderArr(0, 59);
|
||||
dateArr.value[1] = getOrderArr(0, 59)
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dateArr.value[1] = getCycleArr(rule, 60, true)
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
@ -442,7 +442,7 @@ function getMinArr(rule) {
|
||||
}
|
||||
// 获取"秒"数组
|
||||
function getSecondArr(rule) {
|
||||
dateArr.value[0] = getOrderArr(0, 59);
|
||||
dateArr.value[0] = getOrderArr(0, 59)
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dateArr.value[0] = getCycleArr(rule, 60, true)
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
@ -453,86 +453,86 @@ function getSecondArr(rule) {
|
||||
}
|
||||
// 根据传进来的min-max返回一个顺序的数组
|
||||
function getOrderArr(min, max) {
|
||||
let arr = [];
|
||||
let arr = []
|
||||
for (let i = min; i <= max; i++) {
|
||||
arr.push(i);
|
||||
arr.push(i)
|
||||
}
|
||||
return arr;
|
||||
return arr
|
||||
}
|
||||
// 根据规则中指定的零散值返回一个数组
|
||||
function getAssignArr(rule) {
|
||||
let arr = [];
|
||||
let assiginArr = rule.split(',');
|
||||
let arr = []
|
||||
let assiginArr = rule.split(',')
|
||||
for (let i = 0; i < assiginArr.length; i++) {
|
||||
arr[i] = Number(assiginArr[i])
|
||||
}
|
||||
arr.sort(compare)
|
||||
return arr;
|
||||
return arr
|
||||
}
|
||||
// 根据一定算术规则计算返回一个数组
|
||||
function getAverageArr(rule, limit) {
|
||||
let arr = [];
|
||||
let agArr = rule.split('/');
|
||||
let min = Number(agArr[0]);
|
||||
let step = Number(agArr[1]);
|
||||
let arr = []
|
||||
let agArr = rule.split('/')
|
||||
let min = Number(agArr[0])
|
||||
let step = Number(agArr[1])
|
||||
while (min <= limit) {
|
||||
arr.push(min);
|
||||
min += step;
|
||||
arr.push(min)
|
||||
min += step
|
||||
}
|
||||
return arr;
|
||||
return arr
|
||||
}
|
||||
// 根据规则返回一个具有周期性的数组
|
||||
function getCycleArr(rule, limit, status) {
|
||||
// status--表示是否从0开始(则从1开始)
|
||||
let arr = [];
|
||||
let cycleArr = rule.split('-');
|
||||
let min = Number(cycleArr[0]);
|
||||
let max = Number(cycleArr[1]);
|
||||
let arr = []
|
||||
let cycleArr = rule.split('-')
|
||||
let min = Number(cycleArr[0])
|
||||
let max = Number(cycleArr[1])
|
||||
if (min > max) {
|
||||
max += limit;
|
||||
max += limit
|
||||
}
|
||||
for (let i = min; i <= max; i++) {
|
||||
let add = 0;
|
||||
let add = 0
|
||||
if (status === false && i % limit === 0) {
|
||||
add = limit;
|
||||
add = limit
|
||||
}
|
||||
arr.push(Math.round(i % limit + add))
|
||||
}
|
||||
arr.sort(compare)
|
||||
return arr;
|
||||
return arr
|
||||
}
|
||||
// 比较数字大小(用于Array.sort)
|
||||
function compare(value1, value2) {
|
||||
if (value2 - value1 > 0) {
|
||||
return -1;
|
||||
return -1
|
||||
} else {
|
||||
return 1;
|
||||
return 1
|
||||
}
|
||||
}
|
||||
// 格式化日期格式如:2017-9-19 18:04:33
|
||||
function formatDate(value, type) {
|
||||
// 计算日期相关值
|
||||
let time = typeof value == 'number' ? new Date(value) : value;
|
||||
let Y = time.getFullYear();
|
||||
let M = time.getMonth() + 1;
|
||||
let D = time.getDate();
|
||||
let h = time.getHours();
|
||||
let m = time.getMinutes();
|
||||
let s = time.getSeconds();
|
||||
let week = time.getDay();
|
||||
let time = typeof value == 'number' ? new Date(value) : value
|
||||
let Y = time.getFullYear()
|
||||
let M = time.getMonth() + 1
|
||||
let D = time.getDate()
|
||||
let h = time.getHours()
|
||||
let m = time.getMinutes()
|
||||
let s = time.getSeconds()
|
||||
let week = time.getDay()
|
||||
// 如果传递了type的话
|
||||
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') {
|
||||
// 在quartz中 1为星期日
|
||||
return week + 1;
|
||||
return week + 1
|
||||
}
|
||||
}
|
||||
// 检查日期是否存在
|
||||
function checkDate(value) {
|
||||
let time = new Date(value);
|
||||
let time = new Date(value)
|
||||
let format = formatDate(time)
|
||||
return value === format;
|
||||
return value === format
|
||||
}
|
||||
onMounted(() => {
|
||||
expressionChange()
|
||||
|
||||
@ -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 cycle01 = ref(0)
|
||||
const cycle02 = ref(0)
|
||||
const average01 = ref(0)
|
||||
const cycle01 = ref(fullYear)
|
||||
const cycle02 = ref(fullYear + 1)
|
||||
const average01 = ref(fullYear)
|
||||
const average02 = ref(1)
|
||||
const checkboxList = ref([])
|
||||
const checkCopy = ref([])
|
||||
const checkCopy = ref([fullYear])
|
||||
|
||||
const cycleTotal = computed(() => {
|
||||
cycle01.value = props.check(cycle01.value, fullYear.value, maxFullYear.value - 1)
|
||||
cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear.value)
|
||||
cycle01.value = props.check(cycle01.value, fullYear, maxFullYear - 1)
|
||||
cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear)
|
||||
return cycle01.value + '-' + cycle02.value
|
||||
})
|
||||
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)
|
||||
return average01.value + '/' + average02.value
|
||||
})
|
||||
@ -97,8 +99,8 @@ function changeRadioValue(value) {
|
||||
radioValue.value = 3
|
||||
} else if (value.indexOf("/") > -1) {
|
||||
const indexArr = value.split('/')
|
||||
average01.value = Number(indexArr[1])
|
||||
average02.value = Number(indexArr[0])
|
||||
average01.value = Number(indexArr[0])
|
||||
average02.value = Number(indexArr[1])
|
||||
radioValue.value = 4
|
||||
} else {
|
||||
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
|
||||
@ -129,14 +131,6 @@ function onRadioChange() {
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-for="(item, index) in options">
|
||||
<template v-if="values.includes(item.value)">
|
||||
<template v-if="isValueMatch(item.value)">
|
||||
<span
|
||||
v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)"
|
||||
:key="item.value"
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
<script setup>
|
||||
// 记录未匹配的项
|
||||
const unmatchArray = ref([]);
|
||||
const unmatchArray = ref([])
|
||||
|
||||
const props = defineProps({
|
||||
// 数据
|
||||
@ -45,33 +45,38 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: ",",
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const values = computed(() => {
|
||||
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 (props.value === null || typeof props.value === 'undefined' || props.value === '') return []
|
||||
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(() => {
|
||||
unmatchArray.value = [];
|
||||
unmatchArray.value = []
|
||||
// 没有value不显示
|
||||
if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false
|
||||
// 传入值为数组
|
||||
let unmatch = false // 添加一个标志来判断是否有未匹配项
|
||||
values.value.forEach(item => {
|
||||
if (!props.options.some(v => v.value === item)) {
|
||||
if (!props.options.some(v => v.value == item)) {
|
||||
unmatchArray.value.push(item)
|
||||
unmatch = true // 如果有未匹配项,将标志设置为true
|
||||
}
|
||||
})
|
||||
return unmatch // 返回标志的值
|
||||
});
|
||||
})
|
||||
|
||||
function handleArray(array) {
|
||||
if (array.length === 0) return "";
|
||||
if (array.length === 0) return ""
|
||||
return array.reduce((pre, cur) => {
|
||||
return pre + " " + cur;
|
||||
});
|
||||
return pre + " " + cur
|
||||
})
|
||||
}
|
||||
|
||||
function isValueMatch(itemValue) {
|
||||
return values.value.some(val => val == itemValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -27,17 +27,18 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { QuillEditor } from "@vueup/vue-quill";
|
||||
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import axios from 'axios'
|
||||
import { QuillEditor } from "@vueup/vue-quill"
|
||||
import "@vueup/vue-quill/dist/vue-quill.snow.css"
|
||||
import { getToken } from "@/utils/auth"
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const quillEditorRef = ref();
|
||||
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传的图片服务器地址
|
||||
const quillEditorRef = ref()
|
||||
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload") // 上传的图片服务器地址
|
||||
const headers = ref({
|
||||
Authorization: "Bearer " + getToken()
|
||||
});
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
/* 编辑器的内容 */
|
||||
@ -69,7 +70,7 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: "url",
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const options = ref({
|
||||
theme: "snow",
|
||||
@ -92,59 +93,60 @@ const options = ref({
|
||||
},
|
||||
placeholder: "请输入内容",
|
||||
readOnly: props.readOnly
|
||||
});
|
||||
})
|
||||
|
||||
const styles = computed(() => {
|
||||
let style = {};
|
||||
let style = {}
|
||||
if (props.minHeight) {
|
||||
style.minHeight = `${props.minHeight}px`;
|
||||
style.minHeight = `${props.minHeight}px`
|
||||
}
|
||||
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) => {
|
||||
if (v !== content.value) {
|
||||
content.value = v == undefined ? "<p></p>" : v;
|
||||
content.value = v == undefined ? "<p></p>" : v
|
||||
}
|
||||
}, { immediate: true });
|
||||
}, { immediate: true })
|
||||
|
||||
// 如果设置了上传地址则自定义图片上传事件
|
||||
onMounted(() => {
|
||||
if (props.type == 'url') {
|
||||
let quill = quillEditorRef.value.getQuill();
|
||||
let toolbar = quill.getModule("toolbar");
|
||||
let quill = quillEditorRef.value.getQuill()
|
||||
let toolbar = quill.getModule("toolbar")
|
||||
toolbar.addHandler("image", (value) => {
|
||||
if (value) {
|
||||
proxy.$refs.uploadRef.click();
|
||||
proxy.$refs.uploadRef.click()
|
||||
} else {
|
||||
quill.format("image", false);
|
||||
quill.format("image", false)
|
||||
}
|
||||
});
|
||||
})
|
||||
quill.root.addEventListener('paste', handlePasteCapture, true)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// 上传前校检格式和大小
|
||||
function handleBeforeUpload(file) {
|
||||
const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
|
||||
const isJPG = type.includes(file.type);
|
||||
const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"]
|
||||
const isJPG = type.includes(file.type)
|
||||
//检验文件格式
|
||||
if (!isJPG) {
|
||||
proxy.$modal.msgError(`图片格式错误!`);
|
||||
return false;
|
||||
proxy.$modal.msgError(`图片格式错误!`)
|
||||
return false
|
||||
}
|
||||
// 校检文件大小
|
||||
if (props.fileSize) {
|
||||
const isLt = file.size / 1024 / 1024 < props.fileSize;
|
||||
const isLt = file.size / 1024 / 1024 < props.fileSize
|
||||
if (!isLt) {
|
||||
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
|
||||
return false;
|
||||
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
// 上传成功处理
|
||||
@ -152,21 +154,44 @@ function handleUploadSuccess(res, file) {
|
||||
// 如果上传成功
|
||||
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为服务器返回的图片链接地址
|
||||
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 {
|
||||
proxy.$modal.msgError("图片插入失败");
|
||||
proxy.$modal.msgError("图片插入失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 上传失败处理
|
||||
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>
|
||||
|
||||
|
||||
137
src/components/ExcelImportDialog/index.vue
Normal 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>仅允许导入xls、xlsx格式文件。</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>
|
||||
@ -5,6 +5,7 @@
|
||||
:action="uploadFileUrl"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:file-list="fileList"
|
||||
:data="data"
|
||||
:limit="limit"
|
||||
:on-error="handleUploadError"
|
||||
:on-exceed="handleExceed"
|
||||
@ -13,25 +14,26 @@
|
||||
:headers="headers"
|
||||
class="upload-file-uploader"
|
||||
ref="fileUpload"
|
||||
v-if="!disabled"
|
||||
>
|
||||
<!-- 上传按钮 -->
|
||||
<el-button type="primary">选取文件</el-button>
|
||||
</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="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
|
||||
的文件
|
||||
</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">
|
||||
<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>
|
||||
</el-link>
|
||||
<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"> 删除</el-link>
|
||||
</div>
|
||||
</li>
|
||||
</transition-group>
|
||||
@ -39,132 +41,152 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getToken } from "@/utils/auth";
|
||||
import { getToken } from "@/utils/auth"
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String, Object, Array],
|
||||
// 上传接口地址
|
||||
action: {
|
||||
type: String,
|
||||
default: "/common/upload"
|
||||
},
|
||||
// 上传携带的参数
|
||||
data: {
|
||||
type: Object
|
||||
},
|
||||
// 数量限制
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
default: 5
|
||||
},
|
||||
// 大小限制(MB)
|
||||
fileSize: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
default: 5
|
||||
},
|
||||
// 文件类型, 例如['png', 'jpg', 'jpeg']
|
||||
fileType: {
|
||||
type: Array,
|
||||
default: () => ["doc", "xls", "ppt", "txt", "pdf"],
|
||||
default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"]
|
||||
},
|
||||
// 是否显示提示
|
||||
isShowTip: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 禁用组件(仅查看文件)
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 拖动排序
|
||||
drag: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const emit = defineEmits();
|
||||
const number = ref(0);
|
||||
const uploadList = ref([]);
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API;
|
||||
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传文件服务器地址
|
||||
const headers = ref({ Authorization: "Bearer " + getToken() });
|
||||
const fileList = ref([]);
|
||||
const { proxy } = getCurrentInstance()
|
||||
const emit = defineEmits()
|
||||
const number = ref(0)
|
||||
const uploadList = ref([])
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API
|
||||
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传文件服务器地址
|
||||
const headers = ref({ Authorization: "Bearer " + getToken() })
|
||||
const fileList = ref([])
|
||||
const showTip = computed(
|
||||
() => props.isShowTip && (props.fileType || props.fileSize)
|
||||
);
|
||||
)
|
||||
|
||||
watch(() => props.modelValue, 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 => {
|
||||
if (typeof item === "string") {
|
||||
item = { name: item, url: item };
|
||||
item = { name: item, url: item }
|
||||
}
|
||||
item.uid = item.uid || new Date().getTime() + temp++;
|
||||
return item;
|
||||
});
|
||||
item.uid = item.uid || new Date().getTime() + temp++
|
||||
return item
|
||||
})
|
||||
} else {
|
||||
fileList.value = [];
|
||||
return [];
|
||||
fileList.value = []
|
||||
return []
|
||||
}
|
||||
},{ deep: true, immediate: true });
|
||||
},{ deep: true, immediate: true })
|
||||
|
||||
// 上传前校检格式和大小
|
||||
function handleBeforeUpload(file) {
|
||||
// 校检文件类型
|
||||
if (props.fileType.length) {
|
||||
const fileName = file.name.split('.');
|
||||
const fileExt = fileName[fileName.length - 1];
|
||||
const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName[fileName.length - 1]
|
||||
const isTypeOk = props.fileType.indexOf(fileExt) >= 0
|
||||
if (!isTypeOk) {
|
||||
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}格式文件!`);
|
||||
return false;
|
||||
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}格式文件!`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
// 校检文件名是否包含特殊字符
|
||||
if (file.name.includes(',')) {
|
||||
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!');
|
||||
return false;
|
||||
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
|
||||
return false
|
||||
}
|
||||
// 校检文件大小
|
||||
if (props.fileSize) {
|
||||
const isLt = file.size / 1024 / 1024 < props.fileSize;
|
||||
const isLt = file.size / 1024 / 1024 < props.fileSize
|
||||
if (!isLt) {
|
||||
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
|
||||
return false;
|
||||
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
proxy.$modal.loading("正在上传文件,请稍候...");
|
||||
number.value++;
|
||||
return true;
|
||||
proxy.$modal.loading("正在上传文件,请稍候...")
|
||||
number.value++
|
||||
return true
|
||||
}
|
||||
|
||||
// 文件个数超出
|
||||
function handleExceed() {
|
||||
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
|
||||
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
|
||||
}
|
||||
|
||||
// 上传失败
|
||||
function handleUploadError(err) {
|
||||
proxy.$modal.msgError("上传文件失败");
|
||||
proxy.$modal.msgError("上传文件失败")
|
||||
proxy.$modal.closeLoading()
|
||||
}
|
||||
|
||||
// 上传成功回调
|
||||
function handleUploadSuccess(res, file) {
|
||||
if (res.code === 200) {
|
||||
uploadList.value.push({ name: res.fileName, url: res.fileName });
|
||||
uploadedSuccessfully();
|
||||
uploadList.value.push({ name: res.fileName, url: res.fileName })
|
||||
uploadedSuccessfully()
|
||||
} else {
|
||||
number.value--;
|
||||
proxy.$modal.closeLoading();
|
||||
proxy.$modal.msgError(res.msg);
|
||||
proxy.$refs.fileUpload.handleRemove(file);
|
||||
uploadedSuccessfully();
|
||||
number.value--
|
||||
proxy.$modal.closeLoading()
|
||||
proxy.$modal.msgError(res.msg)
|
||||
proxy.$refs.fileUpload.handleRemove(file)
|
||||
uploadedSuccessfully()
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
function handleDelete(index) {
|
||||
fileList.value.splice(index, 1);
|
||||
emit("update:modelValue", listToString(fileList.value));
|
||||
fileList.value.splice(index, 1)
|
||||
emit("update:modelValue", listToString(fileList.value))
|
||||
}
|
||||
|
||||
// 上传结束处理
|
||||
function uploadedSuccessfully() {
|
||||
if (number.value > 0 && uploadList.value.length === number.value) {
|
||||
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
|
||||
uploadList.value = [];
|
||||
number.value = 0;
|
||||
console.log('fileList.value', fileList.value)
|
||||
emit("update:modelValue", listToString(fileList.value));
|
||||
proxy.$modal.closeLoading();
|
||||
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
|
||||
uploadList.value = []
|
||||
number.value = 0
|
||||
emit("update:modelValue", listToString(fileList.value))
|
||||
proxy.$modal.closeLoading()
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,25 +194,41 @@ function uploadedSuccessfully() {
|
||||
function getFileName(name) {
|
||||
// 如果是url那么取最后的名字 如果不是直接返回
|
||||
if (name.lastIndexOf("/") > -1) {
|
||||
return name.slice(name.lastIndexOf("/") + 1);
|
||||
return name.slice(name.lastIndexOf("/") + 1)
|
||||
} else {
|
||||
return name;
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
// 对象转成指定字符串分隔
|
||||
function listToString(list, separator) {
|
||||
let strs = "";
|
||||
separator = separator || ",";
|
||||
let strs = ""
|
||||
separator = separator || ","
|
||||
for (let i in list) {
|
||||
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">
|
||||
.upload-file-uploader {
|
||||
margin-bottom: 5px;
|
||||
|
||||
@ -24,7 +24,7 @@ defineProps({
|
||||
|
||||
const emit = defineEmits()
|
||||
const toggleClick = () => {
|
||||
emit('toggleClick');
|
||||
emit('toggleClick')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,19 +1,78 @@
|
||||
<template>
|
||||
<div :class="{ 'show': show }" class="header-search">
|
||||
<div class="header-search">
|
||||
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
|
||||
<el-select
|
||||
ref="headerSearchSelectRef"
|
||||
v-model="search"
|
||||
:remote-method="querySearch"
|
||||
filterable
|
||||
default-first-option
|
||||
remote
|
||||
placeholder="Search"
|
||||
class="header-search-select"
|
||||
@change="change"
|
||||
<el-dialog
|
||||
v-model="show"
|
||||
width="600"
|
||||
@close="close"
|
||||
@opened="onDialogOpened"
|
||||
:show-close="false"
|
||||
append-to-body
|
||||
>
|
||||
<el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
|
||||
</el-select>
|
||||
<el-input
|
||||
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>
|
||||
</template>
|
||||
|
||||
@ -21,54 +80,66 @@
|
||||
import Fuse from 'fuse.js'
|
||||
import { getNormalPath } from '@/utils/ruoyi'
|
||||
import { isHttp } from '@/utils/validate'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import usePermissionStore from '@/store/modules/permission'
|
||||
|
||||
const search = ref('');
|
||||
const options = ref([]);
|
||||
const searchPool = ref([]);
|
||||
const show = ref(false);
|
||||
const fuse = ref(undefined);
|
||||
const headerSearchSelectRef = ref(null);
|
||||
const router = useRouter();
|
||||
const routes = computed(() => usePermissionStore().routes);
|
||||
const search = ref('')
|
||||
const options = ref([])
|
||||
const searchPool = ref([])
|
||||
const activeIndex = ref(-1)
|
||||
const show = ref(false)
|
||||
const fuse = ref(undefined)
|
||||
const headerSearchSelectRef = ref(null)
|
||||
const router = useRouter()
|
||||
const theme = computed(() => useSettingsStore().theme)
|
||||
const routes = computed(() => usePermissionStore().defaultRoutes)
|
||||
|
||||
function click() {
|
||||
show.value = !show.value
|
||||
if (show.value) {
|
||||
headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
|
||||
options.value = searchPool.value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function onDialogOpened() {
|
||||
nextTick(() => {
|
||||
headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
|
||||
})
|
||||
}
|
||||
|
||||
function close() {
|
||||
headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
|
||||
options.value = []
|
||||
search.value = ''
|
||||
options.value = searchPool.value
|
||||
show.value = false
|
||||
activeIndex.value = -1
|
||||
}
|
||||
|
||||
function change(val) {
|
||||
const path = val.path;
|
||||
const query = val.query;
|
||||
if (isHttp(path)) {
|
||||
const p = val.path
|
||||
const query = val.query
|
||||
if (isHttp(p)) {
|
||||
// http(s):// 路径新窗口打开
|
||||
const pindex = path.indexOf("http");
|
||||
window.open(path.substr(pindex, path.length), "_blank");
|
||||
const pindex = p.indexOf("http")
|
||||
window.open(p.substr(pindex, p.length), "_blank")
|
||||
} else {
|
||||
if (query) {
|
||||
router.push({ path: path, query: JSON.parse(query) });
|
||||
router.push({ path: p, query: JSON.parse(query) })
|
||||
} else {
|
||||
router.push(path)
|
||||
router.push(p)
|
||||
}
|
||||
}
|
||||
|
||||
search.value = ''
|
||||
options.value = []
|
||||
options.value = searchPool.value
|
||||
nextTick(() => {
|
||||
show.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function initFuse(list) {
|
||||
fuse.value = new Fuse(list, {
|
||||
shouldSort: true,
|
||||
threshold: 0.4,
|
||||
location: 0,
|
||||
threshold: 0.2,
|
||||
distance: 100,
|
||||
minMatchCharLength: 1,
|
||||
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 = []) {
|
||||
let res = []
|
||||
|
||||
for (const r of routes) {
|
||||
// skip hidden router
|
||||
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 = {
|
||||
path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
|
||||
title: [...prefixTitle]
|
||||
title: [...prefixTitle],
|
||||
icon: ''
|
||||
}
|
||||
|
||||
if (r.meta && r.meta.title) {
|
||||
data.title = [...data.title, r.meta.title]
|
||||
|
||||
if (r.redirect !== 'noRedirect') {
|
||||
// only push the routes with title
|
||||
// special case: need to exclude parent router without redirect
|
||||
data.icon = r.meta.icon
|
||||
if (r.redirect !== "noRedirect") {
|
||||
res.push(data)
|
||||
}
|
||||
}
|
||||
if (r.query) {
|
||||
data.query = r.query
|
||||
}
|
||||
|
||||
// recursive child routes
|
||||
if (r.children) {
|
||||
const tempRoutes = generateRoutes(r.children, data.path, data.title)
|
||||
if (tempRoutes.length >= 1) {
|
||||
@ -117,28 +181,63 @@ function generateRoutes(routes, basePath = '', prefixTitle = []) {
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
function querySearch(query) {
|
||||
activeIndex.value = -1
|
||||
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 {
|
||||
options.value = []
|
||||
options.value = searchPool.value
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
searchPool.value = generateRoutes(routes.value);
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
searchPool.value = generateRoutes(routes.value)
|
||||
})
|
||||
|
||||
watch(show, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', close)
|
||||
} else {
|
||||
document.body.removeEventListener('click', close)
|
||||
function activeStyle(index) {
|
||||
if (index !== activeIndex.value) return {}
|
||||
return {
|
||||
"background-color": theme.value,
|
||||
"color": "#fff"
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
@ -30,11 +30,11 @@ const props = defineProps({
|
||||
activeIcon: {
|
||||
type: String
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const iconName = ref('');
|
||||
const iconList = ref(icons);
|
||||
const emit = defineEmits(['selected']);
|
||||
const iconName = ref('')
|
||||
const iconList = ref(icons)
|
||||
const emit = defineEmits(['selected'])
|
||||
|
||||
function filterIcons() {
|
||||
iconList.value = icons
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
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) {
|
||||
const p = path.split('assets/icons/svg/')[1].split('.svg')[0];
|
||||
icons.push(p);
|
||||
const p = path.split('assets/icons/svg/')[1].split('.svg')[0]
|
||||
icons.push(p)
|
||||
}
|
||||
|
||||
export default icons
|
||||
@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isExternal } from "@/utils/validate";
|
||||
import { isExternal } from "@/utils/validate"
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
@ -30,41 +30,41 @@ const props = defineProps({
|
||||
type: [Number, String],
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const realSrc = computed(() => {
|
||||
if (!props.src) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
let real_src = props.src.split(",")[0];
|
||||
let real_src = props.src.split(",")[0]
|
||||
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(() => {
|
||||
if (!props.src) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
let real_src_list = props.src.split(",");
|
||||
let srcList = [];
|
||||
let real_src_list = props.src.split(",")
|
||||
let srcList = []
|
||||
real_src_list.forEach(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;
|
||||
});
|
||||
return srcList.push(import.meta.env.VITE_APP_BASE_API + item)
|
||||
})
|
||||
return srcList
|
||||
})
|
||||
|
||||
const realWidth = computed(() =>
|
||||
typeof props.width == "string" ? props.width : `${props.width}px`
|
||||
);
|
||||
)
|
||||
|
||||
const realHeight = computed(() =>
|
||||
typeof props.height == "string" ? props.height : `${props.height}px`
|
||||
);
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -2,10 +2,12 @@
|
||||
<div class="component-upload-image">
|
||||
<el-upload
|
||||
multiple
|
||||
:disabled="disabled"
|
||||
:action="uploadImgUrl"
|
||||
list-type="picture-card"
|
||||
:on-success="handleUploadSuccess"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:data="data"
|
||||
:limit="limit"
|
||||
:on-error="handleUploadError"
|
||||
:on-exceed="handleExceed"
|
||||
@ -20,7 +22,7 @@
|
||||
<el-icon class="avatar-uploader-icon"><plus /></el-icon>
|
||||
</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>
|
||||
@ -46,166 +48,202 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getToken } from "@/utils/auth";
|
||||
import { isExternal } from "@/utils/validate";
|
||||
import { getToken } from "@/utils/auth"
|
||||
import { isExternal } from "@/utils/validate"
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String, Object, Array],
|
||||
// 上传接口地址
|
||||
action: {
|
||||
type: String,
|
||||
default: "/common/upload"
|
||||
},
|
||||
// 上传携带的参数
|
||||
data: {
|
||||
type: Object
|
||||
},
|
||||
// 图片数量限制
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
default: 5
|
||||
},
|
||||
// 大小限制(MB)
|
||||
fileSize: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
default: 5
|
||||
},
|
||||
// 文件类型, 例如['png', 'jpg', 'jpeg']
|
||||
fileType: {
|
||||
type: Array,
|
||||
default: () => ["png", "jpg", "jpeg"],
|
||||
default: () => ["png", "jpg", "jpeg"]
|
||||
},
|
||||
// 是否显示提示
|
||||
isShowTip: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
});
|
||||
// 禁用组件(仅查看图片)
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 拖动排序
|
||||
drag: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const emit = defineEmits();
|
||||
const number = ref(0);
|
||||
const uploadList = ref([]);
|
||||
const dialogImageUrl = ref("");
|
||||
const dialogVisible = ref(false);
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API;
|
||||
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传的图片服务器地址
|
||||
const headers = ref({ Authorization: "Bearer " + getToken() });
|
||||
const fileList = ref([]);
|
||||
const { proxy } = getCurrentInstance()
|
||||
const emit = defineEmits()
|
||||
const number = ref(0)
|
||||
const uploadList = ref([])
|
||||
const dialogImageUrl = ref("")
|
||||
const dialogVisible = ref(false)
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API
|
||||
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传的图片服务器地址
|
||||
const headers = ref({ Authorization: "Bearer " + getToken() })
|
||||
const fileList = ref([])
|
||||
const showTip = computed(
|
||||
() => props.isShowTip && (props.fileType || props.fileSize)
|
||||
);
|
||||
)
|
||||
|
||||
watch(() => props.modelValue, 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 => {
|
||||
if (typeof item === "string") {
|
||||
if (item.indexOf(baseUrl) === -1 && !isExternal(item)) {
|
||||
item = { name: baseUrl + item, url: baseUrl + item };
|
||||
item = { name: baseUrl + item, url: baseUrl + item }
|
||||
} else {
|
||||
item = { name: item, url: item };
|
||||
item = { name: item, url: item }
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return item
|
||||
})
|
||||
} else {
|
||||
fileList.value = [];
|
||||
return [];
|
||||
fileList.value = []
|
||||
return []
|
||||
}
|
||||
},{ deep: true, immediate: true });
|
||||
},{ deep: true, immediate: true })
|
||||
|
||||
// 上传前loading加载
|
||||
function handleBeforeUpload(file) {
|
||||
let isImg = false;
|
||||
let isImg = false
|
||||
if (props.fileType.length) {
|
||||
let fileExtension = "";
|
||||
let fileExtension = ""
|
||||
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 => {
|
||||
if (file.type.indexOf(type) > -1) return true;
|
||||
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
|
||||
return false;
|
||||
});
|
||||
if (file.type.indexOf(type) > -1) return true
|
||||
if (fileExtension && fileExtension.indexOf(type) > -1) return true
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
isImg = file.type.indexOf("image") > -1;
|
||||
isImg = file.type.indexOf("image") > -1
|
||||
}
|
||||
if (!isImg) {
|
||||
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`);
|
||||
return false;
|
||||
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`)
|
||||
return false
|
||||
}
|
||||
if (file.name.includes(',')) {
|
||||
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!');
|
||||
return false;
|
||||
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
|
||||
return false
|
||||
}
|
||||
if (props.fileSize) {
|
||||
const isLt = file.size / 1024 / 1024 < props.fileSize;
|
||||
const isLt = file.size / 1024 / 1024 < props.fileSize
|
||||
if (!isLt) {
|
||||
proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
|
||||
return false;
|
||||
proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
proxy.$modal.loading("正在上传图片,请稍候...");
|
||||
number.value++;
|
||||
proxy.$modal.loading("正在上传图片,请稍候...")
|
||||
number.value++
|
||||
}
|
||||
|
||||
// 文件个数超出
|
||||
function handleExceed() {
|
||||
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
|
||||
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
|
||||
}
|
||||
|
||||
// 上传成功回调
|
||||
function handleUploadSuccess(res, file) {
|
||||
if (res.code === 200) {
|
||||
uploadList.value.push({ name: res.fileName, url: res.fileName });
|
||||
uploadedSuccessfully();
|
||||
uploadList.value.push({ name: res.fileName, url: res.fileName })
|
||||
uploadedSuccessfully()
|
||||
} else {
|
||||
number.value--;
|
||||
proxy.$modal.closeLoading();
|
||||
proxy.$modal.msgError(res.msg);
|
||||
proxy.$refs.imageUpload.handleRemove(file);
|
||||
uploadedSuccessfully();
|
||||
number.value--
|
||||
proxy.$modal.closeLoading()
|
||||
proxy.$modal.msgError(res.msg)
|
||||
proxy.$refs.imageUpload.handleRemove(file)
|
||||
uploadedSuccessfully()
|
||||
}
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
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) {
|
||||
fileList.value.splice(findex, 1);
|
||||
emit("update:modelValue", listToString(fileList.value));
|
||||
return false;
|
||||
fileList.value.splice(findex, 1)
|
||||
emit("update:modelValue", listToString(fileList.value))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 上传结束处理
|
||||
function uploadedSuccessfully() {
|
||||
if (number.value > 0 && uploadList.value.length === number.value) {
|
||||
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
|
||||
uploadList.value = [];
|
||||
number.value = 0;
|
||||
emit("update:modelValue", listToString(fileList.value));
|
||||
proxy.$modal.closeLoading();
|
||||
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
|
||||
uploadList.value = []
|
||||
number.value = 0
|
||||
emit("update:modelValue", listToString(fileList.value))
|
||||
proxy.$modal.closeLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 上传失败
|
||||
function handleUploadError() {
|
||||
proxy.$modal.msgError("上传图片失败");
|
||||
proxy.$modal.closeLoading();
|
||||
proxy.$modal.msgError("上传图片失败")
|
||||
proxy.$modal.closeLoading()
|
||||
}
|
||||
|
||||
// 预览
|
||||
function handlePictureCardPreview(file) {
|
||||
dialogImageUrl.value = file.url;
|
||||
dialogVisible.value = true;
|
||||
dialogImageUrl.value = file.url
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 对象转成指定字符串分隔
|
||||
function listToString(list, separator) {
|
||||
let strs = "";
|
||||
separator = separator || ",";
|
||||
let strs = ""
|
||||
separator = separator || ","
|
||||
for (let i in list) {
|
||||
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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -59,7 +59,7 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits();
|
||||
const emit = defineEmits()
|
||||
const currentPage = computed({
|
||||
get() {
|
||||
return props.page
|
||||
@ -76,6 +76,7 @@ const pageSize = computed({
|
||||
emit('update:limit', val)
|
||||
}
|
||||
})
|
||||
|
||||
function handleSizeChange(val) {
|
||||
if (currentPage.value * val > props.total) {
|
||||
currentPage.value = 1
|
||||
@ -85,13 +86,13 @@ function handleSizeChange(val) {
|
||||
scrollTo(0, 800)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCurrentChange(val) {
|
||||
emit('pagination', { page: val, limit: pageSize.value })
|
||||
if (props.autoScroll) {
|
||||
scrollTo(0, 800)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -1,21 +1,26 @@
|
||||
<template>
|
||||
<div class="top-right-btn" :style="style">
|
||||
<el-row>
|
||||
<!-- <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">-->
|
||||
<!-- <el-button circle icon="Search" @click="toggleSearch()" />-->
|
||||
<!-- </el-tooltip>-->
|
||||
<el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
|
||||
<el-button circle icon="Search" @click="toggleSearch()" />
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item" effect="dark" content="刷新" placement="top">
|
||||
<el-button circle icon="Refresh" @click="refresh()" />
|
||||
</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-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
|
||||
<el-button circle icon="Menu" />
|
||||
<template #dropdown>
|
||||
<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-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>
|
||||
</template>
|
||||
</el-dropdown-menu>
|
||||
@ -27,7 +32,7 @@
|
||||
<el-transfer
|
||||
:titles="['显示', '隐藏']"
|
||||
v-model="value"
|
||||
:data="columns"
|
||||
:data="transferData"
|
||||
@change="dataChange"
|
||||
></el-transfer>
|
||||
</el-dialog>
|
||||
@ -35,87 +40,191 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import cache from '@/plugins/cache'
|
||||
|
||||
const props = defineProps({
|
||||
/* 是否显示检索条件 */
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: true
|
||||
},
|
||||
/* 显隐列信息 */
|
||||
/* 显隐列信息(数组格式、对象格式) */
|
||||
columns: {
|
||||
type: Array,
|
||||
type: [Array, Object],
|
||||
default: () => ({})
|
||||
},
|
||||
/* 是否显示检索图标 */
|
||||
search: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: true
|
||||
},
|
||||
/* 显隐列类型(transfer穿梭框、checkbox复选框) */
|
||||
showColumnsType: {
|
||||
type: String,
|
||||
default: "checkbox",
|
||||
default: "checkbox"
|
||||
},
|
||||
/* 右外边距 */
|
||||
gutter: {
|
||||
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 ret = {};
|
||||
const ret = {}
|
||||
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() {
|
||||
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() {
|
||||
emits("queryTable");
|
||||
emits("queryTable")
|
||||
}
|
||||
|
||||
// 右侧列表元素变化
|
||||
function dataChange(data) {
|
||||
for (let item in props.columns) {
|
||||
const key = props.columns[item].key;
|
||||
props.columns[item].visible = !data.includes(key);
|
||||
if (Array.isArray(props.columns)) {
|
||||
for (let item in props.columns) {
|
||||
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
|
||||
function showColumn() {
|
||||
open.value = true;
|
||||
open.value = true
|
||||
}
|
||||
|
||||
if (props.showColumnsType == 'transfer') {
|
||||
// 显隐列初始默认隐藏列
|
||||
for (let item in props.columns) {
|
||||
if (props.columns[item].visible === false) {
|
||||
value.value.push(parseInt(item));
|
||||
// 如果传入了 storageKey,从 localStorage 恢复列显隐状态
|
||||
if (props.storageKey) {
|
||||
try {
|
||||
const saved = cache.local.getJSON(props.storageKey)
|
||||
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) {
|
||||
props.columns.filter(item => item.label == label)[0].visible = event;
|
||||
// 单勾选
|
||||
function checkboxChange(event, key) {
|
||||
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>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<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() {
|
||||
window.open(url.value)
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<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() {
|
||||
window.open(url.value)
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<script setup>
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
|
||||
const { isFullscreen, enter, exit, toggle } = useFullscreen();
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
@ -16,23 +16,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import useAppStore from "@/store/modules/app";
|
||||
import useAppStore from "@/store/modules/app"
|
||||
|
||||
const appStore = useAppStore();
|
||||
const size = computed(() => appStore.size);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { proxy } = getCurrentInstance();
|
||||
const appStore = useAppStore()
|
||||
const size = computed(() => appStore.size)
|
||||
const { proxy } = getCurrentInstance()
|
||||
const sizeOptions = ref([
|
||||
{ label: "较大", value: "large" },
|
||||
{ label: "默认", value: "default" },
|
||||
{ label: "稍小", value: "small" },
|
||||
]);
|
||||
])
|
||||
|
||||
function handleSetSize(size) {
|
||||
proxy.$modal.loading("正在设置布局大小,请稍候...");
|
||||
appStore.setSize(size);
|
||||
setTimeout("window.location.reload()", 1000);
|
||||
proxy.$modal.loading("正在设置布局大小,请稍候...")
|
||||
appStore.setSize(size)
|
||||
setTimeout("window.location.reload()", 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,30 +1,36 @@
|
||||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
:class="svgClass"
|
||||
:width="resolvedSize"
|
||||
:height="resolvedSize"
|
||||
:style="svgStyle"
|
||||
class="svg-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use :xlink:href="symbolId" />
|
||||
<use :xlink:href="symbolId" :fill="color || 'currentColor'" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
iconClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
default: ''
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: 24
|
||||
default: undefined
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'currentColor'
|
||||
default: ''
|
||||
},
|
||||
customStyle: {
|
||||
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(() => ({
|
||||
verticalAlign: 'middle',
|
||||
color: props.color, // 用 color 替代 fill
|
||||
color: props.color || undefined,
|
||||
...props.customStyle
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import * as components from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
install: (app) => {
|
||||
for (const key in components) {
|
||||
const componentConfig = components[key];
|
||||
app.component(componentConfig.name, componentConfig);
|
||||
}
|
||||
},
|
||||
};
|
||||
install: (app) => {
|
||||
for (const key in components) {
|
||||
const componentConfig = components[key]
|
||||
app.component(componentConfig.name, componentConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
756
src/components/TreePanel/index.vue
Normal 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>
|
||||
@ -22,10 +22,10 @@ const url = computed(() => props.src)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 300);
|
||||
loading.value = false
|
||||
}, 300)
|
||||
window.onresize = function temp() {
|
||||
height.value = document.documentElement.clientHeight - 94.5 + "px;";
|
||||
};
|
||||
height.value = document.documentElement.clientHeight - 94.5 + "px;"
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -2,65 +2,64 @@
|
||||
* v-copyText 复制文本内容
|
||||
* Copyright (c) 2022 ruoyi
|
||||
*/
|
||||
|
||||
export default {
|
||||
beforeMount(el, { value, arg }) {
|
||||
if (arg === "callback") {
|
||||
el.$copyCallback = value;
|
||||
el.$copyCallback = value
|
||||
} else {
|
||||
el.$copyValue = value;
|
||||
el.$copyValue = value
|
||||
const handler = () => {
|
||||
copyTextToClipboard(el.$copyValue);
|
||||
copyTextToClipboard(el.$copyValue)
|
||||
if (el.$copyCallback) {
|
||||
el.$copyCallback(el.$copyValue);
|
||||
el.$copyCallback(el.$copyValue)
|
||||
}
|
||||
};
|
||||
el.addEventListener("click", handler);
|
||||
el.$destroyCopy = () => el.removeEventListener("click", handler);
|
||||
}
|
||||
el.addEventListener("click", handler)
|
||||
el.$destroyCopy = () => el.removeEventListener("click", handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyTextToClipboard(input, { target = document.body } = {}) {
|
||||
const element = document.createElement('textarea');
|
||||
const previouslyFocusedElement = document.activeElement;
|
||||
const element = document.createElement('textarea')
|
||||
const previouslyFocusedElement = document.activeElement
|
||||
|
||||
element.value = input;
|
||||
element.value = input
|
||||
|
||||
// Prevent keyboard from showing on mobile
|
||||
element.setAttribute('readonly', '');
|
||||
element.setAttribute('readonly', '')
|
||||
|
||||
element.style.contain = 'strict';
|
||||
element.style.position = 'absolute';
|
||||
element.style.left = '-9999px';
|
||||
element.style.fontSize = '12pt'; // Prevent zooming on iOS
|
||||
element.style.contain = 'strict'
|
||||
element.style.position = 'absolute'
|
||||
element.style.left = '-9999px'
|
||||
element.style.fontSize = '12pt' // Prevent zooming on iOS
|
||||
|
||||
const selection = document.getSelection();
|
||||
const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0);
|
||||
const selection = document.getSelection()
|
||||
const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0)
|
||||
|
||||
target.append(element);
|
||||
element.select();
|
||||
target.append(element)
|
||||
element.select()
|
||||
|
||||
// Explicit selection workaround for iOS
|
||||
element.selectionStart = 0;
|
||||
element.selectionEnd = input.length;
|
||||
element.selectionStart = 0
|
||||
element.selectionEnd = input.length
|
||||
|
||||
let isSuccess = false;
|
||||
let isSuccess = false
|
||||
try {
|
||||
isSuccess = document.execCommand('copy');
|
||||
isSuccess = document.execCommand('copy')
|
||||
} catch { }
|
||||
|
||||
element.remove();
|
||||
element.remove()
|
||||
|
||||
if (originalRange) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(originalRange);
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(originalRange)
|
||||
}
|
||||
|
||||
// Get the focus back on the previously focused element, if any
|
||||
if (previouslyFocusedElement) {
|
||||
previouslyFocusedElement.focus();
|
||||
previouslyFocusedElement.focus()
|
||||
}
|
||||
|
||||
return isSuccess;
|
||||
return isSuccess
|
||||
}
|
||||
|
||||
@ -2,13 +2,12 @@
|
||||
* v-hasPermi 操作权限处理
|
||||
* Copyright (c) 2019 ruoyi
|
||||
*/
|
||||
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
export default {
|
||||
mounted(el, binding, vnode) {
|
||||
const { value } = binding
|
||||
const all_permission = "*:*:*";
|
||||
const all_permission = "*:*:*"
|
||||
const permissions = useUserStore().permissions
|
||||
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
|
||||
@ -2,13 +2,12 @@
|
||||
* v-hasRole 角色权限处理
|
||||
* Copyright (c) 2019 ruoyi
|
||||
*/
|
||||
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
export default {
|
||||
mounted(el, binding, vnode) {
|
||||
const { value } = binding
|
||||
const super_admin = "admin";
|
||||
const super_admin = "admin"
|
||||
const roles = useUserStore().roles
|
||||
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
|
||||
@ -8,10 +8,12 @@
|
||||
</transition>
|
||||
</router-view>
|
||||
<iframe-toggle />
|
||||
<copyright />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import copyright from "./Copyright/index"
|
||||
import iframeToggle from "./IframeToggle/index"
|
||||
import useTagsViewStore from '@/store/modules/tagsView'
|
||||
|
||||
@ -22,7 +24,7 @@ onMounted(() => {
|
||||
addIframe()
|
||||
})
|
||||
|
||||
watch((route) => {
|
||||
watchEffect(() => {
|
||||
addIframe()
|
||||
})
|
||||
|
||||
@ -34,10 +36,10 @@ function addIframe() {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/assets/styles/variables.module";
|
||||
@use "@/assets/styles/variables.module" as *;
|
||||
.app-main {
|
||||
/* 50= navbar 50 */
|
||||
height: calc(100vh - 84px);
|
||||
height: calc(100% - 84px);
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
@ -54,7 +56,7 @@ function addIframe() {
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
height: calc(100vh - 84px);
|
||||
height: calc(100% - 84px);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
@ -85,4 +87,3 @@ function addIframe() {
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
31
src/layout/components/Copyright/index.vue
Normal 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>
|
||||