skinsuite 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/views/params.d.ts +67 -1
- package/dist/views/params.d.ts.map +1 -1
- package/dist/views/params.js +121 -0
- package/dist/views/params.js.map +1 -1
- package/docs/onboarding.md +13 -3
- package/package.json +1 -1
- package/src/index.ts +9 -2
- package/src/views/params.ts +98 -1
- package/src/vue/ThemeView.vue +7 -6
- package/src/vue/create-theme-state.ts +26 -1
- package/src/vue/theme-scope.ts +87 -4
- package/src/vue/theme-store.ts +11 -0
- package/src/vue/useThemeDebugPanel.ts +15 -15
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,69 @@
|
|
|
9
9
|
> 旧名 `mauth-theme-core` 的 `0.1.0` / `0.1.1` 均已标记 **deprecated** 并保留可安装,不再更新 ——
|
|
10
10
|
> 它们的代码与 `skinsuite@0.1.0` 完全一致,只有包名不同。
|
|
11
11
|
|
|
12
|
+
## 0.4.0 — 2026-10-20
|
|
13
|
+
|
|
14
|
+
**版式意图 store 化** —— URL 一次读入、store 是唯一事实源,与配色侧(`themeId` / `deviceTheme`)完全同形。在此之前版式是"每次渲染现读 `route.query`",debug 面板换版式必须改 URL(触发路由变化);现在与配色一样:URL 只负责**初始化播种**,运行时一切读写走 store。
|
|
15
|
+
|
|
16
|
+
### 新增
|
|
17
|
+
|
|
18
|
+
- **`views/params.ts` 一次性读入半边**(零 import,设备清单由调用方注入)
|
|
19
|
+
- `readUrlViewIntent(query, viewParam, devices)` —— 把 URL 上所有 `th*` 原始值解析成 `{ deviceView, viewIntent }`:设备专属键(`?th_<urlKey>`)逐设备记,通用键(`?th`)原样进 `viewIntent`(不下发副本,由 `deviceViewOf` 兜底)。空串 = 未指定、重复键(数组)= 未指定,与配色侧同口径
|
|
20
|
+
- `viewParamsSnapshot(query, viewParam, devices)` / `sameViewSnapshot(a, b)` —— `th*` 键白名单快照 + 判等:**仅当 URL 原始值变化才同步**,debug 改写(只碰 store)不会误触发。白名单 = `th` + `th_<urlKey>`(**不是**前缀匹配——那会把配色键 `th_color` 卷进来)
|
|
21
|
+
- 类型 `DeviceViewIntent = Partial<Record<string, string>>`
|
|
22
|
+
- **`useThemeScope(keys, colorParam, viewParam)` 新增版式意图面**(与配色侧逐字同构)
|
|
23
|
+
- `deviceView` / `viewIntent` 两个 ref、`deviceViewOf(device)`(专属键 > 通用键 > null)、`setDeviceView(device, id)` / `clearDeviceViewOverride(device)`(显式操作,只写 store)、`syncUrlViewIntent()`(快照判等后重新读入)
|
|
24
|
+
- **`ThemeView.vue` 与调试面板改数据源**:`<ThemeView>` 的 URL 兜底源从"每次渲染现读 `route.query`"换成 `store.deviceViewOf(props.device)`;`useThemeDebugPanel` 的 `pickView` 从 `replaceQuery`(改 URL)换成 `store.setDeviceView` —— **调试换版式零路由变化**
|
|
25
|
+
- **`createThemeState` 的 `viewParam` option** + `watch(() => currentQuery())` 编排:跨页带不同 `th`(/login → /register)仍会同步进 store
|
|
26
|
+
|
|
27
|
+
### 迁移
|
|
28
|
+
|
|
29
|
+
- `ThemeStoreLike`(`vue` 出口的类型契约)新增 `deviceViewOf` / `setDeviceView` / `clearDeviceViewOverride` / `viewIntent` 四个成员;自建 store 壳的宿主需要补齐(oauth21 已同步)
|
|
30
|
+
- 版式/配色参数键形不变(`th` / `th_<urlKey>`、`th_color` / `th_color_<urlKey>`),URL 兼容面无破坏
|
|
31
|
+
|
|
32
|
+
### 说明
|
|
33
|
+
|
|
34
|
+
- 版式意图**不做合法性校验、不落盘**:版式 id 合不合法是渲染期知识(当前包里有没有),由 `pick()` 兜底;版式也没有持久化语义(换包即重置)
|
|
35
|
+
- 详见契约文档 `oauth21/docs/login-theme-routing.md`
|
|
36
|
+
|
|
37
|
+
## 0.3.1 — 2026-10-19
|
|
38
|
+
|
|
39
|
+
- docs:设备判定示例从内核注释挪到宿主 `utils/device`(判定属于宿主,内核只收注入)
|
|
40
|
+
|
|
41
|
+
## 0.3.0 — 2026-10-19
|
|
42
|
+
|
|
43
|
+
### 破坏性变更
|
|
44
|
+
|
|
45
|
+
- **设备判定撤出内核**:`views/device.ts` 的 `resolveCurrentDevice`(视口宽度 / UA 嗅探)删除,改为宿主通过 `app.use(theme, { resolveDevice })` 注入。理由:设备形态判定依赖宿主的断点口径与 UA 词表,属于宿主策略;内核只保留"声明 + 查询"(`defineThemeDevice` / `registeredDevices` / `urlKeyOf` / `containersOf`)。详见 `docs/onboarding.md`
|
|
46
|
+
|
|
47
|
+
## 0.2.3 — 2026-10-19
|
|
48
|
+
|
|
49
|
+
- fix(vite):扫描 `views/*.ts` 时误用 `.js` 后缀拼 glob,声明恒为空(vite 插件认 `.ts` 源文件)
|
|
50
|
+
|
|
51
|
+
## 0.2.2 — 2026-10-19
|
|
52
|
+
|
|
53
|
+
- fix(vite):构建插件(`./vite` 入口)改为以**编译产物**发布(`dist/vite/`),源码只留作类型入口——此前发布的是 TS 源,Node 侧加载即报错
|
|
54
|
+
|
|
55
|
+
## 0.2.1 — 2026-10-19
|
|
56
|
+
|
|
57
|
+
- fix(exports):根入口 `types` 统一指向 `src/`,消除 `dist/src` 双身份(此前 `types` 指向 dist 编译产物,跳转会落到编译目录)
|
|
58
|
+
|
|
59
|
+
## 0.2.0 — 2026-10-19
|
|
60
|
+
|
|
61
|
+
**机制全量下沉** —— 从 0.1.0 的"纯叶子层"扩为完整机制:配色注册表、版式机制、设备机制、主题状态机、构建插件全部收进本包。宿主(`oauth21`)侧只剩声明文件(设备清单 / 页面 glob / 主题目录)与一行 `defineStore` 壳。
|
|
62
|
+
|
|
63
|
+
### 新增
|
|
64
|
+
|
|
65
|
+
- **根入口(`.`)**:`src/registry.ts`(`buildThemeRegistry` / `installThemeRegistry` / `getThemeRecord` / `listThemes` 等查询函数)、`src/manifest.ts`(virtual:theme 清单单例)、`src/scheme.ts`(mode / tone / remote config)、`src/views/device.ts`(设备声明与查询)
|
|
66
|
+
- **Vue 适配层(`./vue`)**:`createThemeState`(状态机编排:scope / intent / runtime 三 composable + 持久化层)、`ThemeRoot.vue` / `ThemeView.vue` / `ThemeDebugPanel.vue`、`theme` 插件(`app.use(theme, { router, resolveDevice })`)、`useThemeSources` / `useParentThemeSync` / `useThemeDebugPanel`、`useThemeStore` 注入键
|
|
67
|
+
- **构建器入口(`./vite`)**:扫描 `src/theme/themes/` 生成注册表、扫 `views/*.ts` 装配版式、注入 `virtual:theme`、`VITE_THEME_DIR` 常量
|
|
68
|
+
- **`themeStoreKey`**:跨组件访问宿主 store 的注入键(调试面板无参化的基础)
|
|
69
|
+
|
|
70
|
+
### 交付形态变化
|
|
71
|
+
|
|
72
|
+
- `exports` 从单一 `.` 扩为 `.` / `./vue` / `./vite` 三个子路径;`./vue` 以源码发布(`.vue` / `.ts` 需宿主构建器处理),`.` 与 `./vite` 走 `dist/`
|
|
73
|
+
- 测试扩到 23 项,`node --test` 仍针对构建产物
|
|
74
|
+
|
|
12
75
|
## 0.1.0 — 2026-09-27
|
|
13
76
|
|
|
14
77
|
首个公开发布。内容 = 主题机制里的**纯叶子层**(抽包 Stage 1),从宿主 [`oauth21`](https://github.com/yijiu2025/CoreFlow) 的 `src/theme/` 原样迁出,宿主侧改为零逻辑转发壳。
|
package/dist/index.d.ts
CHANGED
|
@@ -40,8 +40,8 @@ export type { MauthThemeMeta, MauthThemePackage, MauthThemeColor, MauthThemeReco
|
|
|
40
40
|
export { buildThemeRegistry, installThemeRegistry } from './registry.js';
|
|
41
41
|
export type { ThemeAssetModules } from './registry.js';
|
|
42
42
|
export { listThemes, listColorsOf, toneOfColor, toneOfAnyScope, listColorIdsOfTone, resolveThemeId, getThemeRecord, getDefaultThemeId, isKnownTheme, listColorViews } from './registry.js';
|
|
43
|
-
export { DEVICE_SCOPED_PARAMS, readDeviceParam } from './views/params.js';
|
|
44
|
-
export type { DeviceScopedParam } from './views/params.js';
|
|
43
|
+
export { DEFAULT_VIEW_PARAM, DEVICE_SCOPED_PARAMS, readDeviceParam, readUrlViewIntent, sameViewSnapshot, viewParamsSnapshot } from './views/params.js';
|
|
44
|
+
export type { DeviceScopedParam, DeviceViewIntent } from './views/params.js';
|
|
45
45
|
/**
|
|
46
46
|
* 设备清单机制(只有事实与查询,**判定不在这里**)
|
|
47
47
|
*
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACtB,gBAAgB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnF,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAClF,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,mBAAmB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEzF,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACzE,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EACL,UAAU,EACR,YAAY,EACd,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACf,MAAM,eAAe,CAAC;AAEvB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACtB,gBAAgB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnF,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAClF,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,mBAAmB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEzF,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACzE,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EACL,UAAU,EACR,YAAY,EACd,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACf,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE7E;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC1E,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD;;;;GAIG;AACH,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;GAKG;AACH,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACd,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,kBAAkB,EAClB,qBAAqB,EACrB,cAAc,EACd,aAAa,EACd,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ export { MODE_CYCLE, MODE_LABELS, isThemeMode, normalizeMode } from './scheme.js
|
|
|
35
35
|
export { isSafeTokenName, isSafeTokenValue, isSafeTokenEntry, sanitizeOverrides } from './tokens.js';
|
|
36
36
|
export { buildThemeRegistry, installThemeRegistry } from './registry.js';
|
|
37
37
|
export { listThemes, listColorsOf, toneOfColor, toneOfAnyScope, listColorIdsOfTone, resolveThemeId, getThemeRecord, getDefaultThemeId, isKnownTheme, listColorViews } from './registry.js';
|
|
38
|
-
export { DEVICE_SCOPED_PARAMS, readDeviceParam } from './views/params.js';
|
|
38
|
+
export { DEFAULT_VIEW_PARAM, DEVICE_SCOPED_PARAMS, readDeviceParam, readUrlViewIntent, sameViewSnapshot, viewParamsSnapshot } from './views/params.js';
|
|
39
39
|
/**
|
|
40
40
|
* 设备清单机制(只有事实与查询,**判定不在这里**)
|
|
41
41
|
*
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACtB,gBAAgB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGnF,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGlF,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAUrB,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEzE,OAAO,EACL,UAAU,EACR,YAAY,EACd,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACf,MAAM,eAAe,CAAC;AAEvB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACtB,gBAAgB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGnF,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGlF,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAUrB,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEzE,OAAO,EACL,UAAU,EACR,YAAY,EACd,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACf,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AAG3B;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG1E;;;;GAIG;AACH,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAGzD;;;;;GAKG;AACH,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACd,MAAM,eAAe,CAAC"}
|
package/dist/views/params.d.ts
CHANGED
|
@@ -32,12 +32,25 @@
|
|
|
32
32
|
* 但两侧读的入口不同:配色侧读 `window.location.search`(store 是全局的,不依赖路由),
|
|
33
33
|
* 版式侧读**路由 query**(容器手上只有 `route.query`)—— 所以不是同一个函数,是同一套规则。
|
|
34
34
|
*
|
|
35
|
+
* === 本文件的两个读取形态(2026-10-20 store 化后)===
|
|
36
|
+
* · `readDeviceParam(query, name, urlKey)` —— **单设备**取一个键的值(原样,不校验);
|
|
37
|
+
* 版式 store 化前的容器/面板读取入口,仍是公开 API(宿主自建壳可用)。
|
|
38
|
+
* · `readUrlViewIntent(query, viewParam, devices)` —— **全设备一次性读入**:把 URL 上
|
|
39
|
+
* 所有 `th*` 原始值解析成 `{ deviceView, viewIntent }`,供主题 store 初始化(与配色侧
|
|
40
|
+
* `readUrlIntent` 同形)。theme-scope 的 `deviceView` / `viewIntent` 由它播种。
|
|
41
|
+
* 设备清单由调用方传入(`registeredDevices()`)—— 本文件保持**零 import**,
|
|
42
|
+
* 与 `readDeviceParam` 的 urlKey 传入同构。 *
|
|
35
43
|
* @author yijiu2025
|
|
36
|
-
* @since 2026-09-26(2026-10-18 改下划线 + urlKey:`?view.` → `?th_`、`?theme.` → `?th_color_
|
|
44
|
+
* @since 2026-09-26(2026-10-18 改下划线 + urlKey:`?view.` → `?th_`、`?theme.` → `?th_color_`;
|
|
45
|
+
* 2026-10-20 加 `readUrlViewIntent`:版式意图 store 化的读入半边)
|
|
37
46
|
*/
|
|
38
47
|
/** 支持设备维度的参数名(与配色侧 `readUrlIntent` 的键保持一致) */
|
|
39
48
|
export type DeviceScopedParam = 'th' | 'th_color';
|
|
40
49
|
export declare const DEVICE_SCOPED_PARAMS: readonly DeviceScopedParam[];
|
|
50
|
+
/** 版式意图的参数前缀(配合设备 urlKey 组成 `th_<urlKey>`;配色侧是 `th_color`) */
|
|
51
|
+
export declare const DEFAULT_VIEW_PARAM = "th";
|
|
52
|
+
/** URL 上按设备解析出的版式意图:只记"这台设备被**显式**指定了哪套版式" */
|
|
53
|
+
export type DeviceViewIntent = Partial<Record<string, string>>;
|
|
41
54
|
/**
|
|
42
55
|
* 按**设备**读取版式 / 配色参数
|
|
43
56
|
*
|
|
@@ -56,4 +69,57 @@ export declare const DEVICE_SCOPED_PARAMS: readonly DeviceScopedParam[];
|
|
|
56
69
|
* 「这个缩写名认不认得」由宿主的设备清单归一(`urlKeyOf`)。
|
|
57
70
|
*/
|
|
58
71
|
export declare function readDeviceParam(query: Record<string, unknown> | undefined, name: DeviceScopedParam, deviceKey: string): unknown;
|
|
72
|
+
/**
|
|
73
|
+
* 读取 URL 上的**版式**意图(全设备一次性读入)—— 与 `readUrlIntent`(配色)同形
|
|
74
|
+
*
|
|
75
|
+
* === 每台设备各一条取值链 ===
|
|
76
|
+
* `?th_<urlKey>` → `?th` → (缺则这台设备无显式意图)
|
|
77
|
+
* 即"**设备专属优先,缺了退到通用键**",与 `readDeviceParam` 逐字同构。
|
|
78
|
+
*
|
|
79
|
+
* === 与配色侧 `readUrlIntent` 的两个刻意差别 ===
|
|
80
|
+
* ① **不做合法性校验**:版式 id 合不合法取决于"当前包里有没有这套版式"——渲染期
|
|
81
|
+
* 知识,由 `pick()`(`registry.resolve`)兜底。与 `readDeviceParam` 口径一致
|
|
82
|
+
* (只决定"取哪个键")。
|
|
83
|
+
* ② **通用键不下发**到每台设备:它原样进 `viewIntent`,由 scope 的 `deviceViewOf`
|
|
84
|
+
* 的 `deviceView[device] ?? viewIntent` 兜底。配色侧之所以下发,是因为 `themeId`
|
|
85
|
+
* 有独立的持久化 / 用户操作语义(`deviceTheme` 是"URL 覆盖层");版式没有持久化层。
|
|
86
|
+
*
|
|
87
|
+
* 🔴 **空串 = 未指定**:`?th_<urlKey>=` 不能把 `?th=compact` 顶掉(与 `readDeviceParam`
|
|
88
|
+
* 的 `isPresent()`、配色侧 `present()` 同一口径)。
|
|
89
|
+
* 🔴 **重复键(`?th=a&th=b`,vue-router 给数组)视为未指定**:与配色侧
|
|
90
|
+
* `URLSearchParams.get()` 只取首值的净效果一致——都落声明档,不猜用户想要哪个。
|
|
91
|
+
*
|
|
92
|
+
* @param query 路由 query(设备域 `currentQuery()`;单测可直接传对象,不依赖 window)
|
|
93
|
+
* @param viewParam 版式参数前缀(缺省 `th`)
|
|
94
|
+
* @param devices 设备清单(`{ id, urlKey? }`;调用方传 `registeredDevices()`。
|
|
95
|
+
* `urlKey` 缺省回落 `id`,与 `urlKeyOf()` 同口径)
|
|
96
|
+
*/
|
|
97
|
+
export declare function readUrlViewIntent(query: Record<string, unknown> | undefined, viewParam?: string, devices?: readonly {
|
|
98
|
+
id: string;
|
|
99
|
+
urlKey?: string;
|
|
100
|
+
}[]): {
|
|
101
|
+
deviceView: DeviceViewIntent;
|
|
102
|
+
viewIntent: string | null;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* 取 query 里版式相关键的**原始值快照**(`th` + 各设备 `th_<urlKey>`)
|
|
106
|
+
*
|
|
107
|
+
* 供 theme-scope 的"仅当 URL 原始值变化才同步进 store"判等用:debug 改写只碰 store
|
|
108
|
+
* (不写 URL),快照不变 ⇒ 不同步;跨页带不同 `th`(/login → /register)⇒ 同步。
|
|
109
|
+
*
|
|
110
|
+
* 🔴 键白名单 = `viewParam` 本身 + `viewParam_<urlKey>`(urlKey 来自设备清单)——
|
|
111
|
+
* **不能**用 `startsWith(viewParam + '_')`:那会把配色键 `th_color` /
|
|
112
|
+
* `th_color_<urlKey>` 也收进来(`color` 不是任何设备的 urlKey,白名单天然排除),
|
|
113
|
+
* 导致"换了一条带不同配色的链接"误触发版式同步。
|
|
114
|
+
*
|
|
115
|
+
* @param query 路由 query
|
|
116
|
+
* @param viewParam 版式参数前缀(缺省 `th`)
|
|
117
|
+
* @param devices 设备清单(`{ id, urlKey? }`;调用方传 `registeredDevices()`)
|
|
118
|
+
*/
|
|
119
|
+
export declare function viewParamsSnapshot(query: Record<string, unknown> | undefined, viewParam?: string, devices?: readonly {
|
|
120
|
+
id: string;
|
|
121
|
+
urlKey?: string;
|
|
122
|
+
}[]): Record<string, unknown>;
|
|
123
|
+
/** 两个 `th*` 快照是否等价(键集合 + 各键原始值都相同) */
|
|
124
|
+
export declare function sameViewSnapshot(a: Record<string, unknown>, b: Record<string, unknown>): boolean;
|
|
59
125
|
//# sourceMappingURL=params.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../../src/views/params.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../../src/views/params.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,8CAA8C;AAC9C,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,UAAU,CAAC;AAElD,eAAO,MAAM,oBAAoB,EAAE,SAAS,iBAAiB,EAAuB,CAAC;AAErF,8DAA8D;AAC9D,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAEvC,8CAA8C;AAC9C,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAY/D;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,IAAI,EAAE,iBAAiB,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAIT;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,SAAS,GAAE,MAA2B,EACtC,OAAO,GAAE,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EAAO,GACvD;IAAE,UAAU,EAAE,gBAAgB,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAe7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,SAAS,GAAE,MAA2B,EACtC,OAAO,GAAE,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EAAO,GACvD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOzB;AAED,sCAAsC;AACtC,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAIhG"}
|
package/dist/views/params.js
CHANGED
|
@@ -1,4 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 版式 / 配色参数的**设备维度**解析 —— `?th` / `?th_color`(下划线 + 设备缩写名)
|
|
3
|
+
*
|
|
4
|
+
* === 参数命名(2026-10-18 定稿,统一用下划线、禁用点号)===
|
|
5
|
+
* 版式:`?th`(三端通用) `?th_<设备缩写>`(某设备专属)
|
|
6
|
+
* 配色:`?th_color`(三端通用) `?th_color_<设备缩写>`(某设备专属)
|
|
7
|
+
* 设备专属键的后缀是**设备缩写名(urlKey)**,不是设备 id —— 由设备清单声明
|
|
8
|
+
* (`defineThemeDevice` 的 `urlKey`,缺省回落 `id`),内核不硬编码任何缩写。
|
|
9
|
+
*
|
|
10
|
+
* === 解决什么 ===
|
|
11
|
+
* 同一条链接要能给三台设备各指定一套版式:
|
|
12
|
+
* ```
|
|
13
|
+
* /login?th=compact 三端共用(哪端没有就自动下沉到当前包)
|
|
14
|
+
* /login?th_mobile=default&th_std=compact 各端各指定(mini 走通用/包默认)
|
|
15
|
+
* /login?th_mobile=default&th=default 设备专属优先,通用键兜底
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* === 取值链(每台设备各一条)===
|
|
19
|
+
* `th_<urlKey>` → `th` → (返回 undefined:由调用方走包声明 → 当前包)
|
|
20
|
+
*
|
|
21
|
+
* === 为什么收在内核(而不是各页文件里)===
|
|
22
|
+
* 三个页面(login / register / forgot-password)各有**一份**注册表文件,但"哪个 query
|
|
23
|
+
* key 属于哪台设备"这件事与页面无关 —— 抄三份必然漂移。各页的 `pick*ViewId` 只负责
|
|
24
|
+
* "拿到原始值之后怎么解析(落在哪个包、是否合法)",取哪个键这一步交给本模块。
|
|
25
|
+
* 2026-09-26 抽包 Stage 1 起它住在 `skinsuite`:任何复用这套机制的前端
|
|
26
|
+
* 都直接拿到这份实现,而不是再抄一遍。
|
|
27
|
+
*
|
|
28
|
+
* === 与配色侧同形(很重要)===
|
|
29
|
+
* 配色侧(`oauth21/src/stores/theme.ts` 的 `readUrlIntent`)用同样的键形
|
|
30
|
+
* (`th_color_<urlKey>` / `th_color`)与同样的"设备专属优先"链。**两侧必须同形**,
|
|
31
|
+
* 否则配置者要记两套规则,`?th_color_mobile=blue&th_std=compact` 这种混搭就写不出来。
|
|
32
|
+
* 但两侧读的入口不同:配色侧读 `window.location.search`(store 是全局的,不依赖路由),
|
|
33
|
+
* 版式侧读**路由 query**(容器手上只有 `route.query`)—— 所以不是同一个函数,是同一套规则。
|
|
34
|
+
*
|
|
35
|
+
* === 本文件的两个读取形态(2026-10-20 store 化后)===
|
|
36
|
+
* · `readDeviceParam(query, name, urlKey)` —— **单设备**取一个键的值(原样,不校验);
|
|
37
|
+
* 版式 store 化前的容器/面板读取入口,仍是公开 API(宿主自建壳可用)。
|
|
38
|
+
* · `readUrlViewIntent(query, viewParam, devices)` —— **全设备一次性读入**:把 URL 上
|
|
39
|
+
* 所有 `th*` 原始值解析成 `{ deviceView, viewIntent }`,供主题 store 初始化(与配色侧
|
|
40
|
+
* `readUrlIntent` 同形)。theme-scope 的 `deviceView` / `viewIntent` 由它播种。
|
|
41
|
+
* 设备清单由调用方传入(`registeredDevices()`)—— 本文件保持**零 import**,
|
|
42
|
+
* 与 `readDeviceParam` 的 urlKey 传入同构。 *
|
|
43
|
+
* @author yijiu2025
|
|
44
|
+
* @since 2026-09-26(2026-10-18 改下划线 + urlKey:`?view.` → `?th_`、`?theme.` → `?th_color_`;
|
|
45
|
+
* 2026-10-20 加 `readUrlViewIntent`:版式意图 store 化的读入半边)
|
|
46
|
+
*/
|
|
1
47
|
export const DEVICE_SCOPED_PARAMS = ['th', 'th_color'];
|
|
48
|
+
/** 版式意图的参数前缀(配合设备 urlKey 组成 `th_<urlKey>`;配色侧是 `th_color`) */
|
|
49
|
+
export const DEFAULT_VIEW_PARAM = 'th';
|
|
2
50
|
/** 值"存在且非空" —— 空串 / 空数组视为**未指定**,不顶掉兜底键 */
|
|
3
51
|
function isPresent(value) {
|
|
4
52
|
if (value === undefined || value === null)
|
|
@@ -33,4 +81,77 @@ export function readDeviceParam(query, name, deviceKey) {
|
|
|
33
81
|
return scoped;
|
|
34
82
|
return query?.[name];
|
|
35
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* 读取 URL 上的**版式**意图(全设备一次性读入)—— 与 `readUrlIntent`(配色)同形
|
|
86
|
+
*
|
|
87
|
+
* === 每台设备各一条取值链 ===
|
|
88
|
+
* `?th_<urlKey>` → `?th` → (缺则这台设备无显式意图)
|
|
89
|
+
* 即"**设备专属优先,缺了退到通用键**",与 `readDeviceParam` 逐字同构。
|
|
90
|
+
*
|
|
91
|
+
* === 与配色侧 `readUrlIntent` 的两个刻意差别 ===
|
|
92
|
+
* ① **不做合法性校验**:版式 id 合不合法取决于"当前包里有没有这套版式"——渲染期
|
|
93
|
+
* 知识,由 `pick()`(`registry.resolve`)兜底。与 `readDeviceParam` 口径一致
|
|
94
|
+
* (只决定"取哪个键")。
|
|
95
|
+
* ② **通用键不下发**到每台设备:它原样进 `viewIntent`,由 scope 的 `deviceViewOf`
|
|
96
|
+
* 的 `deviceView[device] ?? viewIntent` 兜底。配色侧之所以下发,是因为 `themeId`
|
|
97
|
+
* 有独立的持久化 / 用户操作语义(`deviceTheme` 是"URL 覆盖层");版式没有持久化层。
|
|
98
|
+
*
|
|
99
|
+
* 🔴 **空串 = 未指定**:`?th_<urlKey>=` 不能把 `?th=compact` 顶掉(与 `readDeviceParam`
|
|
100
|
+
* 的 `isPresent()`、配色侧 `present()` 同一口径)。
|
|
101
|
+
* 🔴 **重复键(`?th=a&th=b`,vue-router 给数组)视为未指定**:与配色侧
|
|
102
|
+
* `URLSearchParams.get()` 只取首值的净效果一致——都落声明档,不猜用户想要哪个。
|
|
103
|
+
*
|
|
104
|
+
* @param query 路由 query(设备域 `currentQuery()`;单测可直接传对象,不依赖 window)
|
|
105
|
+
* @param viewParam 版式参数前缀(缺省 `th`)
|
|
106
|
+
* @param devices 设备清单(`{ id, urlKey? }`;调用方传 `registeredDevices()`。
|
|
107
|
+
* `urlKey` 缺省回落 `id`,与 `urlKeyOf()` 同口径)
|
|
108
|
+
*/
|
|
109
|
+
export function readUrlViewIntent(query, viewParam = DEFAULT_VIEW_PARAM, devices = []) {
|
|
110
|
+
const deviceView = {};
|
|
111
|
+
/** 空串 / 纯空白视为**未指定** —— 与上面 `isPresent()` 同一口径(但不收数组,见上) */
|
|
112
|
+
const present = (raw) => typeof raw === 'string' && raw.trim() !== '' ? raw : null;
|
|
113
|
+
try {
|
|
114
|
+
const anyScope = present(query?.[viewParam]);
|
|
115
|
+
for (const device of devices) {
|
|
116
|
+
const scoped = present(query?.[`${viewParam}_${device.urlKey ?? device.id}`]);
|
|
117
|
+
if (scoped !== null)
|
|
118
|
+
deviceView[device.id] = scoped;
|
|
119
|
+
}
|
|
120
|
+
return { deviceView, viewIntent: anyScope };
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return { deviceView, viewIntent: null };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 取 query 里版式相关键的**原始值快照**(`th` + 各设备 `th_<urlKey>`)
|
|
128
|
+
*
|
|
129
|
+
* 供 theme-scope 的"仅当 URL 原始值变化才同步进 store"判等用:debug 改写只碰 store
|
|
130
|
+
* (不写 URL),快照不变 ⇒ 不同步;跨页带不同 `th`(/login → /register)⇒ 同步。
|
|
131
|
+
*
|
|
132
|
+
* 🔴 键白名单 = `viewParam` 本身 + `viewParam_<urlKey>`(urlKey 来自设备清单)——
|
|
133
|
+
* **不能**用 `startsWith(viewParam + '_')`:那会把配色键 `th_color` /
|
|
134
|
+
* `th_color_<urlKey>` 也收进来(`color` 不是任何设备的 urlKey,白名单天然排除),
|
|
135
|
+
* 导致"换了一条带不同配色的链接"误触发版式同步。
|
|
136
|
+
*
|
|
137
|
+
* @param query 路由 query
|
|
138
|
+
* @param viewParam 版式参数前缀(缺省 `th`)
|
|
139
|
+
* @param devices 设备清单(`{ id, urlKey? }`;调用方传 `registeredDevices()`)
|
|
140
|
+
*/
|
|
141
|
+
export function viewParamsSnapshot(query, viewParam = DEFAULT_VIEW_PARAM, devices = []) {
|
|
142
|
+
const out = {};
|
|
143
|
+
const scopedKeys = new Set(devices.map(d => `${viewParam}_${d.urlKey ?? d.id}`));
|
|
144
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
145
|
+
if (key === viewParam || scopedKeys.has(key))
|
|
146
|
+
out[key] = value;
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
/** 两个 `th*` 快照是否等价(键集合 + 各键原始值都相同) */
|
|
151
|
+
export function sameViewSnapshot(a, b) {
|
|
152
|
+
const ka = Object.keys(a);
|
|
153
|
+
if (ka.length !== Object.keys(b).length)
|
|
154
|
+
return false;
|
|
155
|
+
return ka.every(k => a[k] === b[k]);
|
|
156
|
+
}
|
|
36
157
|
//# sourceMappingURL=params.js.map
|
package/dist/views/params.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"params.js","sourceRoot":"","sources":["../../src/views/params.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"params.js","sourceRoot":"","sources":["../../src/views/params.ts"],"names":[],"mappings":"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"}
|
package/docs/onboarding.md
CHANGED
|
@@ -169,7 +169,12 @@ defineThemeDevice({
|
|
|
169
169
|
所以库只留**注入点**,判定由宿主实现并通过 `app.use(theme, { resolveDevice })` 传进来:
|
|
170
170
|
|
|
171
171
|
```ts
|
|
172
|
-
// src/
|
|
172
|
+
// src/utils/device-resolve.ts(宿主侧)
|
|
173
|
+
//
|
|
174
|
+
// 🔴 别把这段放进 `src/theme/views/`:那是主题域的**纯声明目录**
|
|
175
|
+
// (defineThemePage / defineThemeDevice),会被 virtual:theme 当声明副作用 import,
|
|
176
|
+
// 判定代码会跟着主题域一起进包;而且「theme/ 整包搬到另一个前端」就不成立了。
|
|
177
|
+
// theme/views/devices.ts 只留设备声明(有哪些设备、容器在哪、占哪条路由)。
|
|
173
178
|
import type { DeviceDomainContext } from 'skinsuite/vue';
|
|
174
179
|
import { fallbackDevice, isKnownDevice } from 'skinsuite/vue';
|
|
175
180
|
import { isMobileViewport } from '@/utils/device';
|
|
@@ -196,8 +201,13 @@ import { watchViewport } from '@/utils/device';
|
|
|
196
201
|
watchViewport(() => refreshDevice());
|
|
197
202
|
```
|
|
198
203
|
|
|
199
|
-
**加一种设备**要动的四处:`
|
|
200
|
-
容器组件 / 主题包里加设备目录。库零改动。
|
|
204
|
+
**加一种设备**要动的四处:`theme/views/devices.ts` 一条 `defineThemeDevice` 声明 /
|
|
205
|
+
`utils/device-resolve.ts` 一条判定分支 / 容器组件 / 主题包里加设备目录。库零改动。
|
|
206
|
+
|
|
207
|
+
⚠️ 判定函数依赖「设备域已注册的清单」(`isKnownDevice` / `fallbackDevice`),
|
|
208
|
+
所以 `bootstrap.ts` 要在 `app.use(theme)` **之前** import 一次声明模块
|
|
209
|
+
(`import './theme/views/devices'`)—— 通常 `virtual:theme` 会拉齐,但显式 import
|
|
210
|
+
能让顺序确定,不依赖插件产物。
|
|
201
211
|
|
|
202
212
|
### 4.2 页面契约 `views/<page>.ts`
|
|
203
213
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skinsuite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "多主题 / 多版式机制的纯内核(零运行时依赖):目录名与默认值的白名单常量、主题包契约类型、明暗 mode 与色系 tone、CSS 变量 token 的安全校验、设备维度 URL 参数读取。不碰 DOM / 构建器 / 路由,可在任意前端(Vue / React / Node / SSR)复用。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/index.ts
CHANGED
|
@@ -80,8 +80,15 @@ export {
|
|
|
80
80
|
listColorViews
|
|
81
81
|
} from './registry.js';
|
|
82
82
|
|
|
83
|
-
export {
|
|
84
|
-
|
|
83
|
+
export {
|
|
84
|
+
DEFAULT_VIEW_PARAM,
|
|
85
|
+
DEVICE_SCOPED_PARAMS,
|
|
86
|
+
readDeviceParam,
|
|
87
|
+
readUrlViewIntent,
|
|
88
|
+
sameViewSnapshot,
|
|
89
|
+
viewParamsSnapshot
|
|
90
|
+
} from './views/params.js';
|
|
91
|
+
export type { DeviceScopedParam, DeviceViewIntent } from './views/params.js';
|
|
85
92
|
|
|
86
93
|
/**
|
|
87
94
|
* 设备清单机制(只有事实与查询,**判定不在这里**)
|
package/src/views/params.ts
CHANGED
|
@@ -32,14 +32,30 @@
|
|
|
32
32
|
* 但两侧读的入口不同:配色侧读 `window.location.search`(store 是全局的,不依赖路由),
|
|
33
33
|
* 版式侧读**路由 query**(容器手上只有 `route.query`)—— 所以不是同一个函数,是同一套规则。
|
|
34
34
|
*
|
|
35
|
+
* === 本文件的两个读取形态(2026-10-20 store 化后)===
|
|
36
|
+
* · `readDeviceParam(query, name, urlKey)` —— **单设备**取一个键的值(原样,不校验);
|
|
37
|
+
* 版式 store 化前的容器/面板读取入口,仍是公开 API(宿主自建壳可用)。
|
|
38
|
+
* · `readUrlViewIntent(query, viewParam, devices)` —— **全设备一次性读入**:把 URL 上
|
|
39
|
+
* 所有 `th*` 原始值解析成 `{ deviceView, viewIntent }`,供主题 store 初始化(与配色侧
|
|
40
|
+
* `readUrlIntent` 同形)。theme-scope 的 `deviceView` / `viewIntent` 由它播种。
|
|
41
|
+
* 设备清单由调用方传入(`registeredDevices()`)—— 本文件保持**零 import**,
|
|
42
|
+
* 与 `readDeviceParam` 的 urlKey 传入同构。 *
|
|
35
43
|
* @author yijiu2025
|
|
36
|
-
* @since 2026-09-26(2026-10-18 改下划线 + urlKey:`?view.` → `?th_`、`?theme.` → `?th_color_
|
|
44
|
+
* @since 2026-09-26(2026-10-18 改下划线 + urlKey:`?view.` → `?th_`、`?theme.` → `?th_color_`;
|
|
45
|
+
* 2026-10-20 加 `readUrlViewIntent`:版式意图 store 化的读入半边)
|
|
37
46
|
*/
|
|
47
|
+
|
|
38
48
|
/** 支持设备维度的参数名(与配色侧 `readUrlIntent` 的键保持一致) */
|
|
39
49
|
export type DeviceScopedParam = 'th' | 'th_color';
|
|
40
50
|
|
|
41
51
|
export const DEVICE_SCOPED_PARAMS: readonly DeviceScopedParam[] = ['th', 'th_color'];
|
|
42
52
|
|
|
53
|
+
/** 版式意图的参数前缀(配合设备 urlKey 组成 `th_<urlKey>`;配色侧是 `th_color`) */
|
|
54
|
+
export const DEFAULT_VIEW_PARAM = 'th';
|
|
55
|
+
|
|
56
|
+
/** URL 上按设备解析出的版式意图:只记"这台设备被**显式**指定了哪套版式" */
|
|
57
|
+
export type DeviceViewIntent = Partial<Record<string, string>>;
|
|
58
|
+
|
|
43
59
|
/** 值"存在且非空" —— 空串 / 空数组视为**未指定**,不顶掉兜底键 */
|
|
44
60
|
function isPresent(value: unknown): boolean {
|
|
45
61
|
if (value === undefined || value === null) return false;
|
|
@@ -76,3 +92,84 @@ export function readDeviceParam(
|
|
|
76
92
|
if (isPresent(scoped)) return scoped;
|
|
77
93
|
return query?.[name];
|
|
78
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 读取 URL 上的**版式**意图(全设备一次性读入)—— 与 `readUrlIntent`(配色)同形
|
|
98
|
+
*
|
|
99
|
+
* === 每台设备各一条取值链 ===
|
|
100
|
+
* `?th_<urlKey>` → `?th` → (缺则这台设备无显式意图)
|
|
101
|
+
* 即"**设备专属优先,缺了退到通用键**",与 `readDeviceParam` 逐字同构。
|
|
102
|
+
*
|
|
103
|
+
* === 与配色侧 `readUrlIntent` 的两个刻意差别 ===
|
|
104
|
+
* ① **不做合法性校验**:版式 id 合不合法取决于"当前包里有没有这套版式"——渲染期
|
|
105
|
+
* 知识,由 `pick()`(`registry.resolve`)兜底。与 `readDeviceParam` 口径一致
|
|
106
|
+
* (只决定"取哪个键")。
|
|
107
|
+
* ② **通用键不下发**到每台设备:它原样进 `viewIntent`,由 scope 的 `deviceViewOf`
|
|
108
|
+
* 的 `deviceView[device] ?? viewIntent` 兜底。配色侧之所以下发,是因为 `themeId`
|
|
109
|
+
* 有独立的持久化 / 用户操作语义(`deviceTheme` 是"URL 覆盖层");版式没有持久化层。
|
|
110
|
+
*
|
|
111
|
+
* 🔴 **空串 = 未指定**:`?th_<urlKey>=` 不能把 `?th=compact` 顶掉(与 `readDeviceParam`
|
|
112
|
+
* 的 `isPresent()`、配色侧 `present()` 同一口径)。
|
|
113
|
+
* 🔴 **重复键(`?th=a&th=b`,vue-router 给数组)视为未指定**:与配色侧
|
|
114
|
+
* `URLSearchParams.get()` 只取首值的净效果一致——都落声明档,不猜用户想要哪个。
|
|
115
|
+
*
|
|
116
|
+
* @param query 路由 query(设备域 `currentQuery()`;单测可直接传对象,不依赖 window)
|
|
117
|
+
* @param viewParam 版式参数前缀(缺省 `th`)
|
|
118
|
+
* @param devices 设备清单(`{ id, urlKey? }`;调用方传 `registeredDevices()`。
|
|
119
|
+
* `urlKey` 缺省回落 `id`,与 `urlKeyOf()` 同口径)
|
|
120
|
+
*/
|
|
121
|
+
export function readUrlViewIntent(
|
|
122
|
+
query: Record<string, unknown> | undefined,
|
|
123
|
+
viewParam: string = DEFAULT_VIEW_PARAM,
|
|
124
|
+
devices: readonly { id: string; urlKey?: string }[] = []
|
|
125
|
+
): { deviceView: DeviceViewIntent; viewIntent: string | null } {
|
|
126
|
+
const deviceView: DeviceViewIntent = {};
|
|
127
|
+
/** 空串 / 纯空白视为**未指定** —— 与上面 `isPresent()` 同一口径(但不收数组,见上) */
|
|
128
|
+
const present = (raw: unknown): string | null =>
|
|
129
|
+
typeof raw === 'string' && raw.trim() !== '' ? raw : null;
|
|
130
|
+
try {
|
|
131
|
+
const anyScope = present(query?.[viewParam]);
|
|
132
|
+
for (const device of devices) {
|
|
133
|
+
const scoped = present(query?.[`${viewParam}_${device.urlKey ?? device.id}`]);
|
|
134
|
+
if (scoped !== null) deviceView[device.id] = scoped;
|
|
135
|
+
}
|
|
136
|
+
return { deviceView, viewIntent: anyScope };
|
|
137
|
+
} catch {
|
|
138
|
+
return { deviceView, viewIntent: null };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 取 query 里版式相关键的**原始值快照**(`th` + 各设备 `th_<urlKey>`)
|
|
144
|
+
*
|
|
145
|
+
* 供 theme-scope 的"仅当 URL 原始值变化才同步进 store"判等用:debug 改写只碰 store
|
|
146
|
+
* (不写 URL),快照不变 ⇒ 不同步;跨页带不同 `th`(/login → /register)⇒ 同步。
|
|
147
|
+
*
|
|
148
|
+
* 🔴 键白名单 = `viewParam` 本身 + `viewParam_<urlKey>`(urlKey 来自设备清单)——
|
|
149
|
+
* **不能**用 `startsWith(viewParam + '_')`:那会把配色键 `th_color` /
|
|
150
|
+
* `th_color_<urlKey>` 也收进来(`color` 不是任何设备的 urlKey,白名单天然排除),
|
|
151
|
+
* 导致"换了一条带不同配色的链接"误触发版式同步。
|
|
152
|
+
*
|
|
153
|
+
* @param query 路由 query
|
|
154
|
+
* @param viewParam 版式参数前缀(缺省 `th`)
|
|
155
|
+
* @param devices 设备清单(`{ id, urlKey? }`;调用方传 `registeredDevices()`)
|
|
156
|
+
*/
|
|
157
|
+
export function viewParamsSnapshot(
|
|
158
|
+
query: Record<string, unknown> | undefined,
|
|
159
|
+
viewParam: string = DEFAULT_VIEW_PARAM,
|
|
160
|
+
devices: readonly { id: string; urlKey?: string }[] = []
|
|
161
|
+
): Record<string, unknown> {
|
|
162
|
+
const out: Record<string, unknown> = {};
|
|
163
|
+
const scopedKeys = new Set(devices.map(d => `${viewParam}_${d.urlKey ?? d.id}`));
|
|
164
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
165
|
+
if (key === viewParam || scopedKeys.has(key)) out[key] = value;
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 两个 `th*` 快照是否等价(键集合 + 各键原始值都相同) */
|
|
171
|
+
export function sameViewSnapshot(a: Record<string, unknown>, b: Record<string, unknown>): boolean {
|
|
172
|
+
const ka = Object.keys(a);
|
|
173
|
+
if (ka.length !== Object.keys(b).length) return false;
|
|
174
|
+
return ka.every(k => a[k] === b[k]);
|
|
175
|
+
}
|
package/src/vue/ThemeView.vue
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* === 依赖从哪来(库不 import 宿主 store/router)===
|
|
14
14
|
* · `page`(`pageOf(page)`)→ 页面域自动注册(`defineThemePage`)
|
|
15
15
|
* · `themeStore`(`inject`)→ `<ThemeRoot :themeStore>` provide
|
|
16
|
-
* · `url`(`?th` / `?th_<设备缩写>`)→
|
|
16
|
+
* · `url`(`?th` / `?th_<设备缩写>`)→ **store 版式意图** `deviceViewOf(device)`
|
|
17
|
+
* (2026-10-20 store 化:URL 一次读入,debug 改写不碰路由;原先每次渲染现读
|
|
18
|
+
* 路由 query)
|
|
17
19
|
* · `builtin`(首帧零请求回退)→ 版式注册表 `views.builtin(device)`(eager 静态 import)
|
|
18
20
|
*
|
|
19
21
|
* @author yijiu2025
|
|
@@ -21,10 +23,7 @@
|
|
|
21
23
|
*/
|
|
22
24
|
import { computed, inject, markRaw, shallowRef, watch } from 'vue';
|
|
23
25
|
import type { Component } from 'vue';
|
|
24
|
-
import { readDeviceParam } from '../views/params';
|
|
25
26
|
import { pageOf } from './view-page';
|
|
26
|
-
import { currentQuery } from './device-domain';
|
|
27
|
-
import { urlKeyOf } from './define-theme-device';
|
|
28
27
|
import { themeStoreKey } from './theme-store';
|
|
29
28
|
import type { ThemeStoreLike } from './theme-store';
|
|
30
29
|
|
|
@@ -48,13 +47,15 @@ if (!pageObj) {
|
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
/**
|
|
51
|
-
* 用哪套版式:`?th[_<设备缩写>]
|
|
50
|
+
* 用哪套版式:`?th[_<设备缩写>]`(store 版式意图)> 主题包声明(views.<page>)> 当前包名。
|
|
52
51
|
* 做成 computed(而非 setup 取一次)是因为主题**运行时**才定:后端换配色配置可能晚于本页
|
|
53
52
|
* setup 到达,取一次会漏掉「主题包声明了版式」这种情况;依赖 `packageId` 让它在换包时自动重算。
|
|
53
|
+
* url 档读 store 而非路由 query(2026-10-20):URL 一次读入 `deviceView`/`viewIntent`,
|
|
54
|
+
* debug 改写只写 store ⇒ 换版式零路由变化;`deviceViewOf` 内部读 ref,响应性不变。
|
|
54
55
|
*/
|
|
55
56
|
const viewId = computed(() =>
|
|
56
57
|
pageObj.pick({
|
|
57
|
-
url:
|
|
58
|
+
url: store.deviceViewOf(props.device),
|
|
58
59
|
theme: store.viewFor(props.page, props.device),
|
|
59
60
|
pkg: store.packageId,
|
|
60
61
|
device: props.device
|
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
* @author yijiu2025
|
|
23
23
|
* @since 2026-10-19(从 oauth21/src/stores/theme.ts 下沉编排,pinia 留在宿主)
|
|
24
24
|
*/
|
|
25
|
+
import { watch } from 'vue';
|
|
25
26
|
import { useThemeScope } from './theme-scope';
|
|
26
27
|
import { useThemeIntent } from './theme-intent';
|
|
27
28
|
import { useThemeRuntime } from './theme-runtime';
|
|
29
|
+
import { currentQuery } from './device-domain';
|
|
28
30
|
import { DEFAULT_COLOR_PARAM, DEFAULT_STORAGE_KEYS } from './theme-persistence';
|
|
31
|
+
import { DEFAULT_VIEW_PARAM } from '../views/params';
|
|
29
32
|
import type { ThemeStorageKeys } from './theme-persistence';
|
|
30
33
|
|
|
31
34
|
/** 宿主策略收口点(全可选,默认 = 主题机制约定,oauth21 即默认值本身) */
|
|
@@ -34,6 +37,8 @@ export interface ThemeStateOptions {
|
|
|
34
37
|
storageKeys?: Partial<ThemeStorageKeys>;
|
|
35
38
|
/** URL 配色意图参数前缀(配合设备 urlKey 组成 `th_color_<urlKey>`) */
|
|
36
39
|
colorParam?: string;
|
|
40
|
+
/** URL 版式意图参数前缀(配合设备 urlKey 组成 `th_<urlKey>`) */
|
|
41
|
+
viewParam?: string;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/**
|
|
@@ -53,14 +58,27 @@ export interface ThemeStateOptions {
|
|
|
53
58
|
export function createThemeState(options: ThemeStateOptions = {}) {
|
|
54
59
|
const keys: ThemeStorageKeys = { ...DEFAULT_STORAGE_KEYS, ...options.storageKeys };
|
|
55
60
|
const colorParam = options.colorParam ?? DEFAULT_COLOR_PARAM;
|
|
61
|
+
const viewParam = options.viewParam ?? DEFAULT_VIEW_PARAM;
|
|
56
62
|
|
|
57
63
|
// ① 作用域(共享基础:activeDevice / activePage / activeView / themeRecordFor / isDark 等)
|
|
58
|
-
const scope = useThemeScope(keys, colorParam);
|
|
64
|
+
const scope = useThemeScope(keys, colorParam, viewParam);
|
|
59
65
|
// ② 意图(明暗三态 + 记忆槽 + 色系联动 + 用户操作入口)
|
|
60
66
|
const intent = useThemeIntent(scope, keys);
|
|
61
67
|
// ③ 运行时(token 注入 + 附加样式 + 明暗类 + 外部配置);必须最后创建(内部 watch immediate 依赖前两步)
|
|
62
68
|
const runtime = useThemeRuntime(scope, intent);
|
|
63
69
|
|
|
70
|
+
// -------------------------------------------------------------------------
|
|
71
|
+
// URL 上 `th*` 原始值变化 → 同步版式意图进 store(2026-10-20 store 化)
|
|
72
|
+
//
|
|
73
|
+
// 🔴 debug 面板的 `pickView` 只写 store(`setDeviceView`)**不写 URL**,所以此
|
|
74
|
+
// watch 不会被调试操作误触发;跨页带不同 `th`(/login → /register)仍生效。
|
|
75
|
+
// 🔴 `app.use(theme, { router })` 先于 store 创建(bootstrap 顺序),此处
|
|
76
|
+
// `currentQuery()` 已是响应式路由 query;未装 router 时返回 `{}`(无依赖,不触发)。
|
|
77
|
+
// 🔴 source 包 getter(而非直接传 `currentQuery()`):后者未装 router 时是普通对象,
|
|
78
|
+
// Vue 会 warn "Invalid watch source";包一层后两种形态都合法。
|
|
79
|
+
// -------------------------------------------------------------------------
|
|
80
|
+
watch(() => currentQuery(), () => scope.syncUrlViewIntent(), { deep: true });
|
|
81
|
+
|
|
64
82
|
// -------------------------------------------------------------------------
|
|
65
83
|
// 调试探针(仅 dev + ?debug=theme):getter 形态随 store 实时取最新值
|
|
66
84
|
// -------------------------------------------------------------------------
|
|
@@ -68,6 +86,8 @@ export function createThemeState(options: ThemeStateOptions = {}) {
|
|
|
68
86
|
(window as unknown as Record<string, unknown>).__MAUTH_STORE__ = {
|
|
69
87
|
get themeId() { return scope.themeId.value; },
|
|
70
88
|
get deviceTheme() { return { ...scope.deviceTheme.value }; },
|
|
89
|
+
get viewIntent() { return scope.viewIntent.value; },
|
|
90
|
+
get deviceView() { return { ...scope.deviceView.value }; },
|
|
71
91
|
get mode() { return intent.mode.value; },
|
|
72
92
|
get packageId() { return scope.packageId.value; },
|
|
73
93
|
get activeDevice() { return scope.activeDevice.value; },
|
|
@@ -98,6 +118,11 @@ export function createThemeState(options: ThemeStateOptions = {}) {
|
|
|
98
118
|
themeRecordFor: scope.themeRecordFor,
|
|
99
119
|
setActiveDevice: scope.setActiveDevice,
|
|
100
120
|
setActivePageView: scope.setActivePageView,
|
|
121
|
+
// ---- 版式意图(store 化;与 themeId/deviceTheme 同形)----
|
|
122
|
+
viewIntent: scope.viewIntent,
|
|
123
|
+
deviceViewOf: scope.deviceViewOf,
|
|
124
|
+
setDeviceView: scope.setDeviceView,
|
|
125
|
+
clearDeviceViewOverride: scope.clearDeviceViewOverride,
|
|
101
126
|
// ---- 来自 intent ----
|
|
102
127
|
setMode: intent.setMode,
|
|
103
128
|
cycleMode: intent.cycleMode,
|
package/src/vue/theme-scope.ts
CHANGED
|
@@ -19,13 +19,28 @@
|
|
|
19
19
|
import { computed, ref, watch } from 'vue';
|
|
20
20
|
import { DEFAULT_THEME_PACKAGE, DEFAULT_THEME_PAGE } from '../constants';
|
|
21
21
|
import { getThemeRecord, listThemes } from '../registry';
|
|
22
|
-
import { currentDevice } from './device-domain';
|
|
23
|
-
import { fallbackDevice } from './define-theme-device';
|
|
22
|
+
import { currentDevice, currentQuery } from './device-domain';
|
|
23
|
+
import { fallbackDevice, registeredDevices } from './define-theme-device';
|
|
24
24
|
import { readInitialTheme, readUrlIntent } from './theme-persistence';
|
|
25
25
|
import type { DeviceThemeIntent, ThemeStorageKeys } from './theme-persistence';
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_VIEW_PARAM,
|
|
28
|
+
readUrlViewIntent,
|
|
29
|
+
sameViewSnapshot,
|
|
30
|
+
viewParamsSnapshot
|
|
31
|
+
} from '../views/params';
|
|
32
|
+
import type { DeviceViewIntent } from '../views/params';
|
|
33
|
+
|
|
34
|
+
export function useThemeScope(
|
|
35
|
+
keys: ThemeStorageKeys,
|
|
36
|
+
colorParam: string,
|
|
37
|
+
viewParam: string = DEFAULT_VIEW_PARAM
|
|
38
|
+
) {
|
|
28
39
|
const urlIntent = readUrlIntent(colorParam);
|
|
40
|
+
// 版式意图:URL 一次读入(query 来自设备域 `currentQuery()`,设备清单来自设备域注册表)。
|
|
41
|
+
// 与配色侧读 `window.location.search` 同源不同径——版式意图要**跟着路由走**:/login 与
|
|
42
|
+
// /register 可带不同 `th`;store 化后由编排层 watch `currentQuery()` 保持响应。
|
|
43
|
+
const urlViewIntent = readUrlViewIntent(currentQuery(), viewParam, registeredDevices());
|
|
29
44
|
|
|
30
45
|
/**
|
|
31
46
|
* URL 按设备指定的配色(**响应式**)
|
|
@@ -90,6 +105,66 @@ export function useThemeScope(keys: ThemeStorageKeys, colorParam: string) {
|
|
|
90
105
|
if (activeView.value !== next) activeView.value = next;
|
|
91
106
|
}
|
|
92
107
|
|
|
108
|
+
/* ==========================================================================
|
|
109
|
+
版式意图(2026-10-20 store 化)—— 与配色侧 `deviceTheme` / `themeId` 完全同形
|
|
110
|
+
========================================================================== */
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* URL 按设备**显式**指定的版式(**响应式** ref)
|
|
114
|
+
*
|
|
115
|
+
* 只记设备专属键(`?th_<urlKey>`);通用键(`?th`)在 `viewIntent`。做成 ref 而非
|
|
116
|
+
* 普通常量,是 debug 面板的显式操作会写/清当前设备那条(见 `setDeviceView`)。
|
|
117
|
+
*/
|
|
118
|
+
const deviceView = ref<DeviceViewIntent>({ ...urlViewIntent.deviceView });
|
|
119
|
+
|
|
120
|
+
/** 全局版式意图 = URL 通用键(`?th`);debug 显式写它时不落盘、不写 URL */
|
|
121
|
+
const viewIntent = ref<string | null>(urlViewIntent.viewIntent);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 某设备**显式指定**的版式 id(专属键 > 通用键;都没有返回 null)
|
|
125
|
+
* —— `<ThemeView>` 与调试面板的**唯一数据源**(取代原先每次渲染现读 route.query)。
|
|
126
|
+
*/
|
|
127
|
+
function deviceViewOf(device: string = activeDevice.value): string | null {
|
|
128
|
+
return deviceView.value[device] ?? viewIntent.value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 某设备的版式是否被 URL **专属键**显式指定 → 该设备"锁定" */
|
|
132
|
+
function urlLockedViewOf(device: string): boolean {
|
|
133
|
+
return deviceView.value[device] !== undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 清掉某设备被 URL 写下的版式 —— **只该由用户的显式操作调用**(debug 选回生效版式) */
|
|
137
|
+
function clearDeviceViewOverride(device: string = activeDevice.value): void {
|
|
138
|
+
if (deviceView.value[device] === undefined) return;
|
|
139
|
+
const next = { ...deviceView.value };
|
|
140
|
+
delete next[device];
|
|
141
|
+
deviceView.value = next;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 显式设置某设备的版式(debug 面板用;**只写 store**,不写 URL、不落盘) */
|
|
145
|
+
function setDeviceView(device: string, id: string): void {
|
|
146
|
+
const next = { ...deviceView.value };
|
|
147
|
+
next[device] = id;
|
|
148
|
+
deviceView.value = next;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* URL 上 `th*` **原始值**变化 → 重新读入 store(编排层 watch `currentQuery()` 调它)
|
|
153
|
+
*
|
|
154
|
+
* 🔴 "仅当原始值变化才同步"靠快照判等:debug 改写只碰 store(不写 URL),快照
|
|
155
|
+
* 不变 ⇒ 不会被误冲;跨页带不同 `th`(/login → /register)⇒ 同步,行为与
|
|
156
|
+
* store 化前"每次渲染现读 URL"一致。
|
|
157
|
+
*/
|
|
158
|
+
let lastViewSnapshot = viewParamsSnapshot(currentQuery(), viewParam, registeredDevices());
|
|
159
|
+
function syncUrlViewIntent(): void {
|
|
160
|
+
const next = viewParamsSnapshot(currentQuery(), viewParam, registeredDevices());
|
|
161
|
+
if (sameViewSnapshot(lastViewSnapshot, next)) return;
|
|
162
|
+
lastViewSnapshot = next;
|
|
163
|
+
const fresh = readUrlViewIntent(currentQuery(), viewParam, registeredDevices());
|
|
164
|
+
deviceView.value = { ...fresh.deviceView };
|
|
165
|
+
viewIntent.value = fresh.viewIntent;
|
|
166
|
+
}
|
|
167
|
+
|
|
93
168
|
/**
|
|
94
169
|
* 按设备 + 页面 + 版式解析出**实际生效**的配色记录。
|
|
95
170
|
* 🔴 设备专属 URL 配色优先于全局 themeId:`deviceTheme[device] ?? themeId`。
|
|
@@ -126,8 +201,11 @@ export function useThemeScope(keys: ThemeStorageKeys, colorParam: string) {
|
|
|
126
201
|
|
|
127
202
|
return {
|
|
128
203
|
urlIntent,
|
|
204
|
+
urlViewIntent,
|
|
129
205
|
deviceTheme,
|
|
130
206
|
themeId,
|
|
207
|
+
deviceView,
|
|
208
|
+
viewIntent,
|
|
131
209
|
activeDevice,
|
|
132
210
|
activePage,
|
|
133
211
|
activeView,
|
|
@@ -138,6 +216,11 @@ export function useThemeScope(keys: ThemeStorageKeys, colorParam: string) {
|
|
|
138
216
|
themes,
|
|
139
217
|
urlLockedThemeOf,
|
|
140
218
|
clearDeviceThemeOverride,
|
|
219
|
+
urlLockedViewOf,
|
|
220
|
+
clearDeviceViewOverride,
|
|
221
|
+
deviceViewOf,
|
|
222
|
+
setDeviceView,
|
|
223
|
+
syncUrlViewIntent,
|
|
141
224
|
setActiveDevice,
|
|
142
225
|
setActivePageView,
|
|
143
226
|
themeRecordFor,
|
package/src/vue/theme-store.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* · `isDark` → ThemeRoot 挂 `.dark` 类(纯外观)
|
|
11
11
|
* · `packageId` → ThemeView / 面板选版式的「在哪个包里找」
|
|
12
12
|
* · `viewFor(page)` → 主题包声明档(`views.<page>`)这一档
|
|
13
|
+
* · `deviceViewOf` → URL 版式意图档(`?th` / `?th_<urlKey>`,2026-10-20 store 化)
|
|
14
|
+
* · `setDeviceView` / `clearDeviceViewOverride` → 面板写侧(切版式只写 store,不碰路由)
|
|
13
15
|
* · `setActivePageView` → ThemeView 告知 store「当前页 + 版式」(配色挂在版式下)
|
|
14
16
|
* · `activePage` → 面板展示当前页
|
|
15
17
|
* · `themeId` / `mode` → 面板展示 / 切换配色与明暗
|
|
@@ -41,6 +43,15 @@ export interface ThemeStoreLike {
|
|
|
41
43
|
readonly mode: ThemeMode;
|
|
42
44
|
/** 某页当前声明式版式 id(主题包 `views.<page>` 声明;无声明返回 undefined) */
|
|
43
45
|
viewFor(page: string, device?: string): string | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* 某设备**显式指定**的版式 id(URL 一次读入 + debug 显式操作;专属键 > 通用键,
|
|
48
|
+
* 都没有返回 null)—— ThemeView 的 URL 档数据源(2026-10-20 store 化)
|
|
49
|
+
*/
|
|
50
|
+
deviceViewOf(device?: string): string | null;
|
|
51
|
+
/** 显式设置某设备的版式(debug 面板用;只写 store,不写 URL、不落盘) */
|
|
52
|
+
setDeviceView(device: string, id: string): void;
|
|
53
|
+
/** 清掉某设备被 URL 显式指定的版式意图(用户的显式操作前调用,如 debug 选回生效版式) */
|
|
54
|
+
clearDeviceViewOverride(device?: string): void;
|
|
44
55
|
/** 告知 store「当前是哪一页、哪套版式」(配色挂在版式下,token 注入依赖它) */
|
|
45
56
|
setActivePageView(page: string, viewId: string): void;
|
|
46
57
|
/** 按包/设备/页解析生效配色记录(含 URL 设备覆盖;面板只要 `meta.id` 与 `tone`) */
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* · 设备 → 设备域 `registeredDevices()` / `currentDevice()`(`defineThemeDevice` 自动注册)
|
|
11
11
|
* · 页面 → 页面域 `pageOf()`(`defineThemePage` 自动注册)+ store `activePage`(ThemeView 维护)
|
|
12
12
|
* · 配色查询 → 内核注册表单例(`listColorsOf` / `toneOfColor` / `listColorViews`)
|
|
13
|
-
* ·
|
|
13
|
+
* · 版式意图 → store `deviceViewOf` / `setDeviceView`(2026-10-20 store 化:
|
|
14
|
+
* URL 一次读入,debug 改写只写 store、不碰路由)
|
|
14
15
|
*
|
|
15
16
|
* 于是宿主**零接线**:App.vue 里一行 `<ThemeDebugPanel />`,其余全在库内自动完成。
|
|
16
17
|
*
|
|
@@ -29,11 +30,10 @@
|
|
|
29
30
|
import { computed, inject, ref } from 'vue';
|
|
30
31
|
import type { ComputedRef } from 'vue';
|
|
31
32
|
import { listColorViews, listColorsOf, toneOfColor } from '../registry';
|
|
32
|
-
import { readDeviceParam } from '../views/params';
|
|
33
33
|
import type { ThemeDebugActions, ThemeDebugColor, ThemeDebugModel } from './types';
|
|
34
|
-
import { currentDevice
|
|
34
|
+
import { currentDevice } from './device-domain';
|
|
35
35
|
import { pageOf } from './view-page';
|
|
36
|
-
import { registeredDevices
|
|
36
|
+
import { registeredDevices } from './define-theme-device';
|
|
37
37
|
import { themeStoreKey } from './theme-store';
|
|
38
38
|
import type { ThemeStoreLike } from './theme-store';
|
|
39
39
|
|
|
@@ -78,8 +78,8 @@ export function useThemeDebugPanel(): {
|
|
|
78
78
|
if (!registry) return store.packageId;
|
|
79
79
|
const pkg = store.packageId;
|
|
80
80
|
const dev = device.value;
|
|
81
|
-
//
|
|
82
|
-
const fromUrl = asText(
|
|
81
|
+
// 版式意图来自 store(2026-10-20 store 化):设备专属 > 通用,与容器 pick 同一数据源
|
|
82
|
+
const fromUrl = asText(store.deviceViewOf(dev));
|
|
83
83
|
if (fromUrl && fromUrl !== 'base') {
|
|
84
84
|
const inCurrent = registry.resolve(fromUrl, pkg, dev);
|
|
85
85
|
if (inCurrent) return inCurrent;
|
|
@@ -154,18 +154,18 @@ export function useThemeDebugPanel(): {
|
|
|
154
154
|
},
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
*
|
|
157
|
+
* 切换版式(**只写 store,URL 全程不动** —— 2026-10-20 store 化)
|
|
158
158
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
159
|
+
* 旧实现改 URL 版式键(`replaceQuery`),要先判断"当前设备实际由哪条键决定"
|
|
160
|
+
* (专属键在就改专属键,否则改通用键——改错键会被专属键盖住,"点了没反应");
|
|
161
|
+
* store 化后写**该设备的显式意图**(`setDeviceView`),不存在"改错键"问题,
|
|
162
|
+
* 且只影响当前设备(旧实现只有通用键时,改一下会影响所有设备)。
|
|
163
|
+
* 选回当前生效版式 = 清掉该设备意图(`clearDeviceViewOverride`),回到
|
|
164
|
+
* URL/声明档,与旧实现"删参数"净效果一致。
|
|
163
165
|
*/
|
|
164
166
|
pickView(id) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (id === activeViewId.value) replaceQuery({ [key]: undefined });
|
|
168
|
-
else replaceQuery({ [key]: id });
|
|
167
|
+
if (id === activeViewId.value) store.clearDeviceViewOverride(device.value);
|
|
168
|
+
else store.setDeviceView(device.value, id);
|
|
169
169
|
},
|
|
170
170
|
|
|
171
171
|
/**
|