主题与字号从页面顶栏搬进设置页的「外观」,顶部导航加上预设入口。

顶栏那套汉堡菜单在每页都要单独挂载、样式也重复,收进设置页后只剩一处改;顺手把「整组都没有曾用型号」的配置组整组收起,不再留空卡片。
This commit is contained in:
2026-10-26 01:09:43 +08:00
parent f3303038fe
commit e2413b0677
11 changed files with 251 additions and 462 deletions
+112 -14
View File
@@ -9,6 +9,17 @@ import {
sortByApiProfileUsage,
} from "./api-profile-usage.js";
import { readSniffModels, writeSniffModels } from "./model-sniff-cache.js";
import {
applyColorTheme,
applyUiFontScale,
bumpUiFontScale,
COLOR_THEMES,
getStoredColorTheme,
getStoredUiFontScale,
UI_FONT_SCALE_DEFAULT,
UI_FONT_SCALE_MAX,
UI_FONT_SCALE_MIN,
} from "./theme.js";
let state = {
settings: { activeProfileId: null, activePresetId: null },
@@ -29,6 +40,8 @@ let sniffBootId = "";
let sniffRestoreSeq = 0;
/** 组 id → 是否展开显示该组全部配置(默认只显示曾选用);有搜索词时按匹配显示 */
const showAllProfilesByGroup = new Set();
/** 上一轮渲染里因为一个曾用型号都没有而整组收起的组 id */
let lastHiddenGroupIds = [];
let profileSearchQuery = "";
let editingPersonaId = null;
let activeSection = "api";
@@ -59,6 +72,12 @@ const sectionPreferenceEl = document.getElementById("section-preference");
const sectionPresetEl = document.getElementById("section-preset");
const sectionStorageEl = document.getElementById("section-storage");
const sectionChatEl = document.getElementById("section-chat");
const sectionAppearanceEl = document.getElementById("section-appearance");
const appearanceThemesEl = document.getElementById("appearance-themes");
const appearanceScaleValueEl = document.getElementById("appearance-scale-value");
const appearanceScaleMinusEl = document.getElementById("appearance-scale-minus");
const appearanceScalePlusEl = document.getElementById("appearance-scale-plus");
const appearanceScaleResetEl = document.getElementById("appearance-scale-reset");
const preferenceCollectEveryEl = document.getElementById("preference-collect-every");
const activeSettingsBarEl = document.getElementById("active-settings-bar");
const settingsToastEl = document.getElementById("settings-toast");
@@ -126,6 +145,35 @@ function showToast(message, isError = false) {
}, 4000);
}
/** 外观(主题/字号)只存在本机浏览器里,不走服务端设置。 */
const APPEARANCE_STORAGE_KEYS = ["wa-color-theme", "wa-ui-font-scale"];
function renderAppearanceThemes() {
if (!appearanceThemesEl || appearanceThemesEl.childElementCount) return;
appearanceThemesEl.innerHTML = COLOR_THEMES.map(
(theme) => `<button type="button" class="appearance-theme" data-appearance-theme="${theme.id}" role="radio" aria-checked="false">
<span class="appearance-theme-icon" aria-hidden="true">${theme.icon}</span>
<span class="appearance-theme-copy">
<span class="appearance-theme-name">${escapeHtml(theme.name)}</span>
<span class="appearance-theme-desc">${escapeHtml(theme.description)}</span>
</span>
</button>`,
).join("");
}
function syncAppearanceUi() {
const activeTheme = getStoredColorTheme();
appearanceThemesEl?.querySelectorAll("[data-appearance-theme]").forEach((btn) => {
const on = btn.getAttribute("data-appearance-theme") === activeTheme;
btn.classList.toggle("is-active", on);
btn.setAttribute("aria-checked", String(on));
});
const percent = getStoredUiFontScale();
if (appearanceScaleValueEl) appearanceScaleValueEl.textContent = `${percent}%`;
if (appearanceScaleMinusEl) appearanceScaleMinusEl.disabled = percent <= UI_FONT_SCALE_MIN;
if (appearanceScalePlusEl) appearanceScalePlusEl.disabled = percent >= UI_FONT_SCALE_MAX;
}
function renderActiveBar() {
const profile = state.profiles.find(
(p) => p.id === state.settings.activeProfileId,
@@ -159,6 +207,8 @@ function switchSection(section) {
sectionPresetEl.classList.toggle("hidden", section !== "preset");
sectionStorageEl?.classList.toggle("hidden", section !== "storage");
sectionChatEl?.classList.toggle("hidden", section !== "chat");
sectionAppearanceEl?.classList.toggle("hidden", section !== "appearance");
if (section === "appearance") syncAppearanceUi();
renderPanelHeader();
}
@@ -197,6 +247,9 @@ function renderPanelHeader() {
btn.textContent = "导入 JSON";
btn.addEventListener("click", () => presetFileEl.click());
panelActionsEl.appendChild(btn);
} else if (activeSection === "appearance") {
panelTitleEl.textContent = "外观";
panelSubtitleEl.textContent = "主题与字号,只影响这台浏览器";
} else if (activeSection === "chat") {
panelTitleEl.textContent = "直聊";
panelSubtitleEl.textContent = "卡片拖动的跟手倍率";
@@ -338,6 +391,7 @@ function renderProfiles() {
? state.groups
: [{ id: "", name: "未分组", baseUrl: "" }];
const seen = new Set();
const hiddenGroupIds = [];
let shownGroups = 0;
for (const group of groups) {
@@ -352,6 +406,11 @@ function renderProfiles() {
activeId,
);
if (query && !visible.length) continue;
// 默认「只看曾用」时,整组都没有曾用型号就整组收起,不留空卡片。
if (!showAll && !visible.length) {
hiddenGroupIds.push(group.id);
continue;
}
const hasActive = members.some((p) => p.id === activeId);
const sample = members[0];
@@ -409,19 +468,10 @@ function renderProfiles() {
</div>
</div>`;
if (!visible.length) {
const empty = document.createElement("p");
empty.className = "profile-group-empty";
empty.textContent = query
? "没有匹配的模型"
: "本组还没有曾选用过的型号。点「显示全部」,或「添加型号」。";
card.appendChild(empty);
} else {
const rows = document.createElement("div");
rows.className = "profile-model-rows";
for (const p of visible) rows.appendChild(renderModelRow(p));
card.appendChild(rows);
}
const rows = document.createElement("div");
rows.className = "profile-model-rows";
for (const p of visible) rows.appendChild(renderModelRow(p));
card.appendChild(rows);
profilesListEl.appendChild(card);
shownGroups += 1;
@@ -452,7 +502,17 @@ function renderProfiles() {
shownGroups += 1;
}
if (!shownGroups) {
lastHiddenGroupIds = hiddenGroupIds;
// 一个曾用型号都没有的组整组收起后,留一行提示,需要时还能展开。
if (hiddenGroupIds.length) {
const hint = document.createElement("p");
hint.className = "empty-hint profile-hidden-groups";
hint.innerHTML = `已自动隐藏 ${hiddenGroupIds.length} 个还没有曾用型号的组
<button type="button" class="profile-group-toggle" data-action="reveal-hidden-groups">显示这些组</button>`;
profilesListEl.appendChild(hint);
}
if (!shownGroups && !hiddenGroupIds.length) {
profilesListEl.innerHTML = query
? `<p class="empty-hint">没有匹配「${escapeHtml(profileSearchQuery.trim())}」的配置。</p>`
: '<p class="empty-hint">暂无配置,点击右上角「新增配置」。</p>';
@@ -1525,6 +1585,13 @@ profilesListEl.addEventListener("click", async (e) => {
const action = btn.dataset.action;
try {
if (action === "reveal-hidden-groups") {
for (const hiddenId of lastHiddenGroupIds) {
showAllProfilesByGroup.add(hiddenId);
}
renderProfiles();
return;
}
if (action === "toggle-group-all") {
if (showAllProfilesByGroup.has(id)) showAllProfilesByGroup.delete(id);
else showAllProfilesByGroup.add(id);
@@ -1924,6 +1991,37 @@ document.getElementById("btn-clear-context")?.addEventListener("click", async ()
}
});
appearanceThemesEl?.addEventListener("click", (e) => {
const btn = e.target.closest("[data-appearance-theme]");
if (!btn) return;
applyColorTheme(btn.getAttribute("data-appearance-theme"));
syncAppearanceUi();
});
appearanceScaleMinusEl?.addEventListener("click", () => {
bumpUiFontScale(-1);
syncAppearanceUi();
});
appearanceScalePlusEl?.addEventListener("click", () => {
bumpUiFontScale(1);
syncAppearanceUi();
});
appearanceScaleResetEl?.addEventListener("click", () => {
applyColorTheme("modern");
applyUiFontScale(UI_FONT_SCALE_DEFAULT);
syncAppearanceUi();
showToast("外观已恢复默认");
});
window.addEventListener("storage", (e) => {
if (APPEARANCE_STORAGE_KEYS.includes(e.key)) syncAppearanceUi();
});
renderAppearanceThemes();
syncAppearanceUi();
renderPanelHeader();
loadAll().catch((err) => {
showToast(`加载设置失败: ${err.message}`, true);