mirror of
https://github.com/ringhyacinth/Star-Office-UI
synced 2026-04-21 13:27:19 +00:00
Merge pull request #24 from simonxxooxxoo/feat/office-art-rebuild
feat: office rebuild v2 - home favorites, rollback UX, asset visibility polish
This commit is contained in:
commit
21402e7ce6
3 changed files with 588 additions and 22 deletions
272
backend/app.py
272
backend/app.py
|
|
@ -38,6 +38,9 @@ ROOM_REFERENCE_IMAGE = (
|
|||
else os.path.join(ROOT_DIR, "assets", "room-reference.png")
|
||||
)
|
||||
BG_HISTORY_DIR = os.path.join(ROOT_DIR, "assets", "bg-history")
|
||||
HOME_FAVORITES_DIR = os.path.join(ROOT_DIR, "assets", "home-favorites")
|
||||
HOME_FAVORITES_INDEX_FILE = os.path.join(HOME_FAVORITES_DIR, "index.json")
|
||||
HOME_FAVORITES_MAX = 30
|
||||
ASSET_POSITIONS_FILE = os.path.join(ROOT_DIR, "asset-positions.json")
|
||||
ASSET_DEFAULTS_FILE = os.path.join(ROOT_DIR, "asset-defaults.json")
|
||||
RUNTIME_CONFIG_FILE = os.path.join(ROOT_DIR, "runtime-config.json")
|
||||
|
|
@ -369,7 +372,7 @@ def save_asset_defaults(data):
|
|||
def load_runtime_config():
|
||||
base = {
|
||||
"gemini_api_key": os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY") or "",
|
||||
"gemini_model": os.getenv("GEMINI_MODEL") or "gemini-3.1-flash-image-preview"
|
||||
"gemini_model": os.getenv("GEMINI_MODEL") or "nanobanana-pro"
|
||||
}
|
||||
if os.path.exists(RUNTIME_CONFIG_FILE):
|
||||
try:
|
||||
|
|
@ -389,6 +392,31 @@ def save_runtime_config(data):
|
|||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def _ensure_home_favorites_index():
|
||||
os.makedirs(HOME_FAVORITES_DIR, exist_ok=True)
|
||||
if not os.path.exists(HOME_FAVORITES_INDEX_FILE):
|
||||
with open(HOME_FAVORITES_INDEX_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump({"items": []}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def _load_home_favorites_index():
|
||||
_ensure_home_favorites_index()
|
||||
try:
|
||||
with open(HOME_FAVORITES_INDEX_FILE, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, dict) and isinstance(data.get("items"), list):
|
||||
return data
|
||||
except Exception:
|
||||
pass
|
||||
return {"items": []}
|
||||
|
||||
|
||||
def _save_home_favorites_index(data):
|
||||
_ensure_home_favorites_index()
|
||||
with open(HOME_FAVORITES_INDEX_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def load_join_keys():
|
||||
if os.path.exists(JOIN_KEYS_FILE):
|
||||
try:
|
||||
|
|
@ -1322,6 +1350,163 @@ def assets_restore_reference_background():
|
|||
return jsonify({"ok": False, "msg": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/assets/restore-last-generated-background", methods=["POST"])
|
||||
def assets_restore_last_generated_background():
|
||||
"""Restore office_bg_small.webp from latest bg-history snapshot."""
|
||||
guard = _require_asset_editor_auth()
|
||||
if guard:
|
||||
return guard
|
||||
try:
|
||||
target = FRONTEND_PATH / "office_bg_small.webp"
|
||||
if not target.exists():
|
||||
return jsonify({"ok": False, "msg": "office_bg_small.webp 不存在"}), 404
|
||||
|
||||
if not os.path.isdir(BG_HISTORY_DIR):
|
||||
return jsonify({"ok": False, "msg": "暂无历史底图"}), 404
|
||||
|
||||
files = [
|
||||
os.path.join(BG_HISTORY_DIR, x)
|
||||
for x in os.listdir(BG_HISTORY_DIR)
|
||||
if x.startswith("office_bg_small-") and x.endswith(".webp")
|
||||
]
|
||||
if not files:
|
||||
return jsonify({"ok": False, "msg": "暂无历史底图"}), 404
|
||||
|
||||
latest = max(files, key=lambda p: os.path.getmtime(p))
|
||||
|
||||
bak = target.with_suffix(target.suffix + ".bak")
|
||||
shutil.copy2(target, bak)
|
||||
shutil.copy2(latest, target)
|
||||
|
||||
st = target.stat()
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"path": "office_bg_small.webp",
|
||||
"size": st.st_size,
|
||||
"from": os.path.relpath(latest, ROOT_DIR),
|
||||
"msg": "已回退到最近一次生成底图",
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/assets/home-favorites/list", methods=["GET"])
|
||||
def assets_home_favorites_list():
|
||||
guard = _require_asset_editor_auth()
|
||||
if guard:
|
||||
return guard
|
||||
try:
|
||||
data = _load_home_favorites_index()
|
||||
items = data.get("items") or []
|
||||
out = []
|
||||
for it in items:
|
||||
rel = (it.get("path") or "").strip()
|
||||
if not rel:
|
||||
continue
|
||||
abs_path = os.path.join(ROOT_DIR, rel)
|
||||
if not os.path.exists(abs_path):
|
||||
continue
|
||||
fn = os.path.basename(rel)
|
||||
out.append({
|
||||
"id": it.get("id"),
|
||||
"path": rel,
|
||||
"url": f"/assets/home-favorites/file/{fn}",
|
||||
"thumb_url": f"/assets/home-favorites/file/{fn}",
|
||||
"created_at": it.get("created_at") or "",
|
||||
})
|
||||
out.sort(key=lambda x: x.get("created_at") or "", reverse=True)
|
||||
return jsonify({"ok": True, "items": out})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/assets/home-favorites/file/<path:filename>", methods=["GET"])
|
||||
def assets_home_favorites_file(filename):
|
||||
guard = _require_asset_editor_auth()
|
||||
if guard:
|
||||
return guard
|
||||
return send_from_directory(HOME_FAVORITES_DIR, filename)
|
||||
|
||||
|
||||
@app.route("/assets/home-favorites/save-current", methods=["POST"])
|
||||
def assets_home_favorites_save_current():
|
||||
guard = _require_asset_editor_auth()
|
||||
if guard:
|
||||
return guard
|
||||
try:
|
||||
src = FRONTEND_PATH / "office_bg_small.webp"
|
||||
if not src.exists():
|
||||
return jsonify({"ok": False, "msg": "office_bg_small.webp 不存在"}), 404
|
||||
|
||||
_ensure_home_favorites_index()
|
||||
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
item_id = f"home-{ts}"
|
||||
fn = f"{item_id}.webp"
|
||||
dst = os.path.join(HOME_FAVORITES_DIR, fn)
|
||||
shutil.copy2(str(src), dst)
|
||||
|
||||
idx = _load_home_favorites_index()
|
||||
items = idx.get("items") or []
|
||||
items.insert(0, {
|
||||
"id": item_id,
|
||||
"path": os.path.relpath(dst, ROOT_DIR),
|
||||
"created_at": datetime.now().isoformat(timespec="seconds"),
|
||||
})
|
||||
|
||||
# 控制收藏数量上限,清理最旧项
|
||||
if len(items) > HOME_FAVORITES_MAX:
|
||||
extra = items[HOME_FAVORITES_MAX:]
|
||||
items = items[:HOME_FAVORITES_MAX]
|
||||
for it in extra:
|
||||
try:
|
||||
p = os.path.join(ROOT_DIR, it.get("path") or "")
|
||||
if os.path.exists(p):
|
||||
os.remove(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
idx["items"] = items
|
||||
_save_home_favorites_index(idx)
|
||||
return jsonify({"ok": True, "id": item_id, "path": os.path.relpath(dst, ROOT_DIR), "msg": "已收藏当前地图"})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/assets/home-favorites/apply", methods=["POST"])
|
||||
def assets_home_favorites_apply():
|
||||
guard = _require_asset_editor_auth()
|
||||
if guard:
|
||||
return guard
|
||||
try:
|
||||
data = request.get_json(silent=True) or {}
|
||||
item_id = (data.get("id") or "").strip()
|
||||
if not item_id:
|
||||
return jsonify({"ok": False, "msg": "缺少 id"}), 400
|
||||
|
||||
idx = _load_home_favorites_index()
|
||||
items = idx.get("items") or []
|
||||
hit = next((x for x in items if (x.get("id") or "") == item_id), None)
|
||||
if not hit:
|
||||
return jsonify({"ok": False, "msg": "收藏项不存在"}), 404
|
||||
|
||||
src = os.path.join(ROOT_DIR, hit.get("path") or "")
|
||||
if not os.path.exists(src):
|
||||
return jsonify({"ok": False, "msg": "收藏文件不存在"}), 404
|
||||
|
||||
target = FRONTEND_PATH / "office_bg_small.webp"
|
||||
if not target.exists():
|
||||
return jsonify({"ok": False, "msg": "office_bg_small.webp 不存在"}), 404
|
||||
|
||||
bak = target.with_suffix(target.suffix + ".bak")
|
||||
shutil.copy2(str(target), str(bak))
|
||||
shutil.copy2(src, str(target))
|
||||
|
||||
st = target.stat()
|
||||
return jsonify({"ok": True, "path": "office_bg_small.webp", "size": st.st_size, "from": hit.get("path"), "msg": "已应用收藏地图"})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/assets/auth", methods=["POST"])
|
||||
def assets_auth():
|
||||
try:
|
||||
|
|
@ -1337,7 +1522,11 @@ def assets_auth():
|
|||
|
||||
@app.route("/assets/auth/status", methods=["GET"])
|
||||
def assets_auth_status():
|
||||
return jsonify({"ok": True, "authed": _is_asset_editor_authed()})
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"authed": _is_asset_editor_authed(),
|
||||
"drawer_default_pass": ASSET_DRAWER_PASS_DEFAULT == "1234",
|
||||
})
|
||||
|
||||
|
||||
@app.route("/assets/positions", methods=["GET"])
|
||||
|
|
@ -1433,7 +1622,7 @@ def gemini_config_get():
|
|||
"ok": True,
|
||||
"has_api_key": bool(key),
|
||||
"api_key_masked": masked,
|
||||
"gemini_model": cfg.get("gemini_model") or "gemini-3.1-flash-image-preview",
|
||||
"gemini_model": cfg.get("gemini_model") or "nanobanana-pro",
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": str(e)}), 500
|
||||
|
|
@ -1447,7 +1636,7 @@ def gemini_config_set():
|
|||
try:
|
||||
data = request.get_json(silent=True) or {}
|
||||
api_key = (data.get("api_key") or "").strip()
|
||||
model = (data.get("model") or "").strip() or "gemini-3.1-flash-image-preview"
|
||||
model = (data.get("model") or "").strip() or "nanobanana-pro"
|
||||
payload = {"gemini_model": model}
|
||||
if api_key:
|
||||
payload["gemini_api_key"] = api_key
|
||||
|
|
@ -1457,6 +1646,72 @@ def gemini_config_set():
|
|||
return jsonify({"ok": False, "msg": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/assets/restore-default", methods=["POST"])
|
||||
def assets_restore_default():
|
||||
guard = _require_asset_editor_auth()
|
||||
if guard:
|
||||
return guard
|
||||
try:
|
||||
data = request.get_json(silent=True) or {}
|
||||
rel_path = (data.get("path") or "").strip().lstrip("/")
|
||||
if not rel_path:
|
||||
return jsonify({"ok": False, "msg": "缺少 path"}), 400
|
||||
|
||||
target = (FRONTEND_PATH / rel_path).resolve()
|
||||
try:
|
||||
target.relative_to(FRONTEND_PATH.resolve())
|
||||
except Exception:
|
||||
return jsonify({"ok": False, "msg": "非法 path"}), 400
|
||||
|
||||
if not target.exists():
|
||||
return jsonify({"ok": False, "msg": "目标文件不存在"}), 404
|
||||
|
||||
root, ext = os.path.splitext(str(target))
|
||||
default_path = root + ext + ".default"
|
||||
if not os.path.exists(default_path):
|
||||
return jsonify({"ok": False, "msg": "未找到默认资产快照"}), 404
|
||||
|
||||
# 回滚前保留上一版
|
||||
bak = str(target) + ".bak"
|
||||
if os.path.exists(str(target)):
|
||||
shutil.copy2(str(target), bak)
|
||||
|
||||
shutil.copy2(default_path, str(target))
|
||||
st = os.stat(str(target))
|
||||
return jsonify({"ok": True, "path": rel_path, "size": st.st_size, "msg": "已重置为默认资产"})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/assets/restore-prev", methods=["POST"])
|
||||
def assets_restore_prev():
|
||||
guard = _require_asset_editor_auth()
|
||||
if guard:
|
||||
return guard
|
||||
try:
|
||||
data = request.get_json(silent=True) or {}
|
||||
rel_path = (data.get("path") or "").strip().lstrip("/")
|
||||
if not rel_path:
|
||||
return jsonify({"ok": False, "msg": "缺少 path"}), 400
|
||||
|
||||
target = (FRONTEND_PATH / rel_path).resolve()
|
||||
try:
|
||||
target.relative_to(FRONTEND_PATH.resolve())
|
||||
except Exception:
|
||||
return jsonify({"ok": False, "msg": "非法 path"}), 400
|
||||
|
||||
bak = str(target) + ".bak"
|
||||
if not os.path.exists(bak):
|
||||
return jsonify({"ok": False, "msg": "未找到上一版备份"}), 404
|
||||
|
||||
shutil.copy2(str(target), bak + ".tmp") if os.path.exists(str(target)) else None
|
||||
shutil.copy2(bak, str(target))
|
||||
st = os.stat(str(target))
|
||||
return jsonify({"ok": True, "path": rel_path, "size": st.st_size, "msg": "已回退到上一版"})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/assets/upload", methods=["POST"])
|
||||
def assets_upload():
|
||||
guard = _require_asset_editor_auth()
|
||||
|
|
@ -1483,6 +1738,15 @@ def assets_upload():
|
|||
return jsonify({"ok": False, "msg": "目标文件不存在,请先从 /assets/list 选择 path"}), 404
|
||||
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 首次上传前固化默认资产快照,供“重置为默认资产”使用
|
||||
default_snap = Path(str(target) + ".default")
|
||||
if not default_snap.exists():
|
||||
try:
|
||||
shutil.copy2(target, default_snap)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if backup:
|
||||
bak = target.with_suffix(target.suffix + ".bak")
|
||||
shutil.copy2(target, bak)
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
flask==3.0.2
|
||||
pillow==10.4.0
|
||||
|
|
|
|||
|
|
@ -283,6 +283,19 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
.asset-item.active { border-color: #22c55e; box-shadow: 0 0 0 1px #22c55e inset; }
|
||||
.asset-vis-btn {
|
||||
min-width: 34px;
|
||||
height: 28px;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #4b5563;
|
||||
background: #111827;
|
||||
color: #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
font-family:'ArkPixel', monospace;
|
||||
}
|
||||
.asset-vis-btn:hover { border-color:#22c55e; color:#ecfccb; }
|
||||
.asset-thumb { width:56px; height:56px; object-fit: contain; background:#0b1220; border:1px solid #374151; border-radius:6px; }
|
||||
.asset-meta { line-height: 1.45; }
|
||||
.asset-path { color:#d1fae5; word-break: break-all; }
|
||||
|
|
@ -295,9 +308,20 @@
|
|||
.asset-preview-box { border:1px solid #374151; border-radius:8px; padding:6px; background:#0b1220; margin-bottom:8px; }
|
||||
.asset-preview-title { color:#9ca3af; font-size:11px; margin-bottom:4px; }
|
||||
.asset-preview-img { width:100%; height:92px; object-fit:contain; background:#111827; border:1px solid #1f2937; border-radius:6px; }
|
||||
.home-fav-list { display:flex; gap:8px; overflow-x:auto; padding-bottom:4px; }
|
||||
.home-fav-item { min-width:126px; max-width:126px; border:1px solid #334155; border-radius:8px; background:#111827; padding:6px; }
|
||||
.home-fav-item img { width:100%; height:70px; object-fit:cover; border:1px solid #1f2937; border-radius:6px; image-rendering:pixelated; }
|
||||
.home-fav-meta { color:#9ca3af; font-size:10px; margin-top:4px; line-height:1.3; min-height:24px; }
|
||||
.home-fav-item button { width:100%; margin-top:4px; border:1px solid #4b5563; background:#1f2937; color:#fff; border-radius:6px; padding:4px 6px; font-family:'ArkPixel', monospace; cursor:pointer; }
|
||||
.home-fav-item button:hover { border-color:#22c55e; }
|
||||
#gemini-api-doc-link { color:#86efac; text-decoration: underline; text-underline-offset: 2px; }
|
||||
#gemini-api-doc-link:hover { color:#bbf7d0; }
|
||||
|
||||
|
||||
#asset-move-panel { border:1px solid #334155; background:#0b1220; border-radius:10px; padding:10px; margin-bottom:10px; }
|
||||
#asset-home-actions-panel { border:1px solid #334155; background:#0b1220; border-radius:10px; padding:10px; }
|
||||
#asset-home-actions-panel .asset-toolbar { display:grid; grid-template-columns: 1fr 1fr; gap:8px; }
|
||||
#asset-home-actions-panel .asset-toolbar > button { width:100%; margin:0; }
|
||||
#asset-move-row { justify-content: center; gap:12px; margin-bottom:0; }
|
||||
#asset-move-row .btn-move,
|
||||
#asset-move-row .btn-home,
|
||||
|
|
@ -1032,7 +1056,7 @@
|
|||
<aside id="asset-drawer">
|
||||
<div id="asset-drawer-header">
|
||||
<span>装修房间 · 资产侧边栏</span>
|
||||
<button onclick="toggleAssetDrawer(false)">关闭</button>
|
||||
<button id="btn-close-drawer" onclick="toggleAssetDrawer(false)">关闭</button>
|
||||
</div>
|
||||
<div id="asset-drawer-body">
|
||||
<div id="asset-auth-gate" class="asset-preview-box">
|
||||
|
|
@ -1067,10 +1091,11 @@
|
|||
<summary id="gemini-panel-summary" style="cursor:pointer; color:#cbd5e1;">🔐 API 设置(可折叠)</summary>
|
||||
<div id="asset-gemini-config" style="display:block; margin-top:6px;">
|
||||
<div id="gemini-config-hint" class="asset-sub" style="margin-bottom:4px;">可选:填写你的生图 API Key(留空不影响基础功能)</div>
|
||||
<div class="asset-sub" style="margin-bottom:6px;"><a id="gemini-api-doc-link" href="https://ai.google.dev/gemini-api/docs/api-key?hl=zh-cn" target="_blank" rel="noopener noreferrer">📘 如何申请 Google API Key</a></div>
|
||||
<div id="gemini-mask-status" class="asset-sub" style="margin-bottom:6px; color:#a7f3d0;"></div>
|
||||
<div class="asset-toolbar" style="gap:6px; flex-wrap:wrap;">
|
||||
<input id="gemini-api-key-input" type="password" placeholder="粘贴 GEMINI_API_KEY(不会回显)" style="min-width:220px; flex:1;" autocomplete="new-password" />
|
||||
<button onclick="saveGeminiConfigFromUI()">保存 Key</button>
|
||||
<button id="btn-save-gemini-key" onclick="saveGeminiConfigFromUI()">保存 Key</button>
|
||||
</div>
|
||||
<div id="gemini-config-msg" class="asset-sub" style="margin-top:4px;"></div>
|
||||
</div>
|
||||
|
|
@ -1080,6 +1105,18 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="asset-home-actions-panel" class="asset-preview-box" style="margin-bottom:10px;">
|
||||
<div class="asset-toolbar" style="margin-bottom:6px; gap:8px;">
|
||||
<button id="btn-back-last-bg" class="btn-home" onclick="restoreLastGeneratedBackground()">↩️ 回上一个家</button>
|
||||
<button id="btn-favorite-home" class="btn-home" onclick="saveCurrentHomeFavorite()">⭐ 收藏这个家</button>
|
||||
</div>
|
||||
<div id="asset-home-favorites" class="asset-preview-box" style="margin:0;">
|
||||
<div id="asset-home-favorites-title" class="asset-preview-title">🏠 收藏的家</div>
|
||||
<div id="asset-home-favorites-list" class="home-fav-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="asset-manual-panel">
|
||||
<div class="asset-toolbar">
|
||||
<input id="asset-search" placeholder="搜索资产名(如 desk / sofa / star)" oninput="renderAssetDrawerList()" />
|
||||
|
|
@ -1091,6 +1128,10 @@
|
|||
<button id="asset-choose-btn" onclick="openInlineAssetUploader()">上传替换素材</button>
|
||||
<button id="asset-commit-refresh-btn" onclick="commitAndRefresh()" disabled style="opacity:.55;">确认并刷新</button>
|
||||
</div>
|
||||
<div class="asset-toolbar" style="margin-top:0; margin-bottom:6px; gap:8px;">
|
||||
<button id="asset-reset-default-btn" onclick="resetSelectedAssetToDefault()" disabled style="opacity:.55;">重置为默认资产</button>
|
||||
<button id="asset-restore-prev-btn" onclick="restoreSelectedAssetPrev()" disabled style="opacity:.55;">用上一版</button>
|
||||
</div>
|
||||
<div id="asset-upload-result" class="asset-sub"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1122,7 +1163,12 @@
|
|||
btnIdle: '待命', btnWork: '工作', btnSync: '同步', btnError: '报警', btnDecor: '装修房间',
|
||||
drawerTitle: '装修房间 · 资产侧边栏', drawerClose: '关闭',
|
||||
authTitle: '请输入装修验证码', authPlaceholder: '输入验证码', authVerify: '验证', authDefaultPassHint: '默认密码:1234(可随时让我帮你改,建议改成强密码)',
|
||||
btnMove: '📦 搬新家', btnHome: '🐚 回老家', btnBroker: '🤝 找中介', btnDIY: '🪚 自己装', btnBrokerGo: '听中介的',
|
||||
drawerVisibilityTip: '可见性:点击条目右侧眼睛按钮切换该资产显示',
|
||||
hideDrawer: '👁 隐藏侧边栏', showDrawer: '👁 显示侧边栏',
|
||||
assetHide: '隐藏', assetShow: '显示',
|
||||
resetToDefault: '重置为默认资产', restorePrevAsset: '用上一版',
|
||||
btnMove: '📦 搬新家', btnHome: '🐚 回老家', btnHomeLast: '↩️ 回上一个家', btnHomeFavorite: '⭐ 收藏这个家', btnBroker: '🤝 找中介', btnDIY: '🪚 自己装', btnBrokerGo: '听中介的',
|
||||
homeFavTitle: '🏠 收藏的家', homeFavEmpty: '还没有收藏,先点“⭐ 收藏这个家”', homeFavApply: '替换到当前地图', homeFavSaved: '✅ 已收藏当前地图', homeFavApplied: '✅ 已替换为收藏地图',
|
||||
brokerHint: '你会给龙虾推荐什么样的房子',
|
||||
brokerPromptPh: '例如:故宫主题、莫奈风格、地牢主题、兵马俑主题……',
|
||||
brokerNeedPrompt: '请先输入中介方案描述',
|
||||
|
|
@ -1130,7 +1176,7 @@
|
|||
brokerDone: '✅ 已按中介方案生成并替换底图,正在刷新房间...',
|
||||
moveSuccess: '✅ 搬家成功!',
|
||||
brokerMissingKey: '❌ 生图失败:缺少 GEMINI API Key,请在下方填写并保存后重试',
|
||||
geminiPanelTitle: '🔐 API 设置(可折叠)', geminiHint: '可选:填写你的生图 API Key(留空不影响基础功能)', geminiMaskNoKey: '当前状态:未配置 Key', geminiMaskHasKey: '当前已配置:',
|
||||
geminiPanelTitle: '🔐 API 设置(可折叠)', geminiHint: '可选:填写你的生图 API Key(留空不影响基础功能)', geminiApiDoc: '📘 如何申请 Google API Key', geminiInputPh: '粘贴 GEMINI_API_KEY(不会回显)', geminiSaveKey: '保存 Key', geminiMaskNoKey: '当前状态:未配置 Key', geminiMaskHasKey: '当前已配置:',
|
||||
speedModeLabel: '生成模式', speedFast: '🍌2', speedQuality: '🍌Pro',
|
||||
searchPlaceholder: '搜索资产名(如 desk / sofa / star)', loaded: '已加载', allAssets: '全部资产',
|
||||
chooseImage: '上传替换素材', confirmUpload: '确认并刷新', uploadPending: '待上传', uploadTarget: '目标',
|
||||
|
|
@ -1145,7 +1191,12 @@
|
|||
btnIdle: 'Idle', btnWork: 'Work', btnSync: 'Sync', btnError: 'Alert', btnDecor: 'Decorate Room',
|
||||
drawerTitle: 'Decorate Room · Asset Sidebar', drawerClose: 'Close',
|
||||
authTitle: 'Enter Decor Passcode', authPlaceholder: 'Enter passcode', authVerify: 'Verify', authDefaultPassHint: 'Default passcode: 1234 (ask me anytime to change it; stronger passcode recommended)',
|
||||
btnMove: '📦 New Home', btnHome: '🐚 Go Home', btnBroker: '🤝 Broker', btnDIY: '🪚 DIY', btnBrokerGo: 'Follow Broker',
|
||||
drawerVisibilityTip: 'Visibility: use the eye button on each row to hide/show that asset',
|
||||
hideDrawer: '👁 Hide Drawer', showDrawer: '👁 Show Drawer',
|
||||
assetHide: 'Hide', assetShow: 'Show',
|
||||
resetToDefault: 'Reset to Default', restorePrevAsset: 'Use Previous',
|
||||
btnMove: '📦 New Home', btnHome: '🐚 Go Home', btnHomeLast: '↩️ Last One', btnHomeFavorite: '⭐ Save This Home', btnBroker: '🤝 Broker', btnDIY: '🪚 DIY', btnBrokerGo: 'Follow Broker',
|
||||
homeFavTitle: '🏠 Saved Homes', homeFavEmpty: 'No saved homes yet. Tap “⭐ Save This Home” first.', homeFavApply: 'Apply to Current Map', homeFavSaved: '✅ Current map saved', homeFavApplied: '✅ Applied saved home',
|
||||
brokerHint: 'What kind of house would you recommend for Lobster?',
|
||||
brokerPromptPh: 'e.g. Forbidden City theme, Monet style, dungeon theme, Terracotta Warriors theme...',
|
||||
brokerNeedPrompt: 'Please enter broker style prompt first',
|
||||
|
|
@ -1153,7 +1204,7 @@
|
|||
brokerDone: '✅ Broker plan applied and background replaced, refreshing room...',
|
||||
moveSuccess: '✅ Move successful!',
|
||||
brokerMissingKey: '❌ Generation failed: missing GEMINI API key. Fill it below and retry.',
|
||||
geminiPanelTitle: '🔐 API Settings (collapsible)', geminiHint: 'Optional: set your image API key (base features work without it)', geminiMaskNoKey: 'Current: no key configured', geminiMaskHasKey: 'Configured key:',
|
||||
geminiPanelTitle: '🔐 API Settings (collapsible)', geminiHint: 'Optional: set your image API key (base features work without it)', geminiApiDoc: '📘 How to get a Google API Key', geminiInputPh: 'Paste GEMINI_API_KEY (input hidden)', geminiSaveKey: 'Save Key', geminiMaskNoKey: 'Current: no key configured', geminiMaskHasKey: 'Configured key:',
|
||||
speedModeLabel: 'Render Mode', speedFast: '🍌2', speedQuality: '🍌Pro',
|
||||
searchPlaceholder: 'Search assets (desk / sofa / star)', loaded: 'Loaded', allAssets: 'All Assets',
|
||||
chooseImage: 'Upload Replacement Asset', confirmUpload: 'Confirm & Refresh', uploadPending: 'Pending Upload', uploadTarget: 'Target',
|
||||
|
|
@ -1168,7 +1219,12 @@
|
|||
btnIdle: '待機', btnWork: '作業', btnSync: '同期', btnError: '警報', btnDecor: '部屋を編集',
|
||||
drawerTitle: '部屋編集・アセットサイドバー', drawerClose: '閉じる',
|
||||
authTitle: '編集パスコードを入力', authPlaceholder: 'パスコード入力', authVerify: '認証', authDefaultPassHint: '初期パスコード:1234(いつでも変更を相談可。強固なパス推奨)',
|
||||
btnMove: '📦 引っ越し', btnHome: '🐚 実家に戻る', btnBroker: '🤝 仲介', btnDIY: '🪚 自分で装飾', btnBrokerGo: '仲介に任せる',
|
||||
drawerVisibilityTip: '表示切替:各行右側の目ボタンで資産を表示/非表示',
|
||||
hideDrawer: '👁 サイドバーを隠す', showDrawer: '👁 サイドバーを表示',
|
||||
assetHide: '非表示', assetShow: '表示',
|
||||
resetToDefault: 'デフォルトへ戻す', restorePrevAsset: '前の版へ戻す',
|
||||
btnMove: '📦 引っ越し', btnHome: '🐚 実家に戻る', btnHomeLast: '↩️ ひとつ前へ', btnHomeFavorite: '⭐ この家を保存', btnBroker: '🤝 仲介', btnDIY: '🪚 自分で装飾', btnBrokerGo: '仲介に任せる',
|
||||
homeFavTitle: '🏠 保存した家', homeFavEmpty: 'まだ保存がありません。先に「⭐ この家を保存」を押してください。', homeFavApply: '現在のマップに適用', homeFavSaved: '✅ 現在のマップを保存しました', homeFavApplied: '✅ 保存した家を適用しました',
|
||||
brokerHint: 'ロブスターにはどんな家をおすすめしますか',
|
||||
brokerPromptPh: '例:故宮テーマ、モネ風、ダンジョン風、兵馬俑テーマ…',
|
||||
brokerNeedPrompt: '先に仲介プランの説明を入力してください',
|
||||
|
|
@ -1176,7 +1232,7 @@
|
|||
brokerDone: '✅ 仲介プランを適用して背景を更新しました。部屋を更新中...',
|
||||
moveSuccess: '✅ 引っ越し成功!',
|
||||
brokerMissingKey: '❌ 生成失敗:GEMINI APIキーが未設定です。下で入力して保存してください。',
|
||||
geminiPanelTitle: '🔐 API設定(折りたたみ)', geminiHint: '任意:画像生成APIキーを設定(未設定でも基本機能は利用可)', geminiMaskNoKey: '現在:キー未設定', geminiMaskHasKey: '設定済みキー:',
|
||||
geminiPanelTitle: '🔐 API設定(折りたたみ)', geminiHint: '任意:画像生成APIキーを設定(未設定でも基本機能は利用可)', geminiApiDoc: '📘 Google API Keyの取得方法', geminiInputPh: 'GEMINI_API_KEY を貼り付け(入力は非表示)', geminiSaveKey: 'Keyを保存', geminiMaskNoKey: '現在:キー未設定', geminiMaskHasKey: '設定済みキー:',
|
||||
speedModeLabel: '生成モード', speedFast: '🍌2', speedQuality: '🍌Pro',
|
||||
searchPlaceholder: 'アセット検索(desk / sofa / star)', loaded: '読み込み済み', allAssets: '全アセット',
|
||||
chooseImage: '差し替え素材をアップロード', confirmUpload: '確定して更新', uploadPending: 'アップロード待ち', uploadTarget: '対象',
|
||||
|
|
@ -1230,7 +1286,7 @@
|
|||
|
||||
const drawerTitle = document.querySelector('#asset-drawer-header span');
|
||||
if (drawerTitle) drawerTitle.textContent = t('drawerTitle');
|
||||
const drawerClose = document.querySelector('#asset-drawer-header button');
|
||||
const drawerClose = document.getElementById('btn-close-drawer');
|
||||
if (drawerClose) drawerClose.textContent = t('drawerClose');
|
||||
|
||||
const authTitle = document.querySelector('#asset-auth-gate .asset-preview-title');
|
||||
|
|
@ -1243,6 +1299,9 @@
|
|||
setText('btn-back-home', 'btnHome');
|
||||
const brokerBtn = document.querySelector('#asset-broker-row .btn-broker'); if (brokerBtn) brokerBtn.textContent = t('btnBroker');
|
||||
const diyBtn = document.querySelector('#asset-broker-row .btn-diy'); if (diyBtn) diyBtn.textContent = t('btnDIY');
|
||||
const backLastBtn = document.getElementById('btn-back-last-bg'); if (backLastBtn) backLastBtn.textContent = t('btnHomeLast');
|
||||
const favHomeBtn = document.getElementById('btn-favorite-home'); if (favHomeBtn) favHomeBtn.textContent = t('btnHomeFavorite');
|
||||
const favTitle = document.getElementById('asset-home-favorites-title'); if (favTitle) favTitle.textContent = t('homeFavTitle');
|
||||
const brokerHint = document.querySelector('#asset-broker-panel .asset-sub'); if (brokerHint) brokerHint.textContent = t('brokerHint');
|
||||
const brokerPrompt = document.getElementById('asset-broker-prompt'); if (brokerPrompt) brokerPrompt.placeholder = t('brokerPromptPh');
|
||||
const brokerGoBtn = document.querySelector('#asset-broker-actions button'); if (brokerGoBtn) brokerGoBtn.textContent = t('btnBrokerGo');
|
||||
|
|
@ -1251,6 +1310,9 @@
|
|||
const speedQualityBtn = document.getElementById('speed-quality-btn'); if (speedQualityBtn) speedQualityBtn.textContent = t('speedQuality');
|
||||
const geminiPanelSummary = document.getElementById('gemini-panel-summary'); if (geminiPanelSummary) geminiPanelSummary.textContent = t('geminiPanelTitle');
|
||||
const geminiHint = document.getElementById('gemini-config-hint'); if (geminiHint) geminiHint.textContent = t('geminiHint');
|
||||
const geminiDocLink = document.getElementById('gemini-api-doc-link'); if (geminiDocLink) geminiDocLink.textContent = t('geminiApiDoc');
|
||||
const geminiInput = document.getElementById('gemini-api-key-input'); if (geminiInput) geminiInput.placeholder = t('geminiInputPh');
|
||||
const geminiSaveBtn = document.getElementById('btn-save-gemini-key'); if (geminiSaveBtn) geminiSaveBtn.textContent = t('geminiSaveKey');
|
||||
|
||||
setPh('asset-search', 'searchPlaceholder');
|
||||
|
||||
|
|
@ -1457,7 +1519,10 @@
|
|||
let assetListData = [];
|
||||
let sceneAssetItems = [];
|
||||
let selectedAssetInfo = null;
|
||||
let hiddenAssetPaths = new Set();
|
||||
let assetThumbTimers = [];
|
||||
let homeFavoritesCache = [];
|
||||
let homeFavoritesLoadedAt = 0;
|
||||
|
||||
// 坐标以服务端为准;清理历史本地缓存,避免把素材挪飞
|
||||
let assetPositionOverrides = {};
|
||||
|
|
@ -1878,6 +1943,9 @@
|
|||
}
|
||||
|
||||
function mapAssetPathToSprite(path) {
|
||||
// 背景做特殊映射:即使纹理 key 已变成 office_bg_live_xxx,也能稳定定位到背景对象
|
||||
if ((path || '').includes('office_bg_small.webp') && officeBgSprite) return officeBgSprite;
|
||||
|
||||
const item = sceneAssetItems.find(x => x.path === path && x.ref && x.ref.getBounds);
|
||||
if (item) return item.ref;
|
||||
const cands = pathToTextureCandidates(path);
|
||||
|
|
@ -2026,13 +2094,16 @@
|
|||
|
||||
function updateAssetConfirmButtonState() {
|
||||
const btn = document.getElementById('asset-commit-refresh-btn');
|
||||
const btnReset = document.getElementById('asset-reset-default-btn');
|
||||
const btnPrev = document.getElementById('asset-restore-prev-btn');
|
||||
const panel = document.getElementById('asset-upload-panel');
|
||||
const can = !!(selectedAssetInfo && selectedAssetInfo.path);
|
||||
if (panel) panel.classList.toggle('active', can);
|
||||
if (btn) {
|
||||
btn.disabled = !can;
|
||||
btn.style.opacity = can ? '1' : '.55';
|
||||
}
|
||||
[btn, btnReset, btnPrev].forEach((b) => {
|
||||
if (!b) return;
|
||||
b.disabled = !can;
|
||||
b.style.opacity = can ? '1' : '.55';
|
||||
});
|
||||
}
|
||||
|
||||
function selectAssetInDrawer(path) {
|
||||
|
|
@ -2160,6 +2231,37 @@
|
|||
img.src = `/static/${item.path}?t=${Date.now()}`;
|
||||
}
|
||||
|
||||
function isAssetHidden(path) {
|
||||
return hiddenAssetPaths.has(path || '');
|
||||
}
|
||||
|
||||
function setAssetVisible(path, visible) {
|
||||
const p = (path || '').trim();
|
||||
if (!p) return;
|
||||
if (visible) hiddenAssetPaths.delete(p);
|
||||
else hiddenAssetPaths.add(p);
|
||||
|
||||
const sp = mapAssetPathToSprite(p);
|
||||
if (sp && sp.setVisible) {
|
||||
sp.setVisible(!!visible);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAssetVisibility(path, ev) {
|
||||
if (ev && ev.stopPropagation) ev.stopPropagation();
|
||||
const p = (path || '').trim();
|
||||
if (!p) return;
|
||||
const nextVisible = isAssetHidden(p);
|
||||
setAssetVisible(p, nextVisible);
|
||||
renderAssetDrawerList();
|
||||
const out = document.getElementById('asset-upload-result');
|
||||
if (out) out.textContent = nextVisible ? `✅ 已显示:${p}` : `🙈 已隐藏:${p}`;
|
||||
if (selectedAssetInfo && selectedAssetInfo.path === p) {
|
||||
if (!nextVisible) clearAssetSelectionUI();
|
||||
else applyScenePreview(p);
|
||||
}
|
||||
}
|
||||
|
||||
function renderAssetDrawerList() {
|
||||
const q = (document.getElementById('asset-search')?.value || '').trim().toLowerCase();
|
||||
const list = document.getElementById('asset-list');
|
||||
|
|
@ -2178,7 +2280,11 @@
|
|||
const p = (path || '').toLowerCase();
|
||||
const idx = statePriority.findIndex(x => p.endsWith(x));
|
||||
if (idx >= 0) return idx; // 0~3: 四个主状态最前
|
||||
if (p.includes('guest_anim_')) return 999; // guest 动画放最后
|
||||
|
||||
// 按钮素材最不重要:统一沉到列表末尾
|
||||
if (p.includes('/btn-') || p.includes('btn-') || p.includes('button')) return 1000;
|
||||
|
||||
if (p.includes('guest_anim_')) return 999; // guest 动画靠后
|
||||
return 100;
|
||||
};
|
||||
const rows = baseRows
|
||||
|
|
@ -2201,12 +2307,15 @@
|
|||
const reso = (it.width && it.height) ? `${it.width}×${it.height}` : '-';
|
||||
const displayName = getAssetDisplayName(it.path || '');
|
||||
const thumbId = `asset-thumb-canvas-${(it.path || '').replace(/[^a-zA-Z0-9]/g, '_')}`;
|
||||
const hidden = isAssetHidden(it.path);
|
||||
const visEmoji = hidden ? '🙈' : '👀';
|
||||
return `<div class="asset-item ${isActive ? 'active' : ''}" data-path="${it.path}" onclick="selectAssetInDrawer('${(it.path || '').replace(/'/g, "\\'")}')">
|
||||
<canvas id="${thumbId}" class="asset-thumb" width="56" height="56"></canvas>
|
||||
<div class="asset-meta">
|
||||
<div class="asset-path">${it.path}</div>
|
||||
<div class="asset-sub">${displayName} | ${reso}</div>
|
||||
<div class="asset-sub">${displayName} | ${reso}${hidden ? ' | 已隐藏' : ''}</div>
|
||||
</div>
|
||||
<button class="asset-vis-btn" onclick="toggleAssetVisibility('${(it.path || '').replace(/'/g, "\\'")}', event)">${visEmoji}</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
|
|
@ -2601,7 +2710,7 @@ function toggleBrokerPanel() {
|
|||
if (data && data.ok) {
|
||||
window.geminiConfig = {
|
||||
hasKey: !!data.has_api_key,
|
||||
model: data.gemini_model || 'gemini-3.1-flash-image-preview'
|
||||
model: data.gemini_model || 'nanobanana-pro'
|
||||
};
|
||||
const box = document.getElementById('asset-gemini-config');
|
||||
if (box) box.style.display = 'block';
|
||||
|
|
@ -2627,7 +2736,7 @@ function toggleBrokerPanel() {
|
|||
const res = await fetch('/config/gemini', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ api_key: key, model: 'gemini-3.1-flash-image-preview' })
|
||||
body: JSON.stringify({ api_key: key, model: 'nanobanana-pro' })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
|
|
@ -2754,8 +2863,15 @@ function toggleBrokerPanel() {
|
|||
async function restoreHomeBackground() {
|
||||
const homeBtn = document.getElementById('btn-back-home');
|
||||
flashButtonActive(homeBtn);
|
||||
setWorkingStatus('正在回老家');
|
||||
const out = document.getElementById('asset-move-result') || document.getElementById('asset-upload-result');
|
||||
|
||||
const confirmMsg = '⚠️ 回老家会覆盖当前自定义房间背景(可从 bg-history 恢复历史图)。\n确定继续吗?';
|
||||
if (!window.confirm(confirmMsg)) {
|
||||
out.textContent = '已取消回老家';
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkingStatus('正在回老家');
|
||||
// 点击即刻显示遮罩,先于任何网络调用
|
||||
showRoomLoadingOverlay();
|
||||
out.textContent = '🏡 正在回老家(恢复初始底图)...';
|
||||
|
|
@ -2780,12 +2896,196 @@ function toggleBrokerPanel() {
|
|||
}
|
||||
}
|
||||
|
||||
async function restoreLastGeneratedBackground() {
|
||||
const btn = document.getElementById('btn-back-last-bg');
|
||||
flashButtonActive(btn);
|
||||
const out = document.getElementById('asset-move-result') || document.getElementById('asset-upload-result');
|
||||
|
||||
const confirmMsg = '⚠️ 将回退到最近一次生成的房间背景,确定继续吗?';
|
||||
if (!window.confirm(confirmMsg)) {
|
||||
out.textContent = '已取消回退';
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkingStatus('正在回退到上一次背景');
|
||||
showRoomLoadingOverlay();
|
||||
out.textContent = '↩️ 正在回退到最近一次生成底图...';
|
||||
try {
|
||||
const res = await fetch('/assets/restore-last-generated-background', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
out.textContent = `❌ 回退失败:${data.msg || res.status}`;
|
||||
return;
|
||||
}
|
||||
const ok = await refreshOfficeBackgroundOnly();
|
||||
if (ok) {
|
||||
out.textContent = '✅ 已回退到上一次背景';
|
||||
} else {
|
||||
out.textContent = '✅ 已回退到上一次背景(局部刷新失败,可手动刷新页面)';
|
||||
}
|
||||
try { setState('idle', '已回退到上一次背景'); } catch (e) {}
|
||||
} catch (e) {
|
||||
out.textContent = `❌ 回退失败:${e}`;
|
||||
} finally {
|
||||
hideRoomLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchJsonSafe(url, options = {}) {
|
||||
const res = await fetch(url, options);
|
||||
const ct = (res.headers.get('content-type') || '').toLowerCase();
|
||||
if (!ct.includes('application/json')) {
|
||||
const txt = await res.text();
|
||||
const brief = (txt || '').replace(/\s+/g, ' ').slice(0, 120);
|
||||
throw new Error(`接口未返回 JSON(${res.status}): ${brief || 'empty response'}`);
|
||||
}
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
async function renderHomeFavorites(force = false) {
|
||||
const box = document.getElementById('asset-home-favorites-list');
|
||||
if (!box) return;
|
||||
const now = Date.now();
|
||||
if (!force && homeFavoritesCache.length > 0 && (now - homeFavoritesLoadedAt) < 30000) {
|
||||
// 使用缓存,避免频繁请求
|
||||
} else {
|
||||
try {
|
||||
const data = await fetchJsonSafe('/assets/home-favorites/list', { cache: 'no-store' });
|
||||
if (data && data.ok && Array.isArray(data.items)) {
|
||||
homeFavoritesCache = data.items;
|
||||
homeFavoritesLoadedAt = now;
|
||||
}
|
||||
} catch (e) {
|
||||
const out = document.getElementById('asset-move-result') || document.getElementById('asset-upload-result');
|
||||
if (out) out.textContent = `❌ 收藏列表加载失败:${e.message || e}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!homeFavoritesCache.length) {
|
||||
box.innerHTML = `<div class="asset-sub" style="padding:4px 2px;">${t('homeFavEmpty')}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
box.innerHTML = homeFavoritesCache.map((it) => {
|
||||
const id = (it.id || '').replace(/'/g, "\\'");
|
||||
const thumb = it.thumb_url || it.url || '';
|
||||
const time = it.created_at || '';
|
||||
return `<div class="home-fav-item">
|
||||
<img src="${thumb}" loading="lazy" alt="favorite-home" />
|
||||
<div class="home-fav-meta">${time}</div>
|
||||
<button onclick="applyHomeFavorite('${id}')">${t('homeFavApply')}</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function saveCurrentHomeFavorite() {
|
||||
const btn = document.getElementById('btn-favorite-home');
|
||||
flashButtonActive(btn);
|
||||
const out = document.getElementById('asset-move-result') || document.getElementById('asset-upload-result');
|
||||
try {
|
||||
const data = await fetchJsonSafe('/assets/home-favorites/save-current', { method: 'POST' });
|
||||
if (!data.ok) {
|
||||
out.textContent = `❌ 收藏失败:${data.msg || 'unknown error'}`;
|
||||
return;
|
||||
}
|
||||
out.textContent = t('homeFavSaved');
|
||||
await renderHomeFavorites(true);
|
||||
} catch (e) {
|
||||
out.textContent = `❌ 收藏失败:${e.message || e}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyHomeFavorite(id) {
|
||||
const out = document.getElementById('asset-move-result') || document.getElementById('asset-upload-result');
|
||||
if (!id) return;
|
||||
showRoomLoadingOverlay();
|
||||
setWorkingStatus('正在替换收藏地图');
|
||||
try {
|
||||
const data = await fetchJsonSafe('/assets/home-favorites/apply', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
});
|
||||
if (!data.ok) {
|
||||
out.textContent = `❌ 替换失败:${data.msg || 'unknown error'}`;
|
||||
return;
|
||||
}
|
||||
const ok = await refreshOfficeBackgroundOnly();
|
||||
out.textContent = ok ? t('homeFavApplied') : `${t('homeFavApplied')}(局部刷新失败,可手动刷新页面)`;
|
||||
try { setState('idle', '已应用收藏地图'); } catch (e) {}
|
||||
} catch (e) {
|
||||
out.textContent = `❌ 替换失败:${e.message || e}`;
|
||||
} finally {
|
||||
hideRoomLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
async function resetSelectedAssetToDefault() {
|
||||
const out = document.getElementById('asset-upload-result');
|
||||
const path = selectedAssetInfo && selectedAssetInfo.path;
|
||||
if (!path) {
|
||||
if (out) out.textContent = '请先选择一个资产';
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`⚠️ 确定将 ${path} 重置为默认资产吗?`)) return;
|
||||
try {
|
||||
const res = await fetch('/assets/restore-default', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
if (out) out.textContent = `❌ 重置失败:${data.msg || res.status}`;
|
||||
return;
|
||||
}
|
||||
await refreshSceneObjectByAssetPath(path);
|
||||
if (out) out.textContent = `✅ 已重置为默认资产:${path}`;
|
||||
} catch (e) {
|
||||
if (out) out.textContent = `❌ 重置失败:${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreSelectedAssetPrev() {
|
||||
const out = document.getElementById('asset-upload-result');
|
||||
const path = selectedAssetInfo && selectedAssetInfo.path;
|
||||
if (!path) {
|
||||
if (out) out.textContent = '请先选择一个资产';
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`⚠️ 确定将 ${path} 回退到上一版吗?`)) return;
|
||||
try {
|
||||
const res = await fetch('/assets/restore-prev', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
if (out) out.textContent = `❌ 回退失败:${data.msg || res.status}`;
|
||||
return;
|
||||
}
|
||||
await refreshSceneObjectByAssetPath(path);
|
||||
if (out) out.textContent = `✅ 已回退到上一版:${path}`;
|
||||
} catch (e) {
|
||||
if (out) out.textContent = `❌ 回退失败:${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleAssetDrawer(force) {
|
||||
const drawer = document.getElementById('asset-drawer');
|
||||
const next = (typeof force === 'boolean') ? force : !assetDrawerOpen;
|
||||
assetDrawerOpen = next;
|
||||
drawer.classList.toggle('open', next);
|
||||
document.body.classList.toggle('drawer-open', next);
|
||||
|
||||
const openBtn = document.getElementById('btn-open-drawer');
|
||||
if (openBtn) {
|
||||
openBtn.classList.toggle('is-active', next);
|
||||
openBtn.textContent = t('btnDecor');
|
||||
}
|
||||
const closeBtn = document.getElementById('btn-close-drawer');
|
||||
if (closeBtn) closeBtn.textContent = t('drawerClose');
|
||||
if (next) {
|
||||
assetManualPanelOpen = false;
|
||||
updateAssetAuthUI();
|
||||
|
|
@ -2793,6 +3093,7 @@ function toggleBrokerPanel() {
|
|||
await ensureGeminiConfigLoaded();
|
||||
if (assetDrawerAuthed) {
|
||||
await refreshAssetDrawerList();
|
||||
await renderHomeFavorites(false);
|
||||
bindDrawerFileMeta();
|
||||
} else {
|
||||
const msg = document.getElementById('asset-auth-msg');
|
||||
|
|
|
|||
Loading…
Reference in a new issue