From 5014376b81fbc795dd3515e4f0701b3c6f8367c5 Mon Sep 17 00:00:00 2001 From: shenlei Date: Wed, 29 Jul 2026 15:14:08 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=9A=AE=E8=82=A4?= =?UTF-8?q?=E5=8C=85=E5=B0=B1=E5=9C=B0=E6=9B=B4=E6=96=B0=E4=B8=8E=E6=96=B0?= =?UTF-8?q?=E5=BB=BA=E5=8D=B3=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增皮肤包更新接口,覆盖内容保持文件名不变,避免 APP 配置的主题引用失效 - 上传新皮肤后自动为空主题目录的证书选中该皮肤 - 新建 APP 保存后不关弹窗并回填 upload_key,可直接上传皮肤 Co-Authored-By: Claude Opus 4.8 --- backend/routers/config.py | 29 ++++++++++++++++- frontend/src/views/ConfigView.vue | 52 +++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/backend/routers/config.py b/backend/routers/config.py index 600eef6..24a4c01 100644 --- a/backend/routers/config.py +++ b/backend/routers/config.py @@ -316,7 +316,7 @@ async def create_app(app: dict): apps[new_id] = app config["apps"] = apps save_config(config) - return {"id": new_id, "message": "App 已创建"} + return {"id": new_id, "upload_key": app["upload_key"], "message": "App 已创建"} @router.put("/apps/{app_id}") @@ -410,6 +410,33 @@ async def upload_skin(upload_key: str, file: UploadFile = File(...)): return {"message": "皮肤包上传成功", "name": file.filename} +@router.put("/skins/{upload_key}/{skin_name}") +async def replace_skin(upload_key: str, skin_name: str, file: UploadFile = File(...)): + """替换已有皮肤包内容,保持原文件名不变。 + + APP 配置的 theme 通过文件名(去掉 .zip)引用皮肤包,若上传新文件名会新增一个皮肤而非更新, + 导致原引用失效。此接口就地覆盖指定皮肤,确保被引用的皮肤可以更新。 + """ + if not file.filename or not file.filename.lower().endswith(".zip"): + raise HTTPException(status_code=400, detail="仅支持 .zip 格式") + + config = load_config() + app_id, _ = _find_app_by_upload_key(config, upload_key) + if not app_id: + raise HTTPException(status_code=404, detail="App 不存在") + + app_skins_dir = SKINS_DIR / upload_key + skin_path = app_skins_dir / skin_name + if not skin_path.exists(): + raise HTTPException(status_code=404, detail="皮肤包不存在") + + content = await file.read() + with open(skin_path, "wb") as f: + f.write(content) + + return {"message": "皮肤包已更新", "name": skin_name} + + @router.delete("/skins/{upload_key}/{skin_name}") async def delete_skin(upload_key: str, skin_name: str): """删除指定皮肤包""" diff --git a/frontend/src/views/ConfigView.vue b/frontend/src/views/ConfigView.vue index 4158bc3..d2ceabc 100644 --- a/frontend/src/views/ConfigView.vue +++ b/frontend/src/views/ConfigView.vue @@ -519,6 +519,10 @@
{{ skin.name }} {{ formatSize(skin.size) }} +
@@ -880,6 +884,12 @@ const uploadSkin = async (event) => { throw new Error(err.detail || '上传失败') } await loadSkins() + // 若某证书的主题目录还是空的,自动选中刚上传的皮肤 + const skinTheme = file.name.replace(/\.zip$/i, '') + const certs = appForm.value.certificates || {} + Object.keys(certs).forEach(certType => { + if (!certs[certType].theme) certs[certType].theme = skinTheme + }) event.target.value = '' } catch (e) { alert('上传失败: ' + e.message) @@ -888,6 +898,32 @@ const uploadSkin = async (event) => { } } +const updateSkin = async (skinName, event) => { + const file = event.target.files[0] + if (!file) return + const uploadKey = appForm.value.upload_key + if (!uploadKey) { event.target.value = ''; return } + skinUploading.value = true + try { + const formData = new FormData() + formData.append('file', file) + const res = await authFetch(`/api/config/skins/${uploadKey}/${skinName}`, { + method: 'PUT', + body: formData, + }) + if (!res.ok) { + const err = await res.json() + throw new Error(err.detail || '更新失败') + } + await loadSkins() + } catch (e) { + alert('更新失败: ' + e.message) + } finally { + event.target.value = '' + skinUploading.value = false + } +} + const deleteSkin = async (skinName) => { if (!confirm(`确定删除皮肤包 ${skinName}?`)) return const uploadKey = appForm.value.upload_key @@ -948,9 +984,18 @@ const saveApp = async () => { const err = await res.json().catch(() => ({ detail: '保存失败' })) throw new Error(err.detail || '保存失败') } - showAppModal.value = false await loadData() - alert('保存成功') + if (!editingAppId.value) { + // 新建成功:不关弹窗,转为编辑态并回填 upload_key,让皮肤包上传区立即可用 + const data = await res.json().catch(() => ({})) + editingAppId.value = data.id || editingAppId.value + if (data.upload_key) appForm.value.upload_key = data.upload_key + await loadSkins() + alert('保存成功,现在可以上传皮肤包了') + } else { + showAppModal.value = false + alert('保存成功') + } } catch (e) { alert('保存失败: ' + e.message) } @@ -1160,6 +1205,9 @@ const formatJson = () => { .btn-icon { width: 24px; height: 24px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; line-height: 1; display: flex; align-items: center; justify-content: center; } .btn-danger-icon { background: #fff1f0; color: #ff4d4f; } .btn-danger-icon:hover { background: #ff4d4f; color: white; } +.skin-update-btn { font-size: 12px; color: #1890ff; cursor: pointer; padding: 2px 8px; border: 1px solid #91d5ff; border-radius: 4px; background: #e6f7ff; line-height: 1.4; } +.skin-update-btn:hover { background: #1890ff; color: white; } +.skin-update-btn.disabled { opacity: 0.5; pointer-events: none; } .btn-outline { background: white; border: 1px solid #1890ff; color: #1890ff; cursor: pointer; padding: 6px 16px; border-radius: 6px; font-size: 13px; display: inline-block; } .btn-outline:hover { background: #e6f7ff; } .btn-outline.disabled { opacity: 0.5; cursor: not-allowed; }