diff --git a/ui/env.js b/ui/env.js index f80eb0b..640e136 100644 --- a/ui/env.js +++ b/ui/env.js @@ -11,7 +11,8 @@ export default async ($mod) => { } // Initialize VBase Service - const vbase = new VBase('vb', $mod.scoped); // Relative path + $mod.users = {} + const vbase = new VBase('vb', $mod.scoped, null, $mod.users); // Relative path $mod.$vbase = vbase; // Wrap Axios: add auth header diff --git a/ui/vbase.js b/ui/vbase.js index 5a7a345..6bbcac8 100644 --- a/ui/vbase.js +++ b/ui/vbase.js @@ -14,7 +14,7 @@ export const Level = { }; class VBase { - constructor(scope, baseURL, login_page) { + constructor(scope, baseURL, login_page, users = {}) { if (!scope) throw new Error('VBase: scope is required'); if (!baseURL) baseURL = window.location.origin; if (baseURL === '' || baseURL === '/') baseURL = window.location.origin; @@ -25,10 +25,16 @@ class VBase { this.tokenKey = `vbase_token`; this.refreshTokenKey = `vbase_refresh_token`; this.userKey = `vbase_user`; + this.users = users; this._token = localStorage.getItem(this.tokenKey) || ''; this._refreshToken = localStorage.getItem(this.refreshTokenKey) || ''; this._user = JSON.parse(localStorage.getItem(this.userKey) || 'null'); + this._pendingUserIDs = new Set(); + this._loadingUserIDs = new Set(); + this._resolvedUserIDs = new Set(); + this._pendingUserFlush = null; + this._cachePublicUser(this._user); if (this._token) { this.fetchUser() } @@ -56,6 +62,7 @@ class VBase { this._user = val; if (val) localStorage.setItem(this.userKey, JSON.stringify(val)); else localStorage.removeItem(this.userKey); + this._cachePublicUser(val); } // ========== API 请求 ========== @@ -212,6 +219,30 @@ class VBase { this.token = ''; this.refreshToken = ''; this.user = null; + for (const id of Object.keys(this.users)) { + delete this.users[id]; + } + this._pendingUserIDs.clear(); + this._loadingUserIDs.clear(); + this._resolvedUserIDs.clear(); + this._pendingUserFlush = null; + } + + User(id) { + if (!id) return {}; + + if (!this.users[id]) { + this.users[id] = {}; + } + + if (!this._resolvedUserIDs.has(id) && !this._loadingUserIDs.has(id)) { + this._pendingUserIDs.add(id); + if (!this._pendingUserFlush) { + this._pendingUserFlush = Promise.resolve().then(() => this._flushUserRequests()); + } + } + + return this.users[id]; } isExpired(token) { @@ -341,6 +372,57 @@ class VBase { } ); } + + _cachePublicUser(user) { + if (!user?.id) return; + + const cached = { + ...user, + name: user.name || user.nickname || user.username || '', + icon: user.icon || user.avatar || '', + avatar: user.avatar || user.icon || '', + }; + if (!this.users[cached.id]) { + this.users[cached.id] = {}; + } + Object.assign(this.users[cached.id], cached); + this._resolvedUserIDs.add(cached.id); + this._loadingUserIDs.delete(cached.id); + } + + async _flushUserRequests() { + const ids = [...this._pendingUserIDs].filter(Boolean); + this._pendingUserIDs.clear(); + this._pendingUserFlush = null; + if (ids.length === 0) return; + + for (const id of ids) { + this._loadingUserIDs.add(id); + } + + try { + const res = await this.request('POST', '/api/auth/users', { ids }); + const items = Array.isArray(res?.items) ? res.items : []; + const found = new Set(); + + for (const item of items) { + this._cachePublicUser(item); + found.add(item.id); + } + + for (const id of ids) { + this._loadingUserIDs.delete(id); + if (!found.has(id)) { + this._resolvedUserIDs.add(id); + } + } + } catch (error) { + for (const id of ids) { + this._loadingUserIDs.delete(id); + } + console.warn('VBase: batch fetch users failed', error); + } + } } export default VBase;