@lacqjs/nuxt-dict 0.0.2
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/README.md +0 -0
- package/dist/module.d.mts +13 -0
- package/dist/module.d.ts +13 -0
- package/dist/module.json +13 -0
- package/dist/module.mjs +83 -0
- package/dist/runtime/composables/useDict.d.ts +14 -0
- package/dist/runtime/composables/useDict.js +59 -0
- package/dist/runtime/composables/useDictOptions.d.ts +13 -0
- package/dist/runtime/composables/useDictOptions.js +17 -0
- package/dist/runtime/composables/useDictTree.d.ts +13 -0
- package/dist/runtime/composables/useDictTree.js +53 -0
- package/dist/runtime/composables/useLocale.d.ts +10 -0
- package/dist/runtime/composables/useLocale.js +20 -0
- package/dist/runtime/core/adapter.d.ts +17 -0
- package/dist/runtime/core/adapter.js +50 -0
- package/dist/runtime/core/cache/indexeddb-cache.d.ts +48 -0
- package/dist/runtime/core/cache/indexeddb-cache.js +142 -0
- package/dist/runtime/core/cache/memory-cache.d.ts +25 -0
- package/dist/runtime/core/cache/memory-cache.js +79 -0
- package/dist/runtime/core/cache/version-check.d.ts +26 -0
- package/dist/runtime/core/cache/version-check.js +38 -0
- package/dist/runtime/core/dict-manager.d.ts +74 -0
- package/dist/runtime/core/dict-manager.js +217 -0
- package/dist/runtime/options.d.ts +3 -0
- package/dist/runtime/options.js +33 -0
- package/dist/runtime/plugins/dict.d.ts +2 -0
- package/dist/runtime/plugins/dict.js +130 -0
- package/dist/runtime/types/index.d.ts +171 -0
- package/dist/runtime/types/index.js +0 -0
- package/dist/runtime/utils/dict-translator.d.ts +34 -0
- package/dist/runtime/utils/dict-translator.js +30 -0
- package/dist/runtime/utils/logger.d.ts +10 -0
- package/dist/runtime/utils/logger.js +33 -0
- package/dist/types.d.mts +3 -0
- package/package.json +88 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export class MemoryCache {
|
|
2
|
+
cache;
|
|
3
|
+
maxSize;
|
|
4
|
+
/** 毫秒级 TTL,0 表示永不过期 */
|
|
5
|
+
ttl;
|
|
6
|
+
constructor(maxSize = 200, ttl = 0) {
|
|
7
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
8
|
+
this.maxSize = maxSize;
|
|
9
|
+
this.ttl = ttl;
|
|
10
|
+
}
|
|
11
|
+
/** 读取缓存项,命中后移至末尾(LRU),TTL 过期则清除 */
|
|
12
|
+
get(key) {
|
|
13
|
+
const entry = this.cache.get(key);
|
|
14
|
+
if (!entry) return void 0;
|
|
15
|
+
if (this.ttl > 0 && Date.now() - entry.timestamp > this.ttl) {
|
|
16
|
+
this.cache.delete(key);
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
this.cache.delete(key);
|
|
20
|
+
this.cache.set(key, entry);
|
|
21
|
+
return entry;
|
|
22
|
+
}
|
|
23
|
+
/** 写入缓存项,超过 maxSize 时淘汰最旧条目 */
|
|
24
|
+
set(key, entry) {
|
|
25
|
+
if (this.cache.has(key)) {
|
|
26
|
+
this.cache.delete(key);
|
|
27
|
+
} else {
|
|
28
|
+
this.sweep();
|
|
29
|
+
if (this.cache.size >= this.maxSize) {
|
|
30
|
+
const firstKey = this.cache.keys().next().value;
|
|
31
|
+
if (firstKey !== void 0) {
|
|
32
|
+
this.cache.delete(firstKey);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
this.cache.set(key, entry);
|
|
37
|
+
}
|
|
38
|
+
has(key) {
|
|
39
|
+
const entry = this.cache.get(key);
|
|
40
|
+
if (!entry) return false;
|
|
41
|
+
if (this.ttl > 0 && Date.now() - entry.timestamp > this.ttl) {
|
|
42
|
+
this.cache.delete(key);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
delete(key) {
|
|
48
|
+
this.cache.delete(key);
|
|
49
|
+
}
|
|
50
|
+
clear() {
|
|
51
|
+
this.cache.clear();
|
|
52
|
+
}
|
|
53
|
+
/** 按 key 前缀删除缓存项。用于按仓库名清除内存缓存(key 格式: `{storeName}:...`) */
|
|
54
|
+
deleteByPrefix(prefix) {
|
|
55
|
+
for (const key of this.cache.keys()) {
|
|
56
|
+
if (key.startsWith(prefix)) {
|
|
57
|
+
this.cache.delete(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
get size() {
|
|
62
|
+
this.sweep();
|
|
63
|
+
return this.cache.size;
|
|
64
|
+
}
|
|
65
|
+
keys() {
|
|
66
|
+
this.sweep();
|
|
67
|
+
return Array.from(this.cache.keys());
|
|
68
|
+
}
|
|
69
|
+
/** 清除所有已过期的缓存项 */
|
|
70
|
+
sweep() {
|
|
71
|
+
if (this.ttl <= 0) return;
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
for (const [key, entry] of this.cache) {
|
|
74
|
+
if (now - entry.timestamp > this.ttl) {
|
|
75
|
+
this.cache.delete(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DictAdapter } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 版本检查器,使用 localStorage 存储版本号(替代 IndexedDB meta store)。
|
|
4
|
+
* localStorage 同步读写、无需异步初始化,比 IndexedDB 更简单可靠。
|
|
5
|
+
*
|
|
6
|
+
* 逻辑:
|
|
7
|
+
* 1. 调用远程版本接口 → 失败则抛出,不操作 localStorage,不清理字典
|
|
8
|
+
* 2. 成功则比对 → 版本一致不操作 → 不一致时写入 localStorage、标记 changed
|
|
9
|
+
*/
|
|
10
|
+
export declare class VersionCheck {
|
|
11
|
+
private adapter;
|
|
12
|
+
private storageKey;
|
|
13
|
+
constructor(adapter: DictAdapter, storageKey: string);
|
|
14
|
+
/**
|
|
15
|
+
* 执行版本比对。
|
|
16
|
+
* @returns changed 为 true 时调用方应执行 invalidateAll 清空全部缓存
|
|
17
|
+
*/
|
|
18
|
+
check(storeName: string): Promise<{
|
|
19
|
+
changed: boolean;
|
|
20
|
+
version: string;
|
|
21
|
+
}>;
|
|
22
|
+
/** 从 localStorage 读取版本号,不可用时返回 null */
|
|
23
|
+
private getStoredVersion;
|
|
24
|
+
/** 将版本号写入 localStorage */
|
|
25
|
+
private setStoredVersion;
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class VersionCheck {
|
|
2
|
+
adapter;
|
|
3
|
+
storageKey;
|
|
4
|
+
constructor(adapter, storageKey) {
|
|
5
|
+
this.adapter = adapter;
|
|
6
|
+
this.storageKey = typeof storageKey === "string" ? storageKey : "__NUXT_DICT_VERSION__";
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 执行版本比对。
|
|
10
|
+
* @returns changed 为 true 时调用方应执行 invalidateAll 清空全部缓存
|
|
11
|
+
*/
|
|
12
|
+
async check(storeName) {
|
|
13
|
+
const storedVersion = this.getStoredVersion();
|
|
14
|
+
const remoteVersion = await this.adapter.fetchVersion(storeName);
|
|
15
|
+
const changed = storedVersion !== remoteVersion;
|
|
16
|
+
if (changed) {
|
|
17
|
+
this.setStoredVersion(remoteVersion);
|
|
18
|
+
}
|
|
19
|
+
return { changed, version: remoteVersion };
|
|
20
|
+
}
|
|
21
|
+
/** 从 localStorage 读取版本号,不可用时返回 null */
|
|
22
|
+
getStoredVersion() {
|
|
23
|
+
if (typeof localStorage === "undefined") return null;
|
|
24
|
+
try {
|
|
25
|
+
return localStorage.getItem(this.storageKey);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** 将版本号写入 localStorage */
|
|
31
|
+
setStoredVersion(version) {
|
|
32
|
+
if (typeof localStorage === "undefined") return;
|
|
33
|
+
try {
|
|
34
|
+
localStorage.setItem(this.storageKey, version);
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { IndexedDBCache } from './cache/indexeddb-cache.js';
|
|
2
|
+
import type { DictAdapter, DictEntry } from '../types/index.js';
|
|
3
|
+
export interface DictManagerOptions {
|
|
4
|
+
/** 仓库名 → DictAdapter 映射表。至少包含默认仓库 'dicts' 的 adapter */
|
|
5
|
+
adapters: Map<string, DictAdapter>;
|
|
6
|
+
indexedDB: IndexedDBCache;
|
|
7
|
+
memoryMax: number;
|
|
8
|
+
ttl: number;
|
|
9
|
+
/** localStorage 中存储版本号的 key */
|
|
10
|
+
versionStorageKey: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 字典管理器 —— 核心调度层。
|
|
14
|
+
*
|
|
15
|
+
* 缓存策略:内存缓存 → IndexedDB 持久缓存 → 网络请求
|
|
16
|
+
* 请求去重:对同一 key 的并发请求合并为单次网络调用(pendingRequests)
|
|
17
|
+
*/
|
|
18
|
+
export declare class DictManager {
|
|
19
|
+
private memoryCache;
|
|
20
|
+
private indexedDB;
|
|
21
|
+
private adapters;
|
|
22
|
+
/** 每个仓库独立的版本检查器 */
|
|
23
|
+
private versionChecks;
|
|
24
|
+
/** 正在进行的字典请求,用于去重 */
|
|
25
|
+
private pendingRequests;
|
|
26
|
+
/** 已完成版本检查的仓库集合 */
|
|
27
|
+
private checkedStores;
|
|
28
|
+
/** 正在进行的版本检查请求,用于去重 */
|
|
29
|
+
private pendingVersionChecks;
|
|
30
|
+
locale: import("vue").ShallowRef<string, string>;
|
|
31
|
+
constructor(options: DictManagerOptions);
|
|
32
|
+
/** 构建带存储库命名空间和语言后缀的缓存键 */
|
|
33
|
+
private buildKey;
|
|
34
|
+
/** 根据 storeName 获取对应的 adapter。找不到时回退到默认仓库 'dicts' 的 adapter */
|
|
35
|
+
private getAdapter;
|
|
36
|
+
/** 切换语言,清空内存缓存和待处理请求 */
|
|
37
|
+
setLocale(locale: string): void;
|
|
38
|
+
getLocale(): string;
|
|
39
|
+
/**
|
|
40
|
+
* 惰性版本检查:首次访问该仓库时检查版本变更,按需清理缓存。
|
|
41
|
+
* 并发调用去重 —— 同一仓库的多个 getDict 共享单次版本检查。
|
|
42
|
+
*/
|
|
43
|
+
private ensureVersionChecked;
|
|
44
|
+
/**
|
|
45
|
+
* 获取字典数据。
|
|
46
|
+
* 优先级:内存缓存 → 合并中的请求 → IndexedDB → 网络请求
|
|
47
|
+
*/
|
|
48
|
+
getDict(type: string, storeName?: string): Promise<DictEntry>;
|
|
49
|
+
/** 执行实际的数据获取与缓存写入 */
|
|
50
|
+
private fetchAndCache;
|
|
51
|
+
/**
|
|
52
|
+
* 强制刷新指定字典:跳过缓存,直接从网络获取最新数据。
|
|
53
|
+
* 适用于用户手动刷新、数据变更通知等场景。
|
|
54
|
+
*/
|
|
55
|
+
refresh(type: string, storeName?: string): Promise<DictEntry>;
|
|
56
|
+
/** 翻译 code → label,未命中时回退原样 */
|
|
57
|
+
translate(type: string, code: string | number, storeName?: string): string;
|
|
58
|
+
/** 树形字典中查找 code 的完整层级路径 */
|
|
59
|
+
translatePath(type: string, code: string | number, separator?: string, storeName?: string): string;
|
|
60
|
+
/** DFS 在树形字典中查找目标编码的路径 */
|
|
61
|
+
private findPathInTree;
|
|
62
|
+
/**
|
|
63
|
+
* code 比对 —— 统一转字符串比较。
|
|
64
|
+
* 避免字典配置中数字/字符串编码不一致导致的匹配失败。
|
|
65
|
+
*/
|
|
66
|
+
private codeMatch;
|
|
67
|
+
/** 初始化:版本检查已改为惰性执行,在首次访问仓库时触发 */
|
|
68
|
+
initialize(): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* 失效缓存数据。
|
|
71
|
+
* @param storeName 指定要失效的存储库,不传则清空所有存储库及内存缓存
|
|
72
|
+
*/
|
|
73
|
+
invalidateAll(storeName?: string): Promise<void>;
|
|
74
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { shallowRef } from "vue";
|
|
2
|
+
import { MemoryCache } from "./cache/memory-cache.js";
|
|
3
|
+
import { DEFAULT_STORE_NAME } from "./cache/indexeddb-cache.js";
|
|
4
|
+
import { VersionCheck } from "./cache/version-check.js";
|
|
5
|
+
export class DictManager {
|
|
6
|
+
memoryCache;
|
|
7
|
+
indexedDB;
|
|
8
|
+
adapters;
|
|
9
|
+
/** 每个仓库独立的版本检查器 */
|
|
10
|
+
versionChecks;
|
|
11
|
+
/** 正在进行的字典请求,用于去重 */
|
|
12
|
+
pendingRequests;
|
|
13
|
+
/** 已完成版本检查的仓库集合 */
|
|
14
|
+
checkedStores = /* @__PURE__ */ new Set();
|
|
15
|
+
/** 正在进行的版本检查请求,用于去重 */
|
|
16
|
+
pendingVersionChecks = /* @__PURE__ */ new Map();
|
|
17
|
+
locale = shallowRef("zh-CN");
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.memoryCache = new MemoryCache(options.memoryMax, options.ttl);
|
|
20
|
+
this.indexedDB = options.indexedDB;
|
|
21
|
+
this.adapters = options.adapters;
|
|
22
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
23
|
+
this.versionChecks = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const [storeName, adapter] of options.adapters) {
|
|
25
|
+
const storageKey = storeName === DEFAULT_STORE_NAME ? options.versionStorageKey : `${options.versionStorageKey}__${storeName}`;
|
|
26
|
+
this.versionChecks.set(storeName, new VersionCheck(adapter, storageKey));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** 构建带存储库命名空间和语言后缀的缓存键 */
|
|
30
|
+
buildKey(dictType, storeName) {
|
|
31
|
+
return `${storeName}:${dictType}_${this.locale.value}`;
|
|
32
|
+
}
|
|
33
|
+
/** 根据 storeName 获取对应的 adapter。找不到时回退到默认仓库 'dicts' 的 adapter */
|
|
34
|
+
getAdapter(storeName) {
|
|
35
|
+
return this.adapters.get(storeName) ?? this.adapters.get(DEFAULT_STORE_NAME);
|
|
36
|
+
}
|
|
37
|
+
/** 切换语言,清空内存缓存和待处理请求 */
|
|
38
|
+
setLocale(locale) {
|
|
39
|
+
if (this.locale.value !== locale) {
|
|
40
|
+
this.locale.value = locale;
|
|
41
|
+
this.memoryCache.clear();
|
|
42
|
+
this.pendingRequests.clear();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
getLocale() {
|
|
46
|
+
return this.locale.value;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 惰性版本检查:首次访问该仓库时检查版本变更,按需清理缓存。
|
|
50
|
+
* 并发调用去重 —— 同一仓库的多个 getDict 共享单次版本检查。
|
|
51
|
+
*/
|
|
52
|
+
async ensureVersionChecked(storeName) {
|
|
53
|
+
if (this.checkedStores.has(storeName)) return;
|
|
54
|
+
const pending = this.pendingVersionChecks.get(storeName);
|
|
55
|
+
if (pending) {
|
|
56
|
+
await pending;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const promise = this.versionChecks.get(storeName) ? (async () => {
|
|
60
|
+
const vc = this.versionChecks.get(storeName);
|
|
61
|
+
try {
|
|
62
|
+
if (typeof localStorage !== "undefined") {
|
|
63
|
+
const { changed } = await vc.check(storeName);
|
|
64
|
+
if (changed) {
|
|
65
|
+
await this.invalidateAll(storeName);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
})() : Promise.resolve();
|
|
71
|
+
this.pendingVersionChecks.set(storeName, promise);
|
|
72
|
+
this.checkedStores.add(storeName);
|
|
73
|
+
try {
|
|
74
|
+
await promise;
|
|
75
|
+
} finally {
|
|
76
|
+
this.pendingVersionChecks.delete(storeName);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 获取字典数据。
|
|
81
|
+
* 优先级:内存缓存 → 合并中的请求 → IndexedDB → 网络请求
|
|
82
|
+
*/
|
|
83
|
+
async getDict(type, storeName = DEFAULT_STORE_NAME) {
|
|
84
|
+
await this.ensureVersionChecked(storeName);
|
|
85
|
+
const key = this.buildKey(type, storeName);
|
|
86
|
+
const memoryEntry = this.memoryCache.get(key);
|
|
87
|
+
if (memoryEntry) {
|
|
88
|
+
return memoryEntry.data;
|
|
89
|
+
}
|
|
90
|
+
const pending = this.pendingRequests.get(key);
|
|
91
|
+
if (pending) {
|
|
92
|
+
return pending;
|
|
93
|
+
}
|
|
94
|
+
const promise = this.fetchAndCache(type, storeName, key);
|
|
95
|
+
this.pendingRequests.set(key, promise);
|
|
96
|
+
try {
|
|
97
|
+
return await promise;
|
|
98
|
+
} finally {
|
|
99
|
+
this.pendingRequests.delete(key);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** 执行实际的数据获取与缓存写入 */
|
|
103
|
+
async fetchAndCache(type, storeName, key) {
|
|
104
|
+
const idbEntry = await this.indexedDB.get(storeName, type, this.locale.value);
|
|
105
|
+
if (idbEntry) {
|
|
106
|
+
this.memoryCache.set(key, {
|
|
107
|
+
data: idbEntry.data,
|
|
108
|
+
timestamp: Date.now(),
|
|
109
|
+
version: idbEntry.version
|
|
110
|
+
});
|
|
111
|
+
return idbEntry.data;
|
|
112
|
+
}
|
|
113
|
+
const adapter = this.getAdapter(storeName);
|
|
114
|
+
const response = await adapter.fetchDict(storeName, {
|
|
115
|
+
types: [type],
|
|
116
|
+
locale: this.locale.value
|
|
117
|
+
});
|
|
118
|
+
const entry = response.data[type];
|
|
119
|
+
if (!entry) {
|
|
120
|
+
throw new Error(`Dictionary type "${type}" not found in response`);
|
|
121
|
+
}
|
|
122
|
+
const cacheEntry = {
|
|
123
|
+
data: entry,
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
version: response.version
|
|
126
|
+
};
|
|
127
|
+
this.memoryCache.set(key, cacheEntry);
|
|
128
|
+
try {
|
|
129
|
+
await this.indexedDB.set(storeName, type, this.locale.value, cacheEntry);
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
return entry;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 强制刷新指定字典:跳过缓存,直接从网络获取最新数据。
|
|
136
|
+
* 适用于用户手动刷新、数据变更通知等场景。
|
|
137
|
+
*/
|
|
138
|
+
async refresh(type, storeName = DEFAULT_STORE_NAME) {
|
|
139
|
+
const key = this.buildKey(type, storeName);
|
|
140
|
+
this.memoryCache.delete(key);
|
|
141
|
+
this.pendingRequests.delete(key);
|
|
142
|
+
const adapter = this.getAdapter(storeName);
|
|
143
|
+
const response = await adapter.fetchDict(storeName, {
|
|
144
|
+
types: [type],
|
|
145
|
+
locale: this.locale.value
|
|
146
|
+
});
|
|
147
|
+
const entry = response.data[type];
|
|
148
|
+
if (!entry) {
|
|
149
|
+
throw new Error(`Dictionary type "${type}" not found in response`);
|
|
150
|
+
}
|
|
151
|
+
const cacheEntry = {
|
|
152
|
+
data: entry,
|
|
153
|
+
timestamp: Date.now(),
|
|
154
|
+
version: response.version
|
|
155
|
+
};
|
|
156
|
+
this.memoryCache.set(key, cacheEntry);
|
|
157
|
+
try {
|
|
158
|
+
await this.indexedDB.set(storeName, type, this.locale.value, cacheEntry);
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
return entry;
|
|
162
|
+
}
|
|
163
|
+
/** 翻译 code → label,未命中时回退原样 */
|
|
164
|
+
translate(type, code, storeName = DEFAULT_STORE_NAME) {
|
|
165
|
+
const key = this.buildKey(type, storeName);
|
|
166
|
+
const entry = this.memoryCache.get(key);
|
|
167
|
+
if (!entry) return String(code);
|
|
168
|
+
const item = entry.data.items.find((i) => this.codeMatch(i.code, code));
|
|
169
|
+
return item?.label ?? String(code);
|
|
170
|
+
}
|
|
171
|
+
/** 树形字典中查找 code 的完整层级路径 */
|
|
172
|
+
translatePath(type, code, separator = " / ", storeName = DEFAULT_STORE_NAME) {
|
|
173
|
+
const key = this.buildKey(type, storeName);
|
|
174
|
+
const entry = this.memoryCache.get(key);
|
|
175
|
+
if (!entry || !entry.data.tree) return String(code);
|
|
176
|
+
const path = this.findPathInTree(entry.data.tree, code);
|
|
177
|
+
return path.length > 0 ? path.join(separator) : String(code);
|
|
178
|
+
}
|
|
179
|
+
/** DFS 在树形字典中查找目标编码的路径 */
|
|
180
|
+
findPathInTree(nodes, targetCode) {
|
|
181
|
+
for (const node of nodes) {
|
|
182
|
+
if (this.codeMatch(node.code, targetCode)) {
|
|
183
|
+
return [node.label];
|
|
184
|
+
}
|
|
185
|
+
if (node.children && node.children.length > 0) {
|
|
186
|
+
const childPath = this.findPathInTree(node.children, targetCode);
|
|
187
|
+
if (childPath.length > 0) {
|
|
188
|
+
return [node.label, ...childPath];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* code 比对 —— 统一转字符串比较。
|
|
196
|
+
* 避免字典配置中数字/字符串编码不一致导致的匹配失败。
|
|
197
|
+
*/
|
|
198
|
+
codeMatch(a, b) {
|
|
199
|
+
return String(a) === String(b);
|
|
200
|
+
}
|
|
201
|
+
/** 初始化:版本检查已改为惰性执行,在首次访问仓库时触发 */
|
|
202
|
+
async initialize() {
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 失效缓存数据。
|
|
206
|
+
* @param storeName 指定要失效的存储库,不传则清空所有存储库及内存缓存
|
|
207
|
+
*/
|
|
208
|
+
async invalidateAll(storeName) {
|
|
209
|
+
if (storeName) {
|
|
210
|
+
this.memoryCache.deleteByPrefix(`${storeName}:`);
|
|
211
|
+
} else {
|
|
212
|
+
this.memoryCache.clear();
|
|
213
|
+
}
|
|
214
|
+
this.pendingRequests.clear();
|
|
215
|
+
await this.indexedDB.clear(storeName);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const defaultOptions = {
|
|
2
|
+
enable: true,
|
|
3
|
+
logLevel: 3,
|
|
4
|
+
api: {
|
|
5
|
+
baseURL: "/api",
|
|
6
|
+
dictEndpoint: "/dict/list",
|
|
7
|
+
versionEndpoint: "/dict/version"
|
|
8
|
+
},
|
|
9
|
+
cache: {
|
|
10
|
+
memoryMax: 200,
|
|
11
|
+
ttl: 0,
|
|
12
|
+
indexedDB: {
|
|
13
|
+
enabled: true,
|
|
14
|
+
dbName: "nuxt-dict"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
locale: {
|
|
18
|
+
default: "zh-CN",
|
|
19
|
+
source: "cookie",
|
|
20
|
+
cookieKey: "i18n_redirected",
|
|
21
|
+
queryKey: "lang",
|
|
22
|
+
headerKey: "accept-language",
|
|
23
|
+
paramKey: "lang",
|
|
24
|
+
apiHeaderKey: "X-Locale"
|
|
25
|
+
},
|
|
26
|
+
stores: {},
|
|
27
|
+
ssr: {
|
|
28
|
+
prefetch: []
|
|
29
|
+
},
|
|
30
|
+
version: {
|
|
31
|
+
storageKey: "__NUXT_DICT_VERSION__"
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { watch } from "vue";
|
|
2
|
+
import { defineNuxtPlugin, useRequestEvent, useCookie, useRoute } from "#imports";
|
|
3
|
+
import { DictManager } from "../core/dict-manager.js";
|
|
4
|
+
import { IndexedDBCache, DEFAULT_STORE_NAME } from "../core/cache/indexeddb-cache.js";
|
|
5
|
+
import { createDefaultAdapter } from "../core/adapter.js";
|
|
6
|
+
import { createDictTranslator } from "../utils/dict-translator.js";
|
|
7
|
+
import { createLogger } from "../utils/logger.js";
|
|
8
|
+
import { defaultOptions } from "../options.js";
|
|
9
|
+
function resolveServerLocale(options) {
|
|
10
|
+
const event = useRequestEvent();
|
|
11
|
+
if (!event) return options.locale.default;
|
|
12
|
+
if (options.locale.source === "cookie") {
|
|
13
|
+
const langCookie = useCookie(options.locale.cookieKey);
|
|
14
|
+
return langCookie.value || options.locale.default;
|
|
15
|
+
}
|
|
16
|
+
if (options.locale.source === "header") {
|
|
17
|
+
const lang = event.headers.get(options.locale.headerKey);
|
|
18
|
+
return lang ? lang.split(",")[0]?.split(";")[0]?.trim() ?? options.locale.default : options.locale.default;
|
|
19
|
+
}
|
|
20
|
+
if (options.locale.source === "query") {
|
|
21
|
+
const url = event.node.req.url || "";
|
|
22
|
+
const searchIndex = url.indexOf("?");
|
|
23
|
+
if (searchIndex === -1) return options.locale.default;
|
|
24
|
+
const params = new URLSearchParams(url.slice(searchIndex));
|
|
25
|
+
return params.get(options.locale.queryKey) || options.locale.default;
|
|
26
|
+
}
|
|
27
|
+
return options.locale.default;
|
|
28
|
+
}
|
|
29
|
+
function isAbsoluteURL(url) {
|
|
30
|
+
return /^https?:\/\//iu.test(url);
|
|
31
|
+
}
|
|
32
|
+
function resolveBaseURL(baseURL) {
|
|
33
|
+
if (isAbsoluteURL(baseURL)) {
|
|
34
|
+
return baseURL;
|
|
35
|
+
}
|
|
36
|
+
if (import.meta.client) {
|
|
37
|
+
return baseURL;
|
|
38
|
+
}
|
|
39
|
+
const event = useRequestEvent();
|
|
40
|
+
if (!event) return baseURL;
|
|
41
|
+
const protocol = event.headers.get("x-forwarded-proto") || "http";
|
|
42
|
+
const host = event.headers.get("host");
|
|
43
|
+
return host ? `${protocol}://${host}` : baseURL;
|
|
44
|
+
}
|
|
45
|
+
function resolveClientLocale(options) {
|
|
46
|
+
if (options.locale.source === "cookie") {
|
|
47
|
+
const langCookie = useCookie(options.locale.cookieKey);
|
|
48
|
+
return langCookie.value || options.locale.default;
|
|
49
|
+
}
|
|
50
|
+
if (options.locale.source === "query") {
|
|
51
|
+
const route = useRoute();
|
|
52
|
+
return route.query[options.locale.queryKey] || options.locale.default;
|
|
53
|
+
}
|
|
54
|
+
return options.locale.default;
|
|
55
|
+
}
|
|
56
|
+
async function initClientCache(manager, indexedDB, logger) {
|
|
57
|
+
try {
|
|
58
|
+
await indexedDB.init();
|
|
59
|
+
await manager.initialize();
|
|
60
|
+
} catch (e) {
|
|
61
|
+
logger.warn("IndexedDB init failed:", e);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function executePrefetch(manager, types, logger) {
|
|
65
|
+
try {
|
|
66
|
+
await Promise.all(types.map((type) => manager.getDict(type)));
|
|
67
|
+
} catch (e) {
|
|
68
|
+
logger.warn("SSR prefetch failed:", e);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function createAdapters(options, logger) {
|
|
72
|
+
const adapters = /* @__PURE__ */ new Map();
|
|
73
|
+
const defaultAdapter = options.api.adapter ?? createDefaultAdapter({
|
|
74
|
+
baseURL: resolveBaseURL(options.api.baseURL),
|
|
75
|
+
dictEndpoint: options.api.dictEndpoint,
|
|
76
|
+
versionEndpoint: options.api.versionEndpoint,
|
|
77
|
+
paramKey: options.locale.paramKey,
|
|
78
|
+
apiHeaderKey: options.locale.apiHeaderKey
|
|
79
|
+
});
|
|
80
|
+
adapters.set(DEFAULT_STORE_NAME, defaultAdapter);
|
|
81
|
+
logger.debug(`Dict adapter created for default store '${DEFAULT_STORE_NAME}': ${options.api.baseURL}${options.api.dictEndpoint}`);
|
|
82
|
+
for (const [storeName, storeApi] of Object.entries(options.stores)) {
|
|
83
|
+
const adapter = storeApi.adapter ?? createDefaultAdapter({
|
|
84
|
+
baseURL: resolveBaseURL(storeApi.baseURL ?? options.api.baseURL),
|
|
85
|
+
dictEndpoint: storeApi.dictEndpoint ?? options.api.dictEndpoint,
|
|
86
|
+
versionEndpoint: storeApi.versionEndpoint ?? options.api.versionEndpoint,
|
|
87
|
+
paramKey: options.locale.paramKey,
|
|
88
|
+
apiHeaderKey: options.locale.apiHeaderKey
|
|
89
|
+
});
|
|
90
|
+
adapters.set(storeName, adapter);
|
|
91
|
+
const desc = storeApi.adapter ? "custom adapter" : `${storeApi.baseURL ?? options.api.baseURL}${storeApi.dictEndpoint ?? options.api.dictEndpoint}`;
|
|
92
|
+
logger.debug(`Dict adapter created for store '${storeName}': ${desc}`);
|
|
93
|
+
}
|
|
94
|
+
return adapters;
|
|
95
|
+
}
|
|
96
|
+
const dictPlugin = defineNuxtPlugin(async (nuxtApp) => {
|
|
97
|
+
const options = nuxtApp.$config?.public?.dict ?? defaultOptions;
|
|
98
|
+
if (!options.enable) return;
|
|
99
|
+
const logger = createLogger("nuxt-dict", { level: options.logLevel });
|
|
100
|
+
const adapters = createAdapters(options, logger);
|
|
101
|
+
const indexedDB = new IndexedDBCache(options.cache.indexedDB.dbName);
|
|
102
|
+
const manager = new DictManager({
|
|
103
|
+
adapters,
|
|
104
|
+
indexedDB,
|
|
105
|
+
memoryMax: options.cache.memoryMax,
|
|
106
|
+
ttl: options.cache.ttl,
|
|
107
|
+
versionStorageKey: options.version.storageKey
|
|
108
|
+
});
|
|
109
|
+
const locale = import.meta.server ? resolveServerLocale(options) : resolveClientLocale(options);
|
|
110
|
+
manager.setLocale(locale);
|
|
111
|
+
if (import.meta.client && options.locale.source === "cookie") {
|
|
112
|
+
const langCookie = useCookie(options.locale.cookieKey);
|
|
113
|
+
watch(langCookie, (newLocale) => {
|
|
114
|
+
if (newLocale && newLocale !== manager.getLocale()) {
|
|
115
|
+
logger.debug(`Cookie locale changed to '${newLocale}'`);
|
|
116
|
+
manager.setLocale(newLocale);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (import.meta.client && options.cache.indexedDB.enabled) {
|
|
121
|
+
await initClientCache(manager, indexedDB, logger);
|
|
122
|
+
}
|
|
123
|
+
if (import.meta.server && options.ssr.prefetch.length > 0) {
|
|
124
|
+
await executePrefetch(manager, options.ssr.prefetch, logger);
|
|
125
|
+
}
|
|
126
|
+
const translator = createDictTranslator(manager);
|
|
127
|
+
nuxtApp.provide("dict", translator);
|
|
128
|
+
nuxtApp.provide("dictManager", manager);
|
|
129
|
+
});
|
|
130
|
+
export default dictPlugin;
|