mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 21:11:12 +00:00
注册表编辑器
This commit is contained in:
@@ -7,6 +7,8 @@ API模块
|
|||||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
from unilabos.app.controler import devices, job_add, job_info
|
from unilabos.app.controler import devices, job_add, job_info
|
||||||
from unilabos.app.model import (
|
from unilabos.app.model import (
|
||||||
Resp,
|
Resp,
|
||||||
@@ -19,6 +21,8 @@ from unilabos.app.model import (
|
|||||||
JobFinishReq,
|
JobFinishReq,
|
||||||
)
|
)
|
||||||
from unilabos.app.web.utils.host_utils import get_host_node_info
|
from unilabos.app.web.utils.host_utils import get_host_node_info
|
||||||
|
from unilabos.registry.registry import lab_registry
|
||||||
|
from unilabos.utils.type_check import NoAliasDumper
|
||||||
|
|
||||||
# 创建API路由器
|
# 创建API路由器
|
||||||
api = APIRouter()
|
api = APIRouter()
|
||||||
@@ -603,6 +607,7 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
|
|||||||
file_size = request_data.get("file_size", 0)
|
file_size = request_data.get("file_size", 0)
|
||||||
registry_type = request_data.get("registry_type", "device")
|
registry_type = request_data.get("registry_type", "device")
|
||||||
class_name = request_data.get("class_name")
|
class_name = request_data.get("class_name")
|
||||||
|
module_prefix = request_data.get("module_prefix", "")
|
||||||
|
|
||||||
async def send_log(message: str, level: str = "info"):
|
async def send_log(message: str, level: str = "info"):
|
||||||
"""发送日志消息到客户端"""
|
"""发送日志消息到客户端"""
|
||||||
@@ -656,7 +661,12 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
|
|||||||
|
|
||||||
# 确定模块名
|
# 确定模块名
|
||||||
module_name = file_name.replace(".py", "").replace("-", "_").replace(" ", "_")
|
module_name = file_name.replace(".py", "").replace("-", "_").replace(" ", "_")
|
||||||
|
|
||||||
|
# 如果有 module_prefix,则使用完整的模块路径
|
||||||
|
full_module_name = f"{module_prefix}.{module_name}" if module_prefix else module_name
|
||||||
await send_log(f"使用模块名: {module_name}")
|
await send_log(f"使用模块名: {module_name}")
|
||||||
|
if module_prefix:
|
||||||
|
await send_log(f"完整模块路径: {full_module_name}")
|
||||||
|
|
||||||
# 导入模块
|
# 导入模块
|
||||||
try:
|
try:
|
||||||
@@ -697,7 +707,7 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
|
|||||||
from unilabos.utils.import_manager import get_enhanced_class_info
|
from unilabos.utils.import_manager import get_enhanced_class_info
|
||||||
|
|
||||||
# 分析类信息
|
# 分析类信息
|
||||||
enhanced_info = get_enhanced_class_info(f"{module_name}:{class_name}", use_dynamic=True)
|
enhanced_info = get_enhanced_class_info(f"{full_module_name}:{class_name}", use_dynamic=True)
|
||||||
|
|
||||||
if not enhanced_info.get("dynamic_import_success", False):
|
if not enhanced_info.get("dynamic_import_success", False):
|
||||||
await send_error("动态导入类信息失败")
|
await send_error("动态导入类信息失败")
|
||||||
@@ -705,24 +715,44 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
|
|||||||
|
|
||||||
await send_log("成功分析类信息")
|
await send_log("成功分析类信息")
|
||||||
|
|
||||||
# 生成注册表schema
|
# 根据注册表类型生成不同的schema
|
||||||
|
if registry_type == "resource":
|
||||||
|
# 资源类型的简单结构
|
||||||
|
category_name = file_name.replace(".py", "") if file_name else "unknown"
|
||||||
registry_schema = {
|
registry_schema = {
|
||||||
"class_name": class_name,
|
|
||||||
"module": f"{module_name}:{class_name}",
|
|
||||||
"type": "python",
|
|
||||||
"description": enhanced_info.get("class_docstring", ""),
|
"description": enhanced_info.get("class_docstring", ""),
|
||||||
|
"category": [category_name],
|
||||||
|
"class": {
|
||||||
|
"module": f"{full_module_name}:{class_name}",
|
||||||
|
"type": "python",
|
||||||
|
},
|
||||||
|
"handles": [],
|
||||||
|
"icon": "",
|
||||||
|
"init_param_schema": {},
|
||||||
|
"registry_type": "resource",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"category": [registry_type],
|
"file_path": f"uploaded_file://{file_name}",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# 设备类型的复杂结构
|
||||||
|
registry_schema = {
|
||||||
|
"description": enhanced_info.get("class_docstring", ""),
|
||||||
|
"class": {
|
||||||
|
"module": f"{full_module_name}:{class_name}",
|
||||||
|
"type": "python",
|
||||||
"status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
|
"status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
|
||||||
"action_value_mappings": {},
|
"action_value_mappings": {},
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"handles": [],
|
||||||
"init_param_schema": {},
|
"init_param_schema": {},
|
||||||
"registry_type": registry_type,
|
"registry_type": "device",
|
||||||
"file_path": f"uploaded_file://{file_name}",
|
"file_path": f"uploaded_file://{file_name}",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 处理动作方法
|
# 处理动作方法(仅对设备类型)
|
||||||
for method_name, method_info in enhanced_info["action_methods"].items():
|
for method_name, method_info in enhanced_info["action_methods"].items():
|
||||||
registry_schema["action_value_mappings"][f"auto-{method_name}"] = {
|
registry_schema["class"]["action_value_mappings"][f"auto-{method_name}"] = {
|
||||||
"type": "UniLabJsonCommandAsync" if method_info["is_async"] else "UniLabJsonCommand",
|
"type": "UniLabJsonCommandAsync" if method_info["is_async"] else "UniLabJsonCommand",
|
||||||
"goal": {},
|
"goal": {},
|
||||||
"feedback": {},
|
"feedback": {},
|
||||||
@@ -733,19 +763,54 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
|
|||||||
|
|
||||||
await send_log("成功生成注册表schema")
|
await send_log("成功生成注册表schema")
|
||||||
|
|
||||||
|
# 格式化状态方法信息
|
||||||
|
status_info = {}
|
||||||
|
for status_name, status_data in enhanced_info.get("status_methods", {}).items():
|
||||||
|
status_info[status_name] = {
|
||||||
|
"return_type": status_data.get("return_type", "未知类型"),
|
||||||
|
"docstring": status_data.get("docstring", "无描述"),
|
||||||
|
"is_property": status_data.get("is_property", False),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 格式化动作方法信息
|
||||||
|
action_info = {}
|
||||||
|
for action_name, action_data in enhanced_info.get("action_methods", {}).items():
|
||||||
|
args = action_data.get("args", [])
|
||||||
|
action_info[action_name] = {
|
||||||
|
"param_count": len(args),
|
||||||
|
"params": [
|
||||||
|
{"name": arg.get("name", ""), "type": arg.get("type", ""), "default": arg.get("default")}
|
||||||
|
for arg in args
|
||||||
|
],
|
||||||
|
"is_async": action_data.get("is_async", False),
|
||||||
|
"docstring": action_data.get("docstring", "无描述"),
|
||||||
|
"return_suggestion": "建议返回字典类型 (dict) 以便更好地结构化结果数据",
|
||||||
|
}
|
||||||
|
|
||||||
# 准备结果数据
|
# 准备结果数据
|
||||||
result = {
|
result = {
|
||||||
"class_info": {
|
"class_info": {
|
||||||
"class_name": class_name,
|
"class_name": class_name,
|
||||||
"module_name": module_name,
|
"module_name": module_name,
|
||||||
|
"module_prefix": module_prefix,
|
||||||
|
"full_module_name": full_module_name,
|
||||||
"file_name": file_name,
|
"file_name": file_name,
|
||||||
"file_size": file_size,
|
"file_size": file_size,
|
||||||
"docstring": enhanced_info.get("class_docstring", ""),
|
"docstring": enhanced_info.get("class_docstring", ""),
|
||||||
"dynamic_import_success": enhanced_info.get("dynamic_import_success", False),
|
"dynamic_import_success": enhanced_info.get("dynamic_import_success", False),
|
||||||
|
"registry_type": registry_type,
|
||||||
},
|
},
|
||||||
"registry_schema": registry_schema,
|
"registry_schema": registry_schema,
|
||||||
"action_methods": enhanced_info["action_methods"],
|
"class_analysis": {
|
||||||
"status_methods": enhanced_info["status_methods"],
|
"status_methods": status_info,
|
||||||
|
"action_methods": action_info,
|
||||||
|
"init_params": enhanced_info.get("init_params", []),
|
||||||
|
"status_methods_count": len(status_info),
|
||||||
|
"action_methods_count": len(action_info),
|
||||||
|
},
|
||||||
|
# 保持向后兼容
|
||||||
|
"action_methods": enhanced_info.get("action_methods", {}),
|
||||||
|
"status_methods": enhanced_info.get("status_methods", {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
# 发送结果
|
# 发送结果
|
||||||
@@ -794,6 +859,11 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
|
|||||||
registry_type = request_data.get("registry_type", "device")
|
registry_type = request_data.get("registry_type", "device")
|
||||||
class_name = request_data.get("class_name")
|
class_name = request_data.get("class_name")
|
||||||
module_name = request_data.get("module_name")
|
module_name = request_data.get("module_name")
|
||||||
|
description = request_data.get("description", "")
|
||||||
|
safe_class_name = request_data.get("safe_class_name", "")
|
||||||
|
icon = request_data.get("icon", "")
|
||||||
|
module_prefix = request_data.get("module_prefix", "")
|
||||||
|
handles = request_data.get("handles", [])
|
||||||
|
|
||||||
async def send_log(message: str, level: str = "info"):
|
async def send_log(message: str, level: str = "info"):
|
||||||
"""发送日志消息到客户端"""
|
"""发送日志消息到客户端"""
|
||||||
@@ -862,7 +932,12 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
|
|||||||
# 确定模块名
|
# 确定模块名
|
||||||
if not module_name:
|
if not module_name:
|
||||||
module_name = full_file_path.stem
|
module_name = full_file_path.stem
|
||||||
|
|
||||||
|
# 如果有 module_prefix,则使用完整的模块路径
|
||||||
|
full_module_name = f"{module_prefix}.{module_name}" if module_prefix else module_name
|
||||||
await send_log(f"使用模块名: {module_name}")
|
await send_log(f"使用模块名: {module_name}")
|
||||||
|
if module_prefix:
|
||||||
|
await send_log(f"完整模块路径: {full_module_name}")
|
||||||
|
|
||||||
# 导入模块
|
# 导入模块
|
||||||
try:
|
try:
|
||||||
@@ -930,7 +1005,7 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
|
|||||||
from unilabos.utils.import_manager import get_enhanced_class_info
|
from unilabos.utils.import_manager import get_enhanced_class_info
|
||||||
|
|
||||||
# 分析类信息
|
# 分析类信息
|
||||||
enhanced_info = get_enhanced_class_info(f"{module_name}:{target_class_name}", use_dynamic=True)
|
enhanced_info = get_enhanced_class_info(f"{full_module_name}:{target_class_name}", use_dynamic=True)
|
||||||
|
|
||||||
if not enhanced_info.get("dynamic_import_success", False):
|
if not enhanced_info.get("dynamic_import_success", False):
|
||||||
await send_error("动态导入类信息失败")
|
await send_error("动态导入类信息失败")
|
||||||
@@ -938,55 +1013,124 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
|
|||||||
|
|
||||||
await send_log("成功分析类信息")
|
await send_log("成功分析类信息")
|
||||||
|
|
||||||
# 生成注册表schema
|
# 根据注册表类型生成不同的schema
|
||||||
|
if registry_type == "resource":
|
||||||
|
# 资源类型的简单结构
|
||||||
|
category_name = Path(file_path).stem if file_path else "unknown"
|
||||||
registry_schema = {
|
registry_schema = {
|
||||||
"class_name": target_class_name,
|
"description": description or enhanced_info.get("class_docstring", ""),
|
||||||
"module": f"{module_name}:{target_class_name}",
|
"category": [category_name],
|
||||||
|
"class": {
|
||||||
|
"module": f"{full_module_name}:{target_class_name}",
|
||||||
"type": "python",
|
"type": "python",
|
||||||
"description": enhanced_info.get("class_docstring", ""),
|
},
|
||||||
"version": "1.0.0",
|
"handles": handles,
|
||||||
"category": [registry_type],
|
"icon": icon,
|
||||||
"status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
|
|
||||||
"action_value_mappings": {},
|
|
||||||
"init_param_schema": {},
|
"init_param_schema": {},
|
||||||
"registry_type": registry_type,
|
"registry_type": "resource",
|
||||||
"file_path": str(full_file_path),
|
"version": "1.0.0",
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
# 处理动作方法
|
# 设备类型的复杂结构
|
||||||
for method_name, method_info in enhanced_info["action_methods"].items():
|
registry_schema = {
|
||||||
registry_schema["action_value_mappings"][f"auto-{method_name}"] = {
|
"description": description or enhanced_info.get("class_docstring", ""),
|
||||||
"type": "UniLabJsonCommandAsync" if method_info["is_async"] else "UniLabJsonCommand",
|
"class": {
|
||||||
|
"module": f"{full_module_name}:{target_class_name}",
|
||||||
|
"type": "python",
|
||||||
|
"status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
|
||||||
|
"action_value_mappings": {
|
||||||
|
f"auto-{k}": {
|
||||||
|
"type": "UniLabJsonCommandAsync" if v["is_async"] else "UniLabJsonCommand",
|
||||||
"goal": {},
|
"goal": {},
|
||||||
"feedback": {},
|
"feedback": {},
|
||||||
"result": {},
|
"result": {},
|
||||||
"args": method_info["args"],
|
"schema": lab_registry._generate_unilab_json_command_schema(v["args"], k),
|
||||||
"description": method_info.get("docstring", ""),
|
"goal_default": {i["name"]: i["default"] for i in v["args"]},
|
||||||
|
"handles": [],
|
||||||
|
}
|
||||||
|
# 不生成已配置action的动作
|
||||||
|
for k, v in enhanced_info["action_methods"].items()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"handles": handles,
|
||||||
|
"icon": icon,
|
||||||
|
"init_param_schema": {
|
||||||
|
"config": lab_registry._generate_unilab_json_command_schema(
|
||||||
|
enhanced_info["init_params"], "__init__"
|
||||||
|
)["properties"]["goal"],
|
||||||
|
"data": lab_registry._generate_status_types_schema(enhanced_info["status_methods"]),
|
||||||
|
},
|
||||||
|
"registry_type": "device",
|
||||||
}
|
}
|
||||||
|
|
||||||
await send_log("成功生成注册表schema")
|
await send_log("成功生成注册表schema")
|
||||||
|
|
||||||
# 转换为YAML格式
|
# 创建最终的YAML配置(使用ID作为根键)
|
||||||
import yaml
|
if safe_class_name:
|
||||||
from unilabos.utils.type_check import NoAliasDumper
|
item_id = safe_class_name
|
||||||
|
else:
|
||||||
# 创建最终的YAML配置(使用设备ID作为根键)
|
class_name_safe = (target_class_name or "unknown").lower()
|
||||||
class_name_safe = class_name or "unknown"
|
if registry_type == "resource":
|
||||||
suffix = "_device" if registry_type == "device" else "_resource"
|
# 资源ID通常直接使用类名,不加后缀
|
||||||
device_id = f"{class_name_safe.lower()}{suffix}"
|
item_id = class_name_safe
|
||||||
final_config = {device_id: registry_schema}
|
else:
|
||||||
|
# 设备ID使用类名加_device后缀
|
||||||
|
item_id = f"{class_name_safe}_device"
|
||||||
|
final_config = {item_id: registry_schema}
|
||||||
|
|
||||||
yaml_content = yaml.dump(
|
yaml_content = yaml.dump(
|
||||||
final_config, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper, sort_keys=True
|
final_config, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper, sort_keys=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# 准备结果数据(只保留YAML结果)
|
# 格式化状态方法信息
|
||||||
|
status_info = {}
|
||||||
|
for status_name, status_data in enhanced_info.get("status_methods", {}).items():
|
||||||
|
status_info[status_name] = {
|
||||||
|
"return_type": status_data.get("return_type", "未知类型"),
|
||||||
|
"docstring": status_data.get("docstring", "无描述"),
|
||||||
|
"is_property": status_data.get("is_property", False),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 格式化动作方法信息
|
||||||
|
action_info = {}
|
||||||
|
for action_name, action_data in enhanced_info.get("action_methods", {}).items():
|
||||||
|
args = action_data.get("args", [])
|
||||||
|
action_info[action_name] = {
|
||||||
|
"param_count": len(args),
|
||||||
|
"params": [
|
||||||
|
{"name": arg.get("name", ""), "type": arg.get("type", ""), "default": arg.get("default")}
|
||||||
|
for arg in args
|
||||||
|
],
|
||||||
|
"is_async": action_data.get("is_async", False),
|
||||||
|
"docstring": action_data.get("docstring", "无描述"),
|
||||||
|
"return_suggestion": "建议返回字典类型 (dict) 以便更好地结构化结果数据",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 准备结果数据(包含详细的类分析信息)
|
||||||
result = {
|
result = {
|
||||||
"registry_schema": yaml_content,
|
"registry_schema": yaml_content,
|
||||||
"device_id": device_id,
|
"item_id": item_id,
|
||||||
"class_name": class_name,
|
"registry_type": registry_type,
|
||||||
|
"class_name": target_class_name,
|
||||||
"module_name": module_name,
|
"module_name": module_name,
|
||||||
"file_path": file_path,
|
"file_path": file_path,
|
||||||
|
"config_params": {
|
||||||
|
"safe_class_name": safe_class_name or item_id,
|
||||||
|
"description": description,
|
||||||
|
"icon": icon,
|
||||||
|
"module_prefix": module_prefix,
|
||||||
|
"full_module_name": full_module_name,
|
||||||
|
"handles_count": len(handles),
|
||||||
|
"handles": handles,
|
||||||
|
},
|
||||||
|
"class_analysis": {
|
||||||
|
"class_docstring": enhanced_info.get("class_docstring", ""),
|
||||||
|
"status_methods": status_info,
|
||||||
|
"action_methods": action_info,
|
||||||
|
"init_params": enhanced_info.get("init_params", []),
|
||||||
|
"dynamic_import_success": enhanced_info.get("dynamic_import_success", False),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# 发送结果
|
# 发送结果
|
||||||
@@ -1034,12 +1178,11 @@ def get_file_browser_data(path: str = ""):
|
|||||||
items = []
|
items = []
|
||||||
|
|
||||||
parent_path = target_path.parent
|
parent_path = target_path.parent
|
||||||
relative_parent = parent_path.relative_to(working_dir)
|
|
||||||
items.append(
|
items.append(
|
||||||
{
|
{
|
||||||
"name": "..",
|
"name": "..",
|
||||||
"type": "directory",
|
"type": "directory",
|
||||||
"path": str(relative_parent) if relative_parent != Path(".") else "",
|
"path": str(parent_path),
|
||||||
"size": 0,
|
"size": 0,
|
||||||
"is_parent": True,
|
"is_parent": True,
|
||||||
}
|
}
|
||||||
@@ -1048,16 +1191,12 @@ def get_file_browser_data(path: str = ""):
|
|||||||
# 获取子目录和文件
|
# 获取子目录和文件
|
||||||
try:
|
try:
|
||||||
for item in sorted(target_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
|
for item in sorted(target_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
|
||||||
if item.name.startswith("."): # 跳过隐藏文件
|
|
||||||
continue
|
|
||||||
|
|
||||||
item_type = "directory" if item.is_dir() else "file"
|
item_type = "directory" if item.is_dir() else "file"
|
||||||
relative_path = item.relative_to(working_dir)
|
|
||||||
|
|
||||||
item_info = {
|
item_info = {
|
||||||
"name": item.name,
|
"name": item.name,
|
||||||
"type": item_type,
|
"type": item_type,
|
||||||
"path": str(relative_path),
|
"path": str(item),
|
||||||
"size": item.stat().st_size if item.is_file() else 0,
|
"size": item.stat().st_size if item.is_file() else 0,
|
||||||
"is_python": item.suffix == ".py" if item.is_file() else False,
|
"is_python": item.suffix == ".py" if item.is_file() else False,
|
||||||
"is_parent": False,
|
"is_parent": False,
|
||||||
@@ -1068,7 +1207,7 @@ def get_file_browser_data(path: str = ""):
|
|||||||
|
|
||||||
return Resp(
|
return Resp(
|
||||||
data={
|
data={
|
||||||
"current_path": str(target_path.relative_to(working_dir)) if target_path != working_dir else "",
|
"current_path": str(target_path),
|
||||||
"working_dir": str(working_dir),
|
"working_dir": str(working_dir),
|
||||||
"items": items,
|
"items": items,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ header %}UniLab API{% endblock %} {% block nav %}
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
>主页</a
|
>主页</a
|
||||||
>
|
>
|
||||||
class="nav-tab">状态</a>
|
<a href="/status" class="nav-tab">状态</a>
|
||||||
<a href="/registry-editor" class="nav-tab" target="_blank">注册表编辑</a>
|
<a href="/registry-editor" class="nav-tab" target="_blank">注册表编辑</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block content %}
|
{% endblock %} {% block content %}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %} {% block title %}注册表编辑器 - UniLab{% endblock %}
|
{% extends "base.html" %} {% block title %}注册表编辑器 - UniLab{% endblock %}
|
||||||
{% block header %}注册表编辑器{% endblock %} {% block nav %}
|
{% block header %}注册表编辑器{% endblock %} {% block nav %} {% endblock %} {%
|
||||||
{% endblock %} {% block scripts %}
|
block scripts %}
|
||||||
<style>
|
<style>
|
||||||
.editor-container {
|
.editor-container {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -9,11 +9,11 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.form-section {
|
.form-section {
|
||||||
background: white;
|
background: rgb(212, 226, 243);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
@@ -280,6 +280,77 @@
|
|||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Handle 配置相关样式 */
|
||||||
|
.handle-item {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-item-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-handle-btn {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-handle-btn:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-form-row.full-width {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-form-group label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-form-group input,
|
||||||
|
.handle-form-group select {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 连接状态右上角定位 */
|
/* 连接状态右上角定位 */
|
||||||
.connection-status-fixed {
|
.connection-status-fixed {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -455,6 +526,84 @@
|
|||||||
|
|
||||||
<!-- 右栏:注册表配置结果 -->
|
<!-- 右栏:注册表配置结果 -->
|
||||||
<div class="right-column">
|
<div class="right-column">
|
||||||
|
<!-- 配置参数输入区域 -->
|
||||||
|
<div
|
||||||
|
id="config-params-section"
|
||||||
|
class="form-section"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<h3>配置参数</h3>
|
||||||
|
|
||||||
|
<!-- Module Prefix 输入框 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="module-prefix">Module Prefix</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="module-prefix"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="例如: unilabos.devices.pumps"
|
||||||
|
/>
|
||||||
|
<small class="form-text">模块路径前缀,指定类所在的包位置</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Safe Class Name 输入框 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="safe-class-name">Safe Class Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="safe-class-name"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="自动生成,可自定义"
|
||||||
|
/>
|
||||||
|
<small class="form-text"
|
||||||
|
>将作为注册表中的ID使用,必须是有效的标识符</small
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 设备/资源描述 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description-input">设备/资源描述</label>
|
||||||
|
<textarea
|
||||||
|
id="description-input"
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
placeholder="请输入设备或资源的描述信息..."
|
||||||
|
></textarea>
|
||||||
|
<small class="form-text"
|
||||||
|
>描述信息将显示在注册表中,帮助用户了解设备功能</small
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Icon 输入框 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="icon-input">图标文件名</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="icon-input"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="icon_xxx.webp"
|
||||||
|
/>
|
||||||
|
<small class="form-text">图标文件名,例如:icon_pump.webp</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Handles 配置区域 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Handles 配置</label>
|
||||||
|
<div id="handles-container">
|
||||||
|
<!-- 动态添加的handles项目会在这里显示 -->
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
onclick="addHandleItem()"
|
||||||
|
>
|
||||||
|
添加 Handle
|
||||||
|
</button>
|
||||||
|
<small class="form-text">配置设备/资源的输入输出接口</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- YAML结果显示区域 -->
|
||||||
<div id="results-section" class="form-section" style="display: none">
|
<div id="results-section" class="form-section" style="display: none">
|
||||||
<h3>注册表配置结果</h3>
|
<h3>注册表配置结果</h3>
|
||||||
<div class="yaml-container">
|
<div class="yaml-container">
|
||||||
@@ -485,6 +634,7 @@
|
|||||||
let isConnected = false;
|
let isConnected = false;
|
||||||
let currentPath = '';
|
let currentPath = '';
|
||||||
let selectedFilePath = null;
|
let selectedFilePath = null;
|
||||||
|
let handleCounter = 0;
|
||||||
|
|
||||||
// 页面加载完成后初始化
|
// 页面加载完成后初始化
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
@@ -613,7 +763,12 @@
|
|||||||
// 保存YAML内容供复制使用
|
// 保存YAML内容供复制使用
|
||||||
window.registryYaml = result.registry_schema;
|
window.registryYaml = result.registry_schema;
|
||||||
|
|
||||||
addLog(`生成的设备ID: ${result.device_id}`, 'info');
|
addLog(
|
||||||
|
`生成的${result.registry_type === 'resource' ? '资源' : '设备'}ID: ${
|
||||||
|
result.item_id || result.device_id
|
||||||
|
}`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
addLog('注册表生成完成!', 'success');
|
addLog('注册表生成完成!', 'success');
|
||||||
@@ -717,24 +872,6 @@
|
|||||||
// 清空文件浏览器
|
// 清空文件浏览器
|
||||||
fileBrowser.innerHTML = '';
|
fileBrowser.innerHTML = '';
|
||||||
|
|
||||||
// 添加返回上级目录选项(除了根路径)
|
|
||||||
const currentPath = data.current_path || data.working_dir;
|
|
||||||
if (currentPath && currentPath !== '' && !isRootPath(currentPath)) {
|
|
||||||
const parentItem = document.createElement('div');
|
|
||||||
parentItem.className = 'file-item';
|
|
||||||
parentItem.innerHTML = `
|
|
||||||
<span class="file-icon directory-icon">⬆️</span>
|
|
||||||
<span class="file-name">..</span>
|
|
||||||
<span class="file-size">(返回上级目录)</span>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 计算上级目录路径
|
|
||||||
const parentPath = currentPath.split(/[/\\]/).slice(0, -1).join('/');
|
|
||||||
parentItem.onclick = () => loadFileBrowser(parentPath);
|
|
||||||
|
|
||||||
fileBrowser.appendChild(parentItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.items.length === 0) {
|
if (data.items.length === 0) {
|
||||||
if (fileBrowser.children.length === 0) {
|
if (fileBrowser.children.length === 0) {
|
||||||
fileBrowser.innerHTML =
|
fileBrowser.innerHTML =
|
||||||
@@ -912,14 +1049,31 @@
|
|||||||
// 显示分析状态
|
// 显示分析状态
|
||||||
showFileAnalysisStatus(analysisResult);
|
showFileAnalysisStatus(analysisResult);
|
||||||
|
|
||||||
// 监听类选择变化,启用导入按钮
|
// 监听类选择变化,启用导入按钮和显示配置参数区域
|
||||||
classSelect.onchange = function () {
|
classSelect.onchange = function () {
|
||||||
const importBtn = document.getElementById('import-btn');
|
const importBtn = document.getElementById('import-btn');
|
||||||
|
const configParamsSection = document.getElementById(
|
||||||
|
'config-params-section'
|
||||||
|
);
|
||||||
|
const safeClassNameInput = document.getElementById('safe-class-name');
|
||||||
|
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
importBtn.disabled = false;
|
importBtn.disabled = false;
|
||||||
|
configParamsSection.style.display = 'block';
|
||||||
|
|
||||||
|
// 自动生成Safe Class Name
|
||||||
|
const registryType = document.querySelector(
|
||||||
|
'input[name="registry-type"]:checked'
|
||||||
|
).value;
|
||||||
|
const suffix = registryType === 'device' ? '_device' : '';
|
||||||
|
const safeClassName = this.value.toLowerCase() + suffix;
|
||||||
|
safeClassNameInput.value = safeClassName;
|
||||||
|
|
||||||
addLog(`选择了类: ${this.value}`, 'info');
|
addLog(`选择了类: ${this.value}`, 'info');
|
||||||
|
addLog(`自动生成Safe Class Name: ${safeClassName}`, 'info');
|
||||||
} else {
|
} else {
|
||||||
importBtn.disabled = true;
|
importBtn.disabled = true;
|
||||||
|
configParamsSection.style.display = 'none';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -992,6 +1146,17 @@
|
|||||||
'input[name="registry-type"]:checked'
|
'input[name="registry-type"]:checked'
|
||||||
).value;
|
).value;
|
||||||
|
|
||||||
|
// 获取用户输入的所有配置参数
|
||||||
|
const description = document
|
||||||
|
.getElementById('description-input')
|
||||||
|
.value.trim();
|
||||||
|
const safeClassName = document
|
||||||
|
.getElementById('safe-class-name')
|
||||||
|
.value.trim();
|
||||||
|
const iconInput = document.getElementById('icon-input').value.trim();
|
||||||
|
const modulePrefix = document.getElementById('module-prefix').value.trim();
|
||||||
|
const handlesConfig = getHandlesConfig();
|
||||||
|
|
||||||
updateConnectionStatus('processing', '正在生成注册表...');
|
updateConnectionStatus('processing', '正在生成注册表...');
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
@@ -1001,6 +1166,11 @@
|
|||||||
registry_type: registryType,
|
registry_type: registryType,
|
||||||
class_name: className,
|
class_name: className,
|
||||||
module_name: null, // 模块名自动生成
|
module_name: null, // 模块名自动生成
|
||||||
|
description: description,
|
||||||
|
safe_class_name: safeClassName,
|
||||||
|
icon: iconInput,
|
||||||
|
module_prefix: modulePrefix,
|
||||||
|
handles: handlesConfig,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1010,11 +1180,27 @@
|
|||||||
addLog(`选择的类: ${className}`, 'info');
|
addLog(`选择的类: ${className}`, 'info');
|
||||||
|
|
||||||
// 显示文件信息
|
// 显示文件信息
|
||||||
showFileInfo(selectedFilePath, registryType, className);
|
showFileInfo(
|
||||||
|
selectedFilePath,
|
||||||
|
registryType,
|
||||||
|
className,
|
||||||
|
safeClassName,
|
||||||
|
iconInput,
|
||||||
|
modulePrefix,
|
||||||
|
handlesConfig
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示文件信息
|
// 显示文件信息
|
||||||
function showFileInfo(filePath, registryType, className) {
|
function showFileInfo(
|
||||||
|
filePath,
|
||||||
|
registryType,
|
||||||
|
className,
|
||||||
|
safeClassName,
|
||||||
|
icon,
|
||||||
|
modulePrefix,
|
||||||
|
handles
|
||||||
|
) {
|
||||||
const fileInfo = document.getElementById('file-info');
|
const fileInfo = document.getElementById('file-info');
|
||||||
const fileDetails = document.getElementById('file-details');
|
const fileDetails = document.getElementById('file-details');
|
||||||
|
|
||||||
@@ -1022,8 +1208,24 @@
|
|||||||
<div><strong>文件路径:</strong> ${filePath}</div>
|
<div><strong>文件路径:</strong> ${filePath}</div>
|
||||||
<div><strong>注册表类型:</strong> ${registryType}</div>
|
<div><strong>注册表类型:</strong> ${registryType}</div>
|
||||||
<div><strong>选择的类:</strong> ${className}</div>
|
<div><strong>选择的类:</strong> ${className}</div>
|
||||||
|
<div><strong>Safe Class Name:</strong> ${
|
||||||
|
safeClassName || '自动生成'
|
||||||
|
}</div>
|
||||||
|
<div><strong>Module Prefix:</strong> ${modulePrefix || '无'}</div>
|
||||||
|
<div><strong>图标:</strong> ${icon || '无'}</div>
|
||||||
|
<div><strong>Handles 数量:</strong> ${handles.length}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
if (handles.length > 0) {
|
||||||
|
details += '<div><strong>Handles 配置:</strong><ul>';
|
||||||
|
handles.forEach((handle, index) => {
|
||||||
|
details += `<li>${index + 1}. ${handle.data_key} (${
|
||||||
|
handle.io_type
|
||||||
|
})</li>`;
|
||||||
|
});
|
||||||
|
details += '</ul></div>';
|
||||||
|
}
|
||||||
|
|
||||||
fileDetails.innerHTML = details;
|
fileDetails.innerHTML = details;
|
||||||
fileInfo.style.display = 'block';
|
fileInfo.style.display = 'block';
|
||||||
}
|
}
|
||||||
@@ -1051,8 +1253,20 @@
|
|||||||
const importBtn = document.getElementById('import-btn');
|
const importBtn = document.getElementById('import-btn');
|
||||||
importBtn.disabled = true;
|
importBtn.disabled = true;
|
||||||
|
|
||||||
// 隐藏类选择
|
// 隐藏类选择和配置参数区域
|
||||||
document.getElementById('class-selection-group').style.display = 'none';
|
document.getElementById('class-selection-group').style.display = 'none';
|
||||||
|
document.getElementById('config-params-section').style.display = 'none';
|
||||||
|
|
||||||
|
// 清空所有输入框
|
||||||
|
document.getElementById('description-input').value = '';
|
||||||
|
document.getElementById('safe-class-name').value = '';
|
||||||
|
document.getElementById('icon-input').value = '';
|
||||||
|
document.getElementById('module-prefix').value = '';
|
||||||
|
|
||||||
|
// 清空handles配置
|
||||||
|
const handlesContainer = document.getElementById('handles-container');
|
||||||
|
handlesContainer.innerHTML = '';
|
||||||
|
handleCounter = 0;
|
||||||
|
|
||||||
// 根据当前注册表类型设置默认选项文字
|
// 根据当前注册表类型设置默认选项文字
|
||||||
const registryType = document.querySelector(
|
const registryType = document.querySelector(
|
||||||
@@ -1081,5 +1295,117 @@
|
|||||||
|
|
||||||
addLog('已重置所有设置', 'info');
|
addLog('已重置所有设置', 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加Handle项目
|
||||||
|
function addHandleItem() {
|
||||||
|
handleCounter++;
|
||||||
|
const handlesContainer = document.getElementById('handles-container');
|
||||||
|
|
||||||
|
const handleItem = document.createElement('div');
|
||||||
|
handleItem.className = 'handle-item';
|
||||||
|
handleItem.id = `handle-item-${handleCounter}`;
|
||||||
|
|
||||||
|
handleItem.innerHTML = `
|
||||||
|
<div class="handle-item-header">
|
||||||
|
<span class="handle-item-title">Handle ${handleCounter}</span>
|
||||||
|
<button type="button" class="remove-handle-btn" onclick="removeHandleItem(${handleCounter})" title="删除">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="handle-form-row">
|
||||||
|
<div class="handle-form-group">
|
||||||
|
<label>Data Key</label>
|
||||||
|
<input type="text" placeholder="例如: fluid_port_1" data-field="data_key">
|
||||||
|
</div>
|
||||||
|
<div class="handle-form-group">
|
||||||
|
<label>Handler Key</label>
|
||||||
|
<input type="text" placeholder="例如: port_1" data-field="handler_key">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="handle-form-row">
|
||||||
|
<div class="handle-form-group">
|
||||||
|
<label>Data Source</label>
|
||||||
|
<input type="text" placeholder="例如: executor、handle" data-field="data_source">
|
||||||
|
</div>
|
||||||
|
<div class="handle-form-group">
|
||||||
|
<label>Data Type</label>
|
||||||
|
<input type="text" placeholder="例如: fluid" data-field="data_type">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="handle-form-row">
|
||||||
|
<div class="handle-form-group">
|
||||||
|
<label>IO Type</label>
|
||||||
|
<select data-field="io_type">
|
||||||
|
<option value="">请选择</option>
|
||||||
|
<option value="source">source(输出,右侧)</option>
|
||||||
|
<option value="target">target(输入,左侧)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="handle-form-group">
|
||||||
|
<label>Side</label>
|
||||||
|
<select data-field="side">
|
||||||
|
<option value="">请选择</option>
|
||||||
|
<option value="NORTH">NORTH(上)</option>
|
||||||
|
<option value="SOUTH">SOUTH(下)</option>
|
||||||
|
<option value="EAST">EAST(右)</option>
|
||||||
|
<option value="WEST">WEST(左)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="handle-form-row">
|
||||||
|
<div class="handle-form-group">
|
||||||
|
<label>Label</label>
|
||||||
|
<input type="text" placeholder="显示名称,例如: port_1" data-field="label">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="handle-form-row full-width">
|
||||||
|
<div class="handle-form-group">
|
||||||
|
<label>Description</label>
|
||||||
|
<input type="text" placeholder="例如: 八通阀门端口1" data-field="description">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
handlesContainer.appendChild(handleItem);
|
||||||
|
addLog(`添加了 Handle ${handleCounter}`, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除Handle项目
|
||||||
|
function removeHandleItem(id) {
|
||||||
|
const handleItem = document.getElementById(`handle-item-${id}`);
|
||||||
|
if (handleItem) {
|
||||||
|
handleItem.remove();
|
||||||
|
addLog(`删除了 Handle ${id}`, 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有Handles配置
|
||||||
|
function getHandlesConfig() {
|
||||||
|
const handles = [];
|
||||||
|
const handleItems = document.querySelectorAll('.handle-item');
|
||||||
|
|
||||||
|
handleItems.forEach((item) => {
|
||||||
|
const handleData = {};
|
||||||
|
const inputs = item.querySelectorAll('[data-field]');
|
||||||
|
|
||||||
|
inputs.forEach((input) => {
|
||||||
|
const field = input.getAttribute('data-field');
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
handleData[field] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 只有当至少有data_key时才添加
|
||||||
|
if (handleData.data_key) {
|
||||||
|
handles.push(handleData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return handles;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ block header %}UniLab System Status{% endblock %} {% block top_info %}
|
|||||||
href="/status"
|
href="/status"
|
||||||
class="nav-tab"
|
class="nav-tab"
|
||||||
style="background-color: #4caf50; color: white"
|
style="background-color: #4caf50; color: white"
|
||||||
|
target="_blank"
|
||||||
>状态</a
|
>状态</a
|
||||||
>
|
>
|
||||||
<a href="/registry-editor" class="nav-tab" target="_blank">注册表编辑</a>
|
<a href="/registry-editor" class="nav-tab" target="_blank">注册表编辑</a>
|
||||||
@@ -1720,9 +1721,10 @@ ros2 action send_goal {{ action_info.action_path }} {{ action_info.type_name_con
|
|||||||
const navTabs = document.querySelectorAll('.nav-tab');
|
const navTabs = document.querySelectorAll('.nav-tab');
|
||||||
navTabs.forEach((tab) => {
|
navTabs.forEach((tab) => {
|
||||||
tab.addEventListener('click', function (e) {
|
tab.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
|
||||||
const targetId = this.getAttribute('href');
|
const targetId = this.getAttribute('href');
|
||||||
|
// 只对内部锚链接(以#开头)应用滚动行为
|
||||||
if (targetId && targetId.startsWith('#')) {
|
if (targetId && targetId.startsWith('#')) {
|
||||||
|
e.preventDefault();
|
||||||
const targetElement = document.querySelector(targetId);
|
const targetElement = document.querySelector(targetId);
|
||||||
if (targetElement) {
|
if (targetElement) {
|
||||||
targetElement.scrollIntoView({
|
targetElement.scrollIntoView({
|
||||||
@@ -1730,6 +1732,7 @@ ros2 action send_goal {{ action_info.action_path }} {{ action_info.type_name_con
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 对于外部链接(如 /status, /registry-editor),不调用preventDefault(),让浏览器处理默认行为
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ class Registry:
|
|||||||
}
|
}
|
||||||
device_config["file_path"] = str(file.absolute()).replace("\\", "/")
|
device_config["file_path"] = str(file.absolute()).replace("\\", "/")
|
||||||
device_config["registry_type"] = "device"
|
device_config["registry_type"] = "device"
|
||||||
logger.trace(
|
logger.trace( # type: ignore
|
||||||
f"[UniLab Registry] Device-{current_device_number} File-{i+1}/{len(files)} Add {device_id} "
|
f"[UniLab Registry] Device-{current_device_number} File-{i+1}/{len(files)} Add {device_id} "
|
||||||
+ f"[{data[device_id].get('name', '未命名设备')}]"
|
+ f"[{data[device_id].get('name', '未命名设备')}]"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user