Files
remoteconn-gitea/apps/miniprogram/components/bottom-nav/index.js
2026-03-21 18:57:10 +08:00

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()
}
});