Spaces:
Running
Running
Upload 104 files
Browse files- 前端页面/core/全局配置.js +997 -992
- 前端页面/core/榜单数据来源.js +121 -93
- 前端页面/core/网络请求_缓存管理.js +57 -44
- 前端页面/post/讨论区组件.js +15 -8
- 前端页面/prompt/提示词组件.js +15 -8
- 前端页面/task/任务榜组件.js +15 -8
前端页面/core/全局配置.js
CHANGED
|
@@ -1,992 +1,997 @@
|
|
| 1 |
-
// 前端页面/core/全局配置.js
|
| 2 |
-
// ==========================================
|
| 3 |
-
// ⚙️ 全局配置中心
|
| 4 |
-
// ==========================================
|
| 5 |
-
// 作用:集中管理所有配置常量,便于维护和修改
|
| 6 |
-
// 关联文件:
|
| 7 |
-
// - 网络请求API.js (API基础地址)
|
| 8 |
-
// - 性能优化工具.js (缓存配置)
|
| 9 |
-
// - 侧边栏数据引擎.js (分页配置)
|
| 10 |
-
// - 所有业务组件
|
| 11 |
-
// ==========================================
|
| 12 |
-
// 🏗️ P2架构优化:配置集中化
|
| 13 |
-
// 🏗️ P2质量优化:JSDoc 类型注释
|
| 14 |
-
// ==========================================
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
// ==========================================
|
| 18 |
-
// 🌐 API 配置
|
| 19 |
-
// ==========================================
|
| 20 |
-
|
| 21 |
-
/** @type {{ BASE_URL: string, TIMEOUT: number, VIDEO_UPLOAD_TIMEOUT: number, VIDEO_UPLOAD_RESPONSE_MAX_CHARS: number, LOCAL_CACHE_STATS_TIMEOUT: number, LOCAL_CACHE_CLEAR_TIMEOUT: number, LOCAL_CACHE_RESPONSE_MAX_BYTES: number, SSE_MAX_RESPONSE_BYTES: number, SSE_MAX_LINE_BYTES: number, SSE_MAX_LINES: number, SSE_YIELD_LINES: number, SSE_YIELD_BYTES: number, SSE_YIELD_CHUNKS: number, MAX_RETRIES: number, RETRY_DELAY: number, LIST_RETRIES: number, SEARCH_TIMEOUT: number, SEARCH_RETRIES: number, PROFILE_RESOURCE_TIMEOUT: number, CREATOR_CHART_SCRIPT_URL: string, BACKGROUND_TIMEOUT: number, BANNER_IMAGE_TIMEOUT: number, BACKGROUND_RETRIES: number, WORKFLOW_DOWNLOAD_TIMEOUT: number, CLOUD_DOWN_COOLDOWN: number, SEARCH_COMPONENT_ID: string, IMAGE_PROXY_PATH: string, PROFILE_MEDIA_URL_MAX_BYTES: number, LOCAL_IMAGE_PROXY_PREFIX: string, COMPLETE_IMAGE_QUERY_SUFFIX: string, PRIVATE_GET_PATTERNS: RegExp[], PUBLIC_CONDITIONAL_GET_PATHS: string[], CONDITIONAL_ETAG_PATTERN: RegExp, REQUEST_ID_HEADER: string, REQUEST_ID_PATTERN: RegExp, ENDPOINT_ERROR_SCOPE_HEADER: string, ENDPOINT_ERROR_SCOPE_VALUE: string, REQUEST_BODY_ERRORS: Record<string, { status: number, msgKey: string }> }} */
|
| 22 |
-
export const API = {
|
| 23 |
-
REQUEST_ID_HEADER: "X-Ranking-Request-Id",
|
| 24 |
-
REQUEST_ID_PATTERN: /^(?:[a-f0-9]{32}|ERR_[0-9]{10,13}_[a-f0-9]{1,8})$/,
|
| 25 |
-
ENDPOINT_ERROR_SCOPE_HEADER: "X-Ranking-Error-Scope",
|
| 26 |
-
ENDPOINT_ERROR_SCOPE_VALUE: "endpoint",
|
| 27 |
-
REQUEST_BODY_ERRORS: {
|
| 28 |
-
request_body_invalid: { status: 400, msgKey: 'feedback.invalid_request_data' },
|
| 29 |
-
request_body_disconnected: { status: 400, msgKey: 'feedback.network_unavailable' },
|
| 30 |
-
request_body_too_large: { status: 413, msgKey: 'feedback.request_too_large' },
|
| 31 |
-
request_body_timeout: { status: 408, msgKey: 'feedback.request_receive_timeout' },
|
| 32 |
-
request_body_busy: { status: 429, msgKey: 'feedback.request_busy' },
|
| 33 |
-
request_body_encoding: { status: 415, msgKey: 'feedback.request_encoding_unsupported' },
|
| 34 |
-
},
|
| 35 |
-
// 仅公开榜单使用由正文派生的条件验证器,私有响应仍禁止留存。
|
| 36 |
-
PUBLIC_CONDITIONAL_GET_PATHS: ['/api/items', '/api/creators', '/api/creators/search', '/api/posts', '/api/prompts', '/api/tasks'],
|
| 37 |
-
CONDITIONAL_ETAG_PATTERN: /^W\/"rk-[a-f0-9]{64}"$/,
|
| 38 |
-
|
| 39 |
-
// 云端 API 基础地址
|
| 40 |
-
BASE_URL: "https://zhiwei666-comfyui-ranking-api.hf.space",
|
| 41 |
-
|
| 42 |
-
// 请求超时时间(
|
| 43 |
-
TIMEOUT: 30000,
|
| 44 |
-
VIDEO_UPLOAD_TIMEOUT: 300000,
|
| 45 |
-
VIDEO_UPLOAD_RESPONSE_MAX_CHARS: 65536,
|
| 46 |
-
LOCAL_CACHE_STATS_TIMEOUT: 10000,
|
| 47 |
-
LOCAL_CACHE_CLEAR_TIMEOUT: 60000,
|
| 48 |
-
LOCAL_CACHE_RESPONSE_MAX_BYTES: 8192,
|
| 49 |
-
// 工作流终态编码上限为512 MiB,另保留64 MiB进度数据;行预算包含data前缀。
|
| 50 |
-
SSE_MAX_RESPONSE_BYTES: 576 * 1024 * 1024,
|
| 51 |
-
SSE_MAX_LINE_BYTES: 512 * 1024 * 1024,
|
| 52 |
-
SSE_MAX_LINES: 262144,
|
| 53 |
-
SSE_YIELD_LINES: 256,
|
| 54 |
-
SSE_YIELD_BYTES: 1024 * 1024,
|
| 55 |
-
SSE_YIELD_CHUNKS: 256,
|
| 56 |
-
|
| 57 |
-
// GET 请求重试次数(实际尝试 MAX_RETRIES + 1 次)
|
| 58 |
-
// 🔧 修复:此前本值为 3 且全库无人读取,真正的重试次数硬编码在 网络请求_基础设施.js 中为 2,
|
| 59 |
-
// 现改为由基础设施读取本配置;取值 2 以保持既有的「共 3 次尝试」行为不变
|
| 60 |
-
MAX_RETRIES: 2,
|
| 61 |
-
|
| 62 |
-
// 重试间隔(毫秒,指数退避基数:1s、2s、4s…)
|
| 63 |
-
RETRY_DELAY: 1000,
|
| 64 |
-
|
| 65 |
-
// 🐢 弱网预算分级(仅作用于 GET,由 网络请求_基础设施.js 按 endpoint 路径精确匹配)
|
| 66 |
-
// 默认预算下弱网一次列表请求最长要烧 3×30s + 1s + 2s = 93s,期间用户只能盯骨架屏。
|
| 67 |
-
// 列表类:首屏已可由本地缓存先行上屏(见 侧边栏数据引擎.js),且列表往往是会话的第一个请求、
|
| 68 |
-
// 可能撞上 HF Space 冷启动(本项目无预热机制),故只砍掉一次冗余重试、保留 30s 单次超时,
|
| 69 |
-
// 93s → 61s,冷启动仍有机会在前两次尝试内完成
|
| 70 |
-
LIST_RETRIES: 1,
|
| 71 |
-
// 搜索类:必然发生在列表已加载之后(空间已被唤醒),可放心收紧,93s → 10s×2 + 1s = 21s
|
| 72 |
-
SEARCH_TIMEOUT: 10000,
|
| 73 |
-
SEARCH_RETRIES: 1,
|
| 74 |
-
PROFILE_RESOURCE_TIMEOUT: 5000,
|
| 75 |
-
CREATOR_CHART_SCRIPT_URL: "https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js",
|
| 76 |
-
|
| 77 |
-
// 🐢 后台补新预算(仅作用于 GET,由 网络请求_基础设施.js 按「本地是否已有可兜底数据」判定)
|
| 78 |
-
// 七大列表视图都是「本地数据先上屏 → 再发请求补新」,此时界面上已经有内容,
|
| 79 |
-
// 这次请求纯属后台行为;它每多挂 1 秒就多占 1 个并发额度(全局仅 6 个),
|
| 80 |
-
// 云端不通时会把前台请求(详情页、点赞、轮询)全部挤到排队,表现为「切哪个界面都要等」。
|
| 81 |
-
// 故一旦本地已有该请求的缓存(含过期),单次超时压到 10s 且不再重试,最坏耗时 10s
|
| 82 |
-
BACKGROUND_TIMEOUT: 10000,
|
| 83 |
-
BANNER_IMAGE_TIMEOUT: 75000,
|
| 84 |
-
WORKFLOW_DOWNLOAD_TIMEOUT: 120000, // 工作流流式下载预算(2 分钟)
|
| 85 |
-
BACKGROUND_RETRIES: 0,
|
| 86 |
-
|
| 87 |
-
// ☁️ 云端不可达冷却(消费方见 状态管理.js 的「云端可达性」段落)
|
| 88 |
-
// navigator.onLine 只反映本机网卡,云端单独不可达(HF Space 宕机/冷启动、DNS 污染、被墙)
|
| 89 |
-
// 时它仍为 true,于是每个界面、每次轮询都要从头烧一遍完整重试预算,且互不共享失败结论。
|
| 90 |
-
// 首次确证云端不可达后进入冷却:冷却期内所有 GET 一律不联网(有缓存直接返回、无缓存立即失败),
|
| 91 |
-
// 并发额度全部让给前台。被动探测——冷却一过,下一个真实请求充当探针;
|
| 92 |
-
// 期间任意一次成功响应(含 POST/PUT/DELETE)立即清零,不需等到冷却结束
|
| 93 |
-
CLOUD_DOWN_COOLDOWN: 60 * 1000,
|
| 94 |
-
|
| 95 |
-
// 🔍 创作者搜索请求的取消分组 ID
|
| 96 |
-
// 用户在搜索框继续输入时,侧边栏数据引擎.js 据此撤销上一次仍在途的搜索请求。
|
| 97 |
-
// 不撤销的话,作废请求会持续占用并发额度(最多 6 个)与超时预算,
|
| 98 |
-
// 把最新那次搜索挤到后面排队。仅搜索请求归入本分组,不影响其它任何请求
|
| 99 |
-
SEARCH_COMPONENT_ID: "sidebar-search",
|
| 100 |
-
// 上传相对路径的云端图片读取入口。
|
| 101 |
-
IMAGE_PROXY_PATH: "/api/image_proxy",
|
| 102 |
-
PROFILE_MEDIA_URL_MAX_BYTES: 16384,
|
| 103 |
-
// 本地广告预载要求真实图片,熔断占位图不能作为下载成功。
|
| 104 |
-
LOCAL_IMAGE_PROXY_PREFIX: "/community_hub/image?url=",
|
| 105 |
-
COMPLETE_IMAGE_QUERY_SUFFIX: "&require_complete=1",
|
| 106 |
-
// 身份相关或私有 GET 禁止通用响应缓存;公开列表仍使用原缓存策略。
|
| 107 |
-
PRIVATE_GET_PATTERNS: [
|
| 108 |
-
/^\/api\/(wallet|messages|chats|admin|disputes|users)(?:\/|$)/,
|
| 109 |
-
/^\/api\/my-(posts|prompts|tasks)(?:\/|$)/,
|
| 110 |
-
/^\/api\/(tasks|prompts)\/[^/]+$/
|
| 111 |
-
]
|
| 112 |
-
};
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
export const LOCAL_PERFORMANCE = Object.freeze({
|
| 116 |
-
MAX_ENDPOINT_CHARS: 4096,
|
| 117 |
-
MAX_COUNT: 1000000,
|
| 118 |
-
MAX_DURATION_MS: 600000,
|
| 119 |
-
ITEM_CATEGORIES: Object.freeze({ tool: 'tools', skill: 'skills', app: 'apps', recommend: 'recommends' }),
|
| 120 |
-
LIST_CATEGORIES: Object.freeze({ '/api/creators': 'creators', '/api/creators/search': 'creators', '/api/tasks': 'tasks', '/api/posts': 'posts', '/api/prompts': 'prompts' }),
|
| 121 |
-
LABELS: Object.freeze({ tools: 'nav.tools', skills: 'nav.skills', apps: 'nav.apps', recommends: 'nav.recommends', creators: 'nav.creators', tasks: 'nav.tasks', posts: 'nav.posts', prompts: 'nav.prompts' })
|
| 122 |
-
});
|
| 123 |
-
|
| 124 |
-
export const LOCAL_INSTALL_DIRECTORY = Object.freeze({
|
| 125 |
-
PATH: '/community_hub/open_install_directory',
|
| 126 |
-
TIMEOUT: 25000,
|
| 127 |
-
RESPONSE_MAX_BYTES: 8192
|
| 128 |
-
});
|
| 129 |
-
|
| 130 |
-
export const SKILL_INSTALL = Object.freeze({
|
| 131 |
-
PATH: '/community_hub/skill_install',
|
| 132 |
-
PREFLIGHT_TIMEOUT: 30000,
|
| 133 |
-
CONTROL_TIMEOUT: 45000,
|
| 134 |
-
RESULT_TIMEOUT: 5000,
|
| 135 |
-
STREAM_TIMEOUT: 1200000,
|
| 136 |
-
RESPONSE_MAX_BYTES: 1024 * 1024,
|
| 137 |
-
PATH_MAX_BYTES: 32768,
|
| 138 |
-
MAX_DIRECTORIES: 128,
|
| 139 |
-
MAX_ITEMS: 1,
|
| 140 |
-
MODAL_WIDTH: '640px'
|
| 141 |
-
});
|
| 142 |
-
|
| 143 |
-
export const CHART_LIFECYCLE = Object.freeze({
|
| 144 |
-
WIDTH_WAIT_TIMEOUT: 5000,
|
| 145 |
-
RESIZE_DELAY: 300
|
| 146 |
-
});
|
| 147 |
-
|
| 148 |
-
// 发布视频的本地读取预算,不改变云端上传额度。
|
| 149 |
-
export const LAZY_IMAGES = Object.freeze({
|
| 150 |
-
ROOT_MARGIN: "200px 0px",
|
| 151 |
-
THRESHOLD: 0.01,
|
| 152 |
-
MOUNT_TIMEOUT: 30000,
|
| 153 |
-
READ_TIMEOUT: 30000
|
| 154 |
-
});
|
| 155 |
-
|
| 156 |
-
export const PUBLISH_MEDIA_LIMITS = Object.freeze({
|
| 157 |
-
MAX_IMAGES: 9,
|
| 158 |
-
IMAGE_MAX_EDGE: 1920,
|
| 159 |
-
IMAGE_QUALITY: 0.85,
|
| 160 |
-
VIDEO_MAX_BYTES: 100 * 1024 * 1024,
|
| 161 |
-
VIDEO_MAX_SECONDS: 180,
|
| 162 |
-
VIDEO_METADATA_TIMEOUT: 15000
|
| 163 |
-
});
|
| 164 |
-
|
| 165 |
-
// 上传前处理预算;浏览器解码本身仍由宿主管理。
|
| 166 |
-
export const IMAGE_PROCESS = {
|
| 167 |
-
HEADER_BYTES: 256 * 1024,
|
| 168 |
-
MAX_INPUT_BYTES: 50 * 1024 * 1024,
|
| 169 |
-
MAX_PIXELS: 40 * 1024 * 1024,
|
| 170 |
-
MAX_SOURCE_EDGE: 16384,
|
| 171 |
-
MAX_OUTPUT_EDGE: 4096,
|
| 172 |
-
MAX_OUTPUT_BYTES: 5 * 1024 * 1024,
|
| 173 |
-
TIMEOUT: 15000,
|
| 174 |
-
MIN_QUALITY: 0.3,
|
| 175 |
-
QUALITY_STEP: 0.1,
|
| 176 |
-
AVATAR_MAX_BYTES: 2 * 1024 * 1024,
|
| 177 |
-
AVATAR_SIZE: 512
|
| 178 |
-
};
|
| 179 |
-
|
| 180 |
-
// ==========================================
|
| 181 |
-
// 🔐 管理员配置 (P0安全修复:管理员账号配置化)
|
| 182 |
-
// ==========================================
|
| 183 |
-
// 注意:前端仅用于UI显示控制,后端从环境变量 ADMIN_ACCOUNTS 读取
|
| 184 |
-
// 修改此处需同步修改云端环境变量
|
| 185 |
-
export const ADMIN_ACCOUNTS = new Set(["123456", "888888"]);
|
| 186 |
-
|
| 187 |
-
/**
|
| 188 |
-
* 检查账号是否为管理员
|
| 189 |
-
* @param {string} account - 用户账号
|
| 190 |
-
* @returns {boolean}
|
| 191 |
-
*/
|
| 192 |
-
export function isAdmin(account) {
|
| 193 |
-
return ADMIN_ACCOUNTS.has(account);
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
// ==========================================
|
| 198 |
-
// 💾 缓存配置
|
| 199 |
-
// ==========================================
|
| 200 |
-
export function getSidebarListCacheKey(tab, sort) {
|
| 201 |
-
const list = tab === 'creators' ? 'creators_full' : tab;
|
| 202 |
-
return `ListCache_${list}_${sort}`;
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
const RESOURCE_LIST_PREFIXES = [
|
| 206 |
-
"ListCache_tools_", "ListCache_skills_", "ListCache_apps_", "ListCache_recommends_"
|
| 207 |
-
];
|
| 208 |
-
export const CACHE = {
|
| 209 |
-
// 缓存键前缀
|
| 210 |
-
PREFIX: "ComfyRanking_",
|
| 211 |
-
|
| 212 |
-
// 默认缓存时间(毫秒)
|
| 213 |
-
DEFAULT_TTL: 2 * 60 * 60 * 1000, // 2小时
|
| 214 |
-
|
| 215 |
-
// 各类数据缓存时间
|
| 216 |
-
TTL: {
|
| 217 |
-
LIST_DATA: 5 * 60 * 1000, // 列表数据:5分钟
|
| 218 |
-
DETAIL_DATA: 3 * 60 * 1000, // 详情数据:3分钟
|
| 219 |
-
USER_PROFILE: 2 * 60 * 1000, // 用户资料:2分钟
|
| 220 |
-
WALLET: 30 * 1000, // 钱包数据:30秒
|
| 221 |
-
CREATORS: 5 * 60 * 1000, // 创作者列表:5分钟
|
| 222 |
-
CHAT_HISTORY: 60 * 1000 // 聊天记录:1分钟
|
| 223 |
-
},
|
| 224 |
-
|
| 225 |
-
// 内存缓存最大条目数
|
| 226 |
-
MAX_MEMORY_ITEMS: 100,
|
| 227 |
-
|
| 228 |
-
// 本插件 localStorage 持久缓存的估算容量上限(不影响用户/会话等其它键)
|
| 229 |
-
MAX_STORAGE_BYTES: 2 * 1024 * 1024,
|
| 230 |
-
|
| 231 |
-
// localStorage 键名(旧版兼容)
|
| 232 |
-
SESSION_REMEMBER_TTL: 30 * 24 * 60 * 60 * 1000,
|
| 233 |
-
INSTALLED_VERSION_UNCONFIRMED: "__installed_unconfirmed__", // 安装成功但没有实际版本证明
|
| 234 |
-
SESSION_TEMP_TTL: 24 * 60 * 60 * 1000,
|
| 235 |
-
LEGACY_KEYS: {
|
| 236 |
-
USER: "ComfyCommunity_User",
|
| 237 |
-
TOKEN: "ComfyCommunity_Token",
|
| 238 |
-
LIST_CACHE: "ComfyCommunity_ListCache",
|
| 239 |
-
PROFILE_CACHE: "ComfyCommunity_ProfileCache",
|
| 240 |
-
CHAT_HISTORY: "ComfyCommunity_ChatHistory"
|
| 241 |
-
},
|
| 242 |
-
|
| 243 |
-
// 本地背景存储键(不上传云端)
|
| 244 |
-
LOCAL_KEYS: {
|
| 245 |
-
BANNER_CONFIG: "ComfyRanking_BannerConfig", // 广告配置持久缓存
|
| 246 |
-
CREATORS_SESSION: "ComfyRanking_CreatorsCache", // 沿用创作者临时缓存键
|
| 247 |
-
SIDEBAR_BACKGROUND: "ComfyRanking_SidebarBackground", // 工具背景图 Base64
|
| 248 |
-
PROFILE_BANNER_CACHE: "ComfyRanking_ProfileBannerCache", // 个人资料背景图本地缓存
|
| 249 |
-
ACQUIRED_ITEMS: "ComfyRanking_AcquiredItems", // 已获取的资源记录
|
| 250 |
-
PROFILE_LIST_PREFIX: "ProfileList_", // 兼容旧个人列表业务键
|
| 251 |
-
INSTALLED_VERSION_PREFIX: "ComfyCommunity_LocalVer_", // 沿用既有安装版本键
|
| 252 |
-
ITEM_OWNERSHIP_PREFIX: "ItemOwnership_", // 缓存管理器业务键前缀
|
| 253 |
-
// 🚀 新增:私信和通知本地缓存
|
| 254 |
-
NOTIFICATIONS: "ComfyRanking_Notifications", // 通知消息缓存
|
| 255 |
-
CHAT_LIST: "ComfyRanking_ChatList", // 对话列表缓存
|
| 256 |
-
CHAT_HISTORY_PREFIX: "ComfyRanking_ChatHistory_", // 聊天记录前缀
|
| 257 |
-
NOTIF_CLEAR_TIME: "ComfyRanking_NotifClearTime", // 通知清空时间戳
|
| 258 |
-
CHAT_CLEAR_PREFIX: "ComfyRanking_ChatCleared_" // 聊天清空时间戳前缀
|
| 259 |
-
},
|
| 260 |
-
|
| 261 |
-
REFUND_CACHE_PREFIXES: [
|
| 262 |
-
"api_/api/items", "api_/api/creators", "api_/api/wallet",
|
| 263 |
-
...RESOURCE_LIST_PREFIXES
|
| 264 |
-
],
|
| 265 |
-
RESOURCE_LIST_PREFIXES,
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
//
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
//
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
//
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
//
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
//
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
//
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
//
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
//
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
// =
|
| 506 |
-
//
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
//
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
//
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
//
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
}
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
}
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
}
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
}
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
//
|
| 622 |
-
//
|
| 623 |
-
//
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
//
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
//
|
| 636 |
-
//
|
| 637 |
-
|
| 638 |
-
/
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
/
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
try {
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
if (
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
const
|
| 693 |
-
if (
|
| 694 |
-
|
| 695 |
-
}
|
| 696 |
-
if (!host
|
| 697 |
-
|
| 698 |
-
if (!/^
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
}
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
}
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
*
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
}
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
*
|
| 831 |
-
* @param {
|
| 832 |
-
* @param {string} options.
|
| 833 |
-
* @
|
| 834 |
-
*
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
const
|
| 843 |
-
const
|
| 844 |
-
const
|
| 845 |
-
|
| 846 |
-
//
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
image
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
}
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
}
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
//
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
/
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 前端页面/core/全局配置.js
|
| 2 |
+
// ==========================================
|
| 3 |
+
// ⚙️ 全局配置中心
|
| 4 |
+
// ==========================================
|
| 5 |
+
// 作用:集中管理所有配置常量,便于维护和修改
|
| 6 |
+
// 关联文件:
|
| 7 |
+
// - 网络请求API.js (API基础地址)
|
| 8 |
+
// - 性能优化工具.js (缓存配置)
|
| 9 |
+
// - 侧边栏数据引擎.js (分页配置)
|
| 10 |
+
// - 所有业务组件
|
| 11 |
+
// ==========================================
|
| 12 |
+
// 🏗️ P2架构优化:配置集中化
|
| 13 |
+
// 🏗️ P2质量优化:JSDoc 类型注释
|
| 14 |
+
// ==========================================
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
// ==========================================
|
| 18 |
+
// 🌐 API 配置
|
| 19 |
+
// ==========================================
|
| 20 |
+
|
| 21 |
+
/** @type {{ BASE_URL: string, TIMEOUT: number, VIDEO_UPLOAD_TIMEOUT: number, VIDEO_UPLOAD_RESPONSE_MAX_CHARS: number, LOCAL_CACHE_STATS_TIMEOUT: number, LOCAL_CACHE_CLEAR_TIMEOUT: number, LOCAL_CACHE_RESPONSE_MAX_BYTES: number, SSE_MAX_RESPONSE_BYTES: number, SSE_MAX_LINE_BYTES: number, SSE_MAX_LINES: number, SSE_YIELD_LINES: number, SSE_YIELD_BYTES: number, SSE_YIELD_CHUNKS: number, MAX_RETRIES: number, RETRY_DELAY: number, LIST_RETRIES: number, SEARCH_TIMEOUT: number, SEARCH_RETRIES: number, PROFILE_RESOURCE_TIMEOUT: number, CREATOR_CHART_SCRIPT_URL: string, BACKGROUND_TIMEOUT: number, BANNER_IMAGE_TIMEOUT: number, BACKGROUND_RETRIES: number, WORKFLOW_DOWNLOAD_TIMEOUT: number, CLOUD_DOWN_COOLDOWN: number, SEARCH_COMPONENT_ID: string, IMAGE_PROXY_PATH: string, PROFILE_MEDIA_URL_MAX_BYTES: number, LOCAL_IMAGE_PROXY_PREFIX: string, COMPLETE_IMAGE_QUERY_SUFFIX: string, PRIVATE_GET_PATTERNS: RegExp[], PUBLIC_CONDITIONAL_GET_PATHS: string[], CONDITIONAL_ETAG_PATTERN: RegExp, REQUEST_ID_HEADER: string, REQUEST_ID_PATTERN: RegExp, ENDPOINT_ERROR_SCOPE_HEADER: string, ENDPOINT_ERROR_SCOPE_VALUE: string, REQUEST_BODY_ERRORS: Record<string, { status: number, msgKey: string }> }} */
|
| 22 |
+
export const API = {
|
| 23 |
+
REQUEST_ID_HEADER: "X-Ranking-Request-Id",
|
| 24 |
+
REQUEST_ID_PATTERN: /^(?:[a-f0-9]{32}|ERR_[0-9]{10,13}_[a-f0-9]{1,8})$/,
|
| 25 |
+
ENDPOINT_ERROR_SCOPE_HEADER: "X-Ranking-Error-Scope",
|
| 26 |
+
ENDPOINT_ERROR_SCOPE_VALUE: "endpoint",
|
| 27 |
+
REQUEST_BODY_ERRORS: {
|
| 28 |
+
request_body_invalid: { status: 400, msgKey: 'feedback.invalid_request_data' },
|
| 29 |
+
request_body_disconnected: { status: 400, msgKey: 'feedback.network_unavailable' },
|
| 30 |
+
request_body_too_large: { status: 413, msgKey: 'feedback.request_too_large' },
|
| 31 |
+
request_body_timeout: { status: 408, msgKey: 'feedback.request_receive_timeout' },
|
| 32 |
+
request_body_busy: { status: 429, msgKey: 'feedback.request_busy' },
|
| 33 |
+
request_body_encoding: { status: 415, msgKey: 'feedback.request_encoding_unsupported' },
|
| 34 |
+
},
|
| 35 |
+
// 仅公开榜单使用由正文派生的条件验证器,私有响应仍禁止留存。
|
| 36 |
+
PUBLIC_CONDITIONAL_GET_PATHS: ['/api/items', '/api/creators', '/api/creators/search', '/api/posts', '/api/prompts', '/api/tasks'],
|
| 37 |
+
CONDITIONAL_ETAG_PATTERN: /^W\/"rk-[a-f0-9]{64}"$/,
|
| 38 |
+
|
| 39 |
+
// 云端 API 基础地址
|
| 40 |
+
BASE_URL: "https://zhiwei666-comfyui-ranking-api.hf.space",
|
| 41 |
+
|
| 42 |
+
// 请求超时时间(��秒)
|
| 43 |
+
TIMEOUT: 30000,
|
| 44 |
+
VIDEO_UPLOAD_TIMEOUT: 300000,
|
| 45 |
+
VIDEO_UPLOAD_RESPONSE_MAX_CHARS: 65536,
|
| 46 |
+
LOCAL_CACHE_STATS_TIMEOUT: 10000,
|
| 47 |
+
LOCAL_CACHE_CLEAR_TIMEOUT: 60000,
|
| 48 |
+
LOCAL_CACHE_RESPONSE_MAX_BYTES: 8192,
|
| 49 |
+
// 工作流终态编码上限为512 MiB,另保留64 MiB进度数据;行预算包含data前缀。
|
| 50 |
+
SSE_MAX_RESPONSE_BYTES: 576 * 1024 * 1024,
|
| 51 |
+
SSE_MAX_LINE_BYTES: 512 * 1024 * 1024,
|
| 52 |
+
SSE_MAX_LINES: 262144,
|
| 53 |
+
SSE_YIELD_LINES: 256,
|
| 54 |
+
SSE_YIELD_BYTES: 1024 * 1024,
|
| 55 |
+
SSE_YIELD_CHUNKS: 256,
|
| 56 |
+
|
| 57 |
+
// GET 请求重试次数(实际尝试 MAX_RETRIES + 1 次)
|
| 58 |
+
// 🔧 修复:此前本值为 3 且全库无人读取,真正的重试次数硬编码在 网络请求_基础设施.js 中为 2,
|
| 59 |
+
// 现改为由基础设施读取本配置;取值 2 以保持既有的「共 3 次尝试」行为不变
|
| 60 |
+
MAX_RETRIES: 2,
|
| 61 |
+
|
| 62 |
+
// 重试间隔(毫秒,指数退避基数:1s、2s、4s…)
|
| 63 |
+
RETRY_DELAY: 1000,
|
| 64 |
+
|
| 65 |
+
// 🐢 弱网预算分级(仅作用于 GET,由 网络请求_基础设施.js 按 endpoint 路径精确匹配)
|
| 66 |
+
// 默认预算下弱网一次列表请求最长要烧 3×30s + 1s + 2s = 93s,期间用户只能盯骨架屏。
|
| 67 |
+
// 列表类:首屏已可由本地缓存先行上屏(见 侧边栏数据引擎.js),且列表往往是会话的第一个请求、
|
| 68 |
+
// 可能撞上 HF Space 冷启动(本项目无预热机制),故只砍掉一次冗余重试、保留 30s 单次超时,
|
| 69 |
+
// 93s → 61s,冷启动仍有机会在前两次尝试内完成
|
| 70 |
+
LIST_RETRIES: 1,
|
| 71 |
+
// 搜索类:必然发生在列表已加载之后(空间已被唤醒),可放心收紧,93s → 10s×2 + 1s = 21s
|
| 72 |
+
SEARCH_TIMEOUT: 10000,
|
| 73 |
+
SEARCH_RETRIES: 1,
|
| 74 |
+
PROFILE_RESOURCE_TIMEOUT: 5000,
|
| 75 |
+
CREATOR_CHART_SCRIPT_URL: "https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js",
|
| 76 |
+
|
| 77 |
+
// 🐢 后台补新预算(仅作用于 GET,由 网络请求_基础设施.js 按「本地是否已有可兜底数据」判定)
|
| 78 |
+
// 七大列表视图都是「本地数据先上屏 → 再发请求补新」,此时界面上已经有内容,
|
| 79 |
+
// 这次请求纯属后台行为;它每多挂 1 秒就多占 1 个并发额度(全局仅 6 个),
|
| 80 |
+
// 云端不通时会把前台请求(详情页、点赞、轮询)全部挤到排队,表现为「切哪个界面都要等」。
|
| 81 |
+
// 故一旦本地已有该请求的缓存(含过期),单次超时压到 10s 且不再重试,最坏耗时 10s
|
| 82 |
+
BACKGROUND_TIMEOUT: 10000,
|
| 83 |
+
BANNER_IMAGE_TIMEOUT: 75000,
|
| 84 |
+
WORKFLOW_DOWNLOAD_TIMEOUT: 120000, // 工作流流式下载预算(2 分钟)
|
| 85 |
+
BACKGROUND_RETRIES: 0,
|
| 86 |
+
|
| 87 |
+
// ☁️ 云端不可达冷却(消费方见 状态管理.js 的「云端可达性」段落)
|
| 88 |
+
// navigator.onLine 只反映本机网卡,云端单独不可达(HF Space 宕机/冷启动、DNS 污染、被墙)
|
| 89 |
+
// 时它仍为 true,于是每个界面、每次轮询都要从头烧一遍完整重试预算,且互不共享失败结论。
|
| 90 |
+
// 首次确证云端不可达后进入冷却:冷却期内所有 GET 一律不联网(有缓存直接返回、无缓存立即失败),
|
| 91 |
+
// 并发额度全部让给前台。被动探测——冷却一过,下一个真实请求充当探针;
|
| 92 |
+
// 期间任意一次成功响应(含 POST/PUT/DELETE)立即清零,不需等到冷却结束
|
| 93 |
+
CLOUD_DOWN_COOLDOWN: 60 * 1000,
|
| 94 |
+
|
| 95 |
+
// 🔍 创作者搜索请求的取消分组 ID
|
| 96 |
+
// 用户在搜索框继续输入时,侧边栏数据引擎.js 据此撤销上一次仍在途的搜索请求。
|
| 97 |
+
// 不撤销的话,作废请求会持续占用并发额度(最多 6 个)与超时预算,
|
| 98 |
+
// 把最新那次搜索挤到后面排队。仅搜索请求归入本分组,不影响其它任何请求
|
| 99 |
+
SEARCH_COMPONENT_ID: "sidebar-search",
|
| 100 |
+
// 上传相对路径的云端图片读取入口。
|
| 101 |
+
IMAGE_PROXY_PATH: "/api/image_proxy",
|
| 102 |
+
PROFILE_MEDIA_URL_MAX_BYTES: 16384,
|
| 103 |
+
// 本地广告预载要求真实图片,熔断占位图不能作为下载成功。
|
| 104 |
+
LOCAL_IMAGE_PROXY_PREFIX: "/community_hub/image?url=",
|
| 105 |
+
COMPLETE_IMAGE_QUERY_SUFFIX: "&require_complete=1",
|
| 106 |
+
// 身份相关或私有 GET 禁止通用响应缓存;公开列表仍使用原缓存策略。
|
| 107 |
+
PRIVATE_GET_PATTERNS: [
|
| 108 |
+
/^\/api\/(wallet|messages|chats|admin|disputes|users)(?:\/|$)/,
|
| 109 |
+
/^\/api\/my-(posts|prompts|tasks)(?:\/|$)/,
|
| 110 |
+
/^\/api\/(tasks|prompts)\/[^/]+$/
|
| 111 |
+
]
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
export const LOCAL_PERFORMANCE = Object.freeze({
|
| 116 |
+
MAX_ENDPOINT_CHARS: 4096,
|
| 117 |
+
MAX_COUNT: 1000000,
|
| 118 |
+
MAX_DURATION_MS: 600000,
|
| 119 |
+
ITEM_CATEGORIES: Object.freeze({ tool: 'tools', skill: 'skills', app: 'apps', recommend: 'recommends' }),
|
| 120 |
+
LIST_CATEGORIES: Object.freeze({ '/api/creators': 'creators', '/api/creators/search': 'creators', '/api/tasks': 'tasks', '/api/posts': 'posts', '/api/prompts': 'prompts' }),
|
| 121 |
+
LABELS: Object.freeze({ tools: 'nav.tools', skills: 'nav.skills', apps: 'nav.apps', recommends: 'nav.recommends', creators: 'nav.creators', tasks: 'nav.tasks', posts: 'nav.posts', prompts: 'nav.prompts' })
|
| 122 |
+
});
|
| 123 |
+
|
| 124 |
+
export const LOCAL_INSTALL_DIRECTORY = Object.freeze({
|
| 125 |
+
PATH: '/community_hub/open_install_directory',
|
| 126 |
+
TIMEOUT: 25000,
|
| 127 |
+
RESPONSE_MAX_BYTES: 8192
|
| 128 |
+
});
|
| 129 |
+
|
| 130 |
+
export const SKILL_INSTALL = Object.freeze({
|
| 131 |
+
PATH: '/community_hub/skill_install',
|
| 132 |
+
PREFLIGHT_TIMEOUT: 30000,
|
| 133 |
+
CONTROL_TIMEOUT: 45000,
|
| 134 |
+
RESULT_TIMEOUT: 5000,
|
| 135 |
+
STREAM_TIMEOUT: 1200000,
|
| 136 |
+
RESPONSE_MAX_BYTES: 1024 * 1024,
|
| 137 |
+
PATH_MAX_BYTES: 32768,
|
| 138 |
+
MAX_DIRECTORIES: 128,
|
| 139 |
+
MAX_ITEMS: 1,
|
| 140 |
+
MODAL_WIDTH: '640px'
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
export const CHART_LIFECYCLE = Object.freeze({
|
| 144 |
+
WIDTH_WAIT_TIMEOUT: 5000,
|
| 145 |
+
RESIZE_DELAY: 300
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
// 发布视频的本地读取预算,不改变云端上传额度。
|
| 149 |
+
export const LAZY_IMAGES = Object.freeze({
|
| 150 |
+
ROOT_MARGIN: "200px 0px",
|
| 151 |
+
THRESHOLD: 0.01,
|
| 152 |
+
MOUNT_TIMEOUT: 30000,
|
| 153 |
+
READ_TIMEOUT: 30000
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
export const PUBLISH_MEDIA_LIMITS = Object.freeze({
|
| 157 |
+
MAX_IMAGES: 9,
|
| 158 |
+
IMAGE_MAX_EDGE: 1920,
|
| 159 |
+
IMAGE_QUALITY: 0.85,
|
| 160 |
+
VIDEO_MAX_BYTES: 100 * 1024 * 1024,
|
| 161 |
+
VIDEO_MAX_SECONDS: 180,
|
| 162 |
+
VIDEO_METADATA_TIMEOUT: 15000
|
| 163 |
+
});
|
| 164 |
+
|
| 165 |
+
// 上传前处理预算;浏览器解码本身仍由宿主管理。
|
| 166 |
+
export const IMAGE_PROCESS = {
|
| 167 |
+
HEADER_BYTES: 256 * 1024,
|
| 168 |
+
MAX_INPUT_BYTES: 50 * 1024 * 1024,
|
| 169 |
+
MAX_PIXELS: 40 * 1024 * 1024,
|
| 170 |
+
MAX_SOURCE_EDGE: 16384,
|
| 171 |
+
MAX_OUTPUT_EDGE: 4096,
|
| 172 |
+
MAX_OUTPUT_BYTES: 5 * 1024 * 1024,
|
| 173 |
+
TIMEOUT: 15000,
|
| 174 |
+
MIN_QUALITY: 0.3,
|
| 175 |
+
QUALITY_STEP: 0.1,
|
| 176 |
+
AVATAR_MAX_BYTES: 2 * 1024 * 1024,
|
| 177 |
+
AVATAR_SIZE: 512
|
| 178 |
+
};
|
| 179 |
+
|
| 180 |
+
// ==========================================
|
| 181 |
+
// 🔐 管理员配置 (P0安全修复:管理员账号配置化)
|
| 182 |
+
// ==========================================
|
| 183 |
+
// 注意:前端仅用于UI显示控制,后端从环境变量 ADMIN_ACCOUNTS 读取
|
| 184 |
+
// 修改此处需同步修改云端环境变量
|
| 185 |
+
export const ADMIN_ACCOUNTS = new Set(["123456", "888888"]);
|
| 186 |
+
|
| 187 |
+
/**
|
| 188 |
+
* 检查账号是否为管理员
|
| 189 |
+
* @param {string} account - 用户账号
|
| 190 |
+
* @returns {boolean}
|
| 191 |
+
*/
|
| 192 |
+
export function isAdmin(account) {
|
| 193 |
+
return ADMIN_ACCOUNTS.has(account);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
// ==========================================
|
| 198 |
+
// 💾 缓存配置
|
| 199 |
+
// ==========================================
|
| 200 |
+
export function getSidebarListCacheKey(tab, sort) {
|
| 201 |
+
const list = tab === 'creators' ? 'creators_full' : tab;
|
| 202 |
+
return `ListCache_${list}_${sort}`;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
const RESOURCE_LIST_PREFIXES = [
|
| 206 |
+
"ListCache_tools_", "ListCache_skills_", "ListCache_apps_", "ListCache_recommends_"
|
| 207 |
+
];
|
| 208 |
+
export const CACHE = {
|
| 209 |
+
// 缓存键前缀
|
| 210 |
+
PREFIX: "ComfyRanking_",
|
| 211 |
+
|
| 212 |
+
// 默认缓存时间(毫秒)
|
| 213 |
+
DEFAULT_TTL: 2 * 60 * 60 * 1000, // 2小时
|
| 214 |
+
|
| 215 |
+
// 各类数据缓存时间
|
| 216 |
+
TTL: {
|
| 217 |
+
LIST_DATA: 5 * 60 * 1000, // 列表数据:5分钟
|
| 218 |
+
DETAIL_DATA: 3 * 60 * 1000, // 详情数据:3分钟
|
| 219 |
+
USER_PROFILE: 2 * 60 * 1000, // 用户资料:2分钟
|
| 220 |
+
WALLET: 30 * 1000, // 钱包数据:30秒
|
| 221 |
+
CREATORS: 5 * 60 * 1000, // 创作者列表:5分钟
|
| 222 |
+
CHAT_HISTORY: 60 * 1000 // 聊天记录:1分钟
|
| 223 |
+
},
|
| 224 |
+
|
| 225 |
+
// 内存缓存最大条目数
|
| 226 |
+
MAX_MEMORY_ITEMS: 100,
|
| 227 |
+
|
| 228 |
+
// 本插件 localStorage 持久缓存的估算容量上限(不影响用户/会话等其它键)
|
| 229 |
+
MAX_STORAGE_BYTES: 2 * 1024 * 1024,
|
| 230 |
+
|
| 231 |
+
// localStorage 键名(旧版兼容)
|
| 232 |
+
SESSION_REMEMBER_TTL: 30 * 24 * 60 * 60 * 1000,
|
| 233 |
+
INSTALLED_VERSION_UNCONFIRMED: "__installed_unconfirmed__", // 安装成功但没有实际版本证明
|
| 234 |
+
SESSION_TEMP_TTL: 24 * 60 * 60 * 1000,
|
| 235 |
+
LEGACY_KEYS: {
|
| 236 |
+
USER: "ComfyCommunity_User",
|
| 237 |
+
TOKEN: "ComfyCommunity_Token",
|
| 238 |
+
LIST_CACHE: "ComfyCommunity_ListCache",
|
| 239 |
+
PROFILE_CACHE: "ComfyCommunity_ProfileCache",
|
| 240 |
+
CHAT_HISTORY: "ComfyCommunity_ChatHistory"
|
| 241 |
+
},
|
| 242 |
+
|
| 243 |
+
// 本地背景存储键(不上传云端)
|
| 244 |
+
LOCAL_KEYS: {
|
| 245 |
+
BANNER_CONFIG: "ComfyRanking_BannerConfig", // 广告配置持久缓存
|
| 246 |
+
CREATORS_SESSION: "ComfyRanking_CreatorsCache", // 沿用创作者临时缓存键
|
| 247 |
+
SIDEBAR_BACKGROUND: "ComfyRanking_SidebarBackground", // 工具背景图 Base64
|
| 248 |
+
PROFILE_BANNER_CACHE: "ComfyRanking_ProfileBannerCache", // 个人资料背景图本地缓存
|
| 249 |
+
ACQUIRED_ITEMS: "ComfyRanking_AcquiredItems", // 已获取的资源记录
|
| 250 |
+
PROFILE_LIST_PREFIX: "ProfileList_", // 兼容旧个人列表业务键
|
| 251 |
+
INSTALLED_VERSION_PREFIX: "ComfyCommunity_LocalVer_", // 沿用既有安装版本键
|
| 252 |
+
ITEM_OWNERSHIP_PREFIX: "ItemOwnership_", // 缓存管理器业务键前缀
|
| 253 |
+
// 🚀 新增:私信和通知本地缓存
|
| 254 |
+
NOTIFICATIONS: "ComfyRanking_Notifications", // 通知消息缓存
|
| 255 |
+
CHAT_LIST: "ComfyRanking_ChatList", // 对话列表缓存
|
| 256 |
+
CHAT_HISTORY_PREFIX: "ComfyRanking_ChatHistory_", // 聊天记录前缀
|
| 257 |
+
NOTIF_CLEAR_TIME: "ComfyRanking_NotifClearTime", // 通知清空时间戳
|
| 258 |
+
CHAT_CLEAR_PREFIX: "ComfyRanking_ChatCleared_" // 聊天清空时间戳前缀
|
| 259 |
+
},
|
| 260 |
+
|
| 261 |
+
REFUND_CACHE_PREFIXES: [
|
| 262 |
+
"api_/api/items", "api_/api/creators", "api_/api/wallet",
|
| 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" },
|
| 269 |
+
prompts: { prefix: "PromptsCache_", endpoint: "/api/prompts" }
|
| 270 |
+
},
|
| 271 |
+
|
| 272 |
+
// 🚀 新增:消息轮询配置
|
| 273 |
+
MESSAGE_POLL: {
|
| 274 |
+
INTERVAL: 30 * 1000, // 轮询间隔:30秒
|
| 275 |
+
ENABLED: true // 是否启用定时检查
|
| 276 |
+
}
|
| 277 |
+
};
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
// ==========================================
|
| 281 |
+
// 📜 分页配置
|
| 282 |
+
// ==========================================
|
| 283 |
+
export const PAGINATION = {
|
| 284 |
+
// 工具/应用每页数量
|
| 285 |
+
ITEMS_PAGE_SIZE: 20,
|
| 286 |
+
|
| 287 |
+
// 创作者每页数量
|
| 288 |
+
CREATORS_PAGE_SIZE: 12,
|
| 289 |
+
SIDEBAR_ITEM_FETCH_LIMIT: 200,
|
| 290 |
+
SIDEBAR_CREATOR_FETCH_LIMIT: 500,
|
| 291 |
+
SIDEBAR_CREATOR_SEARCH_LIMIT: 50,
|
| 292 |
+
PROFILE_USERS_PAGE_SIZE: 20,
|
| 293 |
+
PROFILE_RESOURCE_FETCH_LIMIT: 200,
|
| 294 |
+
|
| 295 |
+
// 评论每页数量
|
| 296 |
+
COMMENTS_PAGE_SIZE: 10,
|
| 297 |
+
|
| 298 |
+
// 消息每页数量
|
| 299 |
+
MESSAGES_PAGE_SIZE: 20,
|
| 300 |
+
CHAT_MESSAGES_PAGE_SIZE: 50,
|
| 301 |
+
|
| 302 |
+
// 滚动加载触发阈值(距底部像素)
|
| 303 |
+
SCROLL_THRESHOLD: 300,
|
| 304 |
+
|
| 305 |
+
// 防抖延迟(毫秒)
|
| 306 |
+
DEBOUNCE_DELAY: 100
|
| 307 |
+
};
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
// ==========================================
|
| 311 |
+
// 💰 打赏等级配置
|
| 312 |
+
// ==========================================
|
| 313 |
+
export const TIP_LEVELS = {
|
| 314 |
+
// 每颗星星需要的积分
|
| 315 |
+
POINTS_PER_STAR: 100,
|
| 316 |
+
|
| 317 |
+
// 每个月亮需要的星星数
|
| 318 |
+
STARS_PER_MOON: 5,
|
| 319 |
+
|
| 320 |
+
// 每个太阳需要的月亮数
|
| 321 |
+
MOONS_PER_SUN: 5,
|
| 322 |
+
|
| 323 |
+
// 最高等级太阳数
|
| 324 |
+
MAX_SUNS: 9,
|
| 325 |
+
|
| 326 |
+
// 最高积分(9太阳 = 22500积分)
|
| 327 |
+
MAX_POINTS: 22500,
|
| 328 |
+
|
| 329 |
+
// 等级图标
|
| 330 |
+
ICONS: {
|
| 331 |
+
STAR: "⭐",
|
| 332 |
+
MOON: "🌙",
|
| 333 |
+
SUN: "☀️",
|
| 334 |
+
CROWN: "👑"
|
| 335 |
+
},
|
| 336 |
+
|
| 337 |
+
// 等级称号
|
| 338 |
+
TITLES: {
|
| 339 |
+
MAX_LEVEL: "👑 至尊赞助者",
|
| 340 |
+
HIGH_SUN: "超级赞助者",
|
| 341 |
+
LOW_SUN: "黄金赞助者",
|
| 342 |
+
HIGH_MOON: "白银赞助者",
|
| 343 |
+
LOW_MOON: "青铜赞助者",
|
| 344 |
+
HIGH_STAR: "热心粉丝",
|
| 345 |
+
DEFAULT: "支持者"
|
| 346 |
+
}
|
| 347 |
+
};
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
// ==========================================
|
| 351 |
+
// 🎨 UI 配置
|
| 352 |
+
// ==========================================
|
| 353 |
+
export const UI = {
|
| 354 |
+
INSTALL_SOURCE: Object.freeze({
|
| 355 |
+
MAX_URL_BYTES: 4096,
|
| 356 |
+
CASE_INSENSITIVE_HOSTS: Object.freeze(['github.com'])
|
| 357 |
+
}),
|
| 358 |
+
INSTALL_TRANSFER: Object.freeze({
|
| 359 |
+
MAX_BYTES: 2 * 1024 * 1024 * 1024,
|
| 360 |
+
MAX_RATE: Number.MAX_SAFE_INTEGER,
|
| 361 |
+
BASE: 1024,
|
| 362 |
+
STAGES: Object.freeze(['downloading', 'download_done']),
|
| 363 |
+
UNIT_KEYS: Object.freeze(['install.transfer_b', 'install.transfer_kib', 'install.transfer_mib', 'install.transfer_gib'])
|
| 364 |
+
}),
|
| 365 |
+
|
| 366 |
+
CHAT_MAX_MSG_LENGTH: 1000,
|
| 367 |
+
CHAT_TIME_GAP_SECONDS: 5 * 60,
|
| 368 |
+
MAX_DISPUTE_EVIDENCE: 6,
|
| 369 |
+
RECHARGE_POLL_INTERVAL: 3000,
|
| 370 |
+
RECHARGE_POLL_LIMIT: 100,
|
| 371 |
+
RECHARGE_QR_MAX_BYTES: 2048,
|
| 372 |
+
PROFILE_BANNER_CACHE_BYTES: 512 * 1024,
|
| 373 |
+
// 与云端公开版本接口的缺省值一致;用于网络不可用时的本地展示。
|
| 374 |
+
DEFAULT_PROJECT_VERSION: Object.freeze({ stage: "beta", major: 2, minor: 0, patch: 0 }),
|
| 375 |
+
ABOUT: {
|
| 376 |
+
AUTHOR_URL: "https://space.bilibili.com/2114638644",
|
| 377 |
+
AUTHOR_BADGE_URL: "https://img.shields.io/badge/bilibili-猪的飞行梦-00A1D6?logo=bilibili&logoColor=white"
|
| 378 |
+
},
|
| 379 |
+
// Toast 消息配置
|
| 380 |
+
TOAST: {
|
| 381 |
+
OFFSET: 50,
|
| 382 |
+
GAP: 10,
|
| 383 |
+
DURATION: 3000, // 显示时长(毫秒)
|
| 384 |
+
MAX_VISIBLE: 3, // 最大同时显示数量
|
| 385 |
+
POSITION: "top-center" // 位置
|
| 386 |
+
},
|
| 387 |
+
|
| 388 |
+
// 动画配置
|
| 389 |
+
ANIMATION: {
|
| 390 |
+
DURATION_FAST: 150,
|
| 391 |
+
DURATION_NORMAL: 300,
|
| 392 |
+
DURATION_SLOW: 500,
|
| 393 |
+
EASING: "cubic-bezier(0.25, 0.8, 0.25, 1)"
|
| 394 |
+
},
|
| 395 |
+
|
| 396 |
+
// 颜色主题
|
| 397 |
+
COLORS: {
|
| 398 |
+
PRIMARY: "#2196F3",
|
| 399 |
+
SUCCESS: "#4CAF50",
|
| 400 |
+
WARNING: "#FF9800",
|
| 401 |
+
ERROR: "#F44336",
|
| 402 |
+
INFO: "#2196F3",
|
| 403 |
+
TIP: "#E91E63",
|
| 404 |
+
|
| 405 |
+
// 深色主题背景
|
| 406 |
+
BG_DARK: "var(--comfy-menu-bg)",
|
| 407 |
+
BG_CARD: "var(--comfy-input-bg)",
|
| 408 |
+
BG_INPUT: "var(--comfy-input-bg)",
|
| 409 |
+
|
| 410 |
+
// 边框
|
| 411 |
+
BORDER: "#444",
|
| 412 |
+
BORDER_LIGHT: "#555"
|
| 413 |
+
},
|
| 414 |
+
|
| 415 |
+
// 图片占位图(🚀 关键修复:全部改为内联 SVG,避免外部网络请求)
|
| 416 |
+
PLACEHOLDERS: {
|
| 417 |
+
// 头像占位图(👤 图标)
|
| 418 |
+
AVATAR: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Crect fill='%23333' width='150' height='150' rx='75'/%3E%3Ctext x='75' y='100' text-anchor='middle' fill='%23888' font-size='60'%3E👤%3C/text%3E%3C/svg%3E`,
|
| 419 |
+
// 封面占位图(🖼️ 图标)
|
| 420 |
+
COVER: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='260'%3E%3Crect fill='%23222' width='400' height='260'/%3E%3Ctext x='200' y='140' text-anchor='middle' fill='%23666' font-size='48'%3E🖼️%3C/text%3E%3C/svg%3E`,
|
| 421 |
+
// 小头像占位图(用于列表)
|
| 422 |
+
AVATAR_SMALL: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect fill='%23333' width='40' height='40' rx='20'/%3E%3Ctext x='20' y='26' text-anchor='middle' fill='%23888' font-size='16'%3E👤%3C/text%3E%3C/svg%3E`,
|
| 423 |
+
// 加载占位图
|
| 424 |
+
LOADING: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect fill='%231a1a1a' width='100' height='100'/%3E%3C/svg%3E"
|
| 425 |
+
}
|
| 426 |
+
};
|
| 427 |
+
|
| 428 |
+
// 🚀 导出 PLACEHOLDERS 便于直接使用
|
| 429 |
+
export const PLACEHOLDERS = UI.PLACEHOLDERS;
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
// ==========================================
|
| 433 |
+
// 📝 表单验证配置
|
| 434 |
+
// ==========================================
|
| 435 |
+
export const VALIDATION = {
|
| 436 |
+
// 账号规则
|
| 437 |
+
ACCOUNT: {
|
| 438 |
+
MIN_LENGTH: 6,
|
| 439 |
+
MAX_LENGTH: 20,
|
| 440 |
+
PATTERN: /^[a-zA-Z0-9_]{6,20}$/,
|
| 441 |
+
MESSAGE: "账号仅支持6-20位字母、数字、下划线"
|
| 442 |
+
},
|
| 443 |
+
|
| 444 |
+
// 密码规则
|
| 445 |
+
PASSWORD: {
|
| 446 |
+
MIN_LENGTH: 6,
|
| 447 |
+
PATTERN: /^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]{6,}$/,
|
| 448 |
+
MESSAGE: "密码至少6位,支持字母、数字和常见特殊字符"
|
| 449 |
+
},
|
| 450 |
+
|
| 451 |
+
// 昵称规则
|
| 452 |
+
NAME: {
|
| 453 |
+
MIN_LENGTH: 1,
|
| 454 |
+
MAX_LENGTH: 20,
|
| 455 |
+
MESSAGE: "昵称长度1-20个字符"
|
| 456 |
+
},
|
| 457 |
+
|
| 458 |
+
// 个人介绍规则
|
| 459 |
+
INTRO: {
|
| 460 |
+
MAX_LENGTH: 100,
|
| 461 |
+
MESSAGE: "个人介绍不能超过100字"
|
| 462 |
+
},
|
| 463 |
+
|
| 464 |
+
// 验证码规则
|
| 465 |
+
VERIFY_CODE: {
|
| 466 |
+
LENGTH: 6,
|
| 467 |
+
PATTERN: /^\d{6}$/,
|
| 468 |
+
EXPIRE_SECONDS: 600,
|
| 469 |
+
MESSAGE: "请输入6位数字验证码"
|
| 470 |
+
}
|
| 471 |
+
};
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
// ==========================================
|
| 475 |
+
// 🔧 功能开关
|
| 476 |
+
// ==========================================
|
| 477 |
+
export const FEATURES = {
|
| 478 |
+
// 是否启用调试模式
|
| 479 |
+
DEBUG: false,
|
| 480 |
+
|
| 481 |
+
// 是否启用性能监控
|
| 482 |
+
PERFORMANCE_MONITOR: false,
|
| 483 |
+
|
| 484 |
+
// 是否启用离线模式提示
|
| 485 |
+
OFFLINE_DETECTION: true,
|
| 486 |
+
|
| 487 |
+
// 是否启用图片懒加载
|
| 488 |
+
LAZY_LOAD_IMAGES: true,
|
| 489 |
+
|
| 490 |
+
// 是否启用分页加载
|
| 491 |
+
PAGINATION_ENABLED: true,
|
| 492 |
+
|
| 493 |
+
// 是否启用请求缓存
|
| 494 |
+
REQUEST_CACHE: true,
|
| 495 |
+
|
| 496 |
+
// 是否启用骨架屏
|
| 497 |
+
SKELETON_LOADING: true
|
| 498 |
+
};
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
// ==========================================
|
| 502 |
+
// 🌐 运行环境开关
|
| 503 |
+
// ==========================================
|
| 504 |
+
// 移动端网页版运行时开关:Web 引导页(云端Space代码/移动端网页/index.html)
|
| 505 |
+
// 在模块求值前置位 window.__RANKING_WEB_MODE__ = true;ComfyUI 环境不置位,恒为 false,
|
| 506 |
+
// 本地侧边栏行为零变化。所有「仅本地可用」功能的门控均以此为准
|
| 507 |
+
export const IS_WEB_MODE = typeof window !== "undefined" && window.__RANKING_WEB_MODE__ === true;
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
// ==========================================
|
| 511 |
+
// 📊 统计埋点配置(预留)
|
| 512 |
+
// ==========================================
|
| 513 |
+
export const ANALYTICS = {
|
| 514 |
+
// 是否启用统计
|
| 515 |
+
ENABLED: false,
|
| 516 |
+
|
| 517 |
+
// 统计服务地址
|
| 518 |
+
ENDPOINT: "",
|
| 519 |
+
|
| 520 |
+
// 采样率(0-1)
|
| 521 |
+
SAMPLE_RATE: 1.0
|
| 522 |
+
};
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
// ==========================================
|
| 526 |
+
// 🔗 外部链接
|
| 527 |
+
// ==========================================
|
| 528 |
+
export const LINKS = {
|
| 529 |
+
// GitHub 仓库
|
| 530 |
+
GITHUB_REPO: "https://github.com/a63976659/ComfyUI-Ranking",
|
| 531 |
+
|
| 532 |
+
// 文档地址
|
| 533 |
+
DOCS: "",
|
| 534 |
+
|
| 535 |
+
// 反馈地址
|
| 536 |
+
FEEDBACK: ""
|
| 537 |
+
};
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
// ==========================================
|
| 541 |
+
// 🛠️ 辅助函数
|
| 542 |
+
// ==========================================
|
| 543 |
+
|
| 544 |
+
/**
|
| 545 |
+
* 获取完整的 API 地址
|
| 546 |
+
* @param {string} endpoint - API 端点
|
| 547 |
+
* @returns {string}
|
| 548 |
+
*/
|
| 549 |
+
export function getApiUrl(endpoint) {
|
| 550 |
+
return `${API.BASE_URL}${endpoint}`;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
/**
|
| 554 |
+
* 获取当前登录用户账号
|
| 555 |
+
* @returns {string|null}
|
| 556 |
+
*/
|
| 557 |
+
export function getCurrentAccount() {
|
| 558 |
+
try {
|
| 559 |
+
const userStr = localStorage.getItem(CACHE.LEGACY_KEYS.USER) || sessionStorage.getItem(CACHE.LEGACY_KEYS.USER);
|
| 560 |
+
if (userStr) {
|
| 561 |
+
const userData = JSON.parse(userStr);
|
| 562 |
+
return userData?.user?.account || null;
|
| 563 |
+
}
|
| 564 |
+
} catch (e) {
|
| 565 |
+
console.warn("获取当前账号失败。");
|
| 566 |
+
}
|
| 567 |
+
return null;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
/**
|
| 571 |
+
* 获取账号区分的界面背景图存储键
|
| 572 |
+
* @param {string} [account] - 账号,不传则自动获取当前登录账号
|
| 573 |
+
* @returns {string}
|
| 574 |
+
*/
|
| 575 |
+
export function getBackgroundKey(account) {
|
| 576 |
+
const acc = account || getCurrentAccount();
|
| 577 |
+
return acc
|
| 578 |
+
? `${CACHE.LOCAL_KEYS.SIDEBAR_BACKGROUND}_${acc}`
|
| 579 |
+
: CACHE.LOCAL_KEYS.SIDEBAR_BACKGROUND;
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
/**
|
| 583 |
+
* 获取账号区分的个人资料卡背景缓存键
|
| 584 |
+
* @param {string} [account] - 账号,不传则自动获取当前登录账号
|
| 585 |
+
* @returns {string}
|
| 586 |
+
*/
|
| 587 |
+
export function getBannerCacheKey(account) {
|
| 588 |
+
const acc = account || getCurrentAccount();
|
| 589 |
+
return acc
|
| 590 |
+
? `${CACHE.LOCAL_KEYS.PROFILE_BANNER_CACHE}_${acc}`
|
| 591 |
+
: CACHE.LOCAL_KEYS.PROFILE_BANNER_CACHE;
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
/**
|
| 595 |
+
* 获取缓存键(带前缀)
|
| 596 |
+
* @param {string} key - 缓存键
|
| 597 |
+
* @returns {string}
|
| 598 |
+
*/
|
| 599 |
+
export function getCacheKey(key) {
|
| 600 |
+
return `${CACHE.PREFIX}${key}`;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
/**
|
| 604 |
+
* 检查功能是否启用
|
| 605 |
+
* @param {string} feature - 功能名
|
| 606 |
+
* @returns {boolean}
|
| 607 |
+
*/
|
| 608 |
+
export function isFeatureEnabled(feature) {
|
| 609 |
+
return FEATURES[feature] === true;
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
/**
|
| 613 |
+
* 获取打赏等级配置
|
| 614 |
+
* @returns {Object}
|
| 615 |
+
*/
|
| 616 |
+
export function getTipLevelConfig() {
|
| 617 |
+
return { ...TIP_LEVELS };
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
|
| 621 |
+
// ==========================================
|
| 622 |
+
// 🚀 SWR 头像缓存工具(跨组件共享)
|
| 623 |
+
// ==========================================
|
| 624 |
+
// 实现原理:
|
| 625 |
+
// 1. 从 localStorage 读取缓存,0延迟渲染
|
| 626 |
+
// 2. 后台静默校对云端数据
|
| 627 |
+
// 3. 有更新时自动刷新 DOM
|
| 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;
|
| 634 |
+
|
| 635 |
+
// ==========================================
|
| 636 |
+
// 🔒 HTML 转义(底层公共工具)
|
| 637 |
+
// ==========================================
|
| 638 |
+
// 放在本文件(零依赖的底层模块)而非 components/互动工具函数.js,是因为
|
| 639 |
+
// UI交互提示组件.js 与 打赏等级工具.js 都需要它,而互动工具函数.js 反向依赖这两者
|
| 640 |
+
// (导入 showToast、re-export renderTipBoardHTML),从那里导入会形成循环依赖。
|
| 641 |
+
// 互动工具函数.js 仍以 re-export 方式对外暴露同名函数,既有调用方无需改动。
|
| 642 |
+
|
| 643 |
+
/**
|
| 644 |
+
* HTML转义(统一版:各组件局部副本已归一到此)
|
| 645 |
+
* @param {*} str - 原始字符串(非字符串会先强制转换,兼容数字等入参)
|
| 646 |
+
* @returns {string} 转义后的字符串
|
| 647 |
+
*/
|
| 648 |
+
export function escapeHtml(str) {
|
| 649 |
+
if (str === null || str === undefined || str === "") return "";
|
| 650 |
+
return String(str).replace(/&/g, "&")
|
| 651 |
+
.replace(/</g, "<")
|
| 652 |
+
.replace(/>/g, ">")
|
| 653 |
+
.replace(/"/g, """)
|
| 654 |
+
.replace(/'/g, "'");
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
/** 资料图片只接受明确的图片表示或网络地址,不相对宿主解析业务路径。 */
|
| 658 |
+
export function normalizeProfileImageUrl(value, unwrapProxy = false, serviceBaseUrl = API.BASE_URL) {
|
| 659 |
+
if (typeof value !== 'string' || !value || value.trim() !== value || /[\u0000-\u001f\u007f]/.test(value)) return '';
|
| 660 |
+
let source = value;
|
| 661 |
+
let layers = 0;
|
| 662 |
+
while (source.startsWith(API.LOCAL_IMAGE_PROXY_PREFIX)) {
|
| 663 |
+
if (++layers > 10) return '';
|
| 664 |
+
let encoded = source.slice(API.LOCAL_IMAGE_PROXY_PREFIX.length);
|
| 665 |
+
if (encoded.endsWith(API.COMPLETE_IMAGE_QUERY_SUFFIX)) encoded = encoded.slice(0, -API.COMPLETE_IMAGE_QUERY_SUFFIX.length);
|
| 666 |
+
try { source = decodeURIComponent(encoded); } catch (_) { return ''; }
|
| 667 |
+
if (!source || source.trim() !== source || /[\u0000-\u001f\u007f]/.test(source)) return '';
|
| 668 |
+
}
|
| 669 |
+
if (!layers && /^data:image\/[a-z0-9.+-]+(?:;[^,]*)?,.+$/i.test(source)) return value;
|
| 670 |
+
if (!layers && /^blob:https?:\/\//i.test(source)) {
|
| 671 |
+
try {
|
| 672 |
+
const blob = new URL(source);
|
| 673 |
+
if (typeof location !== 'undefined' && location.origin && blob.origin !== location.origin) return '';
|
| 674 |
+
return blob.origin === 'null' ? '' : value;
|
| 675 |
+
} catch (_) { return ''; }
|
| 676 |
+
}
|
| 677 |
+
if (!/^https?:\/\//i.test(source) || source.includes('\\')) return '';
|
| 678 |
+
try {
|
| 679 |
+
if (value.length > API.PROFILE_MEDIA_URL_MAX_BYTES
|
| 680 |
+
|| encodeURIComponent(value).replace(/%[0-9a-f]{2}/gi, 'x').length > API.PROFILE_MEDIA_URL_MAX_BYTES) return '';
|
| 681 |
+
const url = new URL(source);
|
| 682 |
+
const authority = source.match(/^https?:\/\/([^/?#]*)/i)?.[1];
|
| 683 |
+
if (!authority || authority.includes('@') || authority.includes('%') || authority.endsWith(':') || url.port === '0') return '';
|
| 684 |
+
if (url.username || url.password) return '';
|
| 685 |
+
const host = url.hostname.toLowerCase().replace(/\.$/, '');
|
| 686 |
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(host)
|
| 687 |
+
&& authority.replace(/:[0-9]+$/, '').replace(/\.$/, '').toLowerCase() !== host) return '';
|
| 688 |
+
const base = new URL(serviceBaseUrl);
|
| 689 |
+
const baseHost = base.hostname.toLowerCase().replace(/\.$/, '');
|
| 690 |
+
if (host === baseHost) {
|
| 691 |
+
const expectedPath = base.pathname.replace(/\/$/, '') + API.IMAGE_PROXY_PATH;
|
| 692 |
+
const sourcePath = source.slice(source.indexOf('://') + 3 + authority.length).split(/[?#]/, 1)[0] || '/';
|
| 693 |
+
if (url.protocol !== base.protocol || url.port !== base.port || sourcePath !== expectedPath) return '';
|
| 694 |
+
return unwrapProxy ? source : value;
|
| 695 |
+
}
|
| 696 |
+
if (!host.startsWith('[') && !/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
|
| 697 |
+
const labels = host.split('.');
|
| 698 |
+
if (host.length > 253 || labels.length < 2 || !labels.every(label => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))
|
| 699 |
+
|| /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(labels.at(-1))) return '';
|
| 700 |
+
}
|
| 701 |
+
if (!host || host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local')) return '';
|
| 702 |
+
if (host.startsWith('[')) {
|
| 703 |
+
if (!/^\[[23][0-9a-f]{0,3}:/i.test(host)) return '';
|
| 704 |
+
const pieces = host.slice(1, -1).split('::');
|
| 705 |
+
const left = pieces[0] ? pieces[0].split(':').map(part => parseInt(part, 16)) : [];
|
| 706 |
+
const right = pieces[1] ? pieces[1].split(':').map(part => parseInt(part, 16)) : [];
|
| 707 |
+
const groups = pieces.length === 1 ? left : [...left, ...Array(8 - left.length - right.length).fill(0), ...right];
|
| 708 |
+
const [first, second, third] = groups;
|
| 709 |
+
if (first === 0x2002 || (first === 0x3fff && second < 0x1000) || (first === 0x2001 && second === 0xdb8)) return '';
|
| 710 |
+
if (first === 0x2001 && second < 0x200) {
|
| 711 |
+
const exception = (second === 1 && groups.slice(2, 7).every(part => part === 0) && [1, 2].includes(groups[7]))
|
| 712 |
+
|| second === 3 || (second === 4 && third === 0x112) || (second >= 0x20 && second <= 0x3f);
|
| 713 |
+
if (!exception) return '';
|
| 714 |
+
}
|
| 715 |
+
} else if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
|
| 716 |
+
const rawHost = authority.replace(/:[0-9]+$/, '').replace(/\.$/, '').toLowerCase();
|
| 717 |
+
if (rawHost !== host) return '';
|
| 718 |
+
const [a,b,c,last] = host.split('.').map(Number);
|
| 719 |
+
if (a === 0 || a === 10 || a === 127 || a >= 224 || (a === 100 && b >= 64 && b <= 127)
|
| 720 |
+
|| (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31)
|
| 721 |
+
|| (a === 192 && b === 168) || (a === 198 && (b === 18 || b === 19))
|
| 722 |
+
|| (a === 192 && b === 0 && ((c === 0 && ![9, 10].includes(last)) || c === 2))
|
| 723 |
+
|| (a === 198 && b === 51 && c === 100) || (a === 203 && b === 0 && c === 113)) return '';
|
| 724 |
+
}
|
| 725 |
+
return unwrapProxy ? source : value;
|
| 726 |
+
} catch (_) { return ''; }
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
/** 统一字段映射:将后端 avatarDataUrl 映射为前端 avatar */
|
| 730 |
+
function _mapAvatarField(profile) {
|
| 731 |
+
if (profile && profile.avatarDataUrl && !profile.avatar) {
|
| 732 |
+
profile.avatar = profile.avatarDataUrl;
|
| 733 |
+
}
|
| 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 |
+
return Object.fromEntries(PUBLIC_PROFILE_FIELDS.filter(key => Object.prototype.hasOwnProperty.call(profile, key))
|
| 741 |
+
.map(key => [key, ['avatar', 'avatarDataUrl', 'bannerUrl'].includes(key)
|
| 742 |
+
? normalizeProfileImageUrl(profile[key]) : profile[key]]));
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
function _validProfile(profile) {
|
| 746 |
+
return !!profile && typeof profile === 'object' && !Array.isArray(profile)
|
| 747 |
+
&& ['name', 'avatar', 'avatarDataUrl'].every(field => profile[field] == null || typeof profile[field] === 'string');
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
/**
|
| 751 |
+
* 🚀 获取缓存的用户资料(同步)
|
| 752 |
+
* @param {string} account - 用户账号
|
| 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;
|
| 760 |
+
const profile = JSON.parse(cached);
|
| 761 |
+
if (!_validProfile(profile)) return null;
|
| 762 |
+
const publicProfile = _publicProfile(profile);
|
| 763 |
+
_mapAvatarField(publicProfile);
|
| 764 |
+
// 旧资料缓存可能含联系方式;读取返回过滤副本并尝试迁移,不因迁移失败隐藏公开内容。
|
| 765 |
+
if (JSON.stringify(publicProfile) !== cached) {
|
| 766 |
+
try { localStorage.setItem(PROFILE_CACHE_PREFIX + account, JSON.stringify(publicProfile)); } catch (_) {}
|
| 767 |
+
}
|
| 768 |
+
return publicProfile;
|
| 769 |
+
} catch {
|
| 770 |
+
return null;
|
| 771 |
+
}
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
/**
|
| 775 |
+
* 🚀 保存用户资料到缓存
|
| 776 |
+
* @param {string} account - 用户账号
|
| 777 |
+
* @param {Object} profile - 用户资料
|
| 778 |
+
*/
|
| 779 |
+
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 |
+
|
| 786 |
+
/**
|
| 787 |
+
* 🚀 SWR 模式获取用户资料(先返回缓存,后台校对)
|
| 788 |
+
* @param {string} account - 用户账号
|
| 789 |
+
* @param {Function} apiFn - API 调用函数 (account) => Promise<{data: profile}>
|
| 790 |
+
* @param {Function} onUpdate - 数据更新时的回调 (profile) => void
|
| 791 |
+
* @returns {Object|null} 初始缓存数据
|
| 792 |
+
*/
|
| 793 |
+
export function getProfileWithSWR(account, apiFn, onUpdate) {
|
| 794 |
+
if (!account) return null;
|
| 795 |
+
|
| 796 |
+
// 第一轨:同步返回缓存
|
| 797 |
+
const cached = getCachedProfile(account);
|
| 798 |
+
|
| 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 = () => {
|
| 805 |
+
if (_pendingProfileRequests.get(account) === entry) _pendingProfileRequests.delete(account);
|
| 806 |
+
clearTimeout(entry.timer);
|
| 807 |
+
entry.listeners.clear();
|
| 808 |
+
};
|
| 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;
|
| 816 |
+
const profile = _publicProfile(data);
|
| 817 |
+
_mapAvatarField(profile);
|
| 818 |
+
setCachedProfile(account, profile);
|
| 819 |
+
for (const listener of Array.from(entry.listeners)) {
|
| 820 |
+
try { listener(profile); } catch { /* 单个视图异常不阻断其它订阅者 */ }
|
| 821 |
+
}
|
| 822 |
+
}).catch(() => {}).finally(release);
|
| 823 |
+
}
|
| 824 |
+
if (entry && typeof onUpdate === 'function') entry.listeners.add(onUpdate);
|
| 825 |
+
|
| 826 |
+
return cached;
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
/**
|
| 830 |
+
* 🚀 渲染带 SWR 缓存的头像+名称 HTML
|
| 831 |
+
* @param {Object} options
|
| 832 |
+
* @param {string} options.account - 用户账号
|
| 833 |
+
* @param {string} options.fallbackAvatar - 备用头像 URL(从原始数据中获取)
|
| 834 |
+
* @param {string} options.fallbackName - 备用名称
|
| 835 |
+
* @param {number} options.avatarSize - 头像尺寸 (px)
|
| 836 |
+
* @param {Function} options.apiFn - API 调用函数
|
| 837 |
+
* @param {string} options.containerId - 唯一容器 ID(用于 DOM 更新)
|
| 838 |
+
* @returns {string} HTML 字符串
|
| 839 |
+
*/
|
| 840 |
+
export function renderAvatarWithSWR(options) {
|
| 841 |
+
const { account, fallbackAvatar, fallbackName, avatarSize = 24, apiFn, containerId } = options;
|
| 842 |
+
const renderId = String(++nextAvatarRenderId);
|
| 843 |
+
const size = typeof avatarSize === 'number' || typeof avatarSize === 'string' ? Number(avatarSize) : NaN;
|
| 844 |
+
const safeSize = Number.isFinite(size) && size > 0 ? size : 24;
|
| 845 |
+
|
| 846 |
+
// 从缓存获取
|
| 847 |
+
const cached = getCachedProfile(account);
|
| 848 |
+
const avatar = cached?.avatar || cached?.avatarDataUrl || normalizeProfileImageUrl(fallbackAvatar);
|
| 849 |
+
const name = cached?.name || fallbackName || account || '';
|
| 850 |
+
|
| 851 |
+
// 后台静默校对并更新 DOM
|
| 852 |
+
if (apiFn && containerId) {
|
| 853 |
+
setTimeout(() => {
|
| 854 |
+
const container = document.getElementById(containerId);
|
| 855 |
+
if (!container || container.dataset.profileRender !== renderId) return;
|
| 856 |
+
getProfileWithSWR(account, apiFn, (profile) => {
|
| 857 |
+
if (!container.isConnected || document.getElementById(containerId) !== container
|
| 858 |
+
|| container.dataset.profileRender !== renderId) return;
|
| 859 |
+
|
| 860 |
+
const avatarImg = container.querySelector('.swr-avatar');
|
| 861 |
+
const nameSpan = container.querySelector('.swr-name');
|
| 862 |
+
|
| 863 |
+
if (avatarImg && profile.avatar) {
|
| 864 |
+
if (avatarImg.tagName === 'IMG') avatarImg.src = profile.avatar;
|
| 865 |
+
else {
|
| 866 |
+
const image = document.createElement('img');
|
| 867 |
+
image.className = 'swr-avatar';
|
| 868 |
+
image.alt = '';
|
| 869 |
+
image.style.cssText = avatarImg.style.cssText;
|
| 870 |
+
image.style.objectFit = 'cover';
|
| 871 |
+
image.src = profile.avatar;
|
| 872 |
+
avatarImg.replaceWith(image);
|
| 873 |
+
}
|
| 874 |
+
} else if (avatarImg && avatarImg.tagName !== 'IMG' && profile.name) {
|
| 875 |
+
avatarImg.textContent = Array.from(profile.name)[0]?.toUpperCase() || 'U';
|
| 876 |
+
}
|
| 877 |
+
if (nameSpan && profile.name) {
|
| 878 |
+
nameSpan.textContent = profile.name;
|
| 879 |
+
}
|
| 880 |
+
});
|
| 881 |
+
}, 0);
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
// 生成初始 HTML(无头像时显示首字母背景)
|
| 885 |
+
// 🔒 XSS防护:avatar/name/initial 均来自用户可修改的资料(昵称首字母可能是 < ),
|
| 886 |
+
// 写入 HTML 前必须转义;上方 SWR 刷新路径用的是 .src / .textContent,本身就是安全的。
|
| 887 |
+
const initial = Array.from(String(name || account || 'U'))[0].toUpperCase();
|
| 888 |
+
const avatarHtml = avatar
|
| 889 |
+
? `<img class="swr-avatar" alt="" src="${escapeHtml(avatar)}" style="width: ${safeSize}px; height: ${safeSize}px; border-radius: 50%; object-fit: cover; flex-shrink: 0; background: #333;">`
|
| 890 |
+
: `<div class="swr-avatar" aria-hidden="true" style="width: ${safeSize}px; height: ${safeSize}px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: ${Math.floor(safeSize * 0.5)}px; font-weight: bold; flex-shrink: 0;">${escapeHtml(initial)}</div>`;
|
| 891 |
+
|
| 892 |
+
return `<span id="${escapeHtml(containerId)}" data-profile-render="${renderId}" style="display: inline-flex; align-items: center; gap: 6px;">${avatarHtml}<span class="swr-name">${escapeHtml(name)}</span></span>`;
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
|
| 896 |
+
// ==========================================
|
| 897 |
+
// 📦 导出默认配置对象
|
| 898 |
+
// ==========================================
|
| 899 |
+
export default {
|
| 900 |
+
IMAGE_PROCESS,
|
| 901 |
+
API,
|
| 902 |
+
CACHE,
|
| 903 |
+
PAGINATION,
|
| 904 |
+
TIP_LEVELS,
|
| 905 |
+
UI,
|
| 906 |
+
VALIDATION,
|
| 907 |
+
FEATURES,
|
| 908 |
+
IS_WEB_MODE,
|
| 909 |
+
ANALYTICS,
|
| 910 |
+
LINKS,
|
| 911 |
+
getApiUrl,
|
| 912 |
+
getCacheKey,
|
| 913 |
+
isFeatureEnabled,
|
| 914 |
+
getCurrentAccount,
|
| 915 |
+
getBackgroundKey,
|
| 916 |
+
getBannerCacheKey,
|
| 917 |
+
getCachedProfile,
|
| 918 |
+
setCachedProfile,
|
| 919 |
+
getProfileWithSWR,
|
| 920 |
+
renderAvatarWithSWR
|
| 921 |
+
};
|
| 922 |
+
|
| 923 |
+
|
| 924 |
+
// 提示词定价输入边界,与云端 PromptCreate/PromptUpdate 对应。
|
| 925 |
+
export const PROMPT_LIMITS = Object.freeze({ MAX_PRICE: 10000 });
|
| 926 |
+
|
| 927 |
+
// 任务定价边界,与云端 task_publication/task_terms 对应。
|
| 928 |
+
export const TASK_LIMITS = Object.freeze({ MAX_PRICE: 2147483647, MAX_REFERENCE_IMAGES: 6, DEPOSIT_RATIOS: Object.freeze([10, 20, 30, 50]), TIP_AMOUNTS: Object.freeze([10, 50, 100, 500]) });
|
| 929 |
+
|
| 930 |
+
|
| 931 |
+
export const TYPE_RULES = {
|
| 932 |
+
tool: {
|
| 933 |
+
recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
|
| 934 |
+
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 935 |
+
},
|
| 936 |
+
skill: {
|
| 937 |
+
recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
|
| 938 |
+
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 939 |
+
},
|
| 940 |
+
app: {
|
| 941 |
+
recommendForm: false, resType: { fixed: 'json' },
|
| 942 |
+
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 943 |
+
},
|
| 944 |
+
recommend: {
|
| 945 |
+
recommendForm: true, resType: { lockedByForm: true },
|
| 946 |
+
privateRepoWhenLink: false, price: false, refund: false, originalRequired: false,
|
| 947 |
+
formToResType: { recommend_tool: 'link', recommend_skill: 'link', recommend_app: 'json', recommend_link: 'netdisk' }
|
| 948 |
+
}
|
| 949 |
+
};
|
| 950 |
+
|
| 951 |
+
// 🔧 设置项 localStorage Key
|
| 952 |
+
const SETTINGS_KEY = "ComfyCommunity_Settings";
|
| 953 |
+
|
| 954 |
+
// 🔧 默认设置值
|
| 955 |
+
const DEFAULT_SETTINGS = {
|
| 956 |
+
enableKeyboardShortcuts: false,
|
| 957 |
+
enableLocalPerformanceStats: false,
|
| 958 |
+
showCreatorBanner: true, // 创作者卡片显示背景图
|
| 959 |
+
enableAnimations: true, // ✨ 榜单切换动画
|
| 960 |
+
enableSoundEffects: true, // 🔊 动画音效
|
| 961 |
+
language: 'zh-CN', // 🌐 界面语言
|
| 962 |
+
cacheExpireSeconds: 7200, // ⏱️ 缓存刷新间隔(秒),默认2小时
|
| 963 |
+
// 🚀 后续可扩展更多设置项
|
| 964 |
+
// compactMode: false, // 紧凑模式
|
| 965 |
+
// darkMode: true, // 深色模式
|
| 966 |
+
};
|
| 967 |
+
|
| 968 |
+
/**
|
| 969 |
+
* 📦 获取当前设置
|
| 970 |
+
*/
|
| 971 |
+
export function getSettings(options = {}) {
|
| 972 |
+
try {
|
| 973 |
+
const saved = localStorage.getItem(SETTINGS_KEY);
|
| 974 |
+
if (saved) {
|
| 975 |
+
const value = JSON.parse(saved);
|
| 976 |
+
if (options.strict && (!value || typeof value !== 'object' || Array.isArray(value))) throw new Error('Invalid settings');
|
| 977 |
+
return { ...DEFAULT_SETTINGS, ...value };
|
| 978 |
+
}
|
| 979 |
+
} catch (e) { if (options.strict) throw e; }
|
| 980 |
+
return { ...DEFAULT_SETTINGS };
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
/**
|
| 984 |
+
* 💾 保存设置
|
| 985 |
+
*/
|
| 986 |
+
export function areKeyboardShortcutsEnabled() {
|
| 987 |
+
return getSettings().enableKeyboardShortcuts === true;
|
| 988 |
+
}
|
| 989 |
+
|
| 990 |
+
export function saveSettings(settings) {
|
| 991 |
+
try {
|
| 992 |
+
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
| 993 |
+
// 🔔 广播设置变更事件,让其他组件响应
|
| 994 |
+
window.dispatchEvent(new CustomEvent("comfy-settings-changed", { detail: settings }));
|
| 995 |
+
return true;
|
| 996 |
+
} catch (e) { return false; }
|
| 997 |
+
}
|
前端页面/core/榜单数据来源.js
CHANGED
|
@@ -1,93 +1,121 @@
|
|
| 1 |
-
import { api, requestCancelManager } from './网络请求API.js';
|
| 2 |
-
import { API, CACHE, PAGINATION } from './全局配置.js';
|
| 3 |
-
import { requireListResponseData, getCacheWithMeta, removeCache } from '../components/性能优化工具.js';
|
| 4 |
-
|
| 5 |
-
import { captureCacheWrite } from './网络请求_缓存管理.js';
|
| 6 |
-
import { getResponseCacheSource } from './网络请求_基础设施.js';
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
export
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
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 |
+
|
| 9 |
+
export function captureListRead(cacheKey) {
|
| 10 |
+
const definition = Object.values(CACHE.PUBLIC_LISTS).find(list => typeof cacheKey === 'string' && cacheKey.startsWith(list.prefix));
|
| 11 |
+
if (!definition) throw new TypeError('Unsupported list cache');
|
| 12 |
+
const listCurrent = captureCacheWrite(cacheKey);
|
| 13 |
+
const apiCurrent = captureCacheWrite(`api_${definition.endpoint}`);
|
| 14 |
+
const isCurrent = () => listCurrent() && apiCurrent();
|
| 15 |
+
return response => {
|
| 16 |
+
const source = getResponseCacheSource(response);
|
| 17 |
+
if (!isCurrent() || (source && !source.source?.isAvailable())) {
|
| 18 |
+
const error = new Error();
|
| 19 |
+
error.name = 'ListCacheChangedError';
|
| 20 |
+
throw error;
|
| 21 |
+
}
|
| 22 |
+
const data = requireListResponseData(response);
|
| 23 |
+
return {
|
| 24 |
+
data,
|
| 25 |
+
write(ttl) {
|
| 26 |
+
if (!isCurrent()) return false;
|
| 27 |
+
requireListResponseData({ status: 'success', data });
|
| 28 |
+
if (source) return source.source.copyTo(cacheKey, data);
|
| 29 |
+
writeListCache(cacheKey, data, ttl);
|
| 30 |
+
return true;
|
| 31 |
+
}
|
| 32 |
+
};
|
| 33 |
+
};
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const sidebarResponseSources = new WeakMap();
|
| 37 |
+
export const getSidebarResponseCacheSource = data => sidebarResponseSources.get(data) || null;
|
| 38 |
+
|
| 39 |
+
const itemTypes = Object.freeze({ tools: 'tool', skills: 'skill', apps: 'app', recommends: 'recommend' });
|
| 40 |
+
|
| 41 |
+
// 只读取与校验,不提交视图状态或缓存;提交时由调用方核查界面代次。
|
| 42 |
+
export async function fetchSidebarList({ tab, sort, keyword = '', force = false, background = false }) {
|
| 43 |
+
const options = background
|
| 44 |
+
? { noCache: true, timeout: API.BACKGROUND_TIMEOUT, retries: API.BACKGROUND_RETRIES }
|
| 45 |
+
: (force ? { noCache: true } : {});
|
| 46 |
+
const endpoint = Object.hasOwn(itemTypes, tab) ? "/api/items" : keyword ? "/api/creators/search" : "/api/creators";
|
| 47 |
+
const isCurrent = captureCacheWrite(`api_${endpoint}`);
|
| 48 |
+
let response;
|
| 49 |
+
if (Object.hasOwn(itemTypes, tab)) {
|
| 50 |
+
response = await api.getItems(itemTypes[tab], sort, PAGINATION.SIDEBAR_ITEM_FETCH_LIMIT, options);
|
| 51 |
+
} else if (tab === 'creators') {
|
| 52 |
+
if (keyword) {
|
| 53 |
+
requestCancelManager.cancelAll(API.SEARCH_COMPONENT_ID);
|
| 54 |
+
response = await api.searchCreators(keyword, sort, PAGINATION.SIDEBAR_CREATOR_SEARCH_LIMIT, options);
|
| 55 |
+
} else {
|
| 56 |
+
response = await api.getCreators(sort, PAGINATION.SIDEBAR_CREATOR_FETCH_LIMIT, options);
|
| 57 |
+
}
|
| 58 |
+
} else {
|
| 59 |
+
throw new TypeError('Unsupported sidebar list');
|
| 60 |
+
}
|
| 61 |
+
if (!isCurrent()) {
|
| 62 |
+
const error = new Error();
|
| 63 |
+
error.name = "ListCacheChangedError";
|
| 64 |
+
throw error;
|
| 65 |
+
}
|
| 66 |
+
const source = getResponseCacheSource(response);
|
| 67 |
+
if (source && !source.source?.isAvailable()) {
|
| 68 |
+
const error = new Error();
|
| 69 |
+
error.name = "ListCacheChangedError";
|
| 70 |
+
throw error;
|
| 71 |
+
}
|
| 72 |
+
const data = requireListResponseData(response);
|
| 73 |
+
sidebarResponseSources.delete(data);
|
| 74 |
+
if (source) sidebarResponseSources.set(data, source);
|
| 75 |
+
return data;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function validCreatorList(data) {
|
| 79 |
+
try { requireListResponseData({ status: 'success', data }); }
|
| 80 |
+
catch { return false; }
|
| 81 |
+
return data.every(item => ['name', 'account', 'shortDesc'].every(key =>
|
| 82 |
+
item[key] == null || typeof item[key] === 'string'));
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
export function readSidebarListCache(key, { creators = false } = {}) {
|
| 86 |
+
const miss = { found: false, expired: true, value: null };
|
| 87 |
+
let metadata;
|
| 88 |
+
try {
|
| 89 |
+
metadata = getCacheWithMeta(key, true); // 过期副本仍可显示。
|
| 90 |
+
if (!metadata.found) return metadata;
|
| 91 |
+
} catch { return miss; } // 读取失败不代表数据损坏,不删除未知副本。
|
| 92 |
+
try {
|
| 93 |
+
requireListResponseData({ status: 'success', data: metadata.value });
|
| 94 |
+
if (!creators || validCreatorList(metadata.value)) return metadata;
|
| 95 |
+
} catch { /* 确定无效的列表不进入代理、排序或视图状态。 */ }
|
| 96 |
+
try { removeCache(key); }
|
| 97 |
+
catch { /* 删除失败仍按未命中处理,避免损坏内容阻断界面。 */ }
|
| 98 |
+
return miss;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
export function readCreatorsSessionCache() {
|
| 102 |
+
let raw;
|
| 103 |
+
try { raw = sessionStorage.getItem(CACHE.LOCAL_KEYS.CREATORS_SESSION); }
|
| 104 |
+
catch { return null; } // 无法读取不等于损坏,保留未知内容。
|
| 105 |
+
if (raw === null) return null;
|
| 106 |
+
try {
|
| 107 |
+
const data = JSON.parse(raw);
|
| 108 |
+
if (validCreatorList(data)) return data;
|
| 109 |
+
} catch { /* 仅已读到的损坏记录可尝试删除。 */ }
|
| 110 |
+
try { sessionStorage.removeItem(CACHE.LOCAL_KEYS.CREATORS_SESSION); }
|
| 111 |
+
catch { /* 无法删除不阻断界面。 */ }
|
| 112 |
+
return null;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
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 |
+
}
|
前端页面/core/网络请求_缓存管理.js
CHANGED
|
@@ -35,7 +35,7 @@ const memoryOnlyEntries = new WeakSet();
|
|
| 35 |
|
| 36 |
let cacheClearToken = {};
|
| 37 |
const cacheWriteScopes = [
|
| 38 |
-
...[...CACHE.RESOURCE_LIST_PREFIXES, getSidebarListCacheKey('creators', '')].map(prefix =>
|
| 39 |
({ prefix, matches: key => key.startsWith(prefix), token: {} })),
|
| 40 |
...API.PUBLIC_CONDITIONAL_GET_PATHS.map(path => {
|
| 41 |
const prefix = `api_${path}`;
|
|
@@ -72,7 +72,7 @@ export function captureCacheSource(key) {
|
|
| 72 |
copyTo(targetKey, value) {
|
| 73 |
const normalized = targetKey;
|
| 74 |
if (!isFresh() || typeof normalized !== 'string' ||
|
| 75 |
-
!CACHE.RESOURCE_LIST_PREFIXES.concat(getSidebarListCacheKey('creators', '')).some(prefix => normalized.startsWith(prefix))) return false;
|
| 76 |
_storeCache(normalized, value, cached.expireAt, true, {});
|
| 77 |
return true;
|
| 78 |
}
|
|
@@ -107,10 +107,13 @@ function _isValidCacheEntry(cached) {
|
|
| 107 |
}
|
| 108 |
|
| 109 |
// 同前缀的本地记录/配置使用独立格式;自动缓存清理不能处理它们。
|
| 110 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
if (!key || !key.startsWith(CACHE_PREFIX)) return false;
|
| 112 |
-
return !
|
| 113 |
-
|| key.startsWith(localKey.endsWith('_') ? localKey : localKey + '_'));
|
| 114 |
}
|
| 115 |
|
| 116 |
/**
|
|
@@ -337,8 +340,9 @@ export function getCacheKeysByPrefix(prefix) {
|
|
| 337 |
if (key.startsWith(fullPrefix)) keys.add(key.slice(CACHE_PREFIX.length));
|
| 338 |
}
|
| 339 |
try {
|
| 340 |
-
|
| 341 |
-
|
|
|
|
| 342 |
if (key && key.startsWith(fullPrefix)) keys.add(key.slice(CACHE_PREFIX.length));
|
| 343 |
}
|
| 344 |
} catch {
|
|
@@ -370,13 +374,15 @@ function _enforceStorageBudget(protectedKey = null) {
|
|
| 370 |
&& now - lastStorageBudgetSweepAt < 60_000) return;
|
| 371 |
|
| 372 |
try {
|
|
|
|
|
|
|
| 373 |
const entries = [];
|
| 374 |
const invalidKeys = [];
|
| 375 |
let totalBytes = 0;
|
| 376 |
-
for (let i = 0; i <
|
| 377 |
-
const key =
|
| 378 |
-
if (!_isManagedStorageKey(key)) continue;
|
| 379 |
-
const raw =
|
| 380 |
if (raw === null) continue;
|
| 381 |
let cached;
|
| 382 |
try {
|
|
@@ -394,12 +400,12 @@ function _enforceStorageBudget(protectedKey = null) {
|
|
| 394 |
entries.push({ key, sizeBytes, lastAccessAt: Number.isFinite(cached.lastAccessAt) ? cached.lastAccessAt : writtenAt });
|
| 395 |
totalBytes += sizeBytes;
|
| 396 |
}
|
| 397 |
-
invalidKeys.forEach(key =>
|
| 398 |
|
| 399 |
let evicted = 0;
|
| 400 |
for (const entry of [...entries]) {
|
| 401 |
if (entry.key === protectedKey && entry.sizeBytes > MAX_STORAGE_BYTES) {
|
| 402 |
-
|
| 403 |
totalBytes -= entry.sizeBytes;
|
| 404 |
entries.splice(entries.indexOf(entry), 1);
|
| 405 |
evicted++;
|
|
@@ -408,7 +414,7 @@ function _enforceStorageBudget(protectedKey = null) {
|
|
| 408 |
entries.sort((a, b) => a.lastAccessAt - b.lastAccessAt);
|
| 409 |
while (totalBytes > MAX_STORAGE_BYTES && entries.length > 0) {
|
| 410 |
const oldest = entries.shift();
|
| 411 |
-
|
| 412 |
totalBytes -= oldest.sizeBytes;
|
| 413 |
evicted++;
|
| 414 |
}
|
|
@@ -470,13 +476,14 @@ function _removeCacheByPrefixes(prefixes) {
|
|
| 470 |
}
|
| 471 |
|
| 472 |
try {
|
|
|
|
| 473 |
const keysToRemove = [];
|
| 474 |
-
for (let i = 0; i <
|
| 475 |
-
const k =
|
| 476 |
if (k && isMatch(k)) keysToRemove.push(k);
|
| 477 |
}
|
| 478 |
keysToRemove.forEach(k => {
|
| 479 |
-
|
| 480 |
removed.add(k);
|
| 481 |
});
|
| 482 |
} catch {}
|
|
@@ -495,14 +502,15 @@ export function clearAllCache() {
|
|
| 495 |
storageUsageBytes = null;
|
| 496 |
|
| 497 |
try {
|
|
|
|
| 498 |
const keysToRemove = [];
|
| 499 |
-
for (let i = 0; i <
|
| 500 |
-
const key =
|
| 501 |
if (key && key.startsWith(CACHE_PREFIX)) {
|
| 502 |
keysToRemove.push(key);
|
| 503 |
}
|
| 504 |
}
|
| 505 |
-
keysToRemove.forEach(k =>
|
| 506 |
return true;
|
| 507 |
} catch {
|
| 508 |
return false;
|
|
@@ -528,17 +536,19 @@ function _enforceCacheLimit() {
|
|
| 528 |
// @param {boolean} aggressive - 是否激进模式(同时清除所有项而非仅过期项)
|
| 529 |
function _cleanExpiredStorage(aggressive = false) {
|
| 530 |
try {
|
|
|
|
|
|
|
| 531 |
const now = Date.now();
|
| 532 |
const keysToRemove = [];
|
| 533 |
-
for (let i = 0; i <
|
| 534 |
-
const key =
|
| 535 |
-
if (_isManagedStorageKey(key)) {
|
| 536 |
if (aggressive) {
|
| 537 |
// 激进模式:清除所有缓存项
|
| 538 |
keysToRemove.push(key);
|
| 539 |
} else {
|
| 540 |
let stored;
|
| 541 |
-
try { stored =
|
| 542 |
if (stored === null) continue;
|
| 543 |
try {
|
| 544 |
const cached = JSON.parse(stored);
|
|
@@ -551,7 +561,7 @@ function _cleanExpiredStorage(aggressive = false) {
|
|
| 551 |
}
|
| 552 |
}
|
| 553 |
}
|
| 554 |
-
keysToRemove.forEach(k =>
|
| 555 |
if (keysToRemove.length > 0) {
|
| 556 |
console.log(`🧹 清理 localStorage 过期缓存: ${keysToRemove.length} 个项`);
|
| 557 |
}
|
|
@@ -561,13 +571,15 @@ function _cleanExpiredStorage(aggressive = false) {
|
|
| 561 |
// LRU 淘汰:按最近使用时间删除最旧的 localStorage 缓存项
|
| 562 |
function _evictOldestStorage() {
|
| 563 |
try {
|
|
|
|
|
|
|
| 564 |
const entries = [];
|
| 565 |
const invalidKeys = [];
|
| 566 |
-
for (let i = 0; i <
|
| 567 |
-
const key =
|
| 568 |
-
if (_isManagedStorageKey(key)) {
|
| 569 |
let stored;
|
| 570 |
-
try { stored =
|
| 571 |
if (stored === null) continue;
|
| 572 |
try {
|
| 573 |
const cached = JSON.parse(stored);
|
|
@@ -583,7 +595,7 @@ function _evictOldestStorage() {
|
|
| 583 |
}
|
| 584 |
}
|
| 585 |
|
| 586 |
-
invalidKeys.forEach(key =>
|
| 587 |
if (entries.length === 0) return invalidKeys.length > 0;
|
| 588 |
|
| 589 |
entries.sort((a, b) => a.lastAccessAt - b.lastAccessAt);
|
|
@@ -591,7 +603,7 @@ function _evictOldestStorage() {
|
|
| 591 |
// 🧹 批量淘汰策略:每次淘汰最旧的 30%(温和策略,减少频繁全量扫描)
|
| 592 |
const evictCount = Math.max(1, Math.ceil(entries.length * 0.3));
|
| 593 |
for (let i = 0; i < evictCount; i++) {
|
| 594 |
-
|
| 595 |
}
|
| 596 |
console.warn(`⚠️ LRU 淘汰: 删除了 ${evictCount} 个最旧 localStorage 缓存项`);
|
| 597 |
return true;
|
|
@@ -676,17 +688,17 @@ const CACHE_CONFIG = {
|
|
| 676 |
const CACHE_INVALIDATION_MAP = {
|
| 677 |
// 任务相关
|
| 678 |
"/api/tasks": {
|
| 679 |
-
"POST": ["api_/api/tasks",
|
| 680 |
-
"PUT": ["api_/api/tasks"], // 更新任务,清除列
|
| 681 |
-
"DELETE": ["api_/api/tasks",
|
| 682 |
-
"INTERACTION": ["api_/api/tasks",
|
| 683 |
},
|
| 684 |
// 帖子相关
|
| 685 |
"/api/posts": {
|
| 686 |
-
"POST": ["api_/api/posts",
|
| 687 |
-
"PUT": ["api_/api/posts", CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX], // 更新帖子,清除列表缓存
|
| 688 |
-
"DELETE": ["api_/api/posts",
|
| 689 |
-
"INTERACTION": ["api_/api/posts",
|
| 690 |
},
|
| 691 |
// 评论相关
|
| 692 |
"/api/comments": {
|
|
@@ -701,10 +713,10 @@ const CACHE_INVALIDATION_MAP = {
|
|
| 701 |
"INTERACTION": ["api_/api/items", "api_/api/creators", CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX] // 👍 互动操作(view/like)
|
| 702 |
},
|
| 703 |
"/api/prompts": {
|
| 704 |
-
"POST": ["api_/api/prompts",
|
| 705 |
-
"PUT": ["api_/api/prompts",
|
| 706 |
-
"DELETE": ["api_/api/prompts",
|
| 707 |
-
"INTERACTION": ["api_/api/prompts",
|
| 708 |
},
|
| 709 |
"/api/interactions": { "POST": ["api_/api/items", "api_/api/creators", ...CACHE.RESOURCE_LIST_PREFIXES, CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX] },
|
| 710 |
// 用户相关
|
|
@@ -806,7 +818,8 @@ export function isPrivateApiGet(endpoint) {
|
|
| 806 |
export function purgePrivateApiCache() {
|
| 807 |
const keys = new Set(memoryCache.keys());
|
| 808 |
try {
|
| 809 |
-
|
|
|
|
| 810 |
} catch (_) {}
|
| 811 |
const prefix = CACHE_PREFIX + "api_";
|
| 812 |
for (const fullKey of keys) {
|
|
|
|
| 35 |
|
| 36 |
let cacheClearToken = {};
|
| 37 |
const cacheWriteScopes = [
|
| 38 |
+
...[...CACHE.RESOURCE_LIST_PREFIXES, getSidebarListCacheKey('creators', ''), ...Object.values(CACHE.PUBLIC_LISTS).map(list => list.prefix)].map(prefix =>
|
| 39 |
({ prefix, matches: key => key.startsWith(prefix), token: {} })),
|
| 40 |
...API.PUBLIC_CONDITIONAL_GET_PATHS.map(path => {
|
| 41 |
const prefix = `api_${path}`;
|
|
|
|
| 72 |
copyTo(targetKey, value) {
|
| 73 |
const normalized = targetKey;
|
| 74 |
if (!isFresh() || typeof normalized !== 'string' ||
|
| 75 |
+
!CACHE.RESOURCE_LIST_PREFIXES.concat(getSidebarListCacheKey('creators', ''), Object.values(CACHE.PUBLIC_LISTS).map(list => list.prefix)).some(prefix => normalized.startsWith(prefix))) return false;
|
| 76 |
_storeCache(normalized, value, cached.expireAt, true, {});
|
| 77 |
return true;
|
| 78 |
}
|
|
|
|
| 107 |
}
|
| 108 |
|
| 109 |
// 同前缀的本地记录/配置使用独立格式;自动缓存清理不能处理它们。
|
| 110 |
+
function _storageExclusions() {
|
| 111 |
+
return Object.values(CACHE.LOCAL_KEYS).map(key => [key, key.endsWith('_') ? key : key + '_']);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
function _isManagedStorageKey(key, exclusions) {
|
| 115 |
if (!key || !key.startsWith(CACHE_PREFIX)) return false;
|
| 116 |
+
return !exclusions.some(([exact, prefix]) => key === exact || key.startsWith(prefix));
|
|
|
|
| 117 |
}
|
| 118 |
|
| 119 |
/**
|
|
|
|
| 340 |
if (key.startsWith(fullPrefix)) keys.add(key.slice(CACHE_PREFIX.length));
|
| 341 |
}
|
| 342 |
try {
|
| 343 |
+
const storage = localStorage;
|
| 344 |
+
for (let index = 0; index < storage.length; index++) {
|
| 345 |
+
const key = storage.key(index);
|
| 346 |
if (key && key.startsWith(fullPrefix)) keys.add(key.slice(CACHE_PREFIX.length));
|
| 347 |
}
|
| 348 |
} catch {
|
|
|
|
| 374 |
&& now - lastStorageBudgetSweepAt < 60_000) return;
|
| 375 |
|
| 376 |
try {
|
| 377 |
+
const storage = localStorage;
|
| 378 |
+
const exclusions = _storageExclusions();
|
| 379 |
const entries = [];
|
| 380 |
const invalidKeys = [];
|
| 381 |
let totalBytes = 0;
|
| 382 |
+
for (let i = 0; i < storage.length; i++) {
|
| 383 |
+
const key = storage.key(i);
|
| 384 |
+
if (!_isManagedStorageKey(key, exclusions)) continue;
|
| 385 |
+
const raw = storage.getItem(key);
|
| 386 |
if (raw === null) continue;
|
| 387 |
let cached;
|
| 388 |
try {
|
|
|
|
| 400 |
entries.push({ key, sizeBytes, lastAccessAt: Number.isFinite(cached.lastAccessAt) ? cached.lastAccessAt : writtenAt });
|
| 401 |
totalBytes += sizeBytes;
|
| 402 |
}
|
| 403 |
+
invalidKeys.forEach(key => storage.removeItem(key));
|
| 404 |
|
| 405 |
let evicted = 0;
|
| 406 |
for (const entry of [...entries]) {
|
| 407 |
if (entry.key === protectedKey && entry.sizeBytes > MAX_STORAGE_BYTES) {
|
| 408 |
+
storage.removeItem(entry.key);
|
| 409 |
totalBytes -= entry.sizeBytes;
|
| 410 |
entries.splice(entries.indexOf(entry), 1);
|
| 411 |
evicted++;
|
|
|
|
| 414 |
entries.sort((a, b) => a.lastAccessAt - b.lastAccessAt);
|
| 415 |
while (totalBytes > MAX_STORAGE_BYTES && entries.length > 0) {
|
| 416 |
const oldest = entries.shift();
|
| 417 |
+
storage.removeItem(oldest.key);
|
| 418 |
totalBytes -= oldest.sizeBytes;
|
| 419 |
evicted++;
|
| 420 |
}
|
|
|
|
| 476 |
}
|
| 477 |
|
| 478 |
try {
|
| 479 |
+
const storage = localStorage;
|
| 480 |
const keysToRemove = [];
|
| 481 |
+
for (let i = 0; i < storage.length; i++) {
|
| 482 |
+
const k = storage.key(i);
|
| 483 |
if (k && isMatch(k)) keysToRemove.push(k);
|
| 484 |
}
|
| 485 |
keysToRemove.forEach(k => {
|
| 486 |
+
storage.removeItem(k);
|
| 487 |
removed.add(k);
|
| 488 |
});
|
| 489 |
} catch {}
|
|
|
|
| 502 |
storageUsageBytes = null;
|
| 503 |
|
| 504 |
try {
|
| 505 |
+
const storage = localStorage;
|
| 506 |
const keysToRemove = [];
|
| 507 |
+
for (let i = 0; i < storage.length; i++) {
|
| 508 |
+
const key = storage.key(i);
|
| 509 |
if (key && key.startsWith(CACHE_PREFIX)) {
|
| 510 |
keysToRemove.push(key);
|
| 511 |
}
|
| 512 |
}
|
| 513 |
+
keysToRemove.forEach(k => storage.removeItem(k));
|
| 514 |
return true;
|
| 515 |
} catch {
|
| 516 |
return false;
|
|
|
|
| 536 |
// @param {boolean} aggressive - 是否激进模式(同时清除所有项而非仅过期项)
|
| 537 |
function _cleanExpiredStorage(aggressive = false) {
|
| 538 |
try {
|
| 539 |
+
const storage = localStorage;
|
| 540 |
+
const exclusions = _storageExclusions();
|
| 541 |
const now = Date.now();
|
| 542 |
const keysToRemove = [];
|
| 543 |
+
for (let i = 0; i < storage.length; i++) {
|
| 544 |
+
const key = storage.key(i);
|
| 545 |
+
if (_isManagedStorageKey(key, exclusions)) {
|
| 546 |
if (aggressive) {
|
| 547 |
// 激进模式:清除所有缓存项
|
| 548 |
keysToRemove.push(key);
|
| 549 |
} else {
|
| 550 |
let stored;
|
| 551 |
+
try { stored = storage.getItem(key); } catch { continue; }
|
| 552 |
if (stored === null) continue;
|
| 553 |
try {
|
| 554 |
const cached = JSON.parse(stored);
|
|
|
|
| 561 |
}
|
| 562 |
}
|
| 563 |
}
|
| 564 |
+
keysToRemove.forEach(k => storage.removeItem(k));
|
| 565 |
if (keysToRemove.length > 0) {
|
| 566 |
console.log(`🧹 清理 localStorage 过期缓存: ${keysToRemove.length} 个项`);
|
| 567 |
}
|
|
|
|
| 571 |
// LRU 淘汰:按最近使用时间删除最旧的 localStorage 缓存项
|
| 572 |
function _evictOldestStorage() {
|
| 573 |
try {
|
| 574 |
+
const storage = localStorage;
|
| 575 |
+
const exclusions = _storageExclusions();
|
| 576 |
const entries = [];
|
| 577 |
const invalidKeys = [];
|
| 578 |
+
for (let i = 0; i < storage.length; i++) {
|
| 579 |
+
const key = storage.key(i);
|
| 580 |
+
if (_isManagedStorageKey(key, exclusions)) {
|
| 581 |
let stored;
|
| 582 |
+
try { stored = storage.getItem(key); } catch { continue; }
|
| 583 |
if (stored === null) continue;
|
| 584 |
try {
|
| 585 |
const cached = JSON.parse(stored);
|
|
|
|
| 595 |
}
|
| 596 |
}
|
| 597 |
|
| 598 |
+
invalidKeys.forEach(key => storage.removeItem(key));
|
| 599 |
if (entries.length === 0) return invalidKeys.length > 0;
|
| 600 |
|
| 601 |
entries.sort((a, b) => a.lastAccessAt - b.lastAccessAt);
|
|
|
|
| 603 |
// 🧹 批量淘汰策略:每次淘汰最旧的 30%(温和策略,减少频繁全量扫描)
|
| 604 |
const evictCount = Math.max(1, Math.ceil(entries.length * 0.3));
|
| 605 |
for (let i = 0; i < evictCount; i++) {
|
| 606 |
+
storage.removeItem(entries[i].key);
|
| 607 |
}
|
| 608 |
console.warn(`⚠️ LRU 淘汰: 删除了 ${evictCount} 个最旧 localStorage 缓存项`);
|
| 609 |
return true;
|
|
|
|
| 688 |
const CACHE_INVALIDATION_MAP = {
|
| 689 |
// 任务相关
|
| 690 |
"/api/tasks": {
|
| 691 |
+
"POST": ["api_/api/tasks", CACHE.PUBLIC_LISTS.tasks.prefix], // 创建任务,清除列表缓存
|
| 692 |
+
"PUT": ["api_/api/tasks", CACHE.PUBLIC_LISTS.tasks.prefix], // 更新任务,清除列���缓存
|
| 693 |
+
"DELETE": ["api_/api/tasks", CACHE.PUBLIC_LISTS.tasks.prefix], // 删除任务,清除列表缓存
|
| 694 |
+
"INTERACTION": ["api_/api/tasks", CACHE.PUBLIC_LISTS.tasks.prefix] // 👍 互动操作(view/like/favorite/tip)
|
| 695 |
},
|
| 696 |
// 帖子相关
|
| 697 |
"/api/posts": {
|
| 698 |
+
"POST": ["api_/api/posts", CACHE.PUBLIC_LISTS.posts.prefix, CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX], // 创建帖子,清除列表缓存
|
| 699 |
+
"PUT": ["api_/api/posts", CACHE.PUBLIC_LISTS.posts.prefix, CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX], // 更新帖子,清除列表缓存
|
| 700 |
+
"DELETE": ["api_/api/posts", CACHE.PUBLIC_LISTS.posts.prefix, CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX], // 删除帖子,清除列表缓存
|
| 701 |
+
"INTERACTION": ["api_/api/posts", CACHE.PUBLIC_LISTS.posts.prefix, CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX] // 👍 互动操作(view/like/favorite/tip)
|
| 702 |
},
|
| 703 |
// 评论相关
|
| 704 |
"/api/comments": {
|
|
|
|
| 713 |
"INTERACTION": ["api_/api/items", "api_/api/creators", CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX] // 👍 互动操作(view/like)
|
| 714 |
},
|
| 715 |
"/api/prompts": {
|
| 716 |
+
"POST": ["api_/api/prompts", CACHE.PUBLIC_LISTS.prompts.prefix, CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX],
|
| 717 |
+
"PUT": ["api_/api/prompts", CACHE.PUBLIC_LISTS.prompts.prefix, CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX],
|
| 718 |
+
"DELETE": ["api_/api/prompts", CACHE.PUBLIC_LISTS.prompts.prefix, CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX],
|
| 719 |
+
"INTERACTION": ["api_/api/prompts", CACHE.PUBLIC_LISTS.prompts.prefix, CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX]
|
| 720 |
},
|
| 721 |
"/api/interactions": { "POST": ["api_/api/items", "api_/api/creators", ...CACHE.RESOURCE_LIST_PREFIXES, CACHE.LOCAL_KEYS.PROFILE_LIST_PREFIX] },
|
| 722 |
// 用户相关
|
|
|
|
| 818 |
export function purgePrivateApiCache() {
|
| 819 |
const keys = new Set(memoryCache.keys());
|
| 820 |
try {
|
| 821 |
+
const storage = localStorage;
|
| 822 |
+
for (let index = 0; index < storage.length; index++) keys.add(storage.key(index));
|
| 823 |
} catch (_) {}
|
| 824 |
const prefix = CACHE_PREFIX + "api_";
|
| 825 |
for (const fullKey of keys) {
|
前端页面/post/讨论区组件.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
// 前端页面/post/讨论区组件.js
|
| 2 |
// ==========================================
|
| 3 |
// 💬 讨论区组件(小红书风格瀑布流列表)
|
|
@@ -18,7 +19,7 @@ import { createCardNavigationScope } from "../components/卡片导航生命周
|
|
| 18 |
import { api, captureRequestSession } from "../core/网络请求API.js";
|
| 19 |
import { proxyImages } from "../core/网络请求API.js";
|
| 20 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 21 |
-
import { readListCache,
|
| 22 |
import { applyViewportAnimations, stopViewportAnimations } from "../components/动画音效引擎.js";
|
| 23 |
import { t } from "../components/用户体验增强.js";
|
| 24 |
import { API, CACHE, getCachedProfile, getProfileWithSWR, normalizeProfileImageUrl } from "../core/全局配置.js";
|
|
@@ -26,7 +27,7 @@ import { createRatingStars } from "../social/评论与互动组件.js";
|
|
| 26 |
import { escapeHtml, formatTime, getCacheTTL } from "../components/互动工具函数.js"; // 🧹 P2归一:局部副本已移除
|
| 27 |
|
| 28 |
// 缓存配置
|
| 29 |
-
const CACHE_KEY_PREFIX =
|
| 30 |
const PAGE_SIZE = 20;
|
| 31 |
|
| 32 |
// 缓存当前用户
|
|
@@ -136,9 +137,11 @@ export function createPostsView(currentUser, keyword = "") {
|
|
| 136 |
const loadMorePosts = async (page, pageSize, { isCurrent = () => true } = {}) => {
|
| 137 |
if (disposed || !isCurrent()) return [];
|
| 138 |
try {
|
|
|
|
| 139 |
const res = await api.getPosts(page, pageSize, currentSort);
|
| 140 |
if (disposed || !isCurrent()) return [];
|
| 141 |
-
const
|
|
|
|
| 142 |
|
| 143 |
if (posts.length === 0) {
|
| 144 |
if (searchKeyword && !allPostsData.some(post => _matchesSearch(post, searchKeyword))) {
|
|
@@ -346,6 +349,7 @@ export function createPostsView(currentUser, keyword = "") {
|
|
| 346 |
|
| 347 |
requestGeneration = ++networkRequestGeneration;
|
| 348 |
isLoadingFromNetwork = true;
|
|
|
|
| 349 |
const res = await api.getPosts(page, PAGE_SIZE, requestedSort);
|
| 350 |
if (viewGeneration !== listViewGeneration || requestGeneration !== networkRequestGeneration) {
|
| 351 |
if (requestGeneration === networkRequestGeneration) {
|
|
@@ -354,7 +358,8 @@ export function createPostsView(currentUser, keyword = "") {
|
|
| 354 |
}
|
| 355 |
return;
|
| 356 |
}
|
| 357 |
-
const
|
|
|
|
| 358 |
if (page === 1) firstPageReady = true;
|
| 359 |
const total = res.total || 0;
|
| 360 |
isLoadingFromNetwork = false;
|
|
@@ -362,7 +367,7 @@ export function createPostsView(currentUser, keyword = "") {
|
|
| 362 |
// 缓存第一页数据
|
| 363 |
if (page === 1) {
|
| 364 |
allPostsData = proxyImages(posts);
|
| 365 |
-
|
| 366 |
} else {
|
| 367 |
allPostsData = [...allPostsData, ...proxyImages(posts)];
|
| 368 |
}
|
|
@@ -437,7 +442,7 @@ export function createPostsView(currentUser, keyword = "") {
|
|
| 437 |
renderPostsFromCache(allPostsData);
|
| 438 |
showToast(t('post.network_cache'), "warning");
|
| 439 |
} else {
|
| 440 |
-
renderListLoadError(postsGrid, t('post.load_failed'), () => loadPosts(1, false), t, err);
|
| 441 |
}
|
| 442 |
}
|
| 443 |
}
|
|
@@ -502,6 +507,7 @@ export function createPostsView(currentUser, keyword = "") {
|
|
| 502 |
const requestGeneration = ++networkRequestGeneration;
|
| 503 |
try {
|
| 504 |
isLoadingFromNetwork = true;
|
|
|
|
| 505 |
const res = await api.getPosts(1, PAGE_SIZE, requestedSort, {
|
| 506 |
noCache: true,
|
| 507 |
timeout: API.BACKGROUND_TIMEOUT,
|
|
@@ -514,14 +520,15 @@ export function createPostsView(currentUser, keyword = "") {
|
|
| 514 |
}
|
| 515 |
return;
|
| 516 |
}
|
| 517 |
-
const
|
|
|
|
| 518 |
isLoadingFromNetwork = false;
|
| 519 |
|
| 520 |
if (isEmptyListRefreshWithExistingData(posts, allPostsData)) {
|
| 521 |
console.warn(`🛡️ 讨论区 ${requestedSort} 后台返回空列表,保留当前数据与缓存`);
|
| 522 |
} else {
|
| 523 |
// 更新缓存
|
| 524 |
-
|
| 525 |
|
| 526 |
// 对比新旧数据,有变化时重新渲染
|
| 527 |
if (_postsDataChanged(allPostsData, posts)) {
|
|
|
|
| 1 |
+
import { captureListRead } from "../core/榜单数据来源.js";
|
| 2 |
// 前端页面/post/讨论区组件.js
|
| 3 |
// ==========================================
|
| 4 |
// 💬 讨论区组件(小红书风格瀑布流列表)
|
|
|
|
| 19 |
import { api, captureRequestSession } from "../core/网络请求API.js";
|
| 20 |
import { proxyImages } from "../core/网络请求API.js";
|
| 21 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 22 |
+
import { readListCache, isEmptyListRefreshWithExistingData, createSkeleton, createPaginationLoader, lazyLoadImages, renderListLoadError, renderListSearchMessage } from "../components/性能优化工具.js";
|
| 23 |
import { applyViewportAnimations, stopViewportAnimations } from "../components/动画音效引擎.js";
|
| 24 |
import { t } from "../components/用户体验增强.js";
|
| 25 |
import { API, CACHE, getCachedProfile, getProfileWithSWR, normalizeProfileImageUrl } from "../core/全局配置.js";
|
|
|
|
| 27 |
import { escapeHtml, formatTime, getCacheTTL } from "../components/互动工具函数.js"; // 🧹 P2归一:局部副本已移除
|
| 28 |
|
| 29 |
// 缓存配置
|
| 30 |
+
const CACHE_KEY_PREFIX = CACHE.PUBLIC_LISTS.posts.prefix.slice(0, -1);
|
| 31 |
const PAGE_SIZE = 20;
|
| 32 |
|
| 33 |
// 缓存当前用户
|
|
|
|
| 137 |
const loadMorePosts = async (page, pageSize, { isCurrent = () => true } = {}) => {
|
| 138 |
if (disposed || !isCurrent()) return [];
|
| 139 |
try {
|
| 140 |
+
const readResponse = captureListRead(`${CACHE_KEY_PREFIX}_${currentSort}`);
|
| 141 |
const res = await api.getPosts(page, pageSize, currentSort);
|
| 142 |
if (disposed || !isCurrent()) return [];
|
| 143 |
+
const responseData = readResponse(res);
|
| 144 |
+
const posts = responseData.data;
|
| 145 |
|
| 146 |
if (posts.length === 0) {
|
| 147 |
if (searchKeyword && !allPostsData.some(post => _matchesSearch(post, searchKeyword))) {
|
|
|
|
| 349 |
|
| 350 |
requestGeneration = ++networkRequestGeneration;
|
| 351 |
isLoadingFromNetwork = true;
|
| 352 |
+
const readResponse = captureListRead(cacheKey);
|
| 353 |
const res = await api.getPosts(page, PAGE_SIZE, requestedSort);
|
| 354 |
if (viewGeneration !== listViewGeneration || requestGeneration !== networkRequestGeneration) {
|
| 355 |
if (requestGeneration === networkRequestGeneration) {
|
|
|
|
| 358 |
}
|
| 359 |
return;
|
| 360 |
}
|
| 361 |
+
const responseData = readResponse(res);
|
| 362 |
+
const posts = responseData.data;
|
| 363 |
if (page === 1) firstPageReady = true;
|
| 364 |
const total = res.total || 0;
|
| 365 |
isLoadingFromNetwork = false;
|
|
|
|
| 367 |
// 缓存第一页数据
|
| 368 |
if (page === 1) {
|
| 369 |
allPostsData = proxyImages(posts);
|
| 370 |
+
responseData.write(getCacheTTL()); // 存原始数据,离线时经图片代理后可展示
|
| 371 |
} else {
|
| 372 |
allPostsData = [...allPostsData, ...proxyImages(posts)];
|
| 373 |
}
|
|
|
|
| 442 |
renderPostsFromCache(allPostsData);
|
| 443 |
showToast(t('post.network_cache'), "warning");
|
| 444 |
} else {
|
| 445 |
+
renderListLoadError(postsGrid, err?.name === 'ListCacheChangedError' ? t('feedback.list_changed_retry') : t('post.load_failed'), () => loadPosts(1, false), t, err);
|
| 446 |
}
|
| 447 |
}
|
| 448 |
}
|
|
|
|
| 507 |
const requestGeneration = ++networkRequestGeneration;
|
| 508 |
try {
|
| 509 |
isLoadingFromNetwork = true;
|
| 510 |
+
const readResponse = captureListRead(cacheKey);
|
| 511 |
const res = await api.getPosts(1, PAGE_SIZE, requestedSort, {
|
| 512 |
noCache: true,
|
| 513 |
timeout: API.BACKGROUND_TIMEOUT,
|
|
|
|
| 520 |
}
|
| 521 |
return;
|
| 522 |
}
|
| 523 |
+
const responseData = readResponse(res);
|
| 524 |
+
const posts = responseData.data;
|
| 525 |
isLoadingFromNetwork = false;
|
| 526 |
|
| 527 |
if (isEmptyListRefreshWithExistingData(posts, allPostsData)) {
|
| 528 |
console.warn(`🛡️ 讨论区 ${requestedSort} 后台返回空列表,保留当前数据与缓存`);
|
| 529 |
} else {
|
| 530 |
// 更新缓存
|
| 531 |
+
responseData.write(getCacheTTL());
|
| 532 |
|
| 533 |
// 对比新旧数据,有变化时重新渲染
|
| 534 |
if (_postsDataChanged(allPostsData, posts)) {
|
前端页面/prompt/提示词组件.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
// ==========================================
|
| 2 |
// 🧩 提示词组件(小红书风格瀑布流列表)
|
| 3 |
// ==========================================
|
|
@@ -21,14 +22,14 @@ import { api, proxyImages, captureRequestSession } from "../core/网络请求API
|
|
| 21 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 22 |
// 📦 列表缓存走 readListCache/writeListCache 统一口径;分类缓存形态不同(空数组也是
|
| 23 |
// 有效缓存态),仍用底层 setCache/getCacheWithMeta
|
| 24 |
-
import { setCache, getCacheWithMeta, readListCache,
|
| 25 |
import { applyViewportAnimations, stopViewportAnimations } from "../components/动画音效引擎.js";
|
| 26 |
import { t, tIfExists } from "../components/用户体验增强.js";
|
| 27 |
import { API, CACHE, getCachedProfile, getProfileWithSWR, normalizeProfileImageUrl } from "../core/全局配置.js";
|
| 28 |
import { escapeHtml, formatTime, getCacheTTL } from "../components/互动工具函数.js"; // 🧹 P2归一:局部副本已移除
|
| 29 |
|
| 30 |
// 缓存配置
|
| 31 |
-
const CACHE_KEY_PREFIX =
|
| 32 |
const CATEGORIES_CACHE_PREFIX = "PromptsCategoriesCache"; // 📴 分类缓存键前缀(离线容灾)
|
| 33 |
const PAGE_SIZE = 20;
|
| 34 |
|
|
@@ -279,9 +280,11 @@ export function createPromptsView(currentUser, keyword = "") {
|
|
| 279 |
const loadMorePrompts = async (page, pageSize, { isCurrent = () => true } = {}) => {
|
| 280 |
if (disposed || !isCurrent()) return [];
|
| 281 |
try {
|
|
|
|
| 282 |
const res = await api.getPrompts(currentType, currentCategory, page, pageSize, currentSort);
|
| 283 |
if (disposed || !isCurrent()) return [];
|
| 284 |
-
const
|
|
|
|
| 285 |
|
| 286 |
if (prompts.length === 0) {
|
| 287 |
if (searchKeyword && !allPromptsData.some(prompt => _matchesSearch(prompt, searchKeyword))) {
|
|
@@ -469,6 +472,7 @@ export function createPromptsView(currentUser, keyword = "") {
|
|
| 469 |
|
| 470 |
requestGeneration = ++networkRequestGeneration;
|
| 471 |
isLoadingFromNetwork = true;
|
|
|
|
| 472 |
const res = await api.getPrompts(requestedType, requestedCategory, page, PAGE_SIZE, requestedSort);
|
| 473 |
if (viewGeneration !== listViewGeneration || requestGeneration !== networkRequestGeneration) {
|
| 474 |
if (requestGeneration === networkRequestGeneration) {
|
|
@@ -477,7 +481,8 @@ export function createPromptsView(currentUser, keyword = "") {
|
|
| 477 |
}
|
| 478 |
return;
|
| 479 |
}
|
| 480 |
-
const
|
|
|
|
| 481 |
if (page === 1) firstPageReady = true;
|
| 482 |
const total = res.total || 0;
|
| 483 |
isLoadingFromNetwork = false;
|
|
@@ -485,7 +490,7 @@ export function createPromptsView(currentUser, keyword = "") {
|
|
| 485 |
// 缓存第一页数据
|
| 486 |
if (page === 1) {
|
| 487 |
allPromptsData = proxyImages(prompts);
|
| 488 |
-
|
| 489 |
} else {
|
| 490 |
allPromptsData = [...allPromptsData, ...proxyImages(prompts)];
|
| 491 |
}
|
|
@@ -559,7 +564,7 @@ export function createPromptsView(currentUser, keyword = "") {
|
|
| 559 |
renderPromptsFromCache(allPromptsData);
|
| 560 |
showToast(t('prompt.network_cache'), "warning");
|
| 561 |
} else {
|
| 562 |
-
renderListLoadError(promptsGrid, t('prompt.load_failed'), () => loadPrompts(1, false), t, err);
|
| 563 |
}
|
| 564 |
}
|
| 565 |
}
|
|
@@ -626,6 +631,7 @@ export function createPromptsView(currentUser, keyword = "") {
|
|
| 626 |
const requestGeneration = ++networkRequestGeneration;
|
| 627 |
try {
|
| 628 |
isLoadingFromNetwork = true;
|
|
|
|
| 629 |
const res = await api.getPrompts(requestedType, requestedCategory, 1, PAGE_SIZE, requestedSort, {
|
| 630 |
noCache: true,
|
| 631 |
timeout: API.BACKGROUND_TIMEOUT,
|
|
@@ -638,14 +644,15 @@ export function createPromptsView(currentUser, keyword = "") {
|
|
| 638 |
}
|
| 639 |
return;
|
| 640 |
}
|
| 641 |
-
const
|
|
|
|
| 642 |
isLoadingFromNetwork = false;
|
| 643 |
|
| 644 |
if (isEmptyListRefreshWithExistingData(prompts, allPromptsData)) {
|
| 645 |
console.warn(`🛡️ 提示词 ${requestedType}/${requestedSort} 后台返回空列表,保留当前数据与缓存`);
|
| 646 |
} else {
|
| 647 |
// 更新缓存
|
| 648 |
-
|
| 649 |
|
| 650 |
// 对比新旧数据,有变化时重新渲染
|
| 651 |
if (_promptsDataChanged(allPromptsData, prompts)) {
|
|
|
|
| 1 |
+
import { captureListRead } from "../core/榜单数据来源.js";
|
| 2 |
// ==========================================
|
| 3 |
// 🧩 提示词组件(小红书风格瀑布流列表)
|
| 4 |
// ==========================================
|
|
|
|
| 22 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 23 |
// 📦 列表缓存走 readListCache/writeListCache 统一口径;分类缓存形态不同(空数组也是
|
| 24 |
// 有效缓存态),仍用底层 setCache/getCacheWithMeta
|
| 25 |
+
import { setCache, getCacheWithMeta, readListCache, isEmptyListRefreshWithExistingData, createPaginationLoader, lazyLoadImages, renderListLoadError, renderListSearchMessage } from "../components/性能优化工具.js";
|
| 26 |
import { applyViewportAnimations, stopViewportAnimations } from "../components/动画音效引擎.js";
|
| 27 |
import { t, tIfExists } from "../components/用户体验增强.js";
|
| 28 |
import { API, CACHE, getCachedProfile, getProfileWithSWR, normalizeProfileImageUrl } from "../core/全局配置.js";
|
| 29 |
import { escapeHtml, formatTime, getCacheTTL } from "../components/互动工具函数.js"; // 🧹 P2归一:局部副本已移除
|
| 30 |
|
| 31 |
// 缓存配置
|
| 32 |
+
const CACHE_KEY_PREFIX = CACHE.PUBLIC_LISTS.prompts.prefix.slice(0, -1);
|
| 33 |
const CATEGORIES_CACHE_PREFIX = "PromptsCategoriesCache"; // 📴 分类缓存键前缀(离线容灾)
|
| 34 |
const PAGE_SIZE = 20;
|
| 35 |
|
|
|
|
| 280 |
const loadMorePrompts = async (page, pageSize, { isCurrent = () => true } = {}) => {
|
| 281 |
if (disposed || !isCurrent()) return [];
|
| 282 |
try {
|
| 283 |
+
const readResponse = captureListRead(promptListCacheKey(currentType, currentCategory, currentSort));
|
| 284 |
const res = await api.getPrompts(currentType, currentCategory, page, pageSize, currentSort);
|
| 285 |
if (disposed || !isCurrent()) return [];
|
| 286 |
+
const responseData = readResponse(res);
|
| 287 |
+
const prompts = responseData.data;
|
| 288 |
|
| 289 |
if (prompts.length === 0) {
|
| 290 |
if (searchKeyword && !allPromptsData.some(prompt => _matchesSearch(prompt, searchKeyword))) {
|
|
|
|
| 472 |
|
| 473 |
requestGeneration = ++networkRequestGeneration;
|
| 474 |
isLoadingFromNetwork = true;
|
| 475 |
+
const readResponse = captureListRead(cacheKey);
|
| 476 |
const res = await api.getPrompts(requestedType, requestedCategory, page, PAGE_SIZE, requestedSort);
|
| 477 |
if (viewGeneration !== listViewGeneration || requestGeneration !== networkRequestGeneration) {
|
| 478 |
if (requestGeneration === networkRequestGeneration) {
|
|
|
|
| 481 |
}
|
| 482 |
return;
|
| 483 |
}
|
| 484 |
+
const responseData = readResponse(res);
|
| 485 |
+
const prompts = responseData.data;
|
| 486 |
if (page === 1) firstPageReady = true;
|
| 487 |
const total = res.total || 0;
|
| 488 |
isLoadingFromNetwork = false;
|
|
|
|
| 490 |
// 缓存第一页数据
|
| 491 |
if (page === 1) {
|
| 492 |
allPromptsData = proxyImages(prompts);
|
| 493 |
+
responseData.write(getCacheTTL()); // 存原始数据,离线时经图片代理后可展示
|
| 494 |
} else {
|
| 495 |
allPromptsData = [...allPromptsData, ...proxyImages(prompts)];
|
| 496 |
}
|
|
|
|
| 564 |
renderPromptsFromCache(allPromptsData);
|
| 565 |
showToast(t('prompt.network_cache'), "warning");
|
| 566 |
} else {
|
| 567 |
+
renderListLoadError(promptsGrid, err?.name === 'ListCacheChangedError' ? t('feedback.list_changed_retry') : t('prompt.load_failed'), () => loadPrompts(1, false), t, err);
|
| 568 |
}
|
| 569 |
}
|
| 570 |
}
|
|
|
|
| 631 |
const requestGeneration = ++networkRequestGeneration;
|
| 632 |
try {
|
| 633 |
isLoadingFromNetwork = true;
|
| 634 |
+
const readResponse = captureListRead(cacheKey);
|
| 635 |
const res = await api.getPrompts(requestedType, requestedCategory, 1, PAGE_SIZE, requestedSort, {
|
| 636 |
noCache: true,
|
| 637 |
timeout: API.BACKGROUND_TIMEOUT,
|
|
|
|
| 644 |
}
|
| 645 |
return;
|
| 646 |
}
|
| 647 |
+
const responseData = readResponse(res);
|
| 648 |
+
const prompts = responseData.data;
|
| 649 |
isLoadingFromNetwork = false;
|
| 650 |
|
| 651 |
if (isEmptyListRefreshWithExistingData(prompts, allPromptsData)) {
|
| 652 |
console.warn(`🛡️ 提示词 ${requestedType}/${requestedSort} 后台返回空列表,保留当前数据与缓存`);
|
| 653 |
} else {
|
| 654 |
// 更新缓存
|
| 655 |
+
responseData.write(getCacheTTL());
|
| 656 |
|
| 657 |
// 对比新旧数据,有变化时重新渲染
|
| 658 |
if (_promptsDataChanged(allPromptsData, prompts)) {
|
前端页面/task/任务榜组件.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
// 前端页面/task/任务榜组件.js
|
| 2 |
// ==========================================
|
| 3 |
// 📝 任务榜组件(任务列表视图)
|
|
@@ -20,14 +21,14 @@ import { api, captureRequestSession } from "../core/网络请求API.js";
|
|
| 20 |
import { getTaskApplicantCount } from "./任务数据工具.js";
|
| 21 |
import { proxyImages } from "../core/网络请求API.js";
|
| 22 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 23 |
-
import { readListCache,
|
| 24 |
import { applyViewportAnimations, stopViewportAnimations } from "../components/动画音效引擎.js";
|
| 25 |
import { t } from "../components/用户体验增强.js";
|
| 26 |
import { API, CACHE, getCachedProfile, getProfileWithSWR, normalizeProfileImageUrl } from "../core/全局配置.js";
|
| 27 |
import { escapeHtml, formatTime, getCacheTTL } from "../components/互动工具函数.js"; // 🧹 P2归一:局部副本已移除
|
| 28 |
|
| 29 |
// 缓存配置
|
| 30 |
-
const CACHE_KEY_PREFIX =
|
| 31 |
const PAGE_SIZE = 20;
|
| 32 |
|
| 33 |
// 缓存当前用户
|
|
@@ -263,6 +264,7 @@ export function createTasksView(currentUser, keyword = "") {
|
|
| 263 |
requestGeneration = ++networkRequestGeneration;
|
| 264 |
isLoadingFromNetwork = true;
|
| 265 |
|
|
|
|
| 266 |
const res = await api.getTasks(page, PAGE_SIZE, requestedStatus || null, requestedSort);
|
| 267 |
if (viewGeneration !== listViewGeneration || requestGeneration !== networkRequestGeneration) {
|
| 268 |
if (requestGeneration === networkRequestGeneration) {
|
|
@@ -271,7 +273,8 @@ export function createTasksView(currentUser, keyword = "") {
|
|
| 271 |
}
|
| 272 |
return;
|
| 273 |
}
|
| 274 |
-
const
|
|
|
|
| 275 |
if (page === 1) firstPageReady = true;
|
| 276 |
const total = res.total || 0;
|
| 277 |
isLoadingFromNetwork = false;
|
|
@@ -279,7 +282,7 @@ export function createTasksView(currentUser, keyword = "") {
|
|
| 279 |
// 缓存第一页数据
|
| 280 |
if (page === 1) {
|
| 281 |
allTasksData = tasks;
|
| 282 |
-
|
| 283 |
} else {
|
| 284 |
allTasksData = [...allTasksData, ...tasks];
|
| 285 |
}
|
|
@@ -345,7 +348,7 @@ export function createTasksView(currentUser, keyword = "") {
|
|
| 345 |
showToast(t('task.network_cache'), "warning");
|
| 346 |
continueSearchPagination();
|
| 347 |
} else {
|
| 348 |
-
renderListLoadError(tasksList, t('task.load_failed'), () => loadTasks(1, false), t, err);
|
| 349 |
}
|
| 350 |
}
|
| 351 |
}
|
|
@@ -406,6 +409,7 @@ export function createTasksView(currentUser, keyword = "") {
|
|
| 406 |
const requestGeneration = ++networkRequestGeneration;
|
| 407 |
try {
|
| 408 |
isLoadingFromNetwork = true;
|
|
|
|
| 409 |
const res = await api.getTasks(1, PAGE_SIZE, requestedStatus || null, requestedSort, {
|
| 410 |
noCache: true,
|
| 411 |
timeout: API.BACKGROUND_TIMEOUT,
|
|
@@ -418,14 +422,15 @@ export function createTasksView(currentUser, keyword = "") {
|
|
| 418 |
}
|
| 419 |
return;
|
| 420 |
}
|
| 421 |
-
const
|
|
|
|
| 422 |
isLoadingFromNetwork = false;
|
| 423 |
|
| 424 |
if (isEmptyListRefreshWithExistingData(tasks, allTasksData)) {
|
| 425 |
console.warn(`🛡️ 任务榜 ${requestedSort} 后台返回空列表,保留当前数据与缓存`);
|
| 426 |
} else {
|
| 427 |
// 更新缓存
|
| 428 |
-
|
| 429 |
|
| 430 |
// 对比新旧数据,有变化时重新渲染
|
| 431 |
if (_tasksDataChanged(allTasksData, tasks)) {
|
|
@@ -498,9 +503,11 @@ export function createTasksView(currentUser, keyword = "") {
|
|
| 498 |
loadMore: async (page, pageSize, { isCurrent = () => true } = {}) => {
|
| 499 |
if (disposed || !isCurrent()) return [];
|
| 500 |
try {
|
|
|
|
| 501 |
const res = await api.getTasks(page, pageSize, currentStatus || null, currentSort);
|
| 502 |
if (disposed || !isCurrent()) return [];
|
| 503 |
-
const
|
|
|
|
| 504 |
if (tasks.length === 0) {
|
| 505 |
if (searchKeyword && !allTasksData.some(task => _filterTasksByKeyword([task], searchKeyword).length)) {
|
| 506 |
renderListSearchMessage(tasksList, t('pagination.search_empty'));
|
|
|
|
| 1 |
+
import { captureListRead } from "../core/榜单数据来源.js";
|
| 2 |
// 前端页面/task/任务榜组件.js
|
| 3 |
// ==========================================
|
| 4 |
// 📝 任务榜组件(任务列表视图)
|
|
|
|
| 21 |
import { getTaskApplicantCount } from "./任务数据工具.js";
|
| 22 |
import { proxyImages } from "../core/网络请求API.js";
|
| 23 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 24 |
+
import { readListCache, isEmptyListRefreshWithExistingData, createSkeleton, createPaginationLoader, lazyLoadImages, renderListLoadError, renderListSearchMessage } from "../components/性能优化工具.js";
|
| 25 |
import { applyViewportAnimations, stopViewportAnimations } from "../components/动画音效引擎.js";
|
| 26 |
import { t } from "../components/用户体验增强.js";
|
| 27 |
import { API, CACHE, getCachedProfile, getProfileWithSWR, normalizeProfileImageUrl } from "../core/全局配置.js";
|
| 28 |
import { escapeHtml, formatTime, getCacheTTL } from "../components/互动工具函数.js"; // 🧹 P2归一:局部副本已移除
|
| 29 |
|
| 30 |
// 缓存配置
|
| 31 |
+
const CACHE_KEY_PREFIX = CACHE.PUBLIC_LISTS.tasks.prefix.slice(0, -1);
|
| 32 |
const PAGE_SIZE = 20;
|
| 33 |
|
| 34 |
// 缓存当前用户
|
|
|
|
| 264 |
requestGeneration = ++networkRequestGeneration;
|
| 265 |
isLoadingFromNetwork = true;
|
| 266 |
|
| 267 |
+
const readResponse = captureListRead(cacheKey);
|
| 268 |
const res = await api.getTasks(page, PAGE_SIZE, requestedStatus || null, requestedSort);
|
| 269 |
if (viewGeneration !== listViewGeneration || requestGeneration !== networkRequestGeneration) {
|
| 270 |
if (requestGeneration === networkRequestGeneration) {
|
|
|
|
| 273 |
}
|
| 274 |
return;
|
| 275 |
}
|
| 276 |
+
const responseData = readResponse(res);
|
| 277 |
+
const tasks = responseData.data;
|
| 278 |
if (page === 1) firstPageReady = true;
|
| 279 |
const total = res.total || 0;
|
| 280 |
isLoadingFromNetwork = false;
|
|
|
|
| 282 |
// 缓存第一页数据
|
| 283 |
if (page === 1) {
|
| 284 |
allTasksData = tasks;
|
| 285 |
+
responseData.write(getCacheTTL()); // 持久化到 localStorage,供离线容灾
|
| 286 |
} else {
|
| 287 |
allTasksData = [...allTasksData, ...tasks];
|
| 288 |
}
|
|
|
|
| 348 |
showToast(t('task.network_cache'), "warning");
|
| 349 |
continueSearchPagination();
|
| 350 |
} else {
|
| 351 |
+
renderListLoadError(tasksList, err?.name === 'ListCacheChangedError' ? t('feedback.list_changed_retry') : t('task.load_failed'), () => loadTasks(1, false), t, err);
|
| 352 |
}
|
| 353 |
}
|
| 354 |
}
|
|
|
|
| 409 |
const requestGeneration = ++networkRequestGeneration;
|
| 410 |
try {
|
| 411 |
isLoadingFromNetwork = true;
|
| 412 |
+
const readResponse = captureListRead(cacheKey);
|
| 413 |
const res = await api.getTasks(1, PAGE_SIZE, requestedStatus || null, requestedSort, {
|
| 414 |
noCache: true,
|
| 415 |
timeout: API.BACKGROUND_TIMEOUT,
|
|
|
|
| 422 |
}
|
| 423 |
return;
|
| 424 |
}
|
| 425 |
+
const responseData = readResponse(res);
|
| 426 |
+
const tasks = responseData.data;
|
| 427 |
isLoadingFromNetwork = false;
|
| 428 |
|
| 429 |
if (isEmptyListRefreshWithExistingData(tasks, allTasksData)) {
|
| 430 |
console.warn(`🛡️ 任务榜 ${requestedSort} 后台返回空列表,保留当前数据与缓存`);
|
| 431 |
} else {
|
| 432 |
// 更新缓存
|
| 433 |
+
responseData.write(getCacheTTL());
|
| 434 |
|
| 435 |
// 对比新旧数据,有变化时重新渲染
|
| 436 |
if (_tasksDataChanged(allTasksData, tasks)) {
|
|
|
|
| 503 |
loadMore: async (page, pageSize, { isCurrent = () => true } = {}) => {
|
| 504 |
if (disposed || !isCurrent()) return [];
|
| 505 |
try {
|
| 506 |
+
const readResponse = captureListRead(`${CACHE_KEY_PREFIX}_${currentStatus}_${currentSort}`);
|
| 507 |
const res = await api.getTasks(page, pageSize, currentStatus || null, currentSort);
|
| 508 |
if (disposed || !isCurrent()) return [];
|
| 509 |
+
const responseData = readResponse(res);
|
| 510 |
+
const tasks = responseData.data;
|
| 511 |
if (tasks.length === 0) {
|
| 512 |
if (searchKeyword && !allTasksData.some(task => _filterTasksByKeyword([task], searchKeyword).length)) {
|
| 513 |
renderListSearchMessage(tasksList, t('pagination.search_empty'));
|