Compare commits

..

172 Commits

Author SHA1 Message Date
f18af176f5 Merge remote-tracking branch 'origin/feature/unified-resource-center' into feature/unified-resource-center
# Conflicts:
#	.env.development
#	src/views/flow/components/ParamsDrawer.vue
#	src/views/flow/components/params/ImageAnalysisParams.vue
#	src/views/flow/components/params/MediaAnalysisParams.vue
#	src/views/flow/config.js
#	src/views/flow/vue-flow/FlowNode.vue
#	src/views/resource/device/index.vue
2026-09-16 11:42:33 +08:00
e82dc48483 feat(flow): 新增聊天参数组件和视觉标定管理功能
- 添加 ChatParams.vue 组件用于配置聊天节点参数
- 实现文本来源选择(直接输入或引用上游输出)
- 集成提示词模式设置(固定或自定义)
- 添加 TTS 语音播放开关功能
- 创建视觉标定管理界面(手眼标定和工具 TCP)
- 实现标定方案列表展示和状态管理
- 添加标定会话实时监控和进度显示
- 支持标定结果验证和误差分析
- 提供六自由度人工微调功能
2026-09-16 11:40:47 +08:00
275aeb5f4f feat(flow): 添加视觉定位和闲聊节点功能
- 新增视觉定位执行节点,支持RGBD图像分析和目标定位
- 添加闲聊节点,集成大模型自然语言对话功能
- 配置视觉定位相关的设备绑定和参数设置
- 更新设备资源管理界面,增加机器人筛选功能
- 优化图像分析参数,提高定位精度
- 添加车机发声检测功能到声音分析模块
2026-09-16 11:39:59 +08:00
eae22075f4 feat(websocket): 优化WebSocket连接,支持动态用户ID和token传递 2026-08-26 14:11:09 +08:00
c0869ddb2b fix(vite): 更新monaco-editor插件配置,修复语言工作者设置 2026-08-26 08:45:14 +08:00
4b170fa95a Merge branch 'feature/unified-resource-center' of http://192.168.28.10:18088/SmartBench/CMVR-IOT-UI into feature/unified-resource-center 2026-08-25 17:00:24 +08:00
59a020da32 fix(env): 更新API和WebSocket URL为正确的开发环境地址 2026-08-25 16:54:26 +08:00
e64af95cd1 refactor: 移除VueFlow组件及相关逻辑
- 从VueFlow目录中删除了RuntimeNode.vue、customFlow.vue和index.vue组件。
- 从robotManger目录中移除了ContextMenu.vue和TechButtonWithLine.vue组件。
- 通过移除控制页面导航逻辑和数字转换工具,对index.js进行了整理。
2026-08-25 15:16:09 +08:00
d81dab5604 feat: 更新 jsEncrypt 导入路径,优化 Vite 配置以支持 cmvr-iot-common 2026-08-25 14:04:27 +08:00
73ee3fe080 feat: 重构 2026-08-25 10:26:33 +08:00
815f8d3cde feat: 重构 2026-08-25 10:25:40 +08:00
3909a1f217 refactor(api): 调整API导入路径并更新设备注册相关功能
- 将inspection/robot API导入路径更改为resource/robot
- 更新getRobotDevicesByRobotId等函数的导入路径
- 修改流程序列组件中的API引用路径
- 更新设备注册页面的相关API调用
- 移除AppMain.vue中的过渡动画
- 添加新的resource/device API模块
- 更新语音合成组件的API导入路径
- 修改Node-RED转换工具的API引用
- 调整手动接管方向控制的API导入
- 更新流执行工具的API调用
- 优化流资源角色处理逻辑
- 移除已废弃的设备控制台和配置页面
- 更新相机和灵巧手组件的数据获取方式
- 调整麦克风组件的API引用路径
- 更新VueFlow组件的API导入
- 修改开发环境配置的URL地址和端口设置
2026-08-24 16:50:24 +08:00
657b7b9338 feat(flow): 优化流程试运行功能
- 添加试运行准备状态指示器
- 在试运行按钮上增加加载状态和禁用逻辑
- 实现试运行前自动保存草稿功能
- 为保存草稿功能添加可选的成功消息参数
- 防止试运行和保存操作同时执行的冲突处理
2026-08-21 14:19:48 +08:00
b3dae8db39 feat(flow): 添加子流程功能并优化参数引用逻辑
- 新增子流程节点类型,支持在同一项目中执行已发布工作流
- 添加子流程节点UI组件和相关配置选项
- 重构参数引用逻辑,使用getUpstreamInput替代getInput方法
- 优化cascader组件属性设置,统一使用props配置checkStrictly
- 修正多个参数组件中的handleTypeChange和cascaderChange函数参数传递
- 添加流程输出参数管理功能,支持引用来源选择和类型识别
- 实现子流程运行时状态管理和迭代输出处理
- 增加循环引用检测和资源验证功能
- 更新API接口,添加子流程选项获取方法
- 修复流程引用和依赖关系处理逻辑
2026-08-21 11:48:10 +08:00
e136cf8a31 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-20 13:36:47 +08:00
160527a3ac feat(flow): 添加工作流版本管理和草稿功能
- 新增 flowSaveDraft、flowVersionList、flowVersionDetail 等API接口
- 添加工作流草稿保存和版本历史记录功能
- 实现版本预览、恢复为草稿和回退版本功能
- 在界面添加版本标签显示当前发布版本信息
- 集成版本历史抽屉组件展示发布记录列表
- 添加发布说明输入框和确认对话框
- 实现版本比较和状态跟踪功能
2026-08-20 13:36:24 +08:00
e403befd44 doc: 处理冲突 2026-08-20 13:36:03 +08:00
ad64917603 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev
# Conflicts:
#	src/utils/flowResources.js
#	src/views/flow/components/params/ImageAnalysisParams.vue
2026-08-20 11:47:37 +08:00
311c8c5082 feat(flow): 增加AGV导航阻塞超时配置和图像分析功能增强
- 在AGV导航动作中添加blockedTimeoutMs参数配置,默认值为60000毫秒
- 实现图像分析多种模式:智能分析、描述找图、精确找图
- 增加图像分析异步执行开关功能
- 添加图像分析匹配结果相关输出参数:匹配分数、坐标、中心点等
- 完善图像分析参数验证和错误处理逻辑
- 在任务执行日志中增加失败原因显示功能
- 优化图像分析界面组件的交互和表单验证规则
2026-08-20 11:41:28 +08:00
4ec8ecc3e1 docs(flow): 补充运行配置编辑器注释 2026-08-19 15:18:24 +08:00
dddddbbe3d feat: 试运行只能选中在线的机器人 2026-08-19 15:14:24 +08:00
4bca7389f7 docs(flow): 补充图片分析相关注释 2026-08-19 15:12:29 +08:00
71feae4bbd feat(flow): 添加通用图片分析功能并优化媒体分析节点
- 新增通用图片分析节点类型,支持按提示词分析图片并返回指定内容
- 添加详细的图片分析参数配置界面,包括图片源选择、提示词输入等功能
- 实现图片分析节点的输出参数定义和处理逻辑
- 更新视频分析节点,增加详细输出选项和结果字段
- 重构错误状态恢复功能,更名为运行状态恢复并更新相关UI
- 添加图片分析结果的日志展示支持
- 优化流程资源配置,增加图片分析相关的常量定义
- 更新单节点执行参数构建逻辑,适配新的图片分析功能
2026-08-19 15:00:17 +08:00
47036552b4 Merge branch 'lxl-dev' of http://192.168.28.10:18088/SmartBench/CMVR-IOT-UI into lxl-dev 2026-08-18 16:04:33 +08:00
74dceedbdc Merge branch 'lxl-dev' of http://192.168.28.10:18088/SmartBench/CMVR-IOT-UI into lxl-dev 2026-08-18 16:04:17 +08:00
81ecb383e9 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-18 16:03:12 +08:00
a5ccfc5468 feat(flow): 将媒体分析参数从抽取帧数改为采样帧率
- 将 analysisTuning 配置中的 maxFrames 参数替换为 sampleFps 参数
- 更新 MediaAnalysisParams 组件中的表单项标签为"采样帧率(帧/秒)"
- 修改滑块控件绑定到 formData.analysisTuning.sampleFps 属性
- 更新预设配置中的 FAST、AUTO、ACCURATE 模式参数
- 调整归一化存储调优逻辑以支持新的 sampleFps 参数范围
- 更新采样帧率参数的有效范围为 0.25 到 6 帧/秒
2026-08-18 16:03:02 +08:00
1edb97f606 feat: 优化任务日志自动滚动 2026-08-18 15:49:53 +08:00
36016684a3 doc: 添加注释 2026-08-18 15:32:41 +08:00
7299743bb1 feat(device): 添加安全状态管理API接口
- 实现获取机器人安全状态的API函数
- 实现恢复安全状态的API函数
- 集成系统安全状态相关的请求接口
- 提供机器人ID参数传递功能
- 支持数据传输的安全状态恢复操作
2026-08-18 14:43:15 +08:00
eb056f097c feat(flow): 更新声音类型检测功能并添加系统错误状态清除
- 修改声音事件识别为声音类型检测,调整相关参数配置
- 添加目标声音类型选择功能和预期标签匹配逻辑
- 新增机械臂故障清除API接口和操作按钮
- 实现工作流中清除机器人错误状态的功能和对话框
- 更新任务执行日志中声音分析结果显示逻辑
- 优化声音类型检测的输出参数和验证规则
2026-08-18 14:42:33 +08:00
15f82efb49 feat: 按点位分类巡检任务日志 2026-08-18 14:11:17 +08:00
f26519a824 feat: 动态计算任务日志高度 2026-08-18 13:45:45 +08:00
6a24cd17cc feat: 按选中任务展示运行详情 2026-08-18 13:32:49 +08:00
239065a7d3 docs: 补充任务日志与机械臂节点注释 2026-08-18 11:32:34 +08:00
f944d33d58 feat(flow): 添加机械臂使能控制节点并优化任务执行日志显示
- 新增开启机械臂使能和关闭机械臂使能两个服务节点
- 在机器人设备工具类中注册ARM_ENABLE和ARM_DISABLE操作类型
- 优化运行任务页面中的状态判断逻辑,支持多种终端状态
- 为任务日志组件增加任务状态属性传递
- 增强日志组件中检测项状态推导功能,支持重复执行和分析状态显示
- 添加执行上下文解析以正确处理重复检测项的日志归类
- 改进日志详情过滤,排除执行上下文相关字段
- 实现检测项完成状态和分析结果的正确标识与展示
2026-08-18 11:19:52 +08:00
43b07c94f4 feat: 日志 2026-08-18 10:18:25 +08:00
2fe732f1df style: 调整样式 2026-08-17 17:10:59 +08:00
6be8bc9bfd feat(flow): 添加动作队列步骤独立执行功能和媒体分析增强
- 在动作队列参数组件中添加单独执行按钮,允许独立运行指定步骤
- 实现步骤验证和构建逻辑,支持单步执行的参数处理
- 更新媒体分析参数界面,添加高级设置和参数微调功能
- 增加视频分析的置信度、通过标准等配置选项
- 优化任务执行日志显示,添加分析结果的结构化展示
- 集成分页请求工具,改进日志加载性能和用户体验
- 添加执行步骤索引跟踪,确保执行状态正确管理
- 实现防重复ID机制,保证分页数据完整性
- 新增分析结果解析和可视化展示功能
- 扩展媒体分析类型的处理和结果显示
2026-08-17 16:05:42 +08:00
cad4184a2f feat(flow): 添加动作队列节点拖拽和步骤提取功能
- 实现节点拖拽到动作队列的功能,支持将工作流节点转换为队列步骤
- 添加从动作队列中提取步骤还原为独立节点的功能,支持多种放置方式
- 新增动作队列相关的工具函数和验证逻辑
- 在流程图界面中添加队列拖拽视觉反馈效果
- 扩展动作队列参数面板,增加步骤提取操作选项
- 实现队列步骤与源节点之间的双向转换机制
2026-08-14 15:37:47 +08:00
7de5b5d093 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-14 15:37:32 +08:00
06633c3a80 doc: 处理冲突 2026-08-14 15:23:15 +08:00
32ed2122d3 feat: 更改项目名称 2026-08-14 15:21:27 +08:00
fabfa05db5 fix(websocket): 解决WebSocket重连后频道订阅丢失问题
- 移除调试用的console.log语句
- 添加channelSubscriptions集合跟踪频道订阅状态
- 在WebSocket连接建立时恢复之前的频道订阅
- 添加refreshAfterSocketReconnect函数用于重连后刷新数据
- 在组件挂载时监听socket open事件并绑定重连刷新逻辑
- 在组件卸载时移除socket open事件监听器
2026-08-14 13:31:37 +08:00
e50a6a481f feat(task): 优化任务步骤组件显示和交互
- 添加当前步骤状态显示区域,展示步骤计数和名称
- 实现步骤激活状态的标准化计算逻辑
- 优化滚动到当前步骤的功能,使用watch监听属性变化
- 改进步骤列表的样式布局,设置固定宽度和响应式设计
- 统一两个相似组件的代码结构和功能实现
2026-08-14 12:23:04 +08:00
b185b64de8 feat(flow): 添加自动分配机器人和设备功能
- 在机器人选择下拉框中添加自动分配状态加载提示
- 添加 autoAssigning 状态变量控制自动分配过程中的UI显示
- 实现 getOnlineDevices 函数缓存机器人设备数据避免重复请求
- 新增 planRoleDevices 函数规划角色设备分配逻辑
- 实现 autoAssignRoles 函数自动匹配可用的机器人和设备
- 更新初始化逻辑使用自动分配替代手动设备加载
- 添加验证逻辑防止在自动分配过程中提交表单
2026-08-14 12:08:35 +08:00
1a473635e0 feat(inspection): 添加复制功能支持
- 在检测配置API中新增copyConfig函数
- 在检测项API中新增copyDetect函数
- 在巡检点API中新增copyPoint函数
- 在巡检任务API中新增copyTask函数
- 在智能测试任务管理API中新增copyTask函数
- 在测试用例API中新增copyTestCase函数
- 在各个视图组件中添加复制按钮和处理逻辑
- 增加复制状态loading显示和成功提示
- 扩展表格列宽度以容纳新的复制按钮
2026-08-14 09:07:39 +08:00
7658e29cf2 feat(task): 添加任务重新执行功能
- 在巡检任务页面添加重新执行按钮,支持已完成和失败的任务重新运行
- 在智能测试任务页面添加重新执行功能,允许用户基于历史参数创建新任务
- 在日志页面添加重新执行按钮,方便快速重启历史任务实例
- 实现任务选择高亮显示,改善用户体验
- 添加重新执行API接口,支持按历史参数创建新任务实例
- 实现任务列表滚动定位到选中任务的功能
2026-08-13 17:54:04 +08:00
3d343812a9 feat(flow): 添加动作队列步骤管理功能
- 新增 actionQueue.js 工具函数,包含步骤移动、复制、名称获取等功能
- 在 ActionQueueParams.vue 中实现步骤名称编辑和复制功能
- 添加步骤上下移动和删除操作的工具提示
- 实现步骤名称显示和索引编号的样式优化
- 在 ActionQueueSingleRun.vue 中添加步骤复制和移动功能
- 重构步骤数据结构,添加运行时键值和名称字段
- 更新样式以支持新的步骤头部布局和操作按钮
2026-08-13 17:16:42 +08:00
d3f344338a Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-13 16:37:02 +08:00
360a8b842f feat(flow): 添加媒体分析节点功能
- 新增声音事件识别节点,支持音频分类匹配功能
- 新增视频智能分析节点,支持视频内容理解与结构化输出
- 添加MediaAnalysisParams组件实现媒体分析参数配置界面
- 集成媒体分析节点到参数抽屉组件中
- 优化参数验证逻辑,添加required字段判断条件
2026-08-13 16:36:15 +08:00
0780d298c6 doc: 调价注释 2026-08-13 10:23:55 +08:00
3ccf3e2c00 feat(flow): 优化动作队列参数界面并添加状态标准化功能
- 将动作步骤界面改为折叠面板形式提升用户体验
- 添加动作类型标签显示和步骤帮助功能
- 实现机器人设备参数读取助手对话框
- 为不同动作类型设置默认超时时间
- 统一任务状态数据类型为字符串格式
- 修复多处状态比较的类型匹配问题
2026-08-13 09:40:16 +08:00
3b820be743 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-12 17:58:11 +08:00
16481996b9 feat(flow): 添加动作队列节点功能
- 新增 actionQueue 节点类型,支持在边缘端按顺序连续执行多个动作
- 实现 ActionQueueParams 组件,提供执行设置和动作步骤配置界面
- 添加 ActionQueueSingleRun 组件,支持动作队列的单节点执行
- 更新 flowResources 工具函数,完善设备绑定和资源管理逻辑
- 集成动作队列到流程编辑器,支持参数配置和设备分配
- 优化节点渲染显示,支持多设备用途的角色展示
2026-08-12 17:57:54 +08:00
39af1ad1c5 feat: 运行参数 2026-08-12 16:49:33 +08:00
c99cd51d34 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-12 16:46:07 +08:00
a2cccb09f1 feat(task): 添加运行参数编辑器并优化执行安排对话框
- 集成 RuntimeParamsEditor 组件用于编辑运行参数
- 扩展执行安排对话框宽度从 560px 到 820px
- 在对话框中添加机器人和设备标题区域
- 添加运行参数标题区域并显示参数编辑器
- 集成 getFlowParamsApi 获取流程参数数据
- 使用 loadFlowItemScopedRuntimeRoles 替代旧的角色加载方法
- 添加 runtimeParamItems 和 runtimeParamsEditorRef 响应式变量
- 实现运行参数验证功能并在提交前检查
- 将运行参数传递给任务启动 API
- 优化任务进度更新中的步骤计算逻辑
- 添加运行参数编辑器组件实现参数输入和验证功能
2026-08-12 16:45:48 +08:00
4da4df43e8 feat: 任务队列删除 2026-08-12 16:45:24 +08:00
f4c594742e feat: 手机管理编辑 2026-08-12 16:17:41 +08:00
ab368e1a05 doc: 处理冲突 2026-08-12 15:30:47 +08:00
560b623b70 feat: 排除过滤 2026-08-12 15:29:12 +08:00
9d57865e6a refactor(draggable): 优化拖拽组件绑定逻辑
- 添加 cloneBindingItem 方法用于创建唯一绑定键
- 使用 _bindingKey 替代 name 作为拖拽项标识符
- 修改 removeFromB 方法参数从 id 改为 index 提升删除效率
- 重构数据映射逻辑统一添加 _bindingKey 字段
- 移除重复的已绑定项过滤逻辑简化代码结构
2026-08-12 15:28:38 +08:00
40a787e723 fix(inspection): 修复任务绑定数据初始化问题
- 清空左右列表数据避免残留数据影响
- 添加异步等待确保数据加载完成
- 移除调试日志代码
- 保证任务切换时数据正确重置
2026-08-12 15:11:55 +08:00
4b9d599a3b style: 调整样式 2026-08-12 15:10:58 +08:00
14467a3eb3 feat: 艾玛已选择点位不能拖动到待选择点位 2026-08-12 14:48:15 +08:00
82642db70f refactor(log): 替换任务日志组件为通用执行日志组件
- 移除 inspection 和 intelligenceTest 模块中的重复日志显示代码
- 引入统一的 TaskExecutionLog 组件处理日志渲染逻辑
- 新组件支持多媒体内容展示包括图片、视频和音频
- 实现媒体资源自动解析和预览功能
- 优化滚动到底部逻辑确保日志实时显示
- 采用响应式布局适配不同屏幕尺寸
2026-08-12 14:39:36 +08:00
e88a81049b refactor(flow): 优化流程参数初始化和运行时分配逻辑
- 修改 DeviceUniversal.vue 中的 initData 函数,使用可选链操作符安全访问属性
- 将 setTimeout 替换为 nextTick 确保编辑器值正确设置
- 添加对 props.data 的监听以响应数据变化
- 重构 RobotExecutionTarget.vue 中的初始化函数名称和监听逻辑
- 在 flowExecution.js 中添加 itemOccurrence 字段支持
- 优化 RuntimeAssignmentsEditor.vue 中的设备复用检查逻辑
- 实现基于检测项范围的设备键值生成策略
2026-08-12 14:13:16 +08:00
24b3acd2fd Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-12 13:49:33 +08:00
1f728b338d feat(flow): 添加音频输出支持和播放器组件
- 为MICROPHONE_STOP节点类型添加audio输出类型支持
- 新增audioUrl输出参数用于存储录音播放地址数组
- 在服务节点组件中添加音频播放器显示功能
- 扩展JsonViewer条件判断排除音频类型输出
- 实现音频URL计算逻辑支持数组和字符串格式
- 添加音频播放器样式配置
2026-08-12 13:49:11 +08:00
b692f0c97d style: 样式调整 2026-08-12 13:32:00 +08:00
6b8fb06600 feat: 多检测项配置多机器人 2026-08-12 12:21:06 +08:00
8d2f3e37b4 fix(flow): 解决流程节点类型兼容性和状态处理问题
- 添加 device_universal 节点类型的别名支持
- 实现遗留节点类型别名映射机制
- 修复流程运行结果显示中的空数组处理逻辑
- 优化流程状态判断和时间计算逻辑
- 改进流程调度和错误处理机制
2026-08-12 11:17:19 +08:00
c54f1da126 feat(flow): 新增工作流执行资源配置功能
- 添加了 flowExecution 工具模块用于处理工作流运行时资源配置
- 实现了工作流角色定义的加载、合并和验证功能
- 集成了机器人和设备分配逻辑到任务执行界面
- 更新了 RuntimeAssignmentsEditor 组件以支持动态设备分配
- 移除了静态机器人选择,改为基于流程角色的动态分配
- 优化了工作流资源定义合并逻辑,支持插槽复用配置
- 增强了设备冲突检测和错误处理机制
- 统一了多个视图中的工作流执行资源配置流程
2026-08-12 09:31:45 +08:00
e3a2761683 fix(flow): 解决非机器人节点携带机器人执行目标导致的后端误判问题
- 非机器人节点不再携带 executionTarget 属性,避免后端将空对象误判为无效角色引用
- 添加 FLOW_NODE_RESOURCE_OWNER 和 USER_RESOURCE_OWNER 常量用于资源归属管理
- 为流程节点创建的设备插槽添加 managedBy 字段标识资源归属
- 移除未被任何节点引用的自动注册角色定义和设备使用
- 为用户手动创建的角色和设备插槽标记为 USER_RESOURCE_OWNER 归属
- 在节点更新时自动清理未使用的节点资源,优化资源管理
2026-08-11 17:16:27 +08:00
bbc937ce94 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-11 16:52:51 +08:00
289dd678d0 feat(flow): 添加流程资源配置和执行目标管理功能
- 创建 flowResources 工具模块,包含资源角色创建、设备槽位管理等功能
- 实现机器人执行目标选择组件,支持角色和设备分配设置
- 开发运行时分配编辑器,提供机器人和设备绑定界面
- 定义设备类型元数据和资源验证逻辑
- 实现流程图资源模式标准化和迁移功能
2026-08-11 16:51:58 +08:00
0df5280ebf feat(flow): 重构流程节点设备绑定机制
- 移除原有的 deviceId 参数绑定方式,改用统一的执行目标选择器
- 添加 RobotExecutionTarget 组件用于机器人角色和设备用途选择
- 更新节点参数验证逻辑,确保执行目标正确配置
- 优化流程资源管理,添加角色和设备槽位配置功能
- 改进复制粘贴功能,支持跨窗口节点复制和引用处理
- 重构设备选择对话框,集成到参数表单中统一管理
- 更新流程节点渲染,显示执行角色标签
- 添加流程资源验证和规范化处理逻辑
2026-08-11 16:50:30 +08:00
e78df2ed5d style: 调整文件名称 2026-08-07 16:45:45 +08:00
add482aee2 feat: 拖动到循环体节点连线断开 2026-08-07 16:41:38 +08:00
40bd866f91 fix(flow): preserve Linux component filename casing 2026-08-07 15:40:21 +08:00
dd31fbaef3 doc: 处理冲突 2026-08-07 15:30:19 +08:00
92500c1c21 style: 调整分支节点样式 2026-08-07 15:27:59 +08:00
bfed61d75f fix(flow): correct Linux case-sensitive component import 2026-08-07 15:25:45 +08:00
6c9ead2603 feat: 流程添加注释 2026-08-07 14:38:16 +08:00
8e2afcbaa6 feat(flow): 添加单节点执行机器人选择记忆功能
- 引入 watch 监听器和相关变量用于缓存单节点机器人选择
- 实现按节点ID缓存机器人选择,新节点默认沿用最近一次选择
- 在单节点执行前后记录当前机器人选择到缓存中
- 修改NodeTitle组件传递节点ID用于按节点恢复机器人选择
- 添加试运行参数缓存功能,支持按流程缓存整套参数
- 实现参数层级缓存值恢复,兼容参数结构变化
- 优化试运行关闭和确认时的数据缓存处理逻辑
2026-08-07 11:28:01 +08:00
e6d379f571 Merge remote-tracking branch 'origin/lxl-dev' into lxl-dev 2026-08-07 10:48:57 +08:00
84e1354c88 feat(flow): 优化流程节点配置和单节点执行功能
- 移除未使用的 start.svg 和 end.svg 图标导入
- 删除未使用的 booleanOptions 配置函数
- 为 SPEAKER_PLAYAUDIO 节点添加 nodeType 和 audioPath 参数配置
- 重构单节点参数表单渲染逻辑,使用 computed 属性优化字段映射
- 添加单节点执行状态加载指示器和防重复提交功能
- 实现单节点参数验证和数据构建功能
- 修复流程图边连接逻辑,优化相同边删除操作
- 简化旧流程数据获取逻辑,移除不必要的变量赋值
- 更新节点执行权限判断逻辑,支持更多节点类型过滤
- 优化单节点执行时的动作名称处理和节点类型识别逻辑
- 调整单节点表单样式,移除垂直居中对齐属性
2026-08-07 10:48:18 +08:00
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
357 changed files with 25647 additions and 37292 deletions

View File

@ -1,5 +1,5 @@
# 页面标题
VITE_APP_TITLE = 招商车研AI测评机器人模拟测试平台
VITE_APP_TITLE = 招商车研机器人智能测试管理系统
# 开发环境配置
VITE_APP_ENV = 'development'
@ -7,8 +7,8 @@ VITE_APP_ENV = 'development'
# 招商车研物联网平台/开发环境
VITE_APP_BASE_API = '/dev-api'
VITE_API_URL = http://192.168.0.10:13080
VITE_WS_URL = ws://192.168.0.10:13080/ws
# VITE_API_URL = http://10.148.108.95:13080 //杨溪IP
# VITE_API_URL = http://10.148.108.58:13080 //赵培利IP
VITE_API_URL = http://192.168.0.222:14080
VITE_WS_URL = ws://192.168.0.222:14080/ws
VITE_PORT = 80
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'
@ -10,8 +10,8 @@ VITE_APP_BASE_API = '/prod-api'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip
#node-red 服务地址
VITE_NODE_RED_URL = 'http://10.148.108.59:13080'
VITE_API_URL = http://192.168.0.100:13080
VITE_WS_URL = ws://192.168.0.100:13080/ws
VITE_API_URL = http://192.168.28.10:13080
VITE_WS_URL = ws://192.168.0.201:13080/ws
VITE_INSPECTION_TYPE = "inspection"
VITE_MINIO_URL = "http://192.168.28.10:9000/cmvr-iot/"

View File

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

1
.github/FUNDING.yml vendored Normal file
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
*.sln
*.local
.history
package-lock.json
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
# 安装依赖
yarn --registry=https://registry.npmmirror.com
# 启动服务
yarn dev
# 构建测试环境 yarn build:stage
# 构建生产环境 yarn build:prod
# 前端访问地址 http://localhost:80
pnpm install
pnpm dev
```
## 内置功能
默认地址:
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
10. 登录日志:系统登录日志记录查询包含登录异常。
11. 在线用户:当前系统中活跃用户状态监控。
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
14. 系统接口:根据业务代码自动生成相关的api接口文档。
15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
16. 缓存监控:对系统的缓存信息查询,命令统计等。
17. 在线构建器:拖动表单元素生成相应的HTML代码。
18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。
- 前端:`http://localhost:80`
- 后端:`http://localhost:13080`
- Knife4j:`http://localhost:13080/doc.html`
开发配置位于 `.env.development`:
```dotenv
VITE_APP_TITLE=招商车研物联网平台管理系统
VITE_APP_BASE_API=/dev-api
VITE_API_URL=http://localhost:13080
VITE_WS_URL=ws://localhost:13080/ws
VITE_PORT=80
```
## 构建
```bash
pnpm build:dev
pnpm build:stage
pnpm build:prod
pnpm preview
```
生产构建输出到 `dist`。部署时需要由 Nginx 或网关转发 `/prod-api`,并支持 `/ws` WebSocket、文件上传和媒体请求。
## 目录说明
```text
public/models 机器人 URDF 和网格模型
public/inspection 巡检机械臂模型
src/adapters URDF/Xacro 模型适配
src/api REST API 封装
src/components 公共及业务组件
src/router 公共路由和隐藏业务路由
src/store Pinia 状态管理
src/views/device 设备管理与设备控制
src/views/test 测试业务
src/views/flow 流程编排
src/views/inspection 智能巡检
src/views/evaluate 评价管理
src/views/vi 语音交互
src/views/is 智能座舱
src/views/corpusManger 语料管理
```
业务菜单由后端 `/getRouters` 动态返回。数据库菜单中的 `component` 必须与 `src/views` 下的实际页面路径一致,权限字符必须与后端控制器保持一致。
## 配置说明
- `.env.development`:本地开发地址。
- `.env.staging`:测试环境地址。
- `.env.production`:生产环境地址、Node-RED 和 MinIO。
- `vite.config.js`:代理、端口、Monaco 和构建分包。
- `src/settings.js`:布局、页签和版权配置。
生产密码、访问密钥和内部网络配置应通过部署环境注入,不要提交新的敏感信息。

12
bin/build.bat Normal file
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="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="/favicon.ico">
<title>招商车研物联网平台管理系统</title>
<title>%VITE_APP_TITLE%</title>
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<style>
html,

View File

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

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

View File

@ -1,18 +0,0 @@
import request from '@/utils/request'
// 移动机械臂
export function moveJ(data) {
return request({
url: '/api/humanoid/moveJ',
method: 'post',
data: data
})
}
// 获取机械臂状态
export function status(data) {
return request({
url: '/api/humanoid/status',
method: 'post',
data: data
})
}

View File

@ -1,19 +0,0 @@
import request from '@/utils/request'
// 获取灵巧手状态
export function status(query) {
return request({
url: '/api/dexHand/status',
method: 'get',
params: query
})
}
// 设置灵巧手弯曲角度
export function setDexHandAngle(query) {
return request({
url: '/api/dexHand/angle',
method: 'get',
params: query
})
}

View File

@ -1,63 +0,0 @@
import request from '@/utils/request'
// 发布流程
export function flowDeploy(data) {
return request({
url: '/flow/publish',
method: 'post',
data: data
})
}
export function flowExecute(data) {
return request({
url: '/flow/execute',
method: 'post',
data: data
})
}
export function flowAction(data) {
return request({
url: '/flow/action',
method: 'post',
data: data
})
}
export function flowExecuteTrial(data) {
return request({
url: '/flow/executeTrial',
method: 'post',
data: data
})
}
export function flowView(data) {
return request({
url: '/flow/view',
method: 'post',
data: data
})
}
export function flowPause(instId) {
return request({
url: `/flow/pause/${instId}`,
method: 'post'
})
}
export function flowResume(instId) {
return request({
url: `/flow/resume/${instId}`,
method: 'post'
})
}
export function flowStop(instId) {
return request({
url: `/flow/stop/${instId}`,
method: 'post'
})
}

View File

@ -1,46 +0,0 @@
import request from '@/utils/request'
// 开始录音
export function startMicrophoneApi(data) {
return request({
url: '/api/Microphone/start',
method: 'post',
params: data
})
}
// 暂停录音
export function pauseMicrophoneApi(data) {
return request({
url: '/api/Microphone/pause',
method: 'post',
params: data
})
}
// 恢复录音
export function resumeMicrophoneApi(data) {
return request({
url: '/api/Microphone/resume',
method: 'post',
params: data
})
}
// 停止录音
export function stopMicrophoneApi(data) {
return request({
url: '/api/Microphone/stop',
method: 'post',
params: data
})
}
// 播放音频
export function playSpeaker(data) {
return request({
url: '/api/Speaker/play',
method: 'post',
params: data
})
}

View File

@ -1,45 +0,0 @@
import request from '@/utils/request'
// 查询设备配置信息列表
export function listProfile(query) {
return request({
url: '/device/profile/list',
method: 'get',
params: query
})
}
// 查询设备配置信息详细
export function getProfile(query) {
return request({
url: '/device/profile/parameterInfo/',
method: 'get',
params: query
})
}
// 新增设备配置信息
export function addProfile(data) {
return request({
url: '/device/profile',
method: 'post',
data: data
})
}
// 修改设备配置信息
export function updateProfile(data) {
return request({
url: '/device/profile',
method: 'put',
data: data
})
}
// 删除设备配置信息
export function delProfile(id) {
return request({
url: '/device/profile/' + id,
method: 'delete'
})
}

View File

@ -1,146 +0,0 @@
import request from '@/utils/request'
// 查询设备注册列表
export function listRegister(query) {
return request({
url: '/device/register/list',
method: 'get',
params: query
})
}
// 查询设备注册详细
export function getRegister(id) {
return request({
url: '/device/register/' + id,
method: 'get'
})
}
// 新增设备注册
export function addRegister(data) {
return request({
url: '/device/register',
method: 'post',
data: data
})
}
// 修改设备注册
export function updateRegister(data) {
return request({
url: '/device/register',
method: 'put',
data: data
})
}
// 删除设备注册
export function delRegister(id) {
return request({
url: '/device/register/' + id,
method: 'delete'
})
}
// 设备示教
export function deviceTeach(id) {
return request({
url: '/device/profile/' + id,
method: 'get',
})
}
// 设备示教参数修改
export function deviceUpdateProfile(data) {
return request({
url: '/device/profile/updateProfile',
method: 'put',
data: data,
})
}
// 新增音色
export function createTimbre(data) {
return request({
url: '/show/tts/create',
method: 'post',
headers: {
'Content-Type': 'multipart/form-data',
Authorization: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFiNmEyY2Y0LWM3MjQtNDgyMy1hM2NiLTk4N2NhZWNmMzZiNCJ9.oq-JVRbQuq_ujQ2V0h2jXC3woHPdMQKhnYJ8ixRm-C6Ez4GqJWyHYgmnkw5dbDyhWGdIUH02uv6XT8UiboHD4g',
isToken: false
},
data: data,
})
}
// 获取音色
export function getTts() {
return request({
url: '/show/tts/query',
headers: {
Authorization: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFiNmEyY2Y0LWM3MjQtNDgyMy1hM2NiLTk4N2NhZWNmMzZiNCJ9.oq-JVRbQuq_ujQ2V0h2jXC3woHPdMQKhnYJ8ixRm-C6Ez4GqJWyHYgmnkw5dbDyhWGdIUH02uv6XT8UiboHD4g',
isToken: false
},
method: 'get',
})
}
// 生产语料
export function createCorpus(data) {
return request({
url: '/show/tts/corpus',
headers: {
Authorization: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFiNmEyY2Y0LWM3MjQtNDgyMy1hM2NiLTk4N2NhZWNmMzZiNCJ9.oq-JVRbQuq_ujQ2V0h2jXC3woHPdMQKhnYJ8ixRm-C6Ez4GqJWyHYgmnkw5dbDyhWGdIUH02uv6XT8UiboHD4g',
isToken: false
},
method: 'post',
data: data
})
}
export function getSynthesize(data, config = {}) {
return request({
url: '/show/tts/synthesize',
headers: {
Authorization: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFiNmEyY2Y0LWM3MjQtNDgyMy1hM2NiLTk4N2NhZWNmMzZiNCJ9.oq-JVRbQuq_ujQ2V0h2jXC3woHPdMQKhnYJ8ixRm-C6Ez4GqJWyHYgmnkw5dbDyhWGdIUH02uv6XT8UiboHD4g',
isToken: false
},
responseType: 'blob',
method: 'post',
data: data
}, {
...config
})
}
export function createAdvancedCorpus(data) {
return request({
url: '/show/tts/corpus/advanced',
headers: {
Authorization: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFiNmEyY2Y0LWM3MjQtNDgyMy1hM2NiLTk4N2NhZWNmMzZiNCJ9.oq-JVRbQuq_ujQ2V0h2jXC3woHPdMQKhnYJ8ixRm-C6Ez4GqJWyHYgmnkw5dbDyhWGdIUH02uv6XT8UiboHD4g',
isToken: false
},
method: 'post',
data: data
})
}
export function getAdvancedSynthesize(data, config = {}) {
return request({
url: '/show/tts/synthesize/advanced',
headers: {
Authorization: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFiNmEyY2Y0LWM3MjQtNDgyMy1hM2NiLTk4N2NhZWNmMzZiNCJ9.oq-JVRbQuq_ujQ2V0h2jXC3woHPdMQKhnYJ8ixRm-C6Ez4GqJWyHYgmnkw5dbDyhWGdIUH02uv6XT8UiboHD4g',
isToken: false
},
responseType: 'blob',
method: 'post',
data: data
}, {
...config
})
}
export const getFlowById = (id) => {
return request.get(import.meta.env.VITE_NODE_RED_URL + '/flow/' + id, {})
}

View File

@ -1,44 +0,0 @@
import request from '@/utils/request'
// 查询机器人配置列表
export function listRobot(query) {
return request({
url: '/device/robot/list',
method: 'get',
params: query
})
}
// 查询机器人配置详细
export function getRobot(id) {
return request({
url: '/device/robot/' + id,
method: 'get'
})
}
// 新增机器人配置
export function addRobot(data) {
return request({
url: '/device/robot',
method: 'post',
data: data
})
}
// 修改机器人配置
export function updateRobot(data) {
return request({
url: '/device/robot',
method: 'put',
data: data
})
}
// 删除机器人配置
export function delRobot(id) {
return request({
url: '/device/robot/' + id,
method: 'delete'
})
}

View File

@ -1,72 +0,0 @@
import request from '@/utils/request'
// 暂停播放
export function pause(data) {
return request({
url: '/api/Speaker/pause',
method: 'post',
params: data
})
}
// 播放音频
export function play(data) {
return request({
url: '/api/Speaker/play',
method: 'post',
params: data
})
}
// 恢复播放
export function resume(data) {
return request({
url: '/api/Speaker/resume',
method: 'post',
params: data
})
}
// 获取扬声器状态
export function status(query) {
return request({
url: '/api/Speaker/status',
method: 'get',
params: query
})
}
// 停止播放
export function stop(data) {
return request({
url: '/api/Speaker/stop',
method: 'post',
params: data
})
}
// 获取音量
export function getVolume(query) {
return request({
url: '/api/Speaker/volume',
method: 'get',
params: query
})
}
// 设置音量
export function setVolume(data) {
return request({
url: '/api/Speaker/volume',
method: 'post',
params: data
})
}
// 设置音量
export function getAll() {
return request({
url: '/api/Speaker/getAll',
method: 'get',
})
}

31
src/api/device/system.js Normal file
View File

@ -0,0 +1,31 @@
import request from '@/utils/request'
export function getSafetyState(robotId) {
return request({
url: '/api/system/safetyState',
method: 'get',
params: { robotId },
})
}
/**
* 恢复机器人运行状态;payload 与旧 recoverSafetyState 保持一致,便于调用方渐进迁移。
*/
export function restoreOperationalState(data) {
return request({
url: '/api/system/restoreOperationalState',
method: 'post',
data,
})
}
/**
* @deprecated 请改用 restoreOperationalState,旧接口仅保留给未迁移调用方。
*/
export function recoverSafetyState(data) {
return request({
url: '/api/system/recoverSafetyState',
method: 'post',
data,
})
}

View File

@ -1,86 +0,0 @@
import request from '@/utils/request'
// 查询设备终端配置列表
export function listTerminal(query) {
return request({
url: '/device/terminal/list',
method: 'get',
params: query
})
}
// 查询设备终端配置详细
export function getTerminal(id) {
return request({
url: '/device/terminal/' + id,
method: 'get'
})
}
// 新增设备终端配置
export function addTerminal(data) {
return request({
url: '/device/terminal',
method: 'post',
data: data
})
}
// 修改设备终端配置
export function updateTerminal(data) {
return request({
url: '/device/terminal',
method: 'put',
data: data
})
}
// 删除设备终端配置
export function delTerminal(id) {
return request({
url: '/device/terminal/' + id,
method: 'delete'
})
}
// 获取实例信息
export function getInstInfo(id) {
return request({
url: '/test/inst/context/' + id,
method: 'get'
})
}
// 暂停任务
export function pauseInst(id) {
return request({
url: `/test/inst/pause/${id}`,
method: 'post'
})
}
// 恢复任务
export function resumeInst(id) {
return request({
url: `/test/inst/resume/${id}`,
method: 'post'
})
}
// 恢复任务
export function stopInst(id) {
return request({
url: `/test/inst/stop/${id}`,
method: 'post'
})
}
export function getFlowParamsApi(taskId) {
return request({
url: '/test/orchestration',
method: 'get',
params: {
taskId
}
})
}

123
src/api/flow/flow.js Normal file
View File

@ -0,0 +1,123 @@
import request from '@/utils/request'
// 发布流程
export function flowDeploy(data) {
return request({
url: '/flow/publish',
method: 'post',
data: data
})
}
export function flowSaveDraft(data) {
return request({ url: '/flow/draft', method: 'put', data })
}
export function flowVersionList(itemId) {
return request({ url: `/flow/versions/${itemId}`, method: 'get' })
}
export function flowVersionDetail(versionId) {
return request({ url: `/flow/version/${versionId}`, method: 'get' })
}
export function flowSubFlowOptions(currentItemId) {
return request({
url: '/flow/subflows',
method: 'get',
params: { currentItemId }
})
}
export function flowRestoreDraft(versionId, data) {
return request({ url: `/flow/version/${versionId}/restore-draft`, method: 'post', data })
}
export function flowRollbackVersion(versionId, data) {
return request({ url: `/flow/version/${versionId}/rollback`, method: 'post', data })
}
export function flowExecute(data) {
return request({
url: '/flow/execute',
method: 'post',
data: data
})
}
export function flowAction(data) {
return request({
url: '/flow/action',
method: 'post',
data: data
})
}
export function flowExecuteTrial(data) {
return request({
url: '/flow/executeTrial',
method: 'post',
data: data
})
}
export function flowView(data) {
return request({
url: '/flow/view',
method: 'post',
data: data
})
}
export function flowPause(instId) {
return request({
url: `/flow/pause/${instId}`,
method: 'post'
})
}
export function flowResume(instId) {
return request({
url: `/flow/resume/${instId}`,
method: 'post'
})
}
export function flowStop(instId) {
return request({
url: `/flow/stop/${instId}`,
method: 'post'
})
}
export function getArmStatus(params) {
return request({
url: '/api/arm/getPose',
method: 'get',
params: params
})
}
export function getAgvStatus(params) {
return request({
url: '/api/agv/getRuntimeState',
method: 'get',
params: params
})
}
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
})
}

5
src/api/flow/nodeRed.js Normal file
View File

@ -0,0 +1,5 @@
import request from '@/utils/request'
export function getNodeRedFlowById(id) {
return request.get(`${import.meta.env.VITE_NODE_RED_URL}/flow/${id}`, {})
}

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

View File

@ -0,0 +1,39 @@
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'
})
}
export function copyPoint(id) {
return request({
url: `/inspection/waypoint/copy/${id}`,
method: 'post'
})
}

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

View File

@ -0,0 +1,83 @@
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, data) {
return request({
url: `/inspection/taskInstance/start/${id}`,
method: 'put',
data
})
}
export function rerunRunTask(id) {
return request({
url: `/inspection/taskInstance/rerun/${id}`,
method: 'post'
})
}
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,54 @@
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
})
}
export function copyTask(id) {
return request({
url: `/inspection/task/copy/${id}`,
method: 'post'
})
}

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,76 @@
import request from '@/utils/request'
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, data) {
return request({
url: `/aima/taskinstance/start/${id}`,
method: 'put',
data
})
}
export function rerunTaskinstance(id) {
return request({
url: `/aima/taskinstance/rerun/${id}`,
method: 'post'
})
}
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,54 @@
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
})
}
export function copyTask(id) {
return request({
url: `/aima/task/copy/${id}`,
method: 'post'
})
}

View File

@ -0,0 +1,39 @@
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'
})
}
export function copyTestCase(id) {
return request({
url: `/aima/testcase/copy/${id}`,
method: 'post'
})
}

View File

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

View File

@ -0,0 +1,13 @@
import request from '@/utils/request'
export function listResourceDevices(params) {
return request({ url: '/resource/devices', method: 'get', params })
}
export function getResourceDevice(id) {
return request({ url: `/resource/devices/${encodeURIComponent(id)}`, method: 'get' })
}
export function updateResourceDevice(id, data) {
return request({ url: `/resource/devices/${encodeURIComponent(id)}`, method: 'put', data })
}

24
src/api/resource/map.js Normal file
View File

@ -0,0 +1,24 @@
import request from '@/utils/request'
export function listResourceMaps(params) {
return request({ url: '/resource/maps', method: 'get', params })
}
export function listRobotMaps(robotId) {
return request({
url: `/resource/maps/robot/${encodeURIComponent(robotId)}`,
method: 'get',
})
}
export function addResourceMap(data) {
return request({ url: '/resource/maps', method: 'post', data })
}
export function updateResourceMap(data) {
return request({ url: '/resource/maps', method: 'put', data })
}
export function deleteResourceMap(ids) {
return request({ url: `/resource/maps/${ids}`, method: 'delete' })
}

77
src/api/resource/robot.js Normal file
View File

@ -0,0 +1,77 @@
import request from '@/utils/request'
export function listResourceRobots(params) {
return request({ url: '/resource/robots', method: 'get', params })
}
export function getResourceRobot(robotId) {
return request({ url: `/resource/robots/${encodeURIComponent(robotId)}`, method: 'get' })
}
export function listRobotDevices(robotId, params = {}) {
return request({
url: `/resource/robots/${encodeURIComponent(robotId)}/devices`,
method: 'get',
params,
})
}
export const getRobotList = listResourceRobots
export function addRobot(data) {
return request({ url: '/resource/robots', method: 'post', data })
}
export function updateRobot(data) {
return request({ url: '/resource/robots', method: 'put', data })
}
export function deleteRobot(id) {
return request({ url: `/resource/robots/${id}`, method: 'delete' })
}
export function getRobotDevices(id) {
return request({ url: `/resource/robots/id/${id}/devices`, method: 'get' })
}
export function getRobotDevicesByRobotId(robotId, params = {}) {
return request({
url: `/resource/robots/${encodeURIComponent(robotId)}/devices`,
method: 'get',
params: { onlineOnly: true, ...params },
})
}
export function getMapByRobot(robotRef) {
return request({
url: `/resource/robots/${encodeURIComponent(robotRef)}/remote-maps`,
method: 'get',
})
}
export function getMapJson(params) {
return request({
url: `/resource/robots/${encodeURIComponent(params.robotId)}/download-map`,
method: 'get',
params,
})
}
export function getRobotLocation(robotRef) {
return request({
url: `/resource/robots/${encodeURIComponent(robotRef)}/location`,
method: 'get',
})
}
export function getRobotStatus(id) {
return request({ url: `/resource/robots/id/${id}`, method: 'get' })
}
export function moveRobot(data) {
return request({ url: '/api/agv/setVelocity', method: 'post', data })
}
export function stopRobot(data) {
return request({ url: '/api/agv/stopVelocityControl', method: 'post', data })
}

View File

@ -0,0 +1,29 @@
import request from '@/utils/request'
export function listResourceTerminals(params) {
return request({ url: '/resource/terminals', method: 'get', params })
}
export function addResourceTerminal(data) {
return request({ url: '/resource/terminals', method: 'post', data })
}
export function updateResourceTerminal(data) {
return request({ url: '/resource/terminals', method: 'put', data })
}
export function getResourceTerminal(id) {
return request({ url: `/resource/terminals/${encodeURIComponent(id)}`, method: 'get' })
}
export function deleteResourceTerminals(ids) {
return request({ url: `/resource/terminals/${ids}`, method: 'delete' })
}
export function listTerminalRobots(id) {
return request({ url: `/resource/terminals/${encodeURIComponent(id)}/robots`, method: 'get' })
}
export function updateTerminalRobots(id, robotIds) {
return request({ url: `/resource/terminals/${encodeURIComponent(id)}/robots`, method: 'put', data: robotIds })
}

View File

@ -0,0 +1,55 @@
import request from '@/utils/request'
const base = '/vision/calibration'
export function listCalibrationProfiles(params) {
return request({ url: `${base}/profiles`, method: 'get', params })
}
export function getCalibrationProfile(id) {
return request({ url: `${base}/profiles/${id}`, method: 'get' })
}
export function updateCalibrationCorrection(id, data) {
return request({ url: `${base}/profiles/${id}/correction`, method: 'put', data })
}
export function activateCalibrationProfile(id, forced = false) {
return request({ url: `${base}/profiles/${id}/activate`, method: 'put', data: { forced } })
}
export function invalidateCalibrationProfile(id) {
return request({ url: `${base}/profiles/${id}`, method: 'delete' })
}
export function startHandEyeCalibration(data) {
return request({ url: `${base}/sessions/hand-eye`, method: 'post', data })
}
export function startToolCalibration(data) {
return request({ url: `${base}/sessions/tool`, method: 'post', data })
}
export function startCalibrationVerification(id) {
return request({ url: `${base}/profiles/${id}/verify`, method: 'post' })
}
export function calibrationHeartbeat(id) {
return request({ url: `${base}/sessions/${id}/heartbeat`, method: 'post' })
}
export function getCalibrationSession(id) {
return request({ url: `${base}/sessions/${id}`, method: 'get' })
}
export function stopCalibrationSession(id) {
return request({ url: `${base}/sessions/${id}/stop`, method: 'post' })
}
export function recordToolPivot(id) {
return request({ url: `${base}/sessions/${id}/tool/pivot`, method: 'post' })
}
export function recordToolDirection(id) {
return request({ url: `${base}/sessions/${id}/tool/direction`, method: 'post' })
}

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) {
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) {
return request({

View File

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

View File

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

View File

@ -26,6 +26,13 @@ export function addConfig(data) {
})
}
export function copyConfig(id) {
return request({
url: '/test/config/copy/' + id,
method: 'post'
})
}
// 修改任务流程编排
export function updateConfig(data) {
return request({

View File

@ -26,6 +26,13 @@ export function addDetect(data) {
})
}
export function copyDetect(id) {
return request({
url: '/test/detect/copy/' + id,
method: 'post'
})
}
// 修改检测项配置
export function updateDetect(data) {
return request({

View File

@ -52,6 +52,14 @@ export function listInst(query) {
})
}
// 使用历史实例保存的执行参数创建新的任务实例
export function rerunInst(id) {
return request({
url: `/test/inst/rerun/${id}`,
method: 'post'
})
}
// 查询任务实例日志
export function getInst(query) {
return request({
@ -62,7 +70,7 @@ export function getInst(query) {
}
// 查询任务实例日志
export function getLogicFlow(query) {
export function getFlowRuntimeLog(query) {
return request({
url: '/test/inst/log/query',
method: 'get',
@ -70,3 +78,15 @@ export function getLogicFlow(query) {
})
}
export function pauseInst(id) {
return request({ url: `/test/inst/pause/${id}`, method: 'post' })
}
export function resumeInst(id) {
return request({ url: `/test/inst/resume/${id}`, method: 'post' })
}
export function stopInst(id) {
return request({ url: `/test/inst/stop/${id}`, method: 'post' })
}

View File

@ -1,14 +0,0 @@
import request from "@/utils/request.js";
export function coordinateMarking(data) {
return request({
url: '/show/touch/coordinates',
method: 'post',
headers: {
isToken: false,
'Content-Type': 'multipart/form-data',
'Authorization':'Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImIzY2QwYjFjLWNlNjgtNDEwNy1hYzA1LTdmYTBlNmQ4NzJmNiJ9.2N9E9IDrFeCe-yRIBly60N0kYYmCscdrhyd8KHmhw2dxLi7B_A1i8bcWRTtx9YryFB3AhkvZkGauMx0CdZ0TDg'
},
data: data
})
}

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) {
background: $color;

View File

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

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 {
.main-container {
min-height: 100%;
// min-height: 100%;
height: 100%;
transition: margin-left .28s;
margin-left: $base-sidebar-width;
position: relative;

View File

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

View File

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

View File

@ -26,289 +26,289 @@ watch(() => props.ex, () => expressionChange())
// 表达式值变化时,开始去计算结果
function expressionChange() {
// 计算开始-隐藏结果
isShow.value = false;
isShow.value = false
// 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
let ruleArr = props.ex.split(' ');
let ruleArr = props.ex.split(' ')
// 用于记录进入循环的次数
let nums = 0;
let nums = 0
// 用于暂时存符号时间规则结果的数组
let resultArr = [];
let resultArr = []
// 获取当前时间精确至[年、月、日、时、分、秒]
let nTime = new Date();
let nYear = nTime.getFullYear();
let nMonth = nTime.getMonth() + 1;
let nDay = nTime.getDate();
let nHour = nTime.getHours();
let nMin = nTime.getMinutes();
let nSecond = nTime.getSeconds();
let nTime = new Date()
let nYear = nTime.getFullYear()
let nMonth = nTime.getMonth() + 1
let nDay = nTime.getDate()
let nHour = nTime.getHours()
let nMin = nTime.getMinutes()
let nSecond = nTime.getSeconds()
// 根据规则获取到近100年可能年数组、月数组等等
getSecondArr(ruleArr[0]);
getMinArr(ruleArr[1]);
getHourArr(ruleArr[2]);
getDayArr(ruleArr[3]);
getMonthArr(ruleArr[4]);
getWeekArr(ruleArr[5]);
getYearArr(ruleArr[6], nYear);
getSecondArr(ruleArr[0])
getMinArr(ruleArr[1])
getHourArr(ruleArr[2])
getDayArr(ruleArr[3])
getMonthArr(ruleArr[4])
getWeekArr(ruleArr[5])
getYearArr(ruleArr[6], nYear)
// 将获取到的数组赋值-方便使用
let sDate = dateArr.value[0];
let mDate = dateArr.value[1];
let hDate = dateArr.value[2];
let DDate = dateArr.value[3];
let MDate = dateArr.value[4];
let YDate = dateArr.value[5];
let sDate = dateArr.value[0]
let mDate = dateArr.value[1]
let hDate = dateArr.value[2]
let DDate = dateArr.value[3]
let MDate = dateArr.value[4]
let YDate = dateArr.value[5]
// 获取当前时间在数组中的索引
let sIdx = getIndex(sDate, nSecond);
let mIdx = getIndex(mDate, nMin);
let hIdx = getIndex(hDate, nHour);
let DIdx = getIndex(DDate, nDay);
let MIdx = getIndex(MDate, nMonth);
let YIdx = getIndex(YDate, nYear);
let sIdx = getIndex(sDate, nSecond)
let mIdx = getIndex(mDate, nMin)
let hIdx = getIndex(hDate, nHour)
let DIdx = getIndex(DDate, nDay)
let MIdx = getIndex(MDate, nMonth)
let YIdx = getIndex(YDate, nYear)
// 重置月日时分秒的函数(后面用的比较多)
const resetSecond = function () {
sIdx = 0;
sIdx = 0
nSecond = sDate[sIdx]
}
const resetMin = function () {
mIdx = 0;
mIdx = 0
nMin = mDate[mIdx]
resetSecond();
resetSecond()
}
const resetHour = function () {
hIdx = 0;
hIdx = 0
nHour = hDate[hIdx]
resetMin();
resetMin()
}
const resetDay = function () {
DIdx = 0;
DIdx = 0
nDay = DDate[DIdx]
resetHour();
resetHour()
}
const resetMonth = function () {
MIdx = 0;
MIdx = 0
nMonth = MDate[MIdx]
resetDay();
resetDay()
}
// 如果当前年份不为数组中当前值
if (nYear !== YDate[YIdx]) {
resetMonth();
resetMonth()
}
// 如果当前月份不为数组中当前值
if (nMonth !== MDate[MIdx]) {
resetDay();
resetDay()
}
// 如果当前“日”不为数组中当前值
if (nDay !== DDate[DIdx]) {
resetHour();
resetHour()
}
// 如果当前“时”不为数组中当前值
if (nHour !== hDate[hIdx]) {
resetMin();
resetMin()
}
// 如果当前“分”不为数组中当前值
if (nMin !== mDate[mIdx]) {
resetSecond();
resetSecond()
}
// 循环年份数组
goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
let YY = YDate[Yi];
let YY = YDate[Yi]
// 如果到达最大值时
if (nMonth > MDate[MDate.length - 1]) {
resetMonth();
continue;
resetMonth()
continue
}
// 循环月份数组
goMonth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
// 赋值、方便后面运算
let MM = MDate[Mi];
MM = MM < 10 ? '0' + MM : MM;
MM = MM < 10 ? '0' + MM : MM
// 如果到达最大值时
if (nDay > DDate[DDate.length - 1]) {
resetDay();
resetDay()
if (Mi === MDate.length - 1) {
resetMonth();
continue goYear;
resetMonth()
continue goYear
}
continue;
continue
}
// 循环日期数组
goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
// 赋值、方便后面运算
let DD = DDate[Di];
let thisDD = DD < 10 ? '0' + DD : DD;
let DD = DDate[Di]
let thisDD = DD < 10 ? '0' + DD : DD
// 如果到达最大值时
if (nHour > hDate[hDate.length - 1]) {
resetHour();
resetHour()
if (Di === DDate.length - 1) {
resetDay();
resetDay()
if (Mi === MDate.length - 1) {
resetMonth();
continue goYear;
resetMonth()
continue goYear
}
continue goMonth;
continue goMonth
}
continue;
continue
}
// 判断日期的合法性,不合法的话也是跳出当前循环
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && dayRule.value !== 'workDay' && dayRule.value !== 'lastWeek' && dayRule.value !== 'lastDay') {
resetDay();
continue goMonth;
resetDay()
continue goMonth
}
// 如果日期规则中有值时
if (dayRule.value === 'lastDay') {
// 如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--;
thisDD = DD < 10 ? '0' + DD : DD;
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
} else if (dayRule.value === 'workDay') {
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--;
thisDD = DD < 10 ? '0' + DD : DD;
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
// 获取达到条件的日期是星期X
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week');
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
// 当星期日时
if (thisWeek === 1) {
// 先找下一个日,并判断是否为月底
DD++;
thisDD = DD < 10 ? '0' + DD : DD;
DD++
thisDD = DD < 10 ? '0' + DD : DD
// 判断下一日已经不是合法日期
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD -= 3;
DD -= 3
}
} else if (thisWeek === 7) {
// 当星期6时只需判断不是1号就可进行操作
if (dayRuleSup.value !== 1) {
DD--;
DD--
} else {
DD += 2;
DD += 2
}
}
} else if (dayRule.value === 'weekDay') {
// 如果指定了是星期几
// 获取当前日期是属于星期几
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week');
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
// 校验当前星期是否在星期池(dayRuleSup)中
if (dayRuleSup.value.indexOf(thisWeek) < 0) {
// 如果到达最大值时
if (Di === DDate.length - 1) {
resetDay();
resetDay()
if (Mi === MDate.length - 1) {
resetMonth();
continue goYear;
resetMonth()
continue goYear
}
continue goMonth;
continue goMonth
}
continue;
continue
}
} else if (dayRule.value === 'assWeek') {
// 如果指定了是第几周的星期几
// 获取每月1号是属于星期几
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week');
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
if (dayRuleSup.value[1] >= thisWeek) {
DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1;
DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1
} else {
DD = dayRuleSup.value[0] * 7 + dayRuleSup.value[1] - thisWeek + 1;
DD = dayRuleSup.value[0] * 7 + dayRuleSup.value[1] - thisWeek + 1
}
} else if (dayRule.value === 'lastWeek') {
// 如果指定了每月最后一个星期几
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--;
thisDD = DD < 10 ? '0' + DD : DD;
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
// 获取月末最后一天是星期几
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week');
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
// 找到要求中最近的那个星期几
if (dayRuleSup.value < thisWeek) {
DD -= thisWeek - dayRuleSup.value;
DD -= thisWeek - dayRuleSup.value
} else if (dayRuleSup.value > thisWeek) {
DD -= 7 - (dayRuleSup.value - thisWeek)
}
}
// 判断时间值是否小于10置换成“05”这种格式
DD = DD < 10 ? '0' + DD : DD;
DD = DD < 10 ? '0' + DD : DD
// 循环“时”数组
goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
// 如果到达最大值时
if (nMin > mDate[mDate.length - 1]) {
resetMin();
resetMin()
if (hi === hDate.length - 1) {
resetHour();
resetHour()
if (Di === DDate.length - 1) {
resetDay();
resetDay()
if (Mi === MDate.length - 1) {
resetMonth();
continue goYear;
resetMonth()
continue goYear
}
continue goMonth;
continue goMonth
}
continue goDay;
continue goDay
}
continue;
continue
}
// 循环"分"数组
goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi];
let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi]
// 如果到达最大值时
if (nSecond > sDate[sDate.length - 1]) {
resetSecond();
resetSecond()
if (mi === mDate.length - 1) {
resetMin();
resetMin()
if (hi === hDate.length - 1) {
resetHour();
resetHour()
if (Di === DDate.length - 1) {
resetDay();
resetDay()
if (Mi === MDate.length - 1) {
resetMonth();
continue goYear;
resetMonth()
continue goYear
}
continue goMonth;
continue goMonth
}
continue goDay;
continue goDay
}
continue goHour;
continue goHour
}
continue;
continue
}
// 循环"秒"数组
goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si];
let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si]
// 添加当前时间(时间合法性在日期循环时已经判断)
if (MM !== '00' && DD !== '00') {
resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss)
nums++;
nums++
}
// 如果条数满了就退出循环
if (nums === 5) break goYear;
if (nums === 5) break goYear
// 如果到达最大值时
if (si === sDate.length - 1) {
resetSecond();
resetSecond()
if (mi === mDate.length - 1) {
resetMin();
resetMin()
if (hi === hDate.length - 1) {
resetHour();
resetHour()
if (Di === DDate.length - 1) {
resetDay();
resetDay()
if (Mi === MDate.length - 1) {
resetMonth();
continue goYear;
resetMonth()
continue goYear
}
continue goMonth;
continue goMonth
}
continue goDay;
continue goDay
}
continue goHour;
continue goHour
}
continue goMin;
continue goMin
}
} //goSecond
} //goMin
@ -318,31 +318,31 @@ function expressionChange() {
}
// 判断100年内的结果条数
if (resultArr.length === 0) {
resultList.value = ['没有达到条件的结果!'];
resultList.value = ['没有达到条件的结果!']
} else {
resultList.value = resultArr;
resultList.value = resultArr
if (resultArr.length !== 5) {
resultList.value.push('最近100年内只有上面' + resultArr.length + '条结果!')
}
}
// 计算完成-显示结果
isShow.value = true;
isShow.value = true
}
// 用于计算某位数字在数组中的索引
function getIndex(arr, value) {
if (value <= arr[0] || value > arr[arr.length - 1]) {
return 0;
return 0
} else {
for (let i = 0; i < arr.length - 1; i++) {
if (value > arr[i] && value <= arr[i + 1]) {
return i + 1;
return i + 1
}
}
}
}
// 获取"年"数组
function getYearArr(rule, year) {
dateArr.value[5] = getOrderArr(year, year + 100);
dateArr.value[5] = getOrderArr(year, year + 100)
if (rule !== undefined) {
if (rule.indexOf('-') >= 0) {
dateArr.value[5] = getCycleArr(rule, year + 100, false)
@ -355,7 +355,7 @@ function getYearArr(rule, year) {
}
// 获取"月"数组
function getMonthArr(rule) {
dateArr.value[4] = getOrderArr(1, 12);
dateArr.value[4] = getOrderArr(1, 12)
if (rule.indexOf('-') >= 0) {
dateArr.value[4] = getCycleArr(rule, 12, false)
} else if (rule.indexOf('/') >= 0) {
@ -369,58 +369,58 @@ function getWeekArr(rule) {
// 只有当日期规则的两个值均为“”时则表达日期是有选项的
if (dayRule.value === '' && dayRuleSup.value === '') {
if (rule.indexOf('-') >= 0) {
dayRule.value = 'weekDay';
dayRule.value = 'weekDay'
dayRuleSup.value = getCycleArr(rule, 7, false)
} else if (rule.indexOf('#') >= 0) {
dayRule.value = 'assWeek';
let matchRule = rule.match(/[0-9]{1}/g);
dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])];
dateArr.value[3] = [1];
dayRule.value = 'assWeek'
let matchRule = rule.match(/[0-9]{1}/g)
dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])]
dateArr.value[3] = [1]
if (dayRuleSup.value[1] === 7) {
dayRuleSup.value[1] = 0;
dayRuleSup.value[1] = 0
}
} else if (rule.indexOf('L') >= 0) {
dayRule.value = 'lastWeek';
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0]);
dateArr.value[3] = [31];
dayRule.value = 'lastWeek'
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
dateArr.value[3] = [31]
if (dayRuleSup.value === 7) {
dayRuleSup.value = 0;
dayRuleSup.value = 0
}
} else if (rule !== '*' && rule !== '?') {
dayRule.value = 'weekDay';
dayRule.value = 'weekDay'
dayRuleSup.value = getAssignArr(rule)
}
}
}
// 获取"日"数组-少量为日期规则
function getDayArr(rule) {
dateArr.value[3] = getOrderArr(1, 31);
dayRule.value = '';
dayRuleSup.value = '';
dateArr.value[3] = getOrderArr(1, 31)
dayRule.value = ''
dayRuleSup.value = ''
if (rule.indexOf('-') >= 0) {
dateArr.value[3] = getCycleArr(rule, 31, false)
dayRuleSup.value = 'null';
dayRuleSup.value = 'null'
} else if (rule.indexOf('/') >= 0) {
dateArr.value[3] = getAverageArr(rule, 31)
dayRuleSup.value = 'null';
dayRuleSup.value = 'null'
} else if (rule.indexOf('W') >= 0) {
dayRule.value = 'workDay';
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0]);
dateArr.value[3] = [dayRuleSup.value];
dayRule.value = 'workDay'
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
dateArr.value[3] = [dayRuleSup.value]
} else if (rule.indexOf('L') >= 0) {
dayRule.value = 'lastDay';
dayRuleSup.value = 'null';
dateArr.value[3] = [31];
dayRule.value = 'lastDay'
dayRuleSup.value = 'null'
dateArr.value[3] = [31]
} else if (rule !== '*' && rule !== '?') {
dateArr.value[3] = getAssignArr(rule)
dayRuleSup.value = 'null';
dayRuleSup.value = 'null'
} else if (rule === '*') {
dayRuleSup.value = 'null';
dayRuleSup.value = 'null'
}
}
// 获取"时"数组
function getHourArr(rule) {
dateArr.value[2] = getOrderArr(0, 23);
dateArr.value[2] = getOrderArr(0, 23)
if (rule.indexOf('-') >= 0) {
dateArr.value[2] = getCycleArr(rule, 24, true)
} else if (rule.indexOf('/') >= 0) {
@ -431,7 +431,7 @@ function getHourArr(rule) {
}
// 获取"分"数组
function getMinArr(rule) {
dateArr.value[1] = getOrderArr(0, 59);
dateArr.value[1] = getOrderArr(0, 59)
if (rule.indexOf('-') >= 0) {
dateArr.value[1] = getCycleArr(rule, 60, true)
} else if (rule.indexOf('/') >= 0) {
@ -442,7 +442,7 @@ function getMinArr(rule) {
}
// 获取"秒"数组
function getSecondArr(rule) {
dateArr.value[0] = getOrderArr(0, 59);
dateArr.value[0] = getOrderArr(0, 59)
if (rule.indexOf('-') >= 0) {
dateArr.value[0] = getCycleArr(rule, 60, true)
} else if (rule.indexOf('/') >= 0) {
@ -453,86 +453,86 @@ function getSecondArr(rule) {
}
// 根据传进来的min-max返回一个顺序的数组
function getOrderArr(min, max) {
let arr = [];
let arr = []
for (let i = min; i <= max; i++) {
arr.push(i);
arr.push(i)
}
return arr;
return arr
}
// 根据规则中指定的零散值返回一个数组
function getAssignArr(rule) {
let arr = [];
let assiginArr = rule.split(',');
let arr = []
let assiginArr = rule.split(',')
for (let i = 0; i < assiginArr.length; i++) {
arr[i] = Number(assiginArr[i])
}
arr.sort(compare)
return arr;
return arr
}
// 根据一定算术规则计算返回一个数组
function getAverageArr(rule, limit) {
let arr = [];
let agArr = rule.split('/');
let min = Number(agArr[0]);
let step = Number(agArr[1]);
let arr = []
let agArr = rule.split('/')
let min = Number(agArr[0])
let step = Number(agArr[1])
while (min <= limit) {
arr.push(min);
min += step;
arr.push(min)
min += step
}
return arr;
return arr
}
// 根据规则返回一个具有周期性的数组
function getCycleArr(rule, limit, status) {
// status--表示是否从0开始(则从1开始)
let arr = [];
let cycleArr = rule.split('-');
let min = Number(cycleArr[0]);
let max = Number(cycleArr[1]);
let arr = []
let cycleArr = rule.split('-')
let min = Number(cycleArr[0])
let max = Number(cycleArr[1])
if (min > max) {
max += limit;
max += limit
}
for (let i = min; i <= max; i++) {
let add = 0;
let add = 0
if (status === false && i % limit === 0) {
add = limit;
add = limit
}
arr.push(Math.round(i % limit + add))
}
arr.sort(compare)
return arr;
return arr
}
// 比较数字大小(用于Array.sort)
function compare(value1, value2) {
if (value2 - value1 > 0) {
return -1;
return -1
} else {
return 1;
return 1
}
}
// 格式化日期格式如:2017-9-19 18:04:33
function formatDate(value, type) {
// 计算日期相关值
let time = typeof value == 'number' ? new Date(value) : value;
let Y = time.getFullYear();
let M = time.getMonth() + 1;
let D = time.getDate();
let h = time.getHours();
let m = time.getMinutes();
let s = time.getSeconds();
let week = time.getDay();
let time = typeof value == 'number' ? new Date(value) : value
let Y = time.getFullYear()
let M = time.getMonth() + 1
let D = time.getDate()
let h = time.getHours()
let m = time.getMinutes()
let s = time.getSeconds()
let week = time.getDay()
// 如果传递了type的话
if (type === undefined) {
return Y + '-' + (M < 10 ? '0' + M : M) + '-' + (D < 10 ? '0' + D : D) + ' ' + (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
return Y + '-' + (M < 10 ? '0' + M : M) + '-' + (D < 10 ? '0' + D : D) + ' ' + (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
} else if (type === 'week') {
// 在quartz中 1为星期日
return week + 1;
return week + 1
}
}
// 检查日期是否存在
function checkDate(value) {
let time = new Date(value);
let time = new Date(value)
let format = formatDate(time)
return value === format;
return value === format
}
onMounted(() => {
expressionChange()

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

View File

@ -1,7 +1,7 @@
<template>
<div>
<template v-for="(item, index) in options">
<template v-if="values.includes(item.value)">
<template v-if="isValueMatch(item.value)">
<span
v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)"
:key="item.value"
@ -26,7 +26,7 @@
<script setup>
// 记录未匹配的项
const unmatchArray = ref([]);
const unmatchArray = ref([])
const props = defineProps({
// 数据
@ -45,33 +45,38 @@ const props = defineProps({
type: String,
default: ",",
}
});
})
const values = computed(() => {
if (props.value === null || typeof props.value === 'undefined' || props.value === '') return [];
return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator);
});
if (props.value === null || typeof props.value === 'undefined' || props.value === '') return []
if (typeof props.value === 'number' || typeof props.value === 'boolean') return [props.value]
return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator)
})
const unmatch = computed(() => {
unmatchArray.value = [];
unmatchArray.value = []
// 没有value不显示
if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false
// 传入值为数组
let unmatch = false // 添加一个标志来判断是否有未匹配项
values.value.forEach(item => {
if (!props.options.some(v => v.value === item)) {
if (!props.options.some(v => v.value == item)) {
unmatchArray.value.push(item)
unmatch = true // 如果有未匹配项,将标志设置为true
}
})
return unmatch // 返回标志的值
});
})
function handleArray(array) {
if (array.length === 0) return "";
if (array.length === 0) return ""
return array.reduce((pre, cur) => {
return pre + " " + cur;
});
return pre + " " + cur
})
}
function isValueMatch(itemValue) {
return values.value.some(val => val == itemValue)
}
</script>

View File

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

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>仅允许导入xls、xlsx格式文件。</span>
<el-link v-if="templateUrl" type="primary" underline="never" style="font-size: 12px; vertical-align: baseline" @click="handleDownloadTemplate">下载模板</el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="visible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { getToken } from '@/utils/auth'
const { proxy } = getCurrentInstance()
const props = defineProps({
// 对话框标题
title: {
type: String,
default: '数据导入'
},
// 对话框宽度
width: {
type: String,
default: '400px'
},
// 上传接口地址(必传)
action: {
type: String,
required: true
},
// 模板下载接口地址,不传则不显示下载模板链接
templateAction: {
type: String,
default: ''
},
// 模板文件名前缀
templateFileName: {
type: String,
default: 'template'
},
// 覆盖更新勾选框的说明文字
updateSupportLabel: {
type: String,
default: '是否更新已经存在的数据'
}
})
const emit = defineEmits(['success'])
const uploadRef = ref(null)
const visible = ref(false)
const selectedFile = ref(null)
const isUploading = ref(false)
const updateSupport = ref(false)
const headers = { Authorization: 'Bearer ' + getToken() }
const uploadUrl = computed(() => {
return import.meta.env.VITE_APP_BASE_API + props.action + '?updateSupport=' + (updateSupport.value ? 1 : 0)
})
const templateUrl = computed(() => !!props.templateAction)
// 打开对话框(供父组件通过 ref 调用)
function open() {
updateSupport.value = false
isUploading.value = false
visible.value = true
nextTick(() => {
selectedFile.value = null
uploadRef.value?.clearFiles()
})
}
// 关闭时清理
function handleClose() {
isUploading.value = false
selectedFile.value = null
uploadRef.value?.clearFiles()
}
// 下载模板
function handleDownloadTemplate() {
proxy.download(props.templateAction, {}, `${props.templateFileName}_${new Date().getTime()}.xlsx`)
}
// 上传进度
function handleProgress() {
isUploading.value = true
}
/** 文件选择处理 */
const handleFileChange = (file, fileList) => {
selectedFile.value = file
}
/** 文件删除处理 */
const handleFileRemove = (file, fileList) => {
selectedFile.value = null
}
// 上传成功
function handleSuccess(response) {
visible.value = false
isUploading.value = false
selectedFile.value = null
uploadRef.value?.clearFiles()
proxy.$alert("<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>" + response.msg + '</div>', '导入结果', { dangerouslyUseHTMLString: true })
emit('success')
}
// 提交上传
function handleSubmit() {
const file = selectedFile.value
if (!file || file.length === 0 || !file.name.toLowerCase().endsWith('.xls') && !file.name.toLowerCase().endsWith('.xlsx')) {
proxy.$modal.msgError("请选择后缀为 “xls”或“xlsx”的文件。")
return
}
uploadRef.value.submit()
}
defineExpose({ open })
</script>

View File

@ -5,6 +5,7 @@
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
:data="data"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
@ -13,25 +14,26 @@
:headers="headers"
class="upload-file-uploader"
ref="fileUpload"
v-if="!disabled"
>
<!-- 上传按钮 -->
<el-button type="primary">选取文件</el-button>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip">
<div class="el-upload__tip" v-if="showTip && !disabled">
请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件
</div>
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<transition-group ref="uploadFileList" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
<el-link :href="`${baseUrl}${file.url}`" underline="never" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
<el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
<el-link underline="never" @click="handleDelete(index)" type="danger" v-if="!disabled">&nbsp;删除</el-link>
</div>
</li>
</transition-group>
@ -39,132 +41,152 @@
</template>
<script setup>
import { getToken } from "@/utils/auth";
import { getToken } from "@/utils/auth"
import Sortable from 'sortablejs'
const props = defineProps({
modelValue: [String, Object, Array],
// 上传接口地址
action: {
type: String,
default: "/common/upload"
},
// 上传携带的参数
data: {
type: Object
},
// 数量限制
limit: {
type: Number,
default: 5,
default: 5
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
default: 5
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["doc", "xls", "ppt", "txt", "pdf"],
default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"]
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
},
// 禁用组件(仅查看文件)
disabled: {
type: Boolean,
default: false
},
// 拖动排序
drag: {
type: Boolean,
default: true
}
});
})
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const number = ref(0);
const uploadList = ref([]);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传文件服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref([]);
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const number = ref(0)
const uploadList = ref([])
const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传文件服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() })
const fileList = ref([])
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
);
)
watch(() => props.modelValue, val => {
if (val) {
let temp = 1;
let temp = 1
// 首先将值转为数组
const list = Array.isArray(val) ? val : props.modelValue.split(',');
const list = Array.isArray(val) ? val : props.modelValue.split(',')
// 然后将数组转为对象数组
fileList.value = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item };
item = { name: item, url: item }
}
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
item.uid = item.uid || new Date().getTime() + temp++
return item
})
} else {
fileList.value = [];
return [];
fileList.value = []
return []
}
},{ deep: true, immediate: true });
},{ deep: true, immediate: true })
// 上传前校检格式和大小
function handleBeforeUpload(file) {
// 校检文件类型
if (props.fileType.length) {
const fileName = file.name.split('.');
const fileExt = fileName[fileName.length - 1];
const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
const fileName = file.name.split('.')
const fileExt = fileName[fileName.length - 1]
const isTypeOk = props.fileType.indexOf(fileExt) >= 0
if (!isTypeOk) {
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}格式文件!`);
return false;
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}格式文件!`)
return false
}
}
// 校检文件名是否包含特殊字符
if (file.name.includes(',')) {
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false;
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
return false
}
// 校检文件大小
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
return false;
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
return false
}
}
proxy.$modal.loading("正在上传文件,请稍候...");
number.value++;
return true;
proxy.$modal.loading("正在上传文件,请稍候...")
number.value++
return true
}
// 文件个数超出
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
}
// 上传失败
function handleUploadError(err) {
proxy.$modal.msgError("上传文件失败");
proxy.$modal.msgError("上传文件失败")
proxy.$modal.closeLoading()
}
// 上传成功回调
function handleUploadSuccess(res, file) {
if (res.code === 200) {
uploadList.value.push({ name: res.fileName, url: res.fileName });
uploadedSuccessfully();
uploadList.value.push({ name: res.fileName, url: res.fileName })
uploadedSuccessfully()
} else {
number.value--;
proxy.$modal.closeLoading();
proxy.$modal.msgError(res.msg);
proxy.$refs.fileUpload.handleRemove(file);
uploadedSuccessfully();
number.value--
proxy.$modal.closeLoading()
proxy.$modal.msgError(res.msg)
proxy.$refs.fileUpload.handleRemove(file)
uploadedSuccessfully()
}
}
// 删除文件
function handleDelete(index) {
fileList.value.splice(index, 1);
emit("update:modelValue", listToString(fileList.value));
fileList.value.splice(index, 1)
emit("update:modelValue", listToString(fileList.value))
}
// 上传结束处理
function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
console.log('fileList.value', fileList.value)
emit("update:modelValue", listToString(fileList.value));
proxy.$modal.closeLoading();
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
uploadList.value = []
number.value = 0
emit("update:modelValue", listToString(fileList.value))
proxy.$modal.closeLoading()
}
}
@ -172,25 +194,41 @@ function uploadedSuccessfully() {
function getFileName(name) {
// 如果是url那么取最后的名字 如果不是直接返回
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1);
return name.slice(name.lastIndexOf("/") + 1)
} else {
return name;
return name
}
}
// 对象转成指定字符串分隔
function listToString(list, separator) {
let strs = "";
separator = separator || ",";
let strs = ""
separator = separator || ","
for (let i in list) {
if (list[i].url) {
strs += list[i].url + separator;
strs += list[i].url + separator
}
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
return strs != '' ? strs.substr(0, strs.length - 1) : ''
}
</script>
// 初始化拖拽排序
onMounted(() => {
if (props.drag && !props.disabled) {
nextTick(() => {
const element = proxy.$refs.uploadFileList?.$el || proxy.$refs.uploadFileList
Sortable.create(element, {
ghostClass: 'file-upload-darg',
onEnd: (evt) => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
fileList.value.splice(evt.newIndex, 0, movedItem)
emit('update:modelValue', listToString(fileList.value))
}
})
})
}
})
</script>
<style scoped lang="scss">
.upload-file-uploader {
margin-bottom: 5px;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,12 @@
<div class="component-upload-image">
<el-upload
multiple
:disabled="disabled"
:action="uploadImgUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:data="data"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
@ -20,7 +22,7 @@
<el-icon class="avatar-uploader-icon"><plus /></el-icon>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip">
<div class="el-upload__tip" v-if="showTip && !disabled">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
@ -46,166 +48,202 @@
</template>
<script setup>
import { getToken } from "@/utils/auth";
import { isExternal } from "@/utils/validate";
import { getToken } from "@/utils/auth"
import { isExternal } from "@/utils/validate"
import Sortable from 'sortablejs'
const props = defineProps({
modelValue: [String, Object, Array],
// 上传接口地址
action: {
type: String,
default: "/common/upload"
},
// 上传携带的参数
data: {
type: Object
},
// 图片数量限制
limit: {
type: Number,
default: 5,
default: 5
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
default: 5
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["png", "jpg", "jpeg"],
default: () => ["png", "jpg", "jpeg"]
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
},
});
// 禁用组件(仅查看图片)
disabled: {
type: Boolean,
default: false
},
// 拖动排序
drag: {
type: Boolean,
default: true
}
})
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const number = ref(0);
const uploadList = ref([]);
const dialogImageUrl = ref("");
const dialogVisible = ref(false);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传的图片服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref([]);
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const number = ref(0)
const uploadList = ref([])
const dialogImageUrl = ref("")
const dialogVisible = ref(false)
const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传的图片服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() })
const fileList = ref([])
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
);
)
watch(() => props.modelValue, val => {
if (val) {
// 首先将值转为数组
const list = Array.isArray(val) ? val : props.modelValue.split(",");
const list = Array.isArray(val) ? val : props.modelValue.split(",")
// 然后将数组转为对象数组
fileList.value = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(baseUrl) === -1 && !isExternal(item)) {
item = { name: baseUrl + item, url: baseUrl + item };
item = { name: baseUrl + item, url: baseUrl + item }
} else {
item = { name: item, url: item };
item = { name: item, url: item }
}
}
return item;
});
return item
})
} else {
fileList.value = [];
return [];
fileList.value = []
return []
}
},{ deep: true, immediate: true });
},{ deep: true, immediate: true })
// 上传前loading加载
function handleBeforeUpload(file) {
let isImg = false;
let isImg = false
if (props.fileType.length) {
let fileExtension = "";
let fileExtension = ""
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1)
}
isImg = props.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
if (file.type.indexOf(type) > -1) return true
if (fileExtension && fileExtension.indexOf(type) > -1) return true
return false
})
} else {
isImg = file.type.indexOf("image") > -1;
isImg = file.type.indexOf("image") > -1
}
if (!isImg) {
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`);
return false;
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`)
return false
}
if (file.name.includes(',')) {
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false;
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
return false
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
return false;
proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`)
return false
}
}
proxy.$modal.loading("正在上传图片,请稍候...");
number.value++;
proxy.$modal.loading("正在上传图片,请稍候...")
number.value++
}
// 文件个数超出
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
}
// 上传成功回调
function handleUploadSuccess(res, file) {
if (res.code === 200) {
uploadList.value.push({ name: res.fileName, url: res.fileName });
uploadedSuccessfully();
uploadList.value.push({ name: res.fileName, url: res.fileName })
uploadedSuccessfully()
} else {
number.value--;
proxy.$modal.closeLoading();
proxy.$modal.msgError(res.msg);
proxy.$refs.imageUpload.handleRemove(file);
uploadedSuccessfully();
number.value--
proxy.$modal.closeLoading()
proxy.$modal.msgError(res.msg)
proxy.$refs.imageUpload.handleRemove(file)
uploadedSuccessfully()
}
}
// 删除图片
function handleDelete(file) {
const findex = fileList.value.map(f => f.name).indexOf(file.name);
const findex = fileList.value.map(f => f.name).indexOf(file.name)
if (findex > -1 && uploadList.value.length === number.value) {
fileList.value.splice(findex, 1);
emit("update:modelValue", listToString(fileList.value));
return false;
fileList.value.splice(findex, 1)
emit("update:modelValue", listToString(fileList.value))
return false
}
}
// 上传结束处理
function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit("update:modelValue", listToString(fileList.value));
proxy.$modal.closeLoading();
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
uploadList.value = []
number.value = 0
emit("update:modelValue", listToString(fileList.value))
proxy.$modal.closeLoading()
}
}
// 上传失败
function handleUploadError() {
proxy.$modal.msgError("上传图片失败");
proxy.$modal.closeLoading();
proxy.$modal.msgError("上传图片失败")
proxy.$modal.closeLoading()
}
// 预览
function handlePictureCardPreview(file) {
dialogImageUrl.value = file.url;
dialogVisible.value = true;
dialogImageUrl.value = file.url
dialogVisible.value = true
}
// 对象转成指定字符串分隔
function listToString(list, separator) {
let strs = "";
separator = separator || ",";
let strs = ""
separator = separator || ","
for (let i in list) {
if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
strs += list[i].url.replace(baseUrl, "") + separator;
strs += list[i].url.replace(baseUrl, "") + separator
}
}
return strs != "" ? strs.substr(0, strs.length - 1) : "";
return strs != "" ? strs.substr(0, strs.length - 1) : ""
}
// 初始化拖拽排序
onMounted(() => {
if (props.drag && !props.disabled) {
nextTick(() => {
const element = proxy.$refs.imageUpload?.$el?.querySelector('.el-upload-list')
Sortable.create(element, {
onEnd: (evt) => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
fileList.value.splice(evt.newIndex, 0, movedItem)
emit('update:modelValue', listToString(fileList.value))
}
})
})
}
})
</script>
<style scoped lang="scss">

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