From 4e8741d1a26e8abe4e3c68e3fe6659e04e18701a Mon Sep 17 00:00:00 2001 From: wess09 Date: Wed, 13 May 2026 18:24:47 +0800 Subject: [PATCH] =?UTF-8?q?add=20=E5=AF=BC=E5=85=A5=E6=97=A7=E6=95=B0?= =?UTF-8?q?=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/webui/api.py | 85 +++++++++++++++++++++++++++++++++ module/webui/app.py | 112 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) diff --git a/module/webui/api.py b/module/webui/api.py index 60ec62235..5b15be786 100644 --- a/module/webui/api.py +++ b/module/webui/api.py @@ -321,11 +321,96 @@ async def api_notify_stream(request): ) +async def api_import_legacy_upload(request): + """ + 接收浏览器上传的旧 ALAS 文件夹内容,写入本项目对应位置。 + 前端使用 webkitdirectory 选择文件夹后上传。 + """ + from pathlib import Path + + try: + form = await request.form() + current_root = Path(os.getcwd()).resolve() + + result = { + "config": 0, + "db": 0, + "cl1": 0, + "azurstat": 0, + "skipped": 0, + "errors": 0, + } + + for file in form.getlist('file'): + if not hasattr(file, 'filename') or not file.filename: + continue + + relative_path = file.filename.replace("\\", "/") + filename = Path(relative_path).name + + # 提取根级相对路径:跳过前导 / 和可能的文件夹名前缀 + parts = relative_path.split("/") + # parts[0]='' (前导 /), parts[1]可能是文件夹名或 config/log + start_idx = 1 + if len(parts) >= 3 and parts[1] not in ("config", "log"): + start_idx = 2 # parts[1] 是文件夹名,跳过 + sub_path = "/".join(parts[start_idx:]) + + # 判断是否需要处理该文件 + rel_target = None + + # 只匹配 根目录/config/ 下的 .json/.db(排除 template*) + if sub_path.startswith("config/"): + ext = Path(filename).suffix.lower() + if ext in (".json", ".db") and not filename.lower().startswith("template"): + rel_target = sub_path + + # 只匹配 根目录/log/cl1/ 下的所有文件 + if sub_path.startswith("log/cl1/"): + rel_target = sub_path + + # 只匹配 根目录/log/azurstat_meowofficer_farming.csv + if sub_path == "log/azurstat_meowofficer_farming.csv": + rel_target = sub_path + + if rel_target is None: + result["skipped"] += 1 + continue + + target = current_root / rel_target + + try: + target.parent.mkdir(parents=True, exist_ok=True) + content = await file.read() + target.write_bytes(content) + + if rel_target.startswith("config/"): + ext = Path(filename).suffix.lower() + if ext == ".json": + result["config"] += 1 + else: + result["db"] += 1 + elif rel_target.startswith("log/cl1/"): + result["cl1"] += 1 + elif "azurstat" in rel_target: + result["azurstat"] += 1 + except Exception as e: + logger.error(f"写入失败 {target}: {e}") + result["errors"] += 1 + + logger.info(f"导入完成: {result}") + return JSONResponse({"success": True, "data": result}) + except Exception as e: + logger.error(f"导入API错误: {e}") + return JSONResponse({"success": False, "error": str(e)}, status_code=500) + + api_routes = [ Route("/api/cl1_stats", api_cl1_stats), Route("/api/ap_timeline", api_ap_timeline), Route("/api/notify", api_notify, methods=["POST"]), Route("/api/notify_stream", api_notify_stream), + Route("/api/import_legacy_upload", api_import_legacy_upload, methods=["POST"]), Route("/obs", serve_obs_overlay), WebSocketRoute("/ws/live_screenshot", ws_live_screenshot), ] diff --git a/module/webui/app.py b/module/webui/app.py index e5ec0d17f..1074040f8 100644 --- a/module/webui/app.py +++ b/module/webui/app.py @@ -2563,6 +2563,12 @@ class AlasGUI(Frame): color="menu", ).style(f"--menu-HomePage--") + put_button( + label="导入旧数据", + onclick=self.ui_import_legacy, + color="menu", + ).style(f"--menu-Import--") + # put_button( # label=t("Gui.MenuDevelop.Translate"), # onclick=self.dev_translate, @@ -3050,6 +3056,112 @@ class AlasGUI(Frame): put() + @use_scope("content", clear=True) + def ui_import_legacy(self) -> None: + """Develop 菜单:导入旧 AzurPilot 数据""" + self.init_menu(name="Import") + self.set_title("导入旧数据") + from pywebio.output import put_markdown, put_html, put_buttons, put_scope + import json + + # 检查上一轮导入的结果(通过 sessionStorage 跨刷新传递) + try: + raw = eval_js("(function(){var r=sessionStorage.getItem('import_msg');if(r){sessionStorage.removeItem('import_msg');return r;}return null;})()") + if raw: + info = json.loads(raw) + if info.get("ok"): + d = info["data"] + parts = [] + toast("导入成功", color="success", duration=10) + else: + toast("导入失败:" + info.get("error", "未知错误"), color="error", duration=10) + except Exception: + pass + + def import_legacy_upload(): + toast("请在弹出的窗口中选择旧 AzurPilot/ALAS 根目录", color="info", duration=0) + run_js(""" + (function(){ + var input = document.createElement('input'); + input.type = 'file'; + input.setAttribute('webkitdirectory', ''); + input.setAttribute('multiple', ''); + input.style.display = 'none'; + + input.addEventListener('change', async function(e) { + var files = e.target.files; + document.body.removeChild(input); + if (!files || files.length === 0) return; + + var formData = new FormData(); + var matched = 0, skipped = 0, total = files.length; + + for (var i = 0; i < total; i++) { + var file = files[i]; + var relPath = '/' + file.webkitRelativePath.replace(/\\\\/g, '/'); + var name = relPath.split('/').pop().toLowerCase(); + + var pp = relPath.split('/'); + var si = 1; + if (pp.length >= 3 && pp[1] !== 'config' && pp[1] !== 'log') si = 2; + var subPath = pp.slice(si).join('/'); + + var ok = false; + if (subPath.startsWith('config/')) { + if ((name.endsWith('.json') || name.endsWith('.db')) && !name.startsWith('template')) ok = true; + } else if (subPath.startsWith('log/cl1/')) { + ok = true; + } else if (subPath === 'log/azurstat_meowofficer_farming.csv') { + ok = true; + } + + if (!ok) { skipped++; continue; } + matched++; + formData.append('file', file, relPath); + } + + if (matched === 0) { + sessionStorage.setItem('import_msg', JSON.stringify({ok:false, error:'所选文件夹中没有找到 config/ 或 log/cl1/ 下的匹配文件'})); + location.reload(); + return; + } + + try { + var resp = await fetch('/api/import_legacy_upload', { method: 'POST', body: formData }); + var result = await resp.json(); + if (result.success) { + result.data.total = total; + sessionStorage.setItem('import_msg', JSON.stringify({ok:true, data:result.data, total:total})); + } else { + sessionStorage.setItem('import_msg', JSON.stringify({ok:false, error:result.error || '未知错误'})); + } + } catch (err) { + sessionStorage.setItem('import_msg', JSON.stringify({ok:false, error:'上传请求失败: ' + err.message})); + } + location.reload(); + }); + + document.body.appendChild(input); + input.click(); + })(); + """) + + put_html(build_title_block("导入旧 AzurPilot/ALAS 数据", margin_top=12, margin_bottom=8)) + put_markdown( + "选择旧 AzurPilot/ALAS 根目录后,自动将以下数据导入到当前项目:\n\n" + "**配置 数据文件等**\n\n" + "> 同名文件将被覆盖,建议先备份当前项目。" + ) + + put_scope("import_btn") + with use_scope("import_btn"): + put_buttons( + [ + {"label": "选择旧 AzurPilot/ALAS 文件夹", "value": "upload", "color": "primary"}, + ], + onclick=[import_legacy_upload], + ) + def show(self) -> None: self._show() self.load_home = True