Compare commits

...

84 Commits

Author SHA1 Message Date
80583a526d doc: 注释 2026-08-06 16:50:12 +08:00
1b8e2df145 feat: 检测项拖动调整 2026-08-06 15:40:32 +08:00
5fb3bf4e20 feat: 克隆节点 2026-08-06 15:10:47 +08:00
544711cd73 feat: 试运行不需要要有连接状态为连接的,因为可以选择未连接的机器人进行试运行来保存流程数据 2026-08-06 14:48:28 +08:00
f5b1b70274 feat: 处理轮询时地图刷新的问题 2026-08-06 13:46:08 +08:00
2159b7f228 feat: 加载地图 2026-08-06 11:52:30 +08:00
b332a5461e feat(utils): 添加AGV运行状态工具函数
- 定义AGV运行状态常量映射,包含未知、离线、在线、手动模式等状态
- 实现状态解析函数resolveStatus,支持数字类型转换
- 提供状态标签、类型和CSS类名的导出函数
- 支持11种不同的AGV运行状态及其对应的显示样式
- 添加默认未知状态兜底处理机制
2026-08-06 11:24:50 +08:00
02706a39a1 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev
# Conflicts:
#	src/views/inspection/cockpit/components/MapOverview.vue
2026-08-06 11:24:14 +08:00
e18c4b88be refactor(inspection): 统一AGV运行状态管理并优化机器人列表刷新逻辑
- 将多个组件中的硬编码机器人状态映射替换为统一的 agvRuntimeStatus 工具函数
- 在手动接管和地图概览组件中实现5秒定时刷新机器人列表功能
- 添加 onUnmounted 钩子清理定时器避免内存泄漏
- 重构 MapCanvas 中的机器人位置解析逻辑并添加数据验证
- 为地图概览组件的状态显示添加动态CSS类名支持
2026-08-06 11:23:19 +08:00
2ae34b1f9f feat: 加载机器人 2026-08-06 11:21:51 +08:00
2715d02c22 feat: 智能监控选择机器人 2026-08-06 09:08:53 +08:00
c2ab0aa8f2 style: 调整样式 2026-08-05 17:34:17 +08:00
916e6f5756 feat(flow): 添加节点选择和复制粘贴功能
- 实现了 selectElementsByIds 方法用于批量选择元素
- 更新了选择和拖拽行为配置以支持禁用表单时的操作
- 引入 uuid 库用于生成唯一标识符
- 重构复制粘贴逻辑以支持节点和边的完整复制
- 实现了节点引用映射和锚点ID重映射功能
- 添加了子节点递归选择和引用更新机制
- 优化了粘贴后节点的渲染和选择逻辑
- 更新了拖拽相关的CSS样式以改善用户体验
2026-08-05 15:18:52 +08:00
424df50112 feat: 数字平台加载地图 2026-08-05 14:51:04 +08:00
7c9f813246 feat: 加载站点 2026-08-05 14:17:16 +08:00
ea9314581e feat: 处理冲突 2026-08-05 12:41:29 +08:00
327bf0616c feat: xx 2026-08-05 12:40:41 +08:00
a588cb542a feat(inspection): 机器人列表增加运行状态、电量、位置等信息显示
- 新增运行状态列,显示在线、离线、充电中、巡检中等状态
- 新增电量列,显示电池电量进度条和百分比
- 新增当前位置列,显示AGV位姿坐标信息
- 新增当前地图列,显示机器人所在地图名称
- 在详情表单中新增AGV电量和位姿信息显示
- 添加同步机器人运行状态按钮,可手动刷新机器人实时状态
- 实现电量颜色分级显示,低电量红色警告
- 添加定时刷新功能,后台激活时自动更新列表数据
- 优化设备列表刷新逻辑,避免重复定时器问题
2026-08-05 12:37:43 +08:00
afcba74b35 feat: 处理冲突 2026-08-05 11:54:44 +08:00
b37785b0a2 feat: 处理机械臂运动速度 2026-08-05 11:53:00 +08:00
22e69b79ae feat(inspection): 更新地图页面机器人选择功能
- 优化机器人下拉选择器,添加过滤和加载状态显示
- 新增连接AGV设备的机器人筛选逻辑
- 集成机器人设备API获取AGV设备信息
- 改进机器人列表数据显示格式,显示设备ID
- 添加加载状态指示器提升用户体验
- 修复机器人数据获取逻辑确保仅显示已连接AGV的机器人
2026-08-05 11:51:31 +08:00
b2593bf857 fix(flow): 优化机器人设备绑定逻辑并改进状态提示
- 保留原始设备ID和选项作为默认值,避免数据丢失
- 当机器人无匹配在线设备时显示警告并保留原有设置
- 改进设备选择逻辑,支持设备种类过滤和自动填充
- 添加未触动手动设备节点统计功能
- 更新绑定面板状态显示,区分缺失设备和手动设备
- 优化错误处理机制,失败时恢复原始参数值
- 完善设备匹配成功和失败的消息提示内容
2026-08-05 10:46:11 +08:00
65d219062e feat(flow): 优化设备执行流程配置功能
- 将设备ID参数从输入框改为选择框,并设置为必填项
- 添加设备绑定工具栏显示执行设备选择信息
- 集成机器人设备选择对话框功能
- 实现机器人列表加载和设备选项动态获取
- 添加设备选择表单验证和确认逻辑
- 支持自动选择唯一在线设备并锁定
- 优化键盘事件处理,区分编辑状态和流程操作
- 添加相关的API接口调用和工具函数
- 增加相应的样式定义和用户提示信息
2026-08-05 10:15:34 +08:00
61e07866bd style: 调整样式 2026-08-05 09:53:23 +08:00
e3a566cf28 feat(utils): 添加机器人设备工具函数
- 定义 DEVICE_KIND 枚举映射设备类型
- 创建 DEVICE_KIND_BY_ACTION 映射动作到设备类型
- 实现 getDeviceKindForAction 函数获取动作对应的设备类型
- 添加 toDeviceOptions 函数将设备列表转换为选择框选项格式
- 支持设备名称显示,若设备名存在则显示 "设备名 (设备ID)" 格式
2026-08-05 09:28:45 +08:00
45b190ff02 feat(flow): 优化机器人设备绑定功能
- 将terminalId替换为robotId以统一机器人标识
- 实现机器人设备动态加载和选择功能
- 添加设备类型过滤和状态显示
- 重构设备注册页面为机器人设备管理界面
- 优化API调用参数传递方式
- 添加设备自动匹配和锁定机制
2026-08-05 09:27:55 +08:00
d55f95e7d3 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-04 17:29:32 +08:00
210e4cf716 feat(flow): 将流程节点中的终端ID替换为机器人ID
- 替换singleNodeTerminalId为singleNodeRobotId
- 更新流程图中机器人选择功能,使用机器人ID替代终端ID
- 修改AGV移动到站点功能以使用机器人ID
- 更新任务执行页面中的机器人选择组件
- 重构机器人管理页面,添加设备管理功能
- 实现机器人详情抽屉和设备列表展示
- 添加机器人归档功能和在线状态显示
2026-08-04 17:29:07 +08:00
36c39bb873 fix: 更改请求地址 2026-08-04 16:30:00 +08:00
f4eb49763a Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-04 16:22:13 +08:00
9537129647 feat(camera): 添加RGB流媒体URL获取功能
- 实现了getRgbStreamUrl函数用于获取摄像头RGB流媒体地址
- 集成了request工具进行API请求处理
- 支持通过terminalId和deviceId参数获取对应的流媒体URL
- 添加了API端点/api/edge/camera/rgb-stream-url的GET请求支持
2026-08-04 16:21:54 +08:00
b0e8a27046 feat: 通用指令配置 2026-08-04 16:02:56 +08:00
b49de73aa7 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-04 15:35:54 +08:00
bac6e1aa52 feat(flow): 添加AGV站点列表功能支持
- 在基本节点参数组件中增加对AGV_MOVE_TO_STATION操作的支持
- 添加加载AGV站点列表的按钮和对话框功能
- 实现站点列表的异步加载和选项填充逻辑
- 为站点ID参数添加特殊验证规则确保设备匹配
- 在选择站点时添加设备ID验证防止数据错配
- 添加加载状态指示器和错误处理机制
- 实现站点选项的数据转换和显示格式化功能
2026-08-04 15:35:32 +08:00
75103022c3 feat: 更改模型加载方式 2026-08-04 15:26:56 +08:00
b1e35350a2 feat: 处理冲突 2026-08-04 11:48:47 +08:00
ad6e38d568 feat: 修改关节运动节点 2026-08-04 11:47:24 +08:00
95ea34bbb8 refactor(workflow): 重构工作流配置和界面组件
- 更新 config.js 配置文件结构以支持新的工作流参数
- 修改 flow.js 工作流逻辑实现,优化执行效率
- 重构 index.vue 组件界面,改进用户体验和交互流程
- 统一配置管理方式,提高代码可维护性
- 优化组件渲染性能,减少不必要的重绘
- 调整工作流状态管理机制,增强稳定性
2026-08-04 11:46:14 +08:00
9715101faf feat(flow): 添加机械臂关节运动节点支持
- 在BasicNodeParams.vue中为ARM_MOVE_TO_J操作添加按钮显示逻辑
- 从设备API导入getArmJointState方法用于获取机械臂关节状态
- 实现ARM_MOVE_TO_J操作的表单验证和位置获取逻辑
- 在config.js中添加booleanOptions选项配置
- 添加机械臂关节运动节点配置,包括参数定义和属性设置
2026-08-04 10:54:53 +08:00
bffa17a6a2 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-04 10:39:44 +08:00
a3b3f7a1c3 feat: 添加复制节点 2026-08-04 10:08:57 +08:00
05f4f007a7 feat: 通用控制指令 2026-08-04 09:05:39 +08:00
1a26063ce2 feat: agv获取当前位置 2026-08-03 16:53:30 +08:00
e501d80953 feat: 机械臂提示 2026-08-03 14:43:09 +08:00
758d5f55e6 feat: 日志 2026-08-03 13:38:57 +08:00
7b791bc541 refactor(camera): 重构摄像头视频流处理机制
- 移除原有的双视频元素缓冲机制,改用MediaSource API实现直接流式播放
- 新增RGB和深度图像流的状态管理系统,支持连接、实时、重连、异常状态显示
- 实现直接RGB流播放功能,通过新的getRgbStreamUrl接口获取视频流
- 添加媒体播放器管理机制,支持视频流的创建、销毁和状态维护
- 优化视频播放性能,实现缓冲区管理和实时播放同步
- 简化UI布局结构,移除单一流容器,统一使用流容器组件
- 更新WebSocket插件以支持媒体数据包解析和分发
- 增加重连机制和错误处理,提升视频流播放稳定性
- 移除lodash防抖依赖,改用原生setTimeout实现重连调度
2026-08-03 09:49:53 +08:00
31d6458fd9 feat:处理冲突 2026-07-30 10:31:26 +08:00
c9a6256465 feat: git 忽略 2026-07-30 10:27:27 +08:00
31027439a4 feat(home): 重构首页为现代化仪表板布局
- 替换原有简单标题页面为功能丰富的仪表板界面
- 添加系统概览统计卡片展示设备、流程、任务和实例数量
- 集成实时任务运行状态面板显示当前执行的巡检任务
- 实现能力中心功能网格提供快速导航入口
- 添加最近测试实例列表展示近期执行记录
- 集成权限控制支持不同角色的功能访问限制
- 实现响应式设计适配桌面到移动端各种屏幕尺寸
- 添加动态时间显示个性化问候语和当前时间日期
- 配置深色模式支持提升用户体验
2026-07-30 10:26:02 +08:00
a30903dd70 feat: optimize Vue Flow workflow editor 2026-07-29 16:17:39 +08:00
320d1a9d92 feat: migrate workflows to Vue Flow 2026-07-29 11:43:07 +08:00
1b0f414ba1 feat: migrate frontend to Vue 3 2026-07-28 16:26:07 +08:00
6cefb1a636 feat: 增加transformRecognizeNodeData 2026-07-28 13:44:47 +08:00
998d7d3826 feat: 合并长安巡检和艾玛 2026-07-28 09:22:22 +08:00
f5711a1212 feat: 调整节点引用 2026-07-22 15:59:26 +08:00
c7ad9e3e11 feta: 调整请求方式 2026-07-22 15:54:48 +08:00
5d8974672e feat: 调整流程节点 2026-07-17 10:59:41 +08:00
9ff9194e41 feat: flow 2026-07-08 11:14:40 +08:00
587ba3482e 智能交互 2026-07-08 11:09:51 +08:00
9d37c56b8d feat: 看板数据 2026-07-07 15:58:40 +08:00
a24733f6ea feat: 语料文本 2026-07-07 15:33:43 +08:00
af2f9398b7 feat: 合成语音 2026-07-07 14:12:57 +08:00
36fad9c1ca feat: 分类树 2026-07-06 20:57:20 +08:00
ecd13e2494 feat: 语音智能体 2026-07-06 19:49:57 +08:00
eebd0dc6b7 feat: 调整流程节点 2026-07-06 11:11:59 +08:00
6af1118a3c feat: 加载地图 2026-06-12 17:27:59 +08:00
780665fa9a feat: 任务执行日志 2026-06-05 17:18:47 +08:00
64d6cad887 feat: 执行任务 2026-06-05 16:53:40 +08:00
2d2854814b feat: 执行任务 2026-06-05 14:50:29 +08:00
7590d5a986 feat: 绑定点位 2026-06-05 09:07:44 +08:00
8e53faad52 feat: 执行任务 2026-06-04 16:41:29 +08:00
e9717c85e2 feat: 任务管理 2026-06-02 10:48:03 +08:00
bd2014edf0 feat: 处理冲突 2026-06-02 09:55:25 +08:00
a0b397fc40 feat: 点位 2026-06-02 09:52:53 +08:00
d198189d2c feat: 点位管理和机器人 2026-06-01 11:04:18 +08:00
4e8998216f feat: 机器人管理和地图管理 2026-05-29 13:54:45 +08:00
dcae537302 Merge branch 'dev' of ssh://192.168.28.10:10022/SmartBench/CMVR-IOT-UI into dev 2026-05-28 14:52:19 +08:00
7c449c52d9 ```
build(staging): remove verbose flag from staging build script

Remove the --verbose flag from the build:staging script in package.json
to simplify the build output for staging environment.
```
2026-05-28 14:52:13 +08:00
4de5e21929 feat: 机器人 2026-05-26 14:45:49 +08:00
5b4cdb7140 Merge pull request 'feat: 长安巡检' (#1) from main into dev
Reviewed-on: #1
2026-05-25 09:26:35 +08:00
77223344b5 feat:修改test配置 2026-05-17 09:34:32 +08:00
d2299381ca feat: 修改test配置 2026-05-16 16:49:10 +08:00
be4eef6fd1 Merge branch 'dev' of 10.148.121.100:SmartBench/CMVR-IOT-UI into dev 2026-05-16 16:00:36 +08:00
ec6e238356 feat:怎么镜像打包文件 2026-05-16 15:56:05 +08:00
335 changed files with 45792 additions and 21202 deletions

View File

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

View File

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

View File

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

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

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

5
.gitignore vendored
View File

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

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .

17
Dockerfile.frontend Normal file
View 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
View File

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

12
bin/build.bat Normal file
View File

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

12
bin/package.bat Normal file
View File

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

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

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

View File

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

View File

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

5973
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
pnpm-workspace.yaml Normal file
View File

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

File diff suppressed because one or more lines are too long

54
public/bot.svg Normal file
View 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

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

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

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

461
src/adapters/URDFAdapter.js Normal file
View 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`);
}
}
}

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

View 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'
})
}

View 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'
})
}

9
src/api/device/camera.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getRgbStreamUrl(robotId, deviceId) {
return request({
url: '/api/edge/camera/rgb-stream-url',
method: 'get',
params: { robotId, deviceId }
})
}

View File

@ -5,7 +5,7 @@ export function startMicrophoneApi(data) {
return request({ return request({
url: '/api/Microphone/start', url: '/api/Microphone/start',
method: 'post', method: 'post',
params: data data: data
}) })
} }
@ -14,7 +14,7 @@ export function pauseMicrophoneApi(data) {
return request({ return request({
url: '/api/Microphone/pause', url: '/api/Microphone/pause',
method: 'post', method: 'post',
params: data data: data
}) })
} }
@ -23,7 +23,7 @@ export function resumeMicrophoneApi(data) {
return request({ return request({
url: '/api/Microphone/resume', url: '/api/Microphone/resume',
method: 'post', method: 'post',
params: data data: data
}) })
} }
@ -32,7 +32,7 @@ export function stopMicrophoneApi(data) {
return request({ return request({
url: '/api/Microphone/stop', url: '/api/Microphone/stop',
method: 'post', method: 'post',
params: data data: data
}) })
} }
@ -41,6 +41,6 @@ export function playSpeaker(data) {
return request({ return request({
url: '/api/Speaker/play', url: '/api/Speaker/play',
method: 'post', method: 'post',
params: data data: data
}) })
} }

View File

@ -5,7 +5,7 @@ export function pause(data) {
return request({ return request({
url: '/api/Speaker/pause', url: '/api/Speaker/pause',
method: 'post', method: 'post',
params: data data: data
}) })
} }
@ -14,7 +14,7 @@ export function play(data) {
return request({ return request({
url: '/api/Speaker/play', url: '/api/Speaker/play',
method: 'post', method: 'post',
params: data data: data
}) })
} }
@ -23,7 +23,7 @@ export function resume(data) {
return request({ return request({
url: '/api/Speaker/resume', url: '/api/Speaker/resume',
method: 'post', method: 'post',
params: data data: data
}) })
} }
@ -41,7 +41,7 @@ export function stop(data) {
return request({ return request({
url: '/api/Speaker/stop', url: '/api/Speaker/stop',
method: 'post', method: 'post',
params: data data: data
}) })
} }
@ -59,7 +59,7 @@ export function setVolume(data) {
return request({ return request({
url: '/api/Speaker/volume', url: '/api/Speaker/volume',
method: 'post', method: 'post',
params: data data: data
}) })
} }

View File

@ -61,3 +61,35 @@ export function flowStop(instId) {
method: 'post' 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
})
}
export function getAgvStations(params) {
return request({
url: '/api/agv/listStations',
method: 'get',
params: params
})
}
export function getArmJointState(params) {
return request({
url: '/api/arm/getJointState',
method: 'get',
params: params
})
}

View 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
View 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'
})
}

View 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'
})
}

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

126
src/api/inspection/robot.js Normal file
View File

@ -0,0 +1,126 @@
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'
})
}
export function getRobotDevices(id) {
return request({
url: `/inspection/robot/${id}/devices`,
method: 'get'
})
}
export function getRobotDevicesByRobotId(robotId, params = {}) {
return request({
url: '/inspection/robot/devices',
method: 'get',
params: {
robotId,
onlineOnly: true,
...params
}
})
}
/**
* 获取机器人地图
* @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(id) {
return request({
url: `/inspection/robot/${id}/sync-status`,
method: 'post'
})
}
/**
* 机器人运动控制
* @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
})
}

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

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

View 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'
})
}

View 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'
})
}

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

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

View 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'
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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

View File

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

After

Width:  |  Height:  |  Size: 750 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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

View File

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

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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

View File

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

Before

Width:  |  Height:  |  Size: 211 B

After

Width:  |  Height:  |  Size: 226 B

View File

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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,10 @@
@use './variables.module.scss' as *;
#app { #app {
.main-container { .main-container {
min-height: 100%; // min-height: 100%;
height: 100%;
transition: margin-left .28s; transition: margin-left .28s;
margin-left: $base-sidebar-width; margin-left: $base-sidebar-width;
position: relative; position: relative;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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