first commit
This commit is contained in:
351
apps/miniprogram/components/bottom-nav/index.js
Normal file
351
apps/miniprogram/components/bottom-nav/index.js
Normal file
@@ -0,0 +1,351 @@
|
||||
/* 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()
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user