|
|
@@ -0,0 +1,348 @@
|
|
|
+/**
|
|
|
+ * 大屏昼夜主题:CSS 变量写入 document.documentElement,配置持久化 localStorage。
|
|
|
+ * 详见 docs/THEME.md
|
|
|
+ */
|
|
|
+export const UI_THEME_STORAGE_KEY = "industry_map_ui_theme_v1";
|
|
|
+
|
|
|
+/** 左右侧悬浮栏宽度(px),与主题一并持久化 */
|
|
|
+export const DEFAULT_SIDEBAR_WIDTH_PX = 432;
|
|
|
+export const SIDEBAR_WIDTH_MIN_PX = 280;
|
|
|
+export const SIDEBAR_WIDTH_MAX_PX = 560;
|
|
|
+
|
|
|
+export function clampSidebarWidthPx(px) {
|
|
|
+ if (px === undefined || px === null || px === "") {
|
|
|
+ return DEFAULT_SIDEBAR_WIDTH_PX;
|
|
|
+ }
|
|
|
+ const n = Math.round(Number(px));
|
|
|
+ if (Number.isNaN(n)) return DEFAULT_SIDEBAR_WIDTH_PX;
|
|
|
+ return Math.min(SIDEBAR_WIDTH_MAX_PX, Math.max(SIDEBAR_WIDTH_MIN_PX, n));
|
|
|
+}
|
|
|
+
|
|
|
+/** 写入 :root CSS 变量,供侧栏/搜索框等 calc 使用 */
|
|
|
+export function applySidebarWidthPx(px) {
|
|
|
+ const w = clampSidebarWidthPx(px);
|
|
|
+ document.documentElement.style.setProperty("--ui-sidebar-width", `${w}px`);
|
|
|
+ return w;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 与 `qpjyj.vue` / `appMap.vue` 中底图 `value` 一致。
|
|
|
+ * 仅当当前底图为该项时使用「黑夜」主题,其它底图使用「白天」主题。
|
|
|
+ */
|
|
|
+export const BASE_MAP_KEY_NIGHT = "shmap_blue_web";
|
|
|
+
|
|
|
+export function getUiModeForBaseMap(mapKey) {
|
|
|
+ return mapKey === BASE_MAP_KEY_NIGHT ? "night" : "day";
|
|
|
+}
|
|
|
+
|
|
|
+/** 主题配置对话框可编辑的 Token(保存时只比对这组,避免写入已废弃键) */
|
|
|
+export const THEME_CONFIG_PICKER_KEYS = [
|
|
|
+ "--ui-surface-bg",
|
|
|
+ "--ui-text-title",
|
|
|
+ "--ui-text-1",
|
|
|
+ "--ui-text-2",
|
|
|
+ "--ui-text-3",
|
|
|
+ "--ui-text-desc",
|
|
|
+ "--ui-text-placeholder",
|
|
|
+ "--ui-main",
|
|
|
+];
|
|
|
+
|
|
|
+/** 与文档约定的变量名(含派生/兼容项) */
|
|
|
+export const UI_THEME_VAR_KEYS = [
|
|
|
+ "--ui-surface-bg",
|
|
|
+ "--ui-text-title",
|
|
|
+ "--ui-text-1",
|
|
|
+ "--ui-text-2",
|
|
|
+ "--ui-text-3",
|
|
|
+ "--ui-text-desc",
|
|
|
+ "--ui-text-placeholder",
|
|
|
+ "--ui-main",
|
|
|
+ "--ui-main-hover",
|
|
|
+ "--ui-border-accent",
|
|
|
+ "--ui-search-bg",
|
|
|
+ "--ui-search-border",
|
|
|
+ "--ui-search-text",
|
|
|
+ "--ui-search-secondary",
|
|
|
+ "--ui-search-result-active",
|
|
|
+ "--ui-panel-bg",
|
|
|
+ "--ui-panel-text",
|
|
|
+ "--ui-card-fill",
|
|
|
+ "--ui-card-title-text",
|
|
|
+ "--ui-card-title-shadow",
|
|
|
+ "--ui-card-back-highlight",
|
|
|
+ "--ui-tag-bg",
|
|
|
+ "--ui-tag-text",
|
|
|
+ "--ui-title-section",
|
|
|
+ "--ui-title2",
|
|
|
+ "--ui-link",
|
|
|
+ "--ui-fold-text",
|
|
|
+ "--ui-fold-border",
|
|
|
+ "--ui-sync-label",
|
|
|
+ "--ui-header-en",
|
|
|
+ "--ui-header-title-solid",
|
|
|
+ "--ui-map-bg",
|
|
|
+ "--ui-marker-label-bg",
|
|
|
+ "--ui-marker-label-text",
|
|
|
+ "--ui-dropdown-bg",
|
|
|
+ "--ui-dropdown-border",
|
|
|
+ "--ui-dropdown-text",
|
|
|
+ "--ui-dropdown-hover",
|
|
|
+ "--ui-dropdown-selected-bg",
|
|
|
+ "--ui-modal-surface",
|
|
|
+ "--ui-popup-tip-bg",
|
|
|
+ "--ui-popup-inner-bg",
|
|
|
+ "--ui-popup-text",
|
|
|
+ "--ui-popup-muted",
|
|
|
+ "--ui-popup-accent",
|
|
|
+ "--ui-popup-card-bg",
|
|
|
+ "--ui-divider-main",
|
|
|
+ "--ui-control-highlight",
|
|
|
+];
|
|
|
+
|
|
|
+const SURFACE_NIGHT = "rgba(0, 204, 255, 0.15)";
|
|
|
+const SURFACE_DAY = "rgba(255, 255, 255, 0.26)";
|
|
|
+
|
|
|
+const NIGHT = {
|
|
|
+ "--ui-surface-bg": SURFACE_NIGHT,
|
|
|
+ "--ui-panel-bg": SURFACE_NIGHT,
|
|
|
+ "--ui-search-bg": SURFACE_NIGHT,
|
|
|
+ "--ui-popup-inner-bg": SURFACE_NIGHT,
|
|
|
+ "--ui-popup-card-bg": SURFACE_NIGHT,
|
|
|
+ "--ui-text-title": "#ffffff",
|
|
|
+ "--ui-text-1": "#ffffff",
|
|
|
+ "--ui-text-2": "#a8e8f0",
|
|
|
+ "--ui-text-3": "#8ec9d4",
|
|
|
+ "--ui-text-desc": "#c5cdd1",
|
|
|
+ "--ui-text-placeholder": "rgba(255, 255, 255, 0.45)",
|
|
|
+ "--ui-main": "#1dc8dc",
|
|
|
+ "--ui-main-hover": "#22dcf0",
|
|
|
+ "--ui-border-accent": "#1dc8dc",
|
|
|
+ "--ui-search-border": "#1dc8dc",
|
|
|
+ "--ui-search-result-active": "#1dc8dc",
|
|
|
+ "--ui-card-fill": "rgba(32, 130, 224, 0.195)",
|
|
|
+ "--ui-card-title-text": "#ffffff",
|
|
|
+ "--ui-card-title-shadow": "1px 2px 2px black",
|
|
|
+ "--ui-card-back-highlight": "rgba(31, 199, 255, 0.6)",
|
|
|
+ "--ui-tag-bg": "#063e4f",
|
|
|
+ "--ui-tag-text": "#ffffff",
|
|
|
+ "--ui-title-section": "#a8e8f0",
|
|
|
+ "--ui-title2": "#a8e8f0",
|
|
|
+ "--ui-link": "#22e9ff",
|
|
|
+ "--ui-fold-text": "#ffffff",
|
|
|
+ "--ui-fold-border": "#22e9ff",
|
|
|
+ "--ui-sync-label": "#22dcf0",
|
|
|
+ "--ui-header-en": "#87d9ff",
|
|
|
+ "--ui-header-title-solid": "#ffffff",
|
|
|
+ "--ui-map-bg": "#000000",
|
|
|
+ "--ui-marker-label-bg": "rgba(0, 0, 0, 0.39)",
|
|
|
+ "--ui-marker-label-text": "#ffffff",
|
|
|
+ "--ui-dropdown-bg": "rgba(0, 204, 255, 0.15)",
|
|
|
+ "--ui-dropdown-border": "rgba(0, 204, 255, 0.3)",
|
|
|
+ "--ui-dropdown-text": "#ffffff",
|
|
|
+ "--ui-dropdown-hover": "rgba(0, 204, 255, 0.15)",
|
|
|
+ "--ui-dropdown-selected-bg": "rgba(0, 204, 255, 0.3)",
|
|
|
+ /* 主题配置等模态:高不透明度 + 与白天一致的毛玻璃思路,避免误用下拉半透明底导致透地图 */
|
|
|
+ "--ui-modal-surface": "rgba(8, 36, 46, 0.88)",
|
|
|
+ "--ui-popup-tip-bg": "#1dc8dc",
|
|
|
+ "--ui-popup-text": "#ffffff",
|
|
|
+ "--ui-popup-muted": "#c5cdd1",
|
|
|
+ "--ui-popup-accent": "#22e9ff",
|
|
|
+ "--ui-divider-main": "#1dc8dc",
|
|
|
+ "--ui-control-highlight": "rgba(0, 204, 255, 0.6)",
|
|
|
+ "--ui-panel-text": "#ffffff",
|
|
|
+};
|
|
|
+
|
|
|
+const DAY = {
|
|
|
+ "--ui-surface-bg": SURFACE_DAY,
|
|
|
+ "--ui-panel-bg": SURFACE_DAY,
|
|
|
+ "--ui-search-bg": SURFACE_DAY,
|
|
|
+ "--ui-popup-inner-bg": SURFACE_DAY,
|
|
|
+ "--ui-popup-card-bg": SURFACE_DAY,
|
|
|
+ "--ui-text-title": "#0d7ea0",
|
|
|
+ "--ui-text-1": "#0e6969",
|
|
|
+ "--ui-text-2": "#3d5a66",
|
|
|
+ "--ui-text-3": "#606266",
|
|
|
+ "--ui-text-desc": "#909399",
|
|
|
+ "--ui-text-placeholder": "#c0c4cc",
|
|
|
+ "--ui-main": "#0d7ea0",
|
|
|
+ "--ui-main-hover": "#0a6580",
|
|
|
+ "--ui-border-accent": "#0d7ea0",
|
|
|
+ "--ui-search-border": "#b8c5cc",
|
|
|
+ "--ui-search-result-active": "#0d7ea0",
|
|
|
+ "--ui-card-fill": "rgba(255, 255, 255, 0.98)",
|
|
|
+ "--ui-card-title-text": "#0d7ea0",
|
|
|
+ "--ui-card-title-shadow": "none",
|
|
|
+ "--ui-card-back-highlight": "rgba(13, 126, 160, 0.12)",
|
|
|
+ "--ui-tag-bg": "#e8eef2",
|
|
|
+ "--ui-tag-text": "#0e6969",
|
|
|
+ "--ui-title-section": "#3d5a66",
|
|
|
+ "--ui-title2": "#3d5a66",
|
|
|
+ "--ui-link": "#0d7ea0",
|
|
|
+ "--ui-fold-text": "#0e6969",
|
|
|
+ "--ui-fold-border": "#0d7ea0",
|
|
|
+ "--ui-sync-label": "#0d7ea0",
|
|
|
+ "--ui-header-en": "#0d7ea0",
|
|
|
+ "--ui-header-title-solid": "#1a3a4a",
|
|
|
+ "--ui-map-bg": "#e8ecef",
|
|
|
+ "--ui-marker-label-bg": "rgba(255, 255, 255, 0.92)",
|
|
|
+ "--ui-marker-label-text": "#1a1a1a",
|
|
|
+ "--ui-dropdown-bg": "#ffffff",
|
|
|
+ "--ui-dropdown-border": "#dcdfe6",
|
|
|
+ "--ui-dropdown-text": "#0e6969",
|
|
|
+ "--ui-dropdown-hover": "#f5f7fa",
|
|
|
+ "--ui-dropdown-selected-bg": "#e6f4f8",
|
|
|
+ "--ui-modal-surface": "rgba(255, 255, 255, 0.9)",
|
|
|
+ "--ui-popup-tip-bg": "#0d7ea0",
|
|
|
+ "--ui-popup-text": "#0e6969",
|
|
|
+ "--ui-popup-muted": "#909399",
|
|
|
+ "--ui-popup-accent": "#0d7ea0",
|
|
|
+ "--ui-divider-main": "#0d7ea0",
|
|
|
+ "--ui-control-highlight": "rgba(13, 126, 160, 0.35)",
|
|
|
+ "--ui-panel-text": "#0e6969",
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 将「表面背景」「分级文字」派生到旧 Token,保证全局样式一处配置、多处生效。
|
|
|
+ */
|
|
|
+export function resolveThemeTokens(merged) {
|
|
|
+ const out = { ...merged };
|
|
|
+ const surf =
|
|
|
+ out["--ui-surface-bg"] ||
|
|
|
+ out["--ui-panel-bg"] ||
|
|
|
+ out["--ui-search-bg"] ||
|
|
|
+ out["--ui-popup-inner-bg"];
|
|
|
+ if (surf) {
|
|
|
+ out["--ui-surface-bg"] = surf;
|
|
|
+ out["--ui-panel-bg"] = surf;
|
|
|
+ out["--ui-search-bg"] = surf;
|
|
|
+ out["--ui-popup-inner-bg"] = surf;
|
|
|
+ out["--ui-popup-card-bg"] = surf;
|
|
|
+ }
|
|
|
+ if (out["--ui-text-title"]) {
|
|
|
+ out["--ui-card-title-text"] = out["--ui-text-title"];
|
|
|
+ }
|
|
|
+ if (out["--ui-text-1"]) {
|
|
|
+ out["--ui-panel-text"] = out["--ui-text-1"];
|
|
|
+ out["--ui-popup-text"] = out["--ui-text-1"];
|
|
|
+ out["--ui-search-text"] = out["--ui-text-1"];
|
|
|
+ out["--ui-tag-text"] = out["--ui-text-1"];
|
|
|
+ }
|
|
|
+ if (out["--ui-text-2"]) {
|
|
|
+ out["--ui-title2"] = out["--ui-text-2"];
|
|
|
+ out["--ui-title-section"] = out["--ui-text-2"];
|
|
|
+ }
|
|
|
+ if (out["--ui-text-3"]) {
|
|
|
+ out["--ui-search-secondary"] = out["--ui-text-3"];
|
|
|
+ }
|
|
|
+ if (out["--ui-text-desc"]) {
|
|
|
+ out["--ui-popup-muted"] = out["--ui-text-desc"];
|
|
|
+ }
|
|
|
+ if (out["--ui-main"]) {
|
|
|
+ out["--ui-border-accent"] = out["--ui-main"];
|
|
|
+ out["--ui-popup-accent"] = out["--ui-main"];
|
|
|
+ out["--ui-link"] = out["--ui-main"];
|
|
|
+ out["--ui-search-result-active"] = out["--ui-main"];
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+export function getBaseTokens(mode) {
|
|
|
+ const m = mode === "day" ? DAY : NIGHT;
|
|
|
+ return resolveThemeTokens({ ...m });
|
|
|
+}
|
|
|
+
|
|
|
+/** 深拷贝为 { night, day },缺省补空对象 */
|
|
|
+export function normalizeOverridesByMode(raw) {
|
|
|
+ const empty = { night: {}, day: {} };
|
|
|
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { ...empty };
|
|
|
+ const night =
|
|
|
+ raw.night && typeof raw.night === "object" && !Array.isArray(raw.night)
|
|
|
+ ? { ...raw.night }
|
|
|
+ : {};
|
|
|
+ const day =
|
|
|
+ raw.day && typeof raw.day === "object" && !Array.isArray(raw.day)
|
|
|
+ ? { ...raw.day }
|
|
|
+ : {};
|
|
|
+ return { night, day };
|
|
|
+}
|
|
|
+
|
|
|
+export function loadStoredTheme() {
|
|
|
+ try {
|
|
|
+ const raw = localStorage.getItem(UI_THEME_STORAGE_KEY);
|
|
|
+ if (!raw) {
|
|
|
+ return {
|
|
|
+ mode: "night",
|
|
|
+ overridesByMode: { night: {}, day: {} },
|
|
|
+ sidebarWidthPx: DEFAULT_SIDEBAR_WIDTH_PX,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ const o = JSON.parse(raw);
|
|
|
+ const mode = o.mode === "day" ? "day" : "night";
|
|
|
+ let overridesByMode = { night: {}, day: {} };
|
|
|
+
|
|
|
+ if (
|
|
|
+ o.overridesByMode &&
|
|
|
+ typeof o.overridesByMode === "object" &&
|
|
|
+ !Array.isArray(o.overridesByMode)
|
|
|
+ ) {
|
|
|
+ overridesByMode = normalizeOverridesByMode(o.overridesByMode);
|
|
|
+ } else if (
|
|
|
+ o.overrides &&
|
|
|
+ typeof o.overrides === "object" &&
|
|
|
+ !Array.isArray(o.overrides)
|
|
|
+ ) {
|
|
|
+ /* 旧版:单份 overrides 仅属于当时记录的 mode,避免白天改动污染黑夜 */
|
|
|
+ overridesByMode[mode] = { ...o.overrides };
|
|
|
+ }
|
|
|
+ const sidebarWidthPx = clampSidebarWidthPx(o.sidebarWidthPx);
|
|
|
+ return { mode, overridesByMode, sidebarWidthPx };
|
|
|
+ } catch {
|
|
|
+ return {
|
|
|
+ mode: "night",
|
|
|
+ overridesByMode: { night: {}, day: {} },
|
|
|
+ sidebarWidthPx: DEFAULT_SIDEBAR_WIDTH_PX,
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export function applyUiTheme(mode, overrides = {}) {
|
|
|
+ const base = mode === "day" ? { ...DAY } : { ...NIGHT };
|
|
|
+ const merged = resolveThemeTokens({ ...base, ...overrides });
|
|
|
+ const root = document.documentElement;
|
|
|
+ root.setAttribute("data-ui-theme", mode);
|
|
|
+ Object.keys(merged).forEach((k) => {
|
|
|
+ if (merged[k] != null && merged[k] !== "") {
|
|
|
+ root.style.setProperty(k, merged[k]);
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * @param {"night"|"day"} mode 当前界面模式(与底图一致)
|
|
|
+ * @param {{ night?: Record<string,string>, day?: Record<string,string> }} overridesByMode 昼夜各自覆盖,互不影响
|
|
|
+ */
|
|
|
+export function persistTheme(mode, overridesByMode, sidebarWidthPx) {
|
|
|
+ let prev = {};
|
|
|
+ try {
|
|
|
+ prev = JSON.parse(localStorage.getItem(UI_THEME_STORAGE_KEY) || "{}");
|
|
|
+ } catch {
|
|
|
+ prev = {};
|
|
|
+ }
|
|
|
+ const obm = normalizeOverridesByMode(overridesByMode);
|
|
|
+ const m = mode === "day" ? "day" : "night";
|
|
|
+ const sw =
|
|
|
+ sidebarWidthPx !== undefined && sidebarWidthPx !== null
|
|
|
+ ? clampSidebarWidthPx(sidebarWidthPx)
|
|
|
+ : clampSidebarWidthPx(prev.sidebarWidthPx);
|
|
|
+ localStorage.setItem(
|
|
|
+ UI_THEME_STORAGE_KEY,
|
|
|
+ JSON.stringify({ mode: m, overridesByMode: obm, sidebarWidthPx: sw })
|
|
|
+ );
|
|
|
+ applyUiTheme(m, obm[m] || {});
|
|
|
+ applySidebarWidthPx(sw);
|
|
|
+}
|
|
|
+
|
|
|
+export function getMergedTokens(mode, overrides = {}) {
|
|
|
+ const base = mode === "day" ? { ...DAY } : { ...NIGHT };
|
|
|
+ return resolveThemeTokens({ ...base, ...overrides });
|
|
|
+}
|