352 lines
12 KiB
JavaScript
352 lines
12 KiB
JavaScript
/* global Component, getCurrentPages, wx, require, console */
|
|
|
|
const { getSettings, listServers } = require("../../utils/storage");
|
|
const { getTerminalSessionSnapshot } = require("../../utils/terminalSession");
|
|
const { buildButtonIconThemeMaps, resolveButtonIcon } = require("../../utils/themedIcons");
|
|
const { resolvePageNavigationMethod } = require("../../utils/navigationPolicy");
|
|
const {
|
|
hasActiveTerminalSession,
|
|
openTerminalPage,
|
|
resolveActiveTerminalServerId
|
|
} = require("../../utils/terminalNavigation");
|
|
const { buildPageCopy, normalizeUiLanguage, t } = require("../../utils/i18n");
|
|
const { subscribeLocaleChange } = require("../../utils/localeBus");
|
|
const { subscribeSyncConfigApplied } = require("../../utils/syncConfigBus");
|
|
const { buildSvgButtonPressData, createSvgButtonPressMethods } = require("../../utils/svgButtonFeedback");
|
|
|
|
const SHELL_BUTTON_ACTION = "open-terminal-shell";
|
|
const SHELL_BUTTON_ITEM = Object.freeze({
|
|
action: SHELL_BUTTON_ACTION,
|
|
path: "/pages/terminal/index",
|
|
icon: "/assets/icons/shell.svg",
|
|
isTab: false
|
|
});
|
|
|
|
const loggedRenderProbeKeys = new Set();
|
|
|
|
function shouldUseTextIcons() {
|
|
if (!wx || typeof wx.getAppBaseInfo !== "function") return false;
|
|
try {
|
|
const info = wx.getAppBaseInfo() || {};
|
|
return (
|
|
String(info.platform || "")
|
|
.trim()
|
|
.toLowerCase() === "devtools"
|
|
);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function inspectRenderPayload(payload) {
|
|
const stats = {
|
|
stringCount: 0,
|
|
dataImageCount: 0,
|
|
svgPathCount: 0,
|
|
urlCount: 0,
|
|
maxLength: 0,
|
|
samples: []
|
|
};
|
|
const walk = (value, path, depth) => {
|
|
if (depth > 5) return;
|
|
if (typeof value === "string") {
|
|
stats.stringCount += 1;
|
|
if (value.includes("data:image")) stats.dataImageCount += 1;
|
|
if (value.includes(".svg")) stats.svgPathCount += 1;
|
|
if (value.includes("url(")) stats.urlCount += 1;
|
|
if (value.length > stats.maxLength) stats.maxLength = value.length;
|
|
if (
|
|
stats.samples.length < 6 &&
|
|
(value.includes("data:image") ||
|
|
value.includes(".svg") ||
|
|
value.includes("url(") ||
|
|
value.length >= 120)
|
|
) {
|
|
stats.samples.push({
|
|
path,
|
|
length: value.length,
|
|
preview: value.slice(0, 120)
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (!value || typeof value !== "object") return;
|
|
if (Array.isArray(value)) {
|
|
value.forEach((item, index) => walk(item, `${path}[${index}]`, depth + 1));
|
|
return;
|
|
}
|
|
Object.keys(value).forEach((key) => walk(value[key], path ? `${path}.${key}` : key, depth + 1));
|
|
};
|
|
walk(payload, "", 0);
|
|
return stats;
|
|
}
|
|
|
|
function logRenderProbeOnce(key, label, payload) {
|
|
const normalizedKey = String(key || label || "");
|
|
if (!normalizedKey || loggedRenderProbeKeys.has(normalizedKey)) return;
|
|
loggedRenderProbeKeys.add(normalizedKey);
|
|
console.warn(`[render_probe] ${label}`, inspectRenderPayload(payload));
|
|
}
|
|
|
|
function prependShellItem(page, items) {
|
|
if (String(page || "").trim() === "terminal") {
|
|
return items;
|
|
}
|
|
return [SHELL_BUTTON_ITEM, ...items];
|
|
}
|
|
|
|
/**
|
|
* 全局底部工具条组件:
|
|
* 1. 复刻 Web 底栏语义:左侧返回,右侧按页面上下文展示图标按钮;
|
|
* 2. 图标顺序按 Figma 页面 annotation 固定,不做自动重排;
|
|
* 3. records 页面自身不展示 records 入口,避免重复高亮。
|
|
*/
|
|
Component({
|
|
properties: {
|
|
page: {
|
|
type: String,
|
|
value: ""
|
|
}
|
|
},
|
|
|
|
data: {
|
|
...buildSvgButtonPressData(),
|
|
canGoBack: false,
|
|
backIcon: "/assets/icons/back.svg",
|
|
backPressedIcon: "/assets/icons/back.svg",
|
|
backLabel: t("zh-Hans", "bottomNav.backText"),
|
|
textIconMode: false,
|
|
items: []
|
|
},
|
|
|
|
lifetimes: {
|
|
attached() {
|
|
this.syncCanGoBack();
|
|
this.syncItems();
|
|
this.localeUnsub = subscribeLocaleChange(() => {
|
|
this.syncItems();
|
|
});
|
|
this.syncConfigUnsub = subscribeSyncConfigApplied(() => {
|
|
this.syncCanGoBack();
|
|
this.syncItems();
|
|
});
|
|
},
|
|
|
|
detached() {
|
|
if (typeof this.localeUnsub === "function") {
|
|
this.localeUnsub();
|
|
this.localeUnsub = null;
|
|
}
|
|
if (typeof this.syncConfigUnsub === "function") {
|
|
this.syncConfigUnsub();
|
|
this.syncConfigUnsub = null;
|
|
}
|
|
}
|
|
},
|
|
|
|
pageLifetimes: {
|
|
show() {
|
|
this.syncCanGoBack();
|
|
this.syncItems();
|
|
}
|
|
},
|
|
|
|
observers: {
|
|
page() {
|
|
this.syncItems();
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
currentPathByPage(page) {
|
|
const name = String(page || "").trim();
|
|
if (!name) return "";
|
|
if (name === "about" || name.indexOf("about-") === 0) {
|
|
return "/pages/about/index";
|
|
}
|
|
return `/pages/${name}/index`;
|
|
},
|
|
|
|
syncCanGoBack() {
|
|
const pages = getCurrentPages();
|
|
this.setData({ canGoBack: pages.length > 1 });
|
|
},
|
|
|
|
rawItemsByPage(page) {
|
|
const aboutItem = { path: "/pages/about/index", icon: "/assets/icons/about.svg", isTab: false };
|
|
if (page === "about" || String(page || "").indexOf("about-") === 0) {
|
|
return prependShellItem(page, [
|
|
{ path: "/pages/connect/index", icon: "/assets/icons/serverlist.svg", isTab: false },
|
|
{ path: "/pages/logs/index", icon: "/assets/icons/log.svg", isTab: false },
|
|
{ path: "/pages/settings/index", icon: "/assets/icons/config.svg", isTab: false }
|
|
]);
|
|
}
|
|
if (page === "connect") {
|
|
return prependShellItem(page, [
|
|
{ path: "/pages/logs/index", icon: "/assets/icons/log.svg", isTab: false },
|
|
{
|
|
path: "/pages/records/index",
|
|
icon: "/assets/icons/recordmanager.svg",
|
|
isTab: false
|
|
},
|
|
{ path: "/pages/settings/index", icon: "/assets/icons/config.svg", isTab: false },
|
|
aboutItem
|
|
]);
|
|
}
|
|
if (page === "settings") {
|
|
return prependShellItem(page, [
|
|
{ path: "/pages/connect/index", icon: "/assets/icons/serverlist.svg", isTab: false },
|
|
{ path: "/pages/logs/index", icon: "/assets/icons/log.svg", isTab: false },
|
|
{ path: "/pages/plugins/index", icon: "/assets/icons/plugins.svg", isTab: false },
|
|
{ path: "/pages/records/index", icon: "/assets/icons/recordmanager.svg", isTab: false },
|
|
aboutItem
|
|
]);
|
|
}
|
|
if (page === "plugins") {
|
|
return prependShellItem(page, [
|
|
{ path: "/pages/connect/index", icon: "/assets/icons/serverlist.svg", isTab: false },
|
|
{
|
|
path: "/pages/records/index",
|
|
icon: "/assets/icons/recordmanager.svg",
|
|
isTab: false
|
|
},
|
|
{ path: "/pages/settings/index", icon: "/assets/icons/config.svg", isTab: false },
|
|
aboutItem
|
|
]);
|
|
}
|
|
if (page === "terminal") {
|
|
return [
|
|
{ path: "/pages/connect/index", icon: "/assets/icons/serverlist.svg", isTab: false },
|
|
{ path: "/pages/logs/index", icon: "/assets/icons/log.svg", isTab: false },
|
|
{
|
|
path: "/pages/records/index",
|
|
icon: "/assets/icons/recordmanager.svg",
|
|
isTab: false
|
|
},
|
|
{ path: "/pages/settings/index", icon: "/assets/icons/config.svg", isTab: false },
|
|
aboutItem
|
|
];
|
|
}
|
|
if (page === "records") {
|
|
return prependShellItem(page, [
|
|
{ path: "/pages/connect/index", icon: "/assets/icons/serverlist.svg", isTab: false },
|
|
{ path: "/pages/logs/index", icon: "/assets/icons/log.svg", isTab: false },
|
|
{ path: "/pages/settings/index", icon: "/assets/icons/config.svg", isTab: false },
|
|
aboutItem
|
|
]);
|
|
}
|
|
if (page === "logs") {
|
|
return prependShellItem(page, [
|
|
{ path: "/pages/connect/index", icon: "/assets/icons/serverlist.svg", isTab: false },
|
|
{
|
|
path: "/pages/records/index",
|
|
icon: "/assets/icons/recordmanager.svg",
|
|
isTab: false
|
|
},
|
|
{ path: "/pages/settings/index", icon: "/assets/icons/config.svg", isTab: false },
|
|
aboutItem
|
|
]);
|
|
}
|
|
return prependShellItem(page, [
|
|
{ path: "/pages/connect/index", icon: "/assets/icons/serverlist.svg", isTab: false },
|
|
{ path: "/pages/settings/index", icon: "/assets/icons/config.svg", isTab: false },
|
|
aboutItem
|
|
]);
|
|
},
|
|
|
|
syncItems() {
|
|
const page = this.data.page;
|
|
const settings = getSettings();
|
|
const language = normalizeUiLanguage(settings.uiLanguage);
|
|
const copy = buildPageCopy(language, "bottomNav");
|
|
const { icons: iconMap, activeIcons: activeIconMap, accentIcons: accentIconMap } =
|
|
buildButtonIconThemeMaps(settings);
|
|
const currentPath = this.currentPathByPage(page);
|
|
const sessionSnapshot = getTerminalSessionSnapshot();
|
|
const shellActive = hasActiveTerminalSession(sessionSnapshot, listServers());
|
|
const list = this.rawItemsByPage(page).map((item) => ({
|
|
...item,
|
|
id: String(item.action || item.path || item.icon || ""),
|
|
pressKey: `bottom-nav:${String(item.action || item.path || item.icon || "").trim()}`,
|
|
icon:
|
|
item.action === SHELL_BUTTON_ACTION && shellActive
|
|
? resolveButtonIcon(item.icon, activeIconMap)
|
|
: resolveButtonIcon(item.icon, iconMap),
|
|
pressedIcon: resolveButtonIcon(
|
|
item.icon,
|
|
item.action === SHELL_BUTTON_ACTION && shellActive ? activeIconMap : accentIconMap
|
|
),
|
|
textLabel: this.resolveTextLabel(item.action || item.path, copy),
|
|
active: item.action === SHELL_BUTTON_ACTION ? shellActive : item.path === currentPath,
|
|
connectionActive: item.action === SHELL_BUTTON_ACTION && shellActive
|
|
}));
|
|
const payload = {
|
|
backIcon: iconMap.back || "/assets/icons/back.svg",
|
|
backPressedIcon: accentIconMap.back || iconMap.back || "/assets/icons/back.svg",
|
|
backLabel: copy.backText || t(language, "bottomNav.backText"),
|
|
textIconMode: shouldUseTextIcons() && Object.keys(iconMap).length === 0,
|
|
items: list
|
|
};
|
|
logRenderProbeOnce("bottom-nav.syncItems", "bottom-nav.syncItems", payload);
|
|
this.setData(payload);
|
|
},
|
|
|
|
onBack() {
|
|
if (!this.data.canGoBack) return;
|
|
wx.navigateBack({
|
|
delta: 1,
|
|
fail: () => {
|
|
wx.redirectTo({ url: "/pages/connect/index" });
|
|
}
|
|
});
|
|
},
|
|
|
|
onNavTap(event) {
|
|
const action = String(event.currentTarget.dataset.action || "").trim();
|
|
if (action === SHELL_BUTTON_ACTION) {
|
|
this.onShellTap();
|
|
return;
|
|
}
|
|
const path = event.currentTarget.dataset.path;
|
|
if (!path) return;
|
|
const currentPath = this.currentPathByPage(this.data.page);
|
|
const method = resolvePageNavigationMethod(currentPath, path);
|
|
if (method === "noop") return;
|
|
if (method === "redirectTo") {
|
|
wx.redirectTo({
|
|
url: path,
|
|
fail: () => {
|
|
wx.navigateTo({ url: path });
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
wx.navigateTo({ url: path });
|
|
},
|
|
|
|
onShellTap() {
|
|
const sessionSnapshot = getTerminalSessionSnapshot();
|
|
const serverId = resolveActiveTerminalServerId(sessionSnapshot, listServers());
|
|
const language = normalizeUiLanguage(getSettings().uiLanguage);
|
|
if (!serverId) {
|
|
wx.showModal({
|
|
title: t(language, "bottomNav.modal.noTerminalTitle"),
|
|
content: t(language, "bottomNav.modal.noTerminalContent"),
|
|
showCancel: false
|
|
});
|
|
return;
|
|
}
|
|
openTerminalPage(serverId, true);
|
|
},
|
|
|
|
resolveTextLabel(key, copyInput) {
|
|
const copy = copyInput && typeof copyInput === "object" ? copyInput : buildPageCopy("zh-Hans", "bottomNav");
|
|
const pageTextLabels = (copy && copy.pageTextLabels) || {};
|
|
const normalized = String(key || "").trim();
|
|
return pageTextLabels[normalized] || pageTextLabels.default || "页";
|
|
},
|
|
|
|
...createSvgButtonPressMethods()
|
|
}
|
|
});
|