Spaces:
Running
Running
Upload 104 files
Browse files- 前端页面/core/全局配置.js +50 -4
- 前端页面/core/榜单数据来源.js +3 -1
- 前端页面/core/类型定义.js +1 -0
- 前端页面/core/网络请求_业务API.js +28 -7
- 前端页面/core/网络请求_缓存管理.js +40 -4
- 前端页面/market/资源详情页面组件.js +3 -2
- 前端页面/profile/个人中心_UI模板.js +1 -1
- 前端页面/profile/个人中心视图.js +6 -4
- 前端页面/profile/个人资料工具.js +3 -0
前端页面/core/全局配置.js
CHANGED
|
@@ -263,6 +263,7 @@ export const CACHE = {
|
|
| 263 |
...RESOURCE_LIST_PREFIXES
|
| 264 |
],
|
| 265 |
RESOURCE_LIST_PREFIXES,
|
|
|
|
| 266 |
PUBLIC_LISTS: {
|
| 267 |
tasks: { prefix: "TasksCache_", endpoint: "/api/tasks" },
|
| 268 |
posts: { prefix: "PostsCache_", endpoint: "/api/posts" },
|
|
@@ -628,6 +629,43 @@ export function getTipLevelConfig() {
|
|
| 628 |
// 缓存键:ComfyCommunity_ProfileCache_{account}
|
| 629 |
|
| 630 |
const PROFILE_CACHE_PREFIX = "ComfyCommunity_ProfileCache_";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
const _pendingProfileRequests = new Map(); // 每账号共享请求与全部订阅者
|
| 632 |
const PENDING_TIMEOUT = 30000; // 挂起请求超时清理时间(毫秒)
|
| 633 |
let nextAvatarRenderId = 0;
|
|
@@ -734,12 +772,16 @@ function _mapAvatarField(profile) {
|
|
| 734 |
}
|
| 735 |
|
| 736 |
const PUBLIC_PROFILE_FIELDS = ["account", "name", "avatar", "avatarDataUrl", "intro", "gender", "age",
|
| 737 |
-
"country", "region", "created_at", "followers", "following", "privacy", "is_admin", "bannerUrl",
|
| 738 |
"receivedLikes", "receivedFavorites", "receivedUses"];
|
| 739 |
function _publicProfile(profile) {
|
| 740 |
-
|
| 741 |
.map(key => [key, ['avatar', 'avatarDataUrl', 'bannerUrl'].includes(key)
|
| 742 |
? normalizeProfileImageUrl(profile[key]) : profile[key]]));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
}
|
| 744 |
|
| 745 |
function _validProfile(profile) {
|
|
@@ -753,7 +795,7 @@ function _validProfile(profile) {
|
|
| 753 |
* @returns {Object|null} 缓存的用户资料
|
| 754 |
*/
|
| 755 |
export function getCachedProfile(account) {
|
| 756 |
-
if (!account) return null;
|
| 757 |
try {
|
| 758 |
const cached = localStorage.getItem(PROFILE_CACHE_PREFIX + account);
|
| 759 |
if (!cached) return null;
|
|
@@ -780,6 +822,9 @@ export function setCachedProfile(account, profile) {
|
|
| 780 |
if (!account || !_validProfile(profile)) return;
|
| 781 |
try {
|
| 782 |
localStorage.setItem(PROFILE_CACHE_PREFIX + account, JSON.stringify(_publicProfile(profile)));
|
|
|
|
|
|
|
|
|
|
| 783 |
} catch {}
|
| 784 |
}
|
| 785 |
|
|
@@ -799,6 +844,7 @@ export function getProfileWithSWR(account, apiFn, onUpdate) {
|
|
| 799 |
// 第二轨:同账号只取一次,但每个视图都订阅结果。
|
| 800 |
let entry = _pendingProfileRequests.get(account);
|
| 801 |
if (!entry && typeof apiFn === 'function') {
|
|
|
|
| 802 |
entry = { listeners: new Set(), timer: null };
|
| 803 |
_pendingProfileRequests.set(account, entry);
|
| 804 |
const release = () => {
|
|
@@ -809,7 +855,7 @@ export function getProfileWithSWR(account, apiFn, onUpdate) {
|
|
| 809 |
entry.timer = setTimeout(release, PENDING_TIMEOUT);
|
| 810 |
Promise.resolve().then(() => apiFn(account)).then(res => {
|
| 811 |
// 超时后的旧结果不能覆盖新请求,也不能重新写入旧资料。
|
| 812 |
-
if (_pendingProfileRequests.get(account) !== entry) return;
|
| 813 |
if (res?.status && res.status !== 'success') return;
|
| 814 |
const data = res && Object.hasOwn(res, 'data') ? res.data : res;
|
| 815 |
if (!_validProfile(data)) return;
|
|
|
|
| 263 |
...RESOURCE_LIST_PREFIXES
|
| 264 |
],
|
| 265 |
RESOURCE_LIST_PREFIXES,
|
| 266 |
+
CREATOR_DETAILS_CACHE_PREFIX: "api_/api/creators/",
|
| 267 |
PUBLIC_LISTS: {
|
| 268 |
tasks: { prefix: "TasksCache_", endpoint: "/api/tasks" },
|
| 269 |
posts: { prefix: "PostsCache_", endpoint: "/api/posts" },
|
|
|
|
| 629 |
// 缓存键:ComfyCommunity_ProfileCache_{account}
|
| 630 |
|
| 631 |
const PROFILE_CACHE_PREFIX = "ComfyCommunity_ProfileCache_";
|
| 632 |
+
let profileCacheDefault = { readable: true };
|
| 633 |
+
const profileCacheStates = new Map();
|
| 634 |
+
const profileCacheState = account => profileCacheStates.get(account) || profileCacheDefault;
|
| 635 |
+
function rememberProfileCacheState(account, readable) {
|
| 636 |
+
if (!profileCacheStates.has(account) && profileCacheStates.size >= CACHE.MAX_MEMORY_ITEMS) {
|
| 637 |
+
profileCacheStates.clear();
|
| 638 |
+
profileCacheDefault = { readable: false };
|
| 639 |
+
for (const pending of _pendingProfileRequests.values()) {
|
| 640 |
+
clearTimeout(pending.timer);
|
| 641 |
+
pending.listeners.clear();
|
| 642 |
+
}
|
| 643 |
+
_pendingProfileRequests.clear();
|
| 644 |
+
}
|
| 645 |
+
const state = { readable };
|
| 646 |
+
profileCacheStates.set(account, state);
|
| 647 |
+
return state;
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
export function captureProfileRead(account) {
|
| 651 |
+
const state = profileCacheStates.get(account) || rememberProfileCacheState(account, profileCacheDefault.readable);
|
| 652 |
+
return () => profileCacheState(account) === state;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
export function invalidateCachedProfiles(accounts) {
|
| 656 |
+
for (const account of new Set(accounts)) {
|
| 657 |
+
if (typeof account !== 'string' || !account.trim()) continue;
|
| 658 |
+
rememberProfileCacheState(account, false);
|
| 659 |
+
const pending = _pendingProfileRequests.get(account);
|
| 660 |
+
if (pending) {
|
| 661 |
+
_pendingProfileRequests.delete(account);
|
| 662 |
+
clearTimeout(pending.timer);
|
| 663 |
+
pending.listeners.clear();
|
| 664 |
+
}
|
| 665 |
+
try { localStorage.removeItem(PROFILE_CACHE_PREFIX + account); } catch {}
|
| 666 |
+
}
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
const _pendingProfileRequests = new Map(); // 每账号共享请求与全部订阅者
|
| 670 |
const PENDING_TIMEOUT = 30000; // 挂起请求超时清理时间(毫秒)
|
| 671 |
let nextAvatarRenderId = 0;
|
|
|
|
| 772 |
}
|
| 773 |
|
| 774 |
const PUBLIC_PROFILE_FIELDS = ["account", "name", "avatar", "avatarDataUrl", "intro", "gender", "age",
|
| 775 |
+
"country", "region", "created_at", "followers", "following", "following_count", "privacy", "is_admin", "bannerUrl",
|
| 776 |
"receivedLikes", "receivedFavorites", "receivedUses"];
|
| 777 |
function _publicProfile(profile) {
|
| 778 |
+
const result = Object.fromEntries(PUBLIC_PROFILE_FIELDS.filter(key => Object.prototype.hasOwnProperty.call(profile, key))
|
| 779 |
.map(key => [key, ['avatar', 'avatarDataUrl', 'bannerUrl'].includes(key)
|
| 780 |
? normalizeProfileImageUrl(profile[key]) : profile[key]]));
|
| 781 |
+
result.following_count = Number.isSafeInteger(result.following_count) && result.following_count >= 0
|
| 782 |
+
? result.following_count : (Array.isArray(result.following) ? result.following.length : 0);
|
| 783 |
+
if (result.privacy?.follows === true) result.following = [];
|
| 784 |
+
return result;
|
| 785 |
}
|
| 786 |
|
| 787 |
function _validProfile(profile) {
|
|
|
|
| 795 |
* @returns {Object|null} 缓存的用户资料
|
| 796 |
*/
|
| 797 |
export function getCachedProfile(account) {
|
| 798 |
+
if (!account || !profileCacheState(account).readable) return null;
|
| 799 |
try {
|
| 800 |
const cached = localStorage.getItem(PROFILE_CACHE_PREFIX + account);
|
| 801 |
if (!cached) return null;
|
|
|
|
| 822 |
if (!account || !_validProfile(profile)) return;
|
| 823 |
try {
|
| 824 |
localStorage.setItem(PROFILE_CACHE_PREFIX + account, JSON.stringify(_publicProfile(profile)));
|
| 825 |
+
const state = profileCacheStates.get(account);
|
| 826 |
+
if (state) state.readable = true;
|
| 827 |
+
else rememberProfileCacheState(account, true);
|
| 828 |
} catch {}
|
| 829 |
}
|
| 830 |
|
|
|
|
| 844 |
// 第二轨:同账号只取一次,但每个视图都订阅结果。
|
| 845 |
let entry = _pendingProfileRequests.get(account);
|
| 846 |
if (!entry && typeof apiFn === 'function') {
|
| 847 |
+
const isCurrent = captureProfileRead(account);
|
| 848 |
entry = { listeners: new Set(), timer: null };
|
| 849 |
_pendingProfileRequests.set(account, entry);
|
| 850 |
const release = () => {
|
|
|
|
| 855 |
entry.timer = setTimeout(release, PENDING_TIMEOUT);
|
| 856 |
Promise.resolve().then(() => apiFn(account)).then(res => {
|
| 857 |
// 超时后的旧结果不能覆盖新请求,也不能重新写入旧资料。
|
| 858 |
+
if (_pendingProfileRequests.get(account) !== entry || !isCurrent()) return;
|
| 859 |
if (res?.status && res.status !== 'success') return;
|
| 860 |
const data = res && Object.hasOwn(res, 'data') ? res.data : res;
|
| 861 |
if (!_validProfile(data)) return;
|
前端页面/core/榜单数据来源.js
CHANGED
|
@@ -2,7 +2,7 @@ import { api, requestCancelManager } from './网络请求API.js';
|
|
| 2 |
import { API, CACHE, PAGINATION } from './全局配置.js';
|
| 3 |
import { requireListResponseData, getCacheWithMeta, removeCache, writeListCache } from '../components/性能优化工具.js';
|
| 4 |
|
| 5 |
-
import { captureCacheWrite } from './网络请求_缓存管理.js';
|
| 6 |
import { getResponseCacheSource } from './网络请求_基础设施.js';
|
| 7 |
|
| 8 |
|
|
@@ -99,6 +99,7 @@ export function readSidebarListCache(key, { creators = false } = {}) {
|
|
| 99 |
}
|
| 100 |
|
| 101 |
export function readCreatorsSessionCache() {
|
|
|
|
| 102 |
let raw;
|
| 103 |
try { raw = sessionStorage.getItem(CACHE.LOCAL_KEYS.CREATORS_SESSION); }
|
| 104 |
catch { return null; } // 无法读取不等于损坏,保留未知内容。
|
|
@@ -116,6 +117,7 @@ export function writeCreatorsSessionCache(data) {
|
|
| 116 |
if (!validCreatorList(data)) return false;
|
| 117 |
try {
|
| 118 |
sessionStorage.setItem(CACHE.LOCAL_KEYS.CREATORS_SESSION, JSON.stringify(data));
|
|
|
|
| 119 |
return true;
|
| 120 |
} catch { return false; }
|
| 121 |
}
|
|
|
|
| 2 |
import { API, CACHE, PAGINATION } from './全局配置.js';
|
| 3 |
import { requireListResponseData, getCacheWithMeta, removeCache, writeListCache } from '../components/性能优化工具.js';
|
| 4 |
|
| 5 |
+
import { captureCacheWrite, isCreatorsSessionCacheCurrent, markCreatorsSessionCacheCurrent } from './网络请求_缓存管理.js';
|
| 6 |
import { getResponseCacheSource } from './网络请求_基础设施.js';
|
| 7 |
|
| 8 |
|
|
|
|
| 99 |
}
|
| 100 |
|
| 101 |
export function readCreatorsSessionCache() {
|
| 102 |
+
if (!isCreatorsSessionCacheCurrent()) return null;
|
| 103 |
let raw;
|
| 104 |
try { raw = sessionStorage.getItem(CACHE.LOCAL_KEYS.CREATORS_SESSION); }
|
| 105 |
catch { return null; } // 无法读取不等于损坏,保留未知内容。
|
|
|
|
| 117 |
if (!validCreatorList(data)) return false;
|
| 118 |
try {
|
| 119 |
sessionStorage.setItem(CACHE.LOCAL_KEYS.CREATORS_SESSION, JSON.stringify(data));
|
| 120 |
+
markCreatorsSessionCacheCurrent();
|
| 121 |
return true;
|
| 122 |
} catch { return false; }
|
| 123 |
}
|
前端页面/core/类型定义.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
| 29 |
* @property {number} [created_at] - 注册时间戳
|
| 30 |
* @property {string[]} [followers] - 粉丝列表
|
| 31 |
* @property {string[]} [following] - 关注列表
|
|
|
|
| 32 |
* @property {PrivacySettings} [privacy] - 隐私设置
|
| 33 |
*/
|
| 34 |
|
|
|
|
| 29 |
* @property {number} [created_at] - 注册时间戳
|
| 30 |
* @property {string[]} [followers] - 粉丝列表
|
| 31 |
* @property {string[]} [following] - 关注列表
|
| 32 |
+
* @property {number} [following_count] - 关注人数
|
| 33 |
* @property {PrivacySettings} [privacy] - 隐私设置
|
| 34 |
*/
|
| 35 |
|
前端页面/core/网络请求_业务API.js
CHANGED
|
@@ -8,9 +8,9 @@
|
|
| 8 |
// ==========================================
|
| 9 |
|
| 10 |
import { request } from "./网络请求_基础设施.js";
|
| 11 |
-
import { API } from "./全局配置.js";
|
| 12 |
import { t } from "../components/用户体验_国际化.js";
|
| 13 |
-
import { removeCache } from "./网络请求_缓存管理.js";
|
| 14 |
import { requireListResponseData } from "../components/性能优化工具.js";
|
| 15 |
|
| 16 |
function requestPublicList(endpoint, options = {}) {
|
|
@@ -63,15 +63,36 @@ const api = {
|
|
| 63 |
},
|
| 64 |
|
| 65 |
async getUserProfile(account, options = {}) { return request(`/api/users/${account}`, options); },
|
| 66 |
-
async updateUserProfile(account, data) {
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
async getCreators(sort, limit, options = {}) { return requestPublicList(`/api/creators?sort=${sort}&limit=${limit}`, options); },
|
| 71 |
// 🔍 带上取消分组 ID:用户继续输入时,上一次仍在途的搜索请求可被 requestCancelManager 撤销,
|
| 72 |
// 不再占用并发额度与超时预算(调用时机见 侧边栏数据引擎.js 的创作者搜索分支)
|
| 73 |
async searchCreators(keyword, sort, limit = 50, options = {}) { return requestPublicList(`/api/creators/search?keyword=${encodeURIComponent(keyword)}&sort=${sort}&limit=${limit}`, { componentId: API.SEARCH_COMPONENT_ID, ...options }); },
|
| 74 |
-
async getCreatorDetails(account, options = {}) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
async uploadFile(file, fileType, timeout = 120000) {
|
| 76 |
const formData = new FormData();
|
| 77 |
formData.append("file", file);
|
|
|
|
| 8 |
// ==========================================
|
| 9 |
|
| 10 |
import { request } from "./网络请求_基础设施.js";
|
| 11 |
+
import { API, CACHE, invalidateCachedProfiles } from "./全局配置.js";
|
| 12 |
import { t } from "../components/用户体验_国际化.js";
|
| 13 |
+
import { removeCache, invalidateFollowCaches, captureCacheWrite } from "./网络请求_缓存管理.js";
|
| 14 |
import { requireListResponseData } from "../components/性能优化工具.js";
|
| 15 |
|
| 16 |
function requestPublicList(endpoint, options = {}) {
|
|
|
|
| 63 |
},
|
| 64 |
|
| 65 |
async getUserProfile(account, options = {}) { return request(`/api/users/${account}`, options); },
|
| 66 |
+
async updateUserProfile(account, data) {
|
| 67 |
+
const response = await request(`/api/users/${account}`, { method: "PUT", body: data });
|
| 68 |
+
if (response?.status === 'success') invalidateCachedProfiles([account]);
|
| 69 |
+
return response;
|
| 70 |
+
},
|
| 71 |
+
async updateUserSettings(account, data) {
|
| 72 |
+
const response = await request(`/api/users/${account}/settings`, { method: "PUT", body: data });
|
| 73 |
+
if (response?.status === 'success') invalidateCachedProfiles([account]);
|
| 74 |
+
return response;
|
| 75 |
+
},
|
| 76 |
+
async updatePrivacy(account, privacy) {
|
| 77 |
+
const response = await request(`/api/users/${account}/privacy`, { method: "PUT", body: privacy });
|
| 78 |
+
if (response?.status === 'success') invalidateCachedProfiles([account]);
|
| 79 |
+
return response;
|
| 80 |
+
},
|
| 81 |
+
async toggleFollow(userId, targetAccount, isActive) {
|
| 82 |
+
const response = await request("/api/users/follow", { method: "POST", body: { user_id: userId, target_account: targetAccount, is_active: isActive } });
|
| 83 |
+
if (response?.status === 'success') invalidateFollowCaches(userId, targetAccount);
|
| 84 |
+
return response;
|
| 85 |
+
},
|
| 86 |
async getCreators(sort, limit, options = {}) { return requestPublicList(`/api/creators?sort=${sort}&limit=${limit}`, options); },
|
| 87 |
// 🔍 带上取消分组 ID:用户继续输入时,上一次仍在途的搜索请求可被 requestCancelManager 撤销,
|
| 88 |
// 不再占用并发额度与超时预算(调用时机见 侧边栏数据引擎.js 的创作者搜索分支)
|
| 89 |
async searchCreators(keyword, sort, limit = 50, options = {}) { return requestPublicList(`/api/creators/search?keyword=${encodeURIComponent(keyword)}&sort=${sort}&limit=${limit}`, { componentId: API.SEARCH_COMPONENT_ID, ...options }); },
|
| 90 |
+
async getCreatorDetails(account, options = {}) {
|
| 91 |
+
const current = captureCacheWrite(`${CACHE.CREATOR_DETAILS_CACHE_PREFIX}${account}/details`);
|
| 92 |
+
const response = await request(`/api/creators/${account}/details`, options);
|
| 93 |
+
if (!current()) throw new Error(t('feedback.list_changed_retry'));
|
| 94 |
+
return response;
|
| 95 |
+
},
|
| 96 |
async uploadFile(file, fileType, timeout = 120000) {
|
| 97 |
const formData = new FormData();
|
| 98 |
formData.append("file", file);
|
前端页面/core/网络请求_缓存管理.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
| 9 |
// - 性能优化工具.js (重导出缓存函数,保持向后兼容)
|
| 10 |
// ==========================================
|
| 11 |
|
| 12 |
-
import { API, CACHE, getSidebarListCacheKey } from "./全局配置.js";
|
| 13 |
|
| 14 |
// ==========================================
|
| 15 |
// 📦 缓存底层实现
|
|
@@ -40,7 +40,12 @@ const cacheWriteScopes = [
|
|
| 40 |
...API.PUBLIC_CONDITIONAL_GET_PATHS.map(path => {
|
| 41 |
const prefix = `api_${path}`;
|
| 42 |
return { prefix, matches: key => key.split(/[?#]/)[0] === prefix, token: {} };
|
| 43 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
];
|
| 45 |
|
| 46 |
function _cacheScopeKey(key) {
|
|
@@ -54,6 +59,25 @@ export function captureCacheWrite(key) {
|
|
| 54 |
return () => cacheClearToken === clearToken && (!scope || scope.token === token);
|
| 55 |
}
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
function _revokeCacheWrites(keys, prefix = false) {
|
| 58 |
const normalized = (prefix ? keys.map(_cacheScopeKey) : keys).filter(key => typeof key === 'string' && key.length > 0);
|
| 59 |
for (const scope of cacheWriteScopes) {
|
|
@@ -698,6 +722,12 @@ const CACHE_CONFIG = {
|
|
| 698 |
|
| 699 |
// 🚀 P1优化:精确缓存失效映射(按 HTTP 方法细分)
|
| 700 |
// 根据修改的 endpoint 和 HTTP 方法精确清除相关缓存
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 701 |
const CACHE_INVALIDATION_MAP = {
|
| 702 |
// 任务相关
|
| 703 |
"/api/tasks": {
|
|
@@ -715,8 +745,8 @@ const CACHE_INVALIDATION_MAP = {
|
|
| 715 |
},
|
| 716 |
// 评论相关
|
| 717 |
"/api/comments": {
|
| 718 |
-
"POST":
|
| 719 |
-
"DELETE":
|
| 720 |
},
|
| 721 |
// 商品相关
|
| 722 |
"/api/items": {
|
|
@@ -792,6 +822,10 @@ export function invalidateRelatedCache(endpoint, method = null) {
|
|
| 792 |
patterns = ["ComfyRanking_ListCache_"];
|
| 793 |
}
|
| 794 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 795 |
// 🔧 修复:原实现只遍历 localStorage,漏清 memoryCache —— 而 getCache/getCacheWithMeta
|
| 796 |
// 都优先读内存 Map,POST 成功后内存里的旧列表仍会命中并被直接返回(连请求都不发),
|
| 797 |
// 表现为「发布/点赞已成功但界面还是旧数据」,要等内存条目 TTL 到期才自愈。
|
|
@@ -800,6 +834,8 @@ export function invalidateRelatedCache(endpoint, method = null) {
|
|
| 800 |
// ComfyRanking_ComfyCommunity_ProfileCache_,永远匹配不到,改头像/昵称后该缓存清不掉。
|
| 801 |
// 复用两级前缀删除逻辑;同次失效合并为一次存储遍历,兼容带前缀与裸键。
|
| 802 |
const cleared = _removeCacheByPrefixes(patterns);
|
|
|
|
|
|
|
| 803 |
|
| 804 |
if (cleared > 0) {
|
| 805 |
console.log(`🗑️ 精确清除缓存: ${cleared} 个项`);
|
|
|
|
| 9 |
// - 性能优化工具.js (重导出缓存函数,保持向后兼容)
|
| 10 |
// ==========================================
|
| 11 |
|
| 12 |
+
import { API, CACHE, getSidebarListCacheKey, invalidateCachedProfiles } from "./全局配置.js";
|
| 13 |
|
| 14 |
// ==========================================
|
| 15 |
// 📦 缓存底层实现
|
|
|
|
| 40 |
...API.PUBLIC_CONDITIONAL_GET_PATHS.map(path => {
|
| 41 |
const prefix = `api_${path}`;
|
| 42 |
return { prefix, matches: key => key.split(/[?#]/)[0] === prefix, token: {} };
|
| 43 |
+
}),
|
| 44 |
+
{ prefix: CACHE.CREATOR_DETAILS_CACHE_PREFIX, matches: key => {
|
| 45 |
+
const path = key.split(/[?#]/)[0];
|
| 46 |
+
return path.startsWith(CACHE.CREATOR_DETAILS_CACHE_PREFIX)
|
| 47 |
+
&& /^[^/]+\/details$/.test(path.slice(CACHE.CREATOR_DETAILS_CACHE_PREFIX.length));
|
| 48 |
+
}, token: {} }
|
| 49 |
];
|
| 50 |
|
| 51 |
function _cacheScopeKey(key) {
|
|
|
|
| 59 |
return () => cacheClearToken === clearToken && (!scope || scope.token === token);
|
| 60 |
}
|
| 61 |
|
| 62 |
+
let creatorsSessionCurrent = true;
|
| 63 |
+
export function isCreatorsSessionCacheCurrent() {
|
| 64 |
+
return creatorsSessionCurrent;
|
| 65 |
+
}
|
| 66 |
+
export function markCreatorsSessionCacheCurrent() {
|
| 67 |
+
creatorsSessionCurrent = true;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function invalidateCreatorsSession() {
|
| 71 |
+
creatorsSessionCurrent = false;
|
| 72 |
+
try { sessionStorage.removeItem(CACHE.LOCAL_KEYS.CREATORS_SESSION); } catch {}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
export function invalidateFollowCaches(account, targetAccount) {
|
| 76 |
+
_removeCacheByPrefixes(['api_/api/creators', getSidebarListCacheKey('creators', '')]);
|
| 77 |
+
invalidateCachedProfiles([account, targetAccount]);
|
| 78 |
+
invalidateCreatorsSession();
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
function _revokeCacheWrites(keys, prefix = false) {
|
| 82 |
const normalized = (prefix ? keys.map(_cacheScopeKey) : keys).filter(key => typeof key === 'string' && key.length > 0);
|
| 83 |
for (const scope of cacheWriteScopes) {
|
|
|
|
| 722 |
|
| 723 |
// 🚀 P1优化:精确缓存失效映射(按 HTTP 方法细分)
|
| 724 |
// 根据修改的 endpoint 和 HTTP 方法精确清除相关缓存
|
| 725 |
+
const COMMENT_CACHE_PREFIXES = [
|
| 726 |
+
"api_/api/comments", "api_/api/items", "api_/api/creators", "api_/api/posts", "api_/api/tasks",
|
| 727 |
+
...CACHE.RESOURCE_LIST_PREFIXES, getSidebarListCacheKey('creators', ''),
|
| 728 |
+
CACHE.PUBLIC_LISTS.posts.prefix, CACHE.PUBLIC_LISTS.tasks.prefix, CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX
|
| 729 |
+
];
|
| 730 |
+
|
| 731 |
const CACHE_INVALIDATION_MAP = {
|
| 732 |
// 任务相关
|
| 733 |
"/api/tasks": {
|
|
|
|
| 745 |
},
|
| 746 |
// 评论相关
|
| 747 |
"/api/comments": {
|
| 748 |
+
"POST": COMMENT_CACHE_PREFIXES,
|
| 749 |
+
"DELETE": COMMENT_CACHE_PREFIXES
|
| 750 |
},
|
| 751 |
// 商品相关
|
| 752 |
"/api/items": {
|
|
|
|
| 822 |
patterns = ["ComfyRanking_ListCache_"];
|
| 823 |
}
|
| 824 |
|
| 825 |
+
if (method === "POST" && /^\/api\/items\/[^/]+\/rating$/.test(endpoint.split(/[?#]/)[0])) {
|
| 826 |
+
patterns = [...patterns, ...CACHE.RESOURCE_LIST_PREFIXES];
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
// 🔧 修复:原实现只遍历 localStorage,漏清 memoryCache —— 而 getCache/getCacheWithMeta
|
| 830 |
// 都优先读内存 Map,POST 成功后内存里的旧列表仍会命中并被直接返回(连请求都不发),
|
| 831 |
// 表现为「发布/点赞已成功但界面还是旧数据」,要等内存条目 TTL 到期才自愈。
|
|
|
|
| 834 |
// ComfyRanking_ComfyCommunity_ProfileCache_,永远匹配不到,改头像/昵称后该缓存清不掉。
|
| 835 |
// 复用两级前缀删除逻辑;同次失效合并为一次存储遍历,兼容带前缀与裸键。
|
| 836 |
const cleared = _removeCacheByPrefixes(patterns);
|
| 837 |
+
if ((method === "POST" || method === "DELETE")
|
| 838 |
+
&& /^\/api\/comments(?:\/|[?#]|$)/.test(endpoint)) invalidateCreatorsSession();
|
| 839 |
|
| 840 |
if (cleared > 0) {
|
| 841 |
console.log(`🗑️ 精确清除缓存: ${cleared} 个项`);
|
前端页面/market/资源详情页面组件.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
| 10 |
// ==========================================
|
| 11 |
|
| 12 |
import { api, proxyImages } from "../core/网络请求API.js";
|
| 13 |
-
import { IS_WEB_MODE, getCachedProfile, setCachedProfile } from "../core/全局配置.js";
|
| 14 |
import { normalizeProfileData } from "../profile/个人资料工具.js";
|
| 15 |
import { openTipModal } from "../profile/个人中心_赞赏组件.js";
|
| 16 |
import { getInstalledVersionStatus, setupResourceInstall } from "./资源安装引擎.js";
|
|
@@ -265,8 +265,9 @@ export function createItemDetailView(itemData, currentUser) {
|
|
| 265 |
// 🐛 Bug修复:跳过系统页面的作者信息查询(如关于页面)
|
| 266 |
if (!itemData.isSystemPage && itemData.author && scope.isCurrent()) {
|
| 267 |
const originalNameEl = container.querySelector("#detail-author-name");
|
|
|
|
| 268 |
api.getUserProfile(itemData.author, scope.readOptions).then(res => {
|
| 269 |
-
if (!scope.isCurrent() || !originalNameEl?.isConnected || container.querySelector("#detail-author-name") !== originalNameEl) return;
|
| 270 |
if (res?.status !== 'success') return;
|
| 271 |
const profile = normalizeProfileData(res.data, itemData.author);
|
| 272 |
setCachedProfile(itemData.author, profile);
|
|
|
|
| 10 |
// ==========================================
|
| 11 |
|
| 12 |
import { api, proxyImages } from "../core/网络请求API.js";
|
| 13 |
+
import { IS_WEB_MODE, getCachedProfile, setCachedProfile, captureProfileRead } from "../core/全局配置.js";
|
| 14 |
import { normalizeProfileData } from "../profile/个人资料工具.js";
|
| 15 |
import { openTipModal } from "../profile/个人中心_赞赏组件.js";
|
| 16 |
import { getInstalledVersionStatus, setupResourceInstall } from "./资源安装引擎.js";
|
|
|
|
| 265 |
// 🐛 Bug修复:跳过系统页面的作者信息查询(如关于页面)
|
| 266 |
if (!itemData.isSystemPage && itemData.author && scope.isCurrent()) {
|
| 267 |
const originalNameEl = container.querySelector("#detail-author-name");
|
| 268 |
+
const profileCurrent = captureProfileRead(itemData.author);
|
| 269 |
api.getUserProfile(itemData.author, scope.readOptions).then(res => {
|
| 270 |
+
if (!profileCurrent() || !scope.isCurrent() || !originalNameEl?.isConnected || container.querySelector("#detail-author-name") !== originalNameEl) return;
|
| 271 |
if (res?.status !== 'success') return;
|
| 272 |
const profile = normalizeProfileData(res.data, itemData.author);
|
| 273 |
setCachedProfile(itemData.author, profile);
|
前端页面/profile/个人中心_UI模板.js
CHANGED
|
@@ -68,7 +68,7 @@ export function buildProfileHTML(userData, isMe, isSettingsView, isFollowing, fo
|
|
| 68 |
<span>👍 ${t('profile.received_likes')}: <strong class="profile-count">${escapeHtml(userData.receivedLikes || 0)}</strong></span>
|
| 69 |
<span>🔖 ${t('profile.received_favorites')}: <strong class="profile-count">${escapeHtml(userData.receivedFavorites || 0)}</strong></span>
|
| 70 |
<button type="button" class="profile-count-link" id="btn-followers">👥 ${t('profile.followers_count')}: <strong class="profile-count">${userData.followers ? userData.followers.length : 0}</strong></button>
|
| 71 |
-
<button type="button" class="profile-count-link" id="btn-following">🏃 ${t('profile.following_count')}: <strong class="profile-count">${escapeHtml(followingCount)}</strong></button>
|
| 72 |
</div>
|
| 73 |
</div>
|
| 74 |
`;
|
|
|
|
| 68 |
<span>👍 ${t('profile.received_likes')}: <strong class="profile-count">${escapeHtml(userData.receivedLikes || 0)}</strong></span>
|
| 69 |
<span>🔖 ${t('profile.received_favorites')}: <strong class="profile-count">${escapeHtml(userData.receivedFavorites || 0)}</strong></span>
|
| 70 |
<button type="button" class="profile-count-link" id="btn-followers">👥 ${t('profile.followers_count')}: <strong class="profile-count">${userData.followers ? userData.followers.length : 0}</strong></button>
|
| 71 |
+
<button type="button" class="profile-count-link" id="btn-following"${!isMe && userData.privacy?.follows === true ? ' disabled' : ''}>🏃 ${t('profile.following_count')}: <strong class="profile-count">${escapeHtml(followingCount)}</strong></button>
|
| 72 |
</div>
|
| 73 |
</div>
|
| 74 |
`;
|
前端页面/profile/个人中心视图.js
CHANGED
|
@@ -12,7 +12,7 @@ import { openRechargeModal } from "../market/资金与钱包_充值组件.js";
|
|
| 12 |
import { openWithdrawModal } from "../market/资金与钱包_提现组件.js";
|
| 13 |
import { buildProfileHTML } from "./个人中心_UI模板.js";
|
| 14 |
import { openTipModal } from "./个人中心_赞赏组件.js";
|
| 15 |
-
import { getBannerCacheKey, getCurrentAccount, IS_WEB_MODE, CACHE, UI, API, getCachedProfile, setCachedProfile, normalizeProfileImageUrl } from "../core/全局配置.js";
|
| 16 |
import { logoutAndClearUserData, eventBus, EVENTS } from "../core/状态管理.js";
|
| 17 |
|
| 18 |
|
|
@@ -152,7 +152,7 @@ export function showUserProfile(initialUserData, currentUser = null, isMe = true
|
|
| 152 |
if (currentUser && userData.followers) { isFollowing = userData.followers.includes(currentUser.account); }
|
| 153 |
|
| 154 |
const privacy = userData.privacy || {};
|
| 155 |
-
const followingCount =
|
| 156 |
|
| 157 |
const tabs = [{ id: "published", label: isMe ? t('profile.my_published') : t('profile.their_published') }];
|
| 158 |
if (isMe) tabs.push({ id: "acquired", label: t('profile.my_purchased') });
|
|
@@ -317,8 +317,9 @@ export function openUserProfileModal(userData) {
|
|
| 317 |
++profileNavigation;
|
| 318 |
const view = showUserProfile(userData, userData, true);
|
| 319 |
window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
|
|
|
|
| 320 |
_fetchProfileAndWallet(userData.account, { own: true, options: view._profileReadOptions }).then(freshData => {
|
| 321 |
-
if (!view._profileScope.isCurrent()) return;
|
| 322 |
mergeStoredProfile(userData.account, freshData, sessionStorages(), CACHE.LEGACY_KEYS.USER);
|
| 323 |
view.updateData(freshData);
|
| 324 |
if (freshData.bannerUrl) syncBannerCache(userData.account, freshData.bannerUrl, view._profileScope,
|
|
@@ -329,6 +330,7 @@ export function openUserProfileModal(userData) {
|
|
| 329 |
export async function openOtherUserProfileModal(targetAccount, currentUser) {
|
| 330 |
if (currentUser && currentUser.account === targetAccount) return openUserProfileModal(currentUser);
|
| 331 |
const generation = ++profileNavigation;
|
|
|
|
| 332 |
const sessionMatches = captureRequestSession();
|
| 333 |
const componentId = `profile-open-${++profileSequence}`;
|
| 334 |
let activeContainer = null;
|
|
@@ -347,7 +349,7 @@ export async function openOtherUserProfileModal(targetAccount, currentUser) {
|
|
| 347 |
const unsubscribe = subscribeTaskSessionChanges(force => { if (force || !sessionMatches()) cancel(); }, {
|
| 348 |
target: window, eventBus, events: EVENTS, userKey: CACHE.LEGACY_KEYS.USER, tokenKey: CACHE.LEGACY_KEYS.TOKEN
|
| 349 |
});
|
| 350 |
-
const current = () => !obsolete && generation === profileNavigation && sessionMatches()
|
| 351 |
&& (!activeContainer || activeContainer._profileScope.isCurrent());
|
| 352 |
try {
|
| 353 |
const freshData = await _fetchProfileAndWallet(targetAccount, {
|
|
|
|
| 12 |
import { openWithdrawModal } from "../market/资金与钱包_提现组件.js";
|
| 13 |
import { buildProfileHTML } from "./个人中心_UI模板.js";
|
| 14 |
import { openTipModal } from "./个人中心_赞赏组件.js";
|
| 15 |
+
import { getBannerCacheKey, getCurrentAccount, IS_WEB_MODE, CACHE, UI, API, getCachedProfile, setCachedProfile, captureProfileRead, normalizeProfileImageUrl } from "../core/全局配置.js";
|
| 16 |
import { logoutAndClearUserData, eventBus, EVENTS } from "../core/状态管理.js";
|
| 17 |
|
| 18 |
|
|
|
|
| 152 |
if (currentUser && userData.followers) { isFollowing = userData.followers.includes(currentUser.account); }
|
| 153 |
|
| 154 |
const privacy = userData.privacy || {};
|
| 155 |
+
const followingCount = userData.following_count;
|
| 156 |
|
| 157 |
const tabs = [{ id: "published", label: isMe ? t('profile.my_published') : t('profile.their_published') }];
|
| 158 |
if (isMe) tabs.push({ id: "acquired", label: t('profile.my_purchased') });
|
|
|
|
| 317 |
++profileNavigation;
|
| 318 |
const view = showUserProfile(userData, userData, true);
|
| 319 |
window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
|
| 320 |
+
const profileCurrent = captureProfileRead(userData.account);
|
| 321 |
_fetchProfileAndWallet(userData.account, { own: true, options: view._profileReadOptions }).then(freshData => {
|
| 322 |
+
if (!view._profileScope.isCurrent() || !profileCurrent()) return;
|
| 323 |
mergeStoredProfile(userData.account, freshData, sessionStorages(), CACHE.LEGACY_KEYS.USER);
|
| 324 |
view.updateData(freshData);
|
| 325 |
if (freshData.bannerUrl) syncBannerCache(userData.account, freshData.bannerUrl, view._profileScope,
|
|
|
|
| 330 |
export async function openOtherUserProfileModal(targetAccount, currentUser) {
|
| 331 |
if (currentUser && currentUser.account === targetAccount) return openUserProfileModal(currentUser);
|
| 332 |
const generation = ++profileNavigation;
|
| 333 |
+
const profileCurrent = captureProfileRead(targetAccount);
|
| 334 |
const sessionMatches = captureRequestSession();
|
| 335 |
const componentId = `profile-open-${++profileSequence}`;
|
| 336 |
let activeContainer = null;
|
|
|
|
| 349 |
const unsubscribe = subscribeTaskSessionChanges(force => { if (force || !sessionMatches()) cancel(); }, {
|
| 350 |
target: window, eventBus, events: EVENTS, userKey: CACHE.LEGACY_KEYS.USER, tokenKey: CACHE.LEGACY_KEYS.TOKEN
|
| 351 |
});
|
| 352 |
+
const current = () => !obsolete && generation === profileNavigation && sessionMatches() && profileCurrent()
|
| 353 |
&& (!activeContainer || activeContainer._profileScope.isCurrent());
|
| 354 |
try {
|
| 355 |
const freshData = await _fetchProfileAndWallet(targetAccount, {
|
前端页面/profile/个人资料工具.js
CHANGED
|
@@ -20,6 +20,9 @@ export function normalizeProfileData(value, account, own = false) {
|
|
| 20 |
}
|
| 21 |
result.privacy = Object.fromEntries(['follows', 'followers', 'likes', 'favorites', 'downloads']
|
| 22 |
.map(key => [key, value.privacy?.[key] === true]));
|
|
|
|
|
|
|
|
|
|
| 23 |
result.is_admin = value.is_admin === true || (own && value.isAdmin === true);
|
| 24 |
if (own) Object.assign(result, normalizeWalletData({ ...value, status: 'success' }));
|
| 25 |
return result;
|
|
|
|
| 20 |
}
|
| 21 |
result.privacy = Object.fromEntries(['follows', 'followers', 'likes', 'favorites', 'downloads']
|
| 22 |
.map(key => [key, value.privacy?.[key] === true]));
|
| 23 |
+
result.following_count = !own && Number.isSafeInteger(value.following_count) && value.following_count >= 0
|
| 24 |
+
? value.following_count : result.following.length;
|
| 25 |
+
if (!own && result.privacy.follows) result.following = [];
|
| 26 |
result.is_admin = value.is_admin === true || (own && value.isAdmin === true);
|
| 27 |
if (own) Object.assign(result, normalizeWalletData({ ...value, status: 'success' }));
|
| 28 |
return result;
|