Spaces:
Running
Running
Upload 103 files
Browse files- 前端页面/components/互动工具函数.js +4 -7
- 前端页面/components/动画音效引擎.js +45 -19
- 前端页面/components/图片展示测试.js +2 -2
- 前端页面/components/图片沙盒组件.js +1 -1
- 前端页面/components/性能优化工具.js +42 -2
- 前端页面/components/用户体验_国际化.js +14 -0
- 前端页面/core/侧边栏主程序.js +3 -3
- 前端页面/core/侧边栏数据引擎.js +2 -2
- 前端页面/core/全局配置.js +3 -1
- 前端页面/core/网络请求_基础设施.js +29 -20
- 前端页面/core/网络请求_缓存管理.js +17 -1
- 前端页面/post/讨论区组件.js +1 -1
- 前端页面/profile/个人中心视图.js +9 -9
- 前端页面/profile/个人列表组件.js +3 -3
- 前端页面/prompt/提示词组件.js +1 -1
- 前端页面/social/私信聊天组件.js +14 -4
- 前端页面/styles/主题基础.css +25 -0
- 前端页面/task/任务榜组件.js +1 -1
前端页面/components/互动工具函数.js
CHANGED
|
@@ -43,20 +43,20 @@ export async function recordView(apiMethod, contentId, cachePrefix, onSuccess =
|
|
| 43 |
return null;
|
| 44 |
}
|
| 45 |
|
| 46 |
-
DEBUG && console.warn(
|
| 47 |
const now = Date.now();
|
| 48 |
const cacheKey = `${cachePrefix}_${contentId}`;
|
| 49 |
const cached = _viewRecordCache[cacheKey];
|
| 50 |
|
| 51 |
// 检查是否在防抖/冷却时间内(成功5分钟,失败30秒)
|
| 52 |
if (cached && now - cached.time < (cached.success ? VIEW_DEBOUNCE_MS : VIEW_ERROR_COOLDOWN_MS)) {
|
| 53 |
-
DEBUG && console.warn(
|
| 54 |
return null; // 防抖中,跳过
|
| 55 |
}
|
| 56 |
|
| 57 |
try {
|
| 58 |
const res = await apiMethod(contentId);
|
| 59 |
-
DEBUG && console.warn(
|
| 60 |
if (res.status === "success") {
|
| 61 |
// 成功后记录时间戳和成功状态
|
| 62 |
_viewRecordCache[cacheKey] = { time: now, success: true };
|
|
@@ -66,10 +66,7 @@ export async function recordView(apiMethod, contentId, cachePrefix, onSuccess =
|
|
| 66 |
} catch (err) {
|
| 67 |
// 失败时记录较短冷却时间,网络恢复后可快速重试
|
| 68 |
_viewRecordCache[cacheKey] = { time: now, success: false };
|
| 69 |
-
|
| 70 |
-
DEBUG && console.warn(`[浏览量调试] 记录${cachePrefix}浏览量失败:`, err.message || err);
|
| 71 |
-
DEBUG && console.warn(`[浏览量调试] 请求参数 - contentId: ${contentId}, cachePrefix: ${cachePrefix}`);
|
| 72 |
-
DEBUG && console.warn(`[浏览量调试] 错误详情:`, err);
|
| 73 |
}
|
| 74 |
return null;
|
| 75 |
}
|
|
|
|
| 43 |
return null;
|
| 44 |
}
|
| 45 |
|
| 46 |
+
DEBUG && console.warn('浏览量记录开始');
|
| 47 |
const now = Date.now();
|
| 48 |
const cacheKey = `${cachePrefix}_${contentId}`;
|
| 49 |
const cached = _viewRecordCache[cacheKey];
|
| 50 |
|
| 51 |
// 检查是否在防抖/冷却时间内(成功5分钟,失败30秒)
|
| 52 |
if (cached && now - cached.time < (cached.success ? VIEW_DEBOUNCE_MS : VIEW_ERROR_COOLDOWN_MS)) {
|
| 53 |
+
DEBUG && console.warn('浏览量记录处于冷却期');
|
| 54 |
return null; // 防抖中,跳过
|
| 55 |
}
|
| 56 |
|
| 57 |
try {
|
| 58 |
const res = await apiMethod(contentId);
|
| 59 |
+
DEBUG && console.warn('浏览量记录收到响应');
|
| 60 |
if (res.status === "success") {
|
| 61 |
// 成功后记录时间戳和成功状态
|
| 62 |
_viewRecordCache[cacheKey] = { time: now, success: true };
|
|
|
|
| 66 |
} catch (err) {
|
| 67 |
// 失败时记录较短冷却时间,网络恢复后可快速重试
|
| 68 |
_viewRecordCache[cacheKey] = { time: now, success: false };
|
| 69 |
+
DEBUG && console.warn('浏览量记录失败');
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
return null;
|
| 72 |
}
|
前端页面/components/动画音效引擎.js
CHANGED
|
@@ -41,22 +41,47 @@ const SOUND_CONFIGS = {
|
|
| 41 |
|
| 42 |
// 通用音效合成播放函数
|
| 43 |
function _playSynthSound(ctx, config) {
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
const
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
-
gainNode.gain.exponentialRampToValueAtTime(0.001, now + dur);
|
| 58 |
-
oscillator.start(now);
|
| 59 |
-
oscillator.stop(now + dur);
|
| 60 |
}
|
| 61 |
|
| 62 |
// 🔧 懒加载 AudioContext(确保在用户交互后创建)
|
|
@@ -65,7 +90,7 @@ function getAudioContext() {
|
|
| 65 |
try {
|
| 66 |
audioContext = new AudioContext();
|
| 67 |
} catch (e) {
|
| 68 |
-
console.warn('AudioContext 创建失败
|
| 69 |
return null;
|
| 70 |
}
|
| 71 |
}
|
|
@@ -79,7 +104,8 @@ async function resumeAudioContext() {
|
|
| 79 |
try {
|
| 80 |
await ctx.resume();
|
| 81 |
} catch (e) {
|
| 82 |
-
console.warn('AudioContext resume 失败
|
|
|
|
| 83 |
}
|
| 84 |
}
|
| 85 |
return ctx;
|
|
@@ -107,7 +133,7 @@ export async function playSound(type = 'whoosh') {
|
|
| 107 |
break;
|
| 108 |
}
|
| 109 |
} catch (e) {
|
| 110 |
-
console.warn('音效播放失败
|
| 111 |
}
|
| 112 |
}
|
| 113 |
|
|
@@ -529,7 +555,7 @@ export function initAnimationEngine() {
|
|
| 529 |
|
| 530 |
// 🔒 在用户首次交互时预热 AudioContext
|
| 531 |
const warmupAudio = () => {
|
| 532 |
-
resumeAudioContext();
|
| 533 |
document.removeEventListener('click', warmupAudio);
|
| 534 |
document.removeEventListener('keydown', warmupAudio);
|
| 535 |
};
|
|
|
|
| 41 |
|
| 42 |
// 通用音效合成播放函数
|
| 43 |
function _playSynthSound(ctx, config) {
|
| 44 |
+
let oscillator = null;
|
| 45 |
+
let gainNode = null;
|
| 46 |
+
let started = false;
|
| 47 |
+
let released = false;
|
| 48 |
+
const release = () => {
|
| 49 |
+
if (released) return;
|
| 50 |
+
released = true;
|
| 51 |
+
if (oscillator) {
|
| 52 |
+
oscillator.onended = null;
|
| 53 |
+
try { oscillator.disconnect(); } catch {}
|
| 54 |
+
}
|
| 55 |
+
if (gainNode) {
|
| 56 |
+
try { gainNode.disconnect(); } catch {}
|
| 57 |
+
}
|
| 58 |
+
};
|
| 59 |
+
try {
|
| 60 |
+
oscillator = ctx.createOscillator();
|
| 61 |
+
gainNode = ctx.createGain();
|
| 62 |
+
oscillator.onended = release;
|
| 63 |
+
oscillator.connect(gainNode);
|
| 64 |
+
gainNode.connect(ctx.destination);
|
| 65 |
+
const now = ctx.currentTime;
|
| 66 |
+
const dur = config.stopTime || config.duration;
|
| 67 |
+
oscillator.type = config.type;
|
| 68 |
+
oscillator.frequency.setValueAtTime(config.freqStart, now);
|
| 69 |
+
oscillator.frequency.exponentialRampToValueAtTime(config.freqEnd, now + config.duration);
|
| 70 |
+
gainNode.gain.setValueAtTime(config.gain, now);
|
| 71 |
+
if (config.gainMid !== undefined) {
|
| 72 |
+
gainNode.gain.linearRampToValueAtTime(config.gainMid, now + config.gainMidTime);
|
| 73 |
+
}
|
| 74 |
+
gainNode.gain.exponentialRampToValueAtTime(0.001, now + dur);
|
| 75 |
+
oscillator.start(now);
|
| 76 |
+
started = true;
|
| 77 |
+
oscillator.stop(now + dur);
|
| 78 |
+
} catch (error) {
|
| 79 |
+
if (started) {
|
| 80 |
+
try { oscillator.stop(); } catch {}
|
| 81 |
+
}
|
| 82 |
+
release();
|
| 83 |
+
throw error;
|
| 84 |
}
|
|
|
|
|
|
|
|
|
|
| 85 |
}
|
| 86 |
|
| 87 |
// 🔧 懒加载 AudioContext(确保在用户交互后创建)
|
|
|
|
| 90 |
try {
|
| 91 |
audioContext = new AudioContext();
|
| 92 |
} catch (e) {
|
| 93 |
+
console.warn('AudioContext 创建失败');
|
| 94 |
return null;
|
| 95 |
}
|
| 96 |
}
|
|
|
|
| 104 |
try {
|
| 105 |
await ctx.resume();
|
| 106 |
} catch (e) {
|
| 107 |
+
console.warn('AudioContext resume 失败');
|
| 108 |
+
return null;
|
| 109 |
}
|
| 110 |
}
|
| 111 |
return ctx;
|
|
|
|
| 133 |
break;
|
| 134 |
}
|
| 135 |
} catch (e) {
|
| 136 |
+
console.warn('音效播放失败');
|
| 137 |
}
|
| 138 |
}
|
| 139 |
|
|
|
|
| 555 |
|
| 556 |
// 🔒 在用户首次交互时预热 AudioContext
|
| 557 |
const warmupAudio = () => {
|
| 558 |
+
if (getSettings().enableSoundEffects) resumeAudioContext();
|
| 559 |
document.removeEventListener('click', warmupAudio);
|
| 560 |
document.removeEventListener('keydown', warmupAudio);
|
| 561 |
};
|
前端页面/components/图片展示测试.js
CHANGED
|
@@ -879,7 +879,7 @@ class CyberImageViewer {
|
|
| 879 |
img.src = url;
|
| 880 |
this._onImageLoaded();
|
| 881 |
} catch (err) {
|
| 882 |
-
console.error('图片加载失败
|
| 883 |
this._onImageError();
|
| 884 |
}
|
| 885 |
}
|
|
@@ -905,7 +905,7 @@ class CyberImageViewer {
|
|
| 905 |
resolve(img);
|
| 906 |
};
|
| 907 |
img.onerror = (err) => {
|
| 908 |
-
console.error('❌ 图片加载失败
|
| 909 |
reject(new Error('图片加载失败'));
|
| 910 |
};
|
| 911 |
img.src = url;
|
|
|
|
| 879 |
img.src = url;
|
| 880 |
this._onImageLoaded();
|
| 881 |
} catch (err) {
|
| 882 |
+
console.error('图片加载失败');
|
| 883 |
this._onImageError();
|
| 884 |
}
|
| 885 |
}
|
|
|
|
| 905 |
resolve(img);
|
| 906 |
};
|
| 907 |
img.onerror = (err) => {
|
| 908 |
+
console.error('❌ 图片加载失败');
|
| 909 |
reject(new Error('图片加载失败'));
|
| 910 |
};
|
| 911 |
img.src = url;
|
前端页面/components/图片沙盒组件.js
CHANGED
|
@@ -269,7 +269,7 @@ export function setupImageSandboxEvents(detailView) {
|
|
| 269 |
try {
|
| 270 |
openImageViewer(imageUrls, currentIndex, { opaque: true });
|
| 271 |
} catch (err) {
|
| 272 |
-
console.error('无法打开图像播放器
|
| 273 |
// 显示用户可见的错误提示
|
| 274 |
import('./UI交互提示组件.js').then(module => {
|
| 275 |
module.showToast(t('image.viewer.load_failed'), 'error');
|
|
|
|
| 269 |
try {
|
| 270 |
openImageViewer(imageUrls, currentIndex, { opaque: true });
|
| 271 |
} catch (err) {
|
| 272 |
+
console.error('无法打开图像播放器');
|
| 273 |
// 显示用户可见的错误提示
|
| 274 |
import('./UI交互提示组件.js').then(module => {
|
| 275 |
module.showToast(t('image.viewer.load_failed'), 'error');
|
前端页面/components/性能优化工具.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
| 15 |
// 🔒 P0安全优化:敏感数据脱敏
|
| 16 |
// ==========================================
|
| 17 |
|
| 18 |
-
import { PAGINATION } from "../core/全局配置.js";
|
| 19 |
// 📦 列表缓存工具需要本地可用的缓存实现;下方「向后兼容重导出」那行是纯转发、
|
| 20 |
// 不会生成本地绑定,故此处必须单独 import(勿误删,否则 readListCache 运行时报错)
|
| 21 |
import { setCache, getCacheWithMeta, removeCache, getCacheKeysByPrefix } from "../core/网络请求_缓存管理.js";
|
|
@@ -1392,7 +1392,7 @@ export function isEmptyListRefreshWithExistingData(nextItems, currentItems) {
|
|
| 1392 |
* 渲染可访问的列表首屏失败状态,并提供显式重试入口。
|
| 1393 |
* 使用 DOM/textContent,避免错误文案进入 HTML sink。
|
| 1394 |
*/
|
| 1395 |
-
export function renderListLoadError(container, message, onRetry, t) {
|
| 1396 |
if (!container) return;
|
| 1397 |
const status = document.createElement("div");
|
| 1398 |
status.className = "comfy-ranking-list-error";
|
|
@@ -1411,6 +1411,46 @@ export function renderListLoadError(container, message, onRetry, t) {
|
|
| 1411 |
|
| 1412 |
status.append(text, retry);
|
| 1413 |
container.replaceChildren(status);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1414 |
}
|
| 1415 |
|
| 1416 |
/**
|
|
|
|
| 15 |
// 🔒 P0安全优化:敏感数据脱敏
|
| 16 |
// ==========================================
|
| 17 |
|
| 18 |
+
import { API, PAGINATION } from "../core/全局配置.js";
|
| 19 |
// 📦 列表缓存工具需要本地可用的缓存实现;下方「向后兼容重导出」那行是纯转发、
|
| 20 |
// 不会生成本地绑定,故此处必须单独 import(勿误删,否则 readListCache 运行时报错)
|
| 21 |
import { setCache, getCacheWithMeta, removeCache, getCacheKeysByPrefix } from "../core/网络请求_缓存管理.js";
|
|
|
|
| 1392 |
* 渲染可访问的列表首屏失败状态,并提供显式重试入口。
|
| 1393 |
* 使用 DOM/textContent,避免错误文案进入 HTML sink。
|
| 1394 |
*/
|
| 1395 |
+
export function renderListLoadError(container, message, onRetry, t, error = null) {
|
| 1396 |
if (!container) return;
|
| 1397 |
const status = document.createElement("div");
|
| 1398 |
status.className = "comfy-ranking-list-error";
|
|
|
|
| 1411 |
|
| 1412 |
status.append(text, retry);
|
| 1413 |
container.replaceChildren(status);
|
| 1414 |
+
const requestId = error?.requestId;
|
| 1415 |
+
if (typeof requestId !== 'string' || !API.REQUEST_ID_PATTERN.test(requestId)) return;
|
| 1416 |
+
|
| 1417 |
+
const details = document.createElement('details');
|
| 1418 |
+
details.className = 'comfy-ranking-request-diagnostic';
|
| 1419 |
+
const summary = document.createElement('summary');
|
| 1420 |
+
summary.textContent = t('feedback.diagnostic_details');
|
| 1421 |
+
const label = document.createElement('label');
|
| 1422 |
+
label.className = 'comfy-ranking-request-diagnostic__label';
|
| 1423 |
+
label.textContent = t('feedback.request_id');
|
| 1424 |
+
const input = document.createElement('input');
|
| 1425 |
+
input.type = 'text'; input.readOnly = true; input.spellcheck = false;
|
| 1426 |
+
input.value = requestId;
|
| 1427 |
+
label.append(input);
|
| 1428 |
+
const copy = document.createElement('button');
|
| 1429 |
+
copy.type = 'button'; copy.textContent = t('common.copy');
|
| 1430 |
+
const feedback = document.createElement('p');
|
| 1431 |
+
feedback.className = 'comfy-ranking-request-diagnostic__feedback';
|
| 1432 |
+
feedback.setAttribute('role', 'status');
|
| 1433 |
+
feedback.setAttribute('aria-live', 'polite');
|
| 1434 |
+
let copying = false;
|
| 1435 |
+
copy.addEventListener('click', async () => {
|
| 1436 |
+
if (copying || !details.isConnected) return;
|
| 1437 |
+
copying = true; copy.setAttribute('aria-busy', 'true');
|
| 1438 |
+
try {
|
| 1439 |
+
if (typeof navigator.clipboard?.writeText !== 'function') throw new Error();
|
| 1440 |
+
await navigator.clipboard.writeText(requestId);
|
| 1441 |
+
if (details.isConnected) feedback.textContent = t('feedback.copied');
|
| 1442 |
+
} catch {
|
| 1443 |
+
if (details.isConnected) {
|
| 1444 |
+
feedback.textContent = t('feedback.copy_id_manually');
|
| 1445 |
+
if (document.activeElement === copy) { input.focus(); input.select(); }
|
| 1446 |
+
}
|
| 1447 |
+
} finally {
|
| 1448 |
+
copying = false;
|
| 1449 |
+
if (details.isConnected) copy.setAttribute('aria-busy', 'false');
|
| 1450 |
+
}
|
| 1451 |
+
});
|
| 1452 |
+
details.append(summary, label, copy, feedback);
|
| 1453 |
+
container.append(details);
|
| 1454 |
}
|
| 1455 |
|
| 1456 |
/**
|
前端页面/components/用户体验_国际化.js
CHANGED
|
@@ -536,6 +536,10 @@ const translations = {
|
|
| 536 |
'feedback.success': '操作成功',
|
| 537 |
'feedback.error': '操作失败,请重试',
|
| 538 |
'feedback.timeout': '请求超时',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
'feedback.response_format_error': '服务器响应格式错误',
|
| 540 |
'feedback.saved': '已保存',
|
| 541 |
'feedback.logged_out': '已退出登录',
|
|
@@ -757,6 +761,9 @@ const translations = {
|
|
| 757 |
'crop.processing': '处理中...',
|
| 758 |
'crop.process_failed': '处理失败,重试',
|
| 759 |
'feedback.processing': '处理中...',
|
|
|
|
|
|
|
|
|
|
| 760 |
'feedback.copied': '已复制到剪贴板',
|
| 761 |
'feedback.copy_failed': '复制失败',
|
| 762 |
'feedback.operation_success': '操作成功',
|
|
@@ -2254,6 +2261,10 @@ const translations = {
|
|
| 2254 |
'feedback.success': 'Success',
|
| 2255 |
'feedback.error': 'Failed, please retry',
|
| 2256 |
'feedback.timeout': 'Request timeout',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2257 |
'feedback.response_format_error': 'The server response format is invalid',
|
| 2258 |
'feedback.saved': 'Saved',
|
| 2259 |
'feedback.logged_out': 'Logged out',
|
|
@@ -2470,6 +2481,9 @@ const translations = {
|
|
| 2470 |
'crop.processing': 'Processing...',
|
| 2471 |
'crop.process_failed': 'Processing failed. Try again.',
|
| 2472 |
'feedback.processing': 'Processing...',
|
|
|
|
|
|
|
|
|
|
| 2473 |
'feedback.copied': 'Copied to clipboard',
|
| 2474 |
'feedback.copy_failed': 'Could not copy',
|
| 2475 |
'feedback.operation_success': 'Operation completed',
|
|
|
|
| 536 |
'feedback.success': '操作成功',
|
| 537 |
'feedback.error': '操作失败,请重试',
|
| 538 |
'feedback.timeout': '请求超时',
|
| 539 |
+
'feedback.network_timeout': '网络请求超时,请检查网络连接',
|
| 540 |
+
'feedback.network_unavailable': '网络连接失败,请检查网络',
|
| 541 |
+
'feedback.http_request_failed': '请求失败(HTTP {status})',
|
| 542 |
+
'feedback.invalid_request_data': '数据格式不正确,请检查填写内容。',
|
| 543 |
'feedback.response_format_error': '服务器响应格式错误',
|
| 544 |
'feedback.saved': '已保存',
|
| 545 |
'feedback.logged_out': '已退出登录',
|
|
|
|
| 761 |
'crop.processing': '处理中...',
|
| 762 |
'crop.process_failed': '处理失败,重试',
|
| 763 |
'feedback.processing': '处理中...',
|
| 764 |
+
'feedback.diagnostic_details': '诊断信息',
|
| 765 |
+
'feedback.request_id': '请求编号',
|
| 766 |
+
'feedback.copy_id_manually': '无法自动复制,请选择上方编号后手动复制。',
|
| 767 |
'feedback.copied': '已复制到剪贴板',
|
| 768 |
'feedback.copy_failed': '复制失败',
|
| 769 |
'feedback.operation_success': '操作成功',
|
|
|
|
| 2261 |
'feedback.success': 'Success',
|
| 2262 |
'feedback.error': 'Failed, please retry',
|
| 2263 |
'feedback.timeout': 'Request timeout',
|
| 2264 |
+
'feedback.network_timeout': 'The network request timed out. Check your connection.',
|
| 2265 |
+
'feedback.network_unavailable': 'Network connection failed. Check your connection.',
|
| 2266 |
+
'feedback.http_request_failed': 'Request failed (HTTP {status})',
|
| 2267 |
+
'feedback.invalid_request_data': 'The submitted data has an invalid format. Check the form.',
|
| 2268 |
'feedback.response_format_error': 'The server response format is invalid',
|
| 2269 |
'feedback.saved': 'Saved',
|
| 2270 |
'feedback.logged_out': 'Logged out',
|
|
|
|
| 2481 |
'crop.processing': 'Processing...',
|
| 2482 |
'crop.process_failed': 'Processing failed. Try again.',
|
| 2483 |
'feedback.processing': 'Processing...',
|
| 2484 |
+
'feedback.diagnostic_details': 'Diagnostic details',
|
| 2485 |
+
'feedback.request_id': 'Request ID',
|
| 2486 |
+
'feedback.copy_id_manually': 'Automatic copying is unavailable. Select the ID above and copy it manually.',
|
| 2487 |
'feedback.copied': 'Copied to clipboard',
|
| 2488 |
'feedback.copy_failed': 'Could not copy',
|
| 2489 |
'feedback.operation_success': 'Operation completed',
|
前端页面/core/侧边栏主程序.js
CHANGED
|
@@ -27,7 +27,7 @@ import { initUXEnhancements, t, getLanguage } from "../components/用户体验
|
|
| 27 |
try {
|
| 28 |
initUXEnhancements();
|
| 29 |
} catch (e) {
|
| 30 |
-
console.warn('🎯 UX 增强初始化失败
|
| 31 |
}
|
| 32 |
|
| 33 |
// 统一设计 tokens 与键盘焦点样式;样式表只挂载一次,保持 ComfyUI 主题变量适配。
|
|
@@ -79,7 +79,7 @@ const BackgroundStore = {
|
|
| 79 |
try {
|
| 80 |
localStorage.setItem(getBackgroundKey(), base64);
|
| 81 |
} catch (e) {
|
| 82 |
-
console.warn("⚠️ 界面背景保存失败(存储空间不足或不可用)
|
| 83 |
}
|
| 84 |
},
|
| 85 |
load() {
|
|
@@ -409,7 +409,7 @@ export function buildSidebarDOM() {
|
|
| 409 |
const view = createPublishTaskView(currentUser, taskData);
|
| 410 |
showInlineView(view);
|
| 411 |
} catch (err) {
|
| 412 |
-
console.error('创建编辑任务视图失败
|
| 413 |
}
|
| 414 |
});
|
| 415 |
|
|
|
|
| 27 |
try {
|
| 28 |
initUXEnhancements();
|
| 29 |
} catch (e) {
|
| 30 |
+
console.warn('🎯 UX 增强初始化失败');
|
| 31 |
}
|
| 32 |
|
| 33 |
// 统一设计 tokens 与键盘焦点样式;样式表只挂载一次,保持 ComfyUI 主题变量适配。
|
|
|
|
| 79 |
try {
|
| 80 |
localStorage.setItem(getBackgroundKey(), base64);
|
| 81 |
} catch (e) {
|
| 82 |
+
console.warn("⚠️ 界面背景保存失败(存储空间不足或不可用)");
|
| 83 |
}
|
| 84 |
},
|
| 85 |
load() {
|
|
|
|
| 409 |
const view = createPublishTaskView(currentUser, taskData);
|
| 410 |
showInlineView(view);
|
| 411 |
} catch (err) {
|
| 412 |
+
console.error('创建编辑任务视图失败');
|
| 413 |
}
|
| 414 |
});
|
| 415 |
|
前端页面/core/侧边栏数据引擎.js
CHANGED
|
@@ -650,7 +650,7 @@ export async function loadSidebarContent({
|
|
| 650 |
renderListLoadError(contentArea, t('common.network_error_retry'), () => loadSidebarContent({
|
| 651 |
tab, sort, keyword, contentArea, currentUser, renderToken, getRenderToken,
|
| 652 |
force: true, expandItemId, isNavigationCurrent
|
| 653 |
-
}), t);
|
| 654 |
return;
|
| 655 |
}
|
| 656 |
}
|
|
@@ -659,7 +659,7 @@ export async function loadSidebarContent({
|
|
| 659 |
renderListLoadError(contentArea, t('common.network_error_retry'), () => loadSidebarContent({
|
| 660 |
tab, sort, keyword, contentArea, currentUser, renderToken, getRenderToken,
|
| 661 |
force: true, expandItemId, isNavigationCurrent
|
| 662 |
-
}), t);
|
| 663 |
}
|
| 664 |
}
|
| 665 |
|
|
|
|
| 650 |
renderListLoadError(contentArea, t('common.network_error_retry'), () => loadSidebarContent({
|
| 651 |
tab, sort, keyword, contentArea, currentUser, renderToken, getRenderToken,
|
| 652 |
force: true, expandItemId, isNavigationCurrent
|
| 653 |
+
}), t, error);
|
| 654 |
return;
|
| 655 |
}
|
| 656 |
}
|
|
|
|
| 659 |
renderListLoadError(contentArea, t('common.network_error_retry'), () => loadSidebarContent({
|
| 660 |
tab, sort, keyword, contentArea, currentUser, renderToken, getRenderToken,
|
| 661 |
force: true, expandItemId, isNavigationCurrent
|
| 662 |
+
}), t, error);
|
| 663 |
}
|
| 664 |
}
|
| 665 |
|
前端页面/core/全局配置.js
CHANGED
|
@@ -18,8 +18,10 @@
|
|
| 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, 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, LOCAL_IMAGE_PROXY_PREFIX: string, COMPLETE_IMAGE_QUERY_SUFFIX: string, PRIVATE_GET_PATTERNS: RegExp[], PUBLIC_CONDITIONAL_GET_PATHS: string[], CONDITIONAL_ETAG_PATTERN: RegExp, ENDPOINT_ERROR_SCOPE_HEADER: string, ENDPOINT_ERROR_SCOPE_VALUE: string }} */
|
| 22 |
export const API = {
|
|
|
|
|
|
|
| 23 |
ENDPOINT_ERROR_SCOPE_HEADER: "X-Ranking-Error-Scope",
|
| 24 |
ENDPOINT_ERROR_SCOPE_VALUE: "endpoint",
|
| 25 |
// 仅公开榜单使用由正文派生的条件验证器,私有响应仍禁止留存。
|
|
|
|
| 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, 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, 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 }} */
|
| 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 |
// 仅公开榜单使用由正文派生的条件验证器,私有响应仍禁止留存。
|
前端页面/core/网络请求_基础设施.js
CHANGED
|
@@ -34,6 +34,26 @@ const BASE_URL = API.BASE_URL;
|
|
| 34 |
// ⚡ P1性能优化:请求去重(相同GET请求只发一次)
|
| 35 |
const pendingRequests = new Map();
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
// 🚀 P3优化:网络状态由 状态管理.js 的 isOnline() 统一提供(见下方离线模式支持),
|
| 38 |
// 本模块不再自行注册 online/offline 监听。
|
| 39 |
|
|
@@ -486,18 +506,7 @@ async function request(endpoint, options = {}) {
|
|
| 486 |
|
| 487 |
if (!response.ok && !result.validated) {
|
| 488 |
if (!responseData || typeof responseData !== "object" || Array.isArray(responseData)) responseData = {};
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
// 🚀 核心修改:增加对 FastAPI 422 报错数组的解析
|
| 492 |
-
if (Array.isArray(responseData.detail)) {
|
| 493 |
-
errorMsg = "数据格式错误: " + responseData.detail.map(e => `${e.loc[e.loc.length-1]} (${e.type})`).join(", ");
|
| 494 |
-
} else if (typeof responseData.detail === "string") {
|
| 495 |
-
errorMsg = responseData.detail;
|
| 496 |
-
} else if (responseData.message) {
|
| 497 |
-
errorMsg = responseData.message;
|
| 498 |
-
} else if (responseData.error) {
|
| 499 |
-
errorMsg = responseData.error;
|
| 500 |
-
}
|
| 501 |
|
| 502 |
// 🚀 P1优化:4xx 错误不重试,5xx 错误可重试
|
| 503 |
if (response.status >= 400 && response.status < 500) {
|
|
@@ -512,7 +521,7 @@ async function request(endpoint, options = {}) {
|
|
| 512 |
window.dispatchEvent(new CustomEvent('comfy-ranking-auth-expired'));
|
| 513 |
}
|
| 514 |
}
|
| 515 |
-
const error = new Error(errorMsg);
|
| 516 |
error.status = response.status;
|
| 517 |
if (typeof responseData.detail === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(responseData.detail)) error.code = responseData.detail;
|
| 518 |
throw error;
|
|
@@ -521,7 +530,7 @@ async function request(endpoint, options = {}) {
|
|
| 521 |
// 应用明确标记且正文可读的503只影响当前接口,仍允许其它榜单联网。
|
| 522 |
if (response.status === 503 && result.bodyConfirmed &&
|
| 523 |
response.headers?.get?.(API.ENDPOINT_ERROR_SCOPE_HEADER) === API.ENDPOINT_ERROR_SCOPE_VALUE) {
|
| 524 |
-
lastError = new Error(errorMsg);
|
| 525 |
lastError.status = response.status;
|
| 526 |
networkLevelFailure = false;
|
| 527 |
break;
|
|
@@ -532,14 +541,14 @@ async function request(endpoint, options = {}) {
|
|
| 532 |
// 与超时/连接失败一样共享冷却,避免切换榜单时每个 endpoint
|
| 533 |
// 都重复访问一次;普通 500 仍只视为单接口服务端错误。
|
| 534 |
if ([502, 503, 504].includes(response.status)) {
|
| 535 |
-
lastError = new Error(errorMsg);
|
| 536 |
lastError.status = response.status;
|
| 537 |
networkLevelFailure = true;
|
| 538 |
markCloudDown(endpoint);
|
| 539 |
break;
|
| 540 |
}
|
| 541 |
|
| 542 |
-
lastError = new Error(errorMsg);
|
| 543 |
lastError.status = response.status;
|
| 544 |
continue; // 5xx 错误重试
|
| 545 |
}
|
|
@@ -606,8 +615,8 @@ async function request(endpoint, options = {}) {
|
|
| 606 |
// 让 GET 请求走过期缓存回退;无缓存时仍以该友好错误抛出。
|
| 607 |
const isTimeout = error.name === 'AbortError';
|
| 608 |
lastError = isTimeout
|
| 609 |
-
? new Error('
|
| 610 |
-
: new Error('
|
| 611 |
lastError.code = isTimeout ? 'network_timeout' : 'network_unavailable';
|
| 612 |
if (fetchStarted) networkLevelFailure = true;
|
| 613 |
// ☁️ 连接层失败(被墙 / DNS 污染 / 拒绝连接 / 证书错误)几乎不会是偶发抖动,
|
|
@@ -643,7 +652,7 @@ async function request(endpoint, options = {}) {
|
|
| 643 |
}
|
| 644 |
}
|
| 645 |
// 仍无缓存,才抛异常
|
| 646 |
-
throw lastError || new Error('
|
| 647 |
})();
|
| 648 |
|
| 649 |
if (privateEntry) requestPromise.finally(() => activePrivateReads.delete(privateEntry)).catch(() => {});
|
|
@@ -710,7 +719,7 @@ export async function requestSSE(endpoint, body, onProgress, options = {}) {
|
|
| 710 |
});
|
| 711 |
|
| 712 |
if (!response.ok) {
|
| 713 |
-
const error = new Error(
|
| 714 |
error.status = response.status;
|
| 715 |
// 仅当流式路由明确不存在/不接受该方法时,调用方可安全回退到旧端点。
|
| 716 |
// 网络错误、超时和服务端错误都可能发生在操作已开始后,不可自动重发。
|
|
|
|
| 34 |
// ⚡ P1性能优化:请求去重(相同GET请求只发一次)
|
| 35 |
const pendingRequests = new Map();
|
| 36 |
|
| 37 |
+
function withRequestDiagnostic(error, response, data = null) {
|
| 38 |
+
let headerId;
|
| 39 |
+
try { headerId = response.headers?.get?.(API.REQUEST_ID_HEADER); } catch {}
|
| 40 |
+
for (const value of [headerId, data?.error_id]) {
|
| 41 |
+
if (typeof value === 'string' && API.REQUEST_ID_PATTERN.test(value)) {
|
| 42 |
+
error.requestId = value;
|
| 43 |
+
break;
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
return error;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
function httpErrorMessage(data, status) {
|
| 50 |
+
if (Array.isArray(data.detail)) return t('feedback.invalid_request_data');
|
| 51 |
+
for (const value of [data.detail, data.message, data.error]) {
|
| 52 |
+
if (typeof value === 'string' && value.trim()) return value;
|
| 53 |
+
}
|
| 54 |
+
return t('feedback.http_request_failed', { status });
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
// 🚀 P3优化:网络状态由 状态管理.js 的 isOnline() 统一提供(见下方离线模式支持),
|
| 58 |
// 本模块不再自行注册 online/offline 监听。
|
| 59 |
|
|
|
|
| 506 |
|
| 507 |
if (!response.ok && !result.validated) {
|
| 508 |
if (!responseData || typeof responseData !== "object" || Array.isArray(responseData)) responseData = {};
|
| 509 |
+
const errorMsg = httpErrorMessage(responseData, response.status);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
|
| 511 |
// 🚀 P1优化:4xx 错误不重试,5xx 错误可重试
|
| 512 |
if (response.status >= 400 && response.status < 500) {
|
|
|
|
| 521 |
window.dispatchEvent(new CustomEvent('comfy-ranking-auth-expired'));
|
| 522 |
}
|
| 523 |
}
|
| 524 |
+
const error = withRequestDiagnostic(new Error(errorMsg), response, responseData);
|
| 525 |
error.status = response.status;
|
| 526 |
if (typeof responseData.detail === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(responseData.detail)) error.code = responseData.detail;
|
| 527 |
throw error;
|
|
|
|
| 530 |
// 应用明确标记且正文可读的503只影响当前接口,仍允许其它榜单联网。
|
| 531 |
if (response.status === 503 && result.bodyConfirmed &&
|
| 532 |
response.headers?.get?.(API.ENDPOINT_ERROR_SCOPE_HEADER) === API.ENDPOINT_ERROR_SCOPE_VALUE) {
|
| 533 |
+
lastError = withRequestDiagnostic(new Error(errorMsg), response, responseData);
|
| 534 |
lastError.status = response.status;
|
| 535 |
networkLevelFailure = false;
|
| 536 |
break;
|
|
|
|
| 541 |
// 与超时/连接失败一样共享冷却,避免切换榜单时每个 endpoint
|
| 542 |
// 都重复访问一次;普通 500 仍只视为单接口服务端错误。
|
| 543 |
if ([502, 503, 504].includes(response.status)) {
|
| 544 |
+
lastError = withRequestDiagnostic(new Error(errorMsg), response, responseData);
|
| 545 |
lastError.status = response.status;
|
| 546 |
networkLevelFailure = true;
|
| 547 |
markCloudDown(endpoint);
|
| 548 |
break;
|
| 549 |
}
|
| 550 |
|
| 551 |
+
lastError = withRequestDiagnostic(new Error(errorMsg), response, responseData);
|
| 552 |
lastError.status = response.status;
|
| 553 |
continue; // 5xx 错误重试
|
| 554 |
}
|
|
|
|
| 615 |
// 让 GET 请求走过期缓存回退;无缓存时仍以该友好错误抛出。
|
| 616 |
const isTimeout = error.name === 'AbortError';
|
| 617 |
lastError = isTimeout
|
| 618 |
+
? new Error(t('feedback.network_timeout'))
|
| 619 |
+
: new Error(t('feedback.network_unavailable'));
|
| 620 |
lastError.code = isTimeout ? 'network_timeout' : 'network_unavailable';
|
| 621 |
if (fetchStarted) networkLevelFailure = true;
|
| 622 |
// ☁️ 连接层失败(被墙 / DNS 污染 / 拒绝连接 / 证书错误)几乎不会是偶发抖动,
|
|
|
|
| 652 |
}
|
| 653 |
}
|
| 654 |
// 仍无缓存,才抛异常
|
| 655 |
+
throw lastError || new Error(t('feedback.error'));
|
| 656 |
})();
|
| 657 |
|
| 658 |
if (privateEntry) requestPromise.finally(() => activePrivateReads.delete(privateEntry)).catch(() => {});
|
|
|
|
| 719 |
});
|
| 720 |
|
| 721 |
if (!response.ok) {
|
| 722 |
+
const error = withRequestDiagnostic(new Error(t('feedback.http_request_failed', { status: response.status })), response);
|
| 723 |
error.status = response.status;
|
| 724 |
// 仅当流式路由明确不存在/不接受该方法时,调用方可安全回退到旧端点。
|
| 725 |
// 网络错误、超时和服务端错误都可能发生在操作已开始后,不可自动重发。
|
前端页面/core/网络请求_缓存管理.js
CHANGED
|
@@ -31,6 +31,7 @@ let lastStorageBudgetSweepAt = 0;
|
|
| 31 |
// 内存缓存(最快)
|
| 32 |
const memoryCache = new Map();
|
| 33 |
const memoryCacheOrder = []; // LRU 顺序追踪
|
|
|
|
| 34 |
|
| 35 |
/**
|
| 36 |
* 生成完整缓存键(带前缀)
|
|
@@ -118,6 +119,7 @@ export function setCache(key, value, ttl = DEFAULT_TTL, persist = true, metadata
|
|
| 118 |
const fk = _fullKey(key);
|
| 119 |
const expireAt = Date.now() + ttl;
|
| 120 |
const cacheData = { schemaVersion: CACHE_SCHEMA_VERSION, value, expireAt, lastAccessAt: Date.now() };
|
|
|
|
| 121 |
if (typeof metadata?.etag === "string" && API.CONDITIONAL_ETAG_PATTERN.test(metadata.etag)) cacheData.etag = metadata.etag;
|
| 122 |
|
| 123 |
// 内存缓存
|
|
@@ -127,7 +129,17 @@ export function setCache(key, value, ttl = DEFAULT_TTL, persist = true, metadata
|
|
| 127 |
|
| 128 |
// 持久化缓存
|
| 129 |
if (persist) {
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
let previousBytes = 0;
|
| 132 |
let previousLengthKnown = false;
|
| 133 |
try {
|
|
@@ -136,6 +148,7 @@ export function setCache(key, value, ttl = DEFAULT_TTL, persist = true, metadata
|
|
| 136 |
previousLengthKnown = true;
|
| 137 |
} catch {}
|
| 138 |
if (_safeStorageWrite(fk, jsonStr)) {
|
|
|
|
| 139 |
if (storageUsageBytes !== null && previousLengthKnown) {
|
| 140 |
storageUsageBytes += (fk.length + jsonStr.length) * 2 - previousBytes;
|
| 141 |
} else {
|
|
@@ -181,6 +194,7 @@ export function getCache(key) {
|
|
| 181 |
// 回填内存缓存
|
| 182 |
memoryCache.set(fk, cached);
|
| 183 |
_updateLRU(fk);
|
|
|
|
| 184 |
_touchStorageEntry(fk, cached);
|
| 185 |
return cached.value;
|
| 186 |
}
|
|
@@ -235,6 +249,7 @@ export function getCacheWithMeta(key, ignoreExpiry = false) {
|
|
| 235 |
// 回填内存缓存
|
| 236 |
memoryCache.set(fk, cached);
|
| 237 |
_updateLRU(fk);
|
|
|
|
| 238 |
_touchStorageEntry(fk, cached);
|
| 239 |
return { value: cached.value, expired, found: true, ...(typeof cached.etag === "string" && API.CONDITIONAL_ETAG_PATTERN.test(cached.etag) ? { etag: cached.etag } : {}) };
|
| 240 |
}
|
|
@@ -283,6 +298,7 @@ export function getCacheKeysByPrefix(prefix) {
|
|
| 283 |
}
|
| 284 |
|
| 285 |
function _touchStorageEntry(key, cached) {
|
|
|
|
| 286 |
try {
|
| 287 |
if (localStorage.getItem(key) === null) return;
|
| 288 |
const now = Date.now();
|
|
|
|
| 31 |
// 内存缓存(最快)
|
| 32 |
const memoryCache = new Map();
|
| 33 |
const memoryCacheOrder = []; // LRU 顺序追踪
|
| 34 |
+
const memoryOnlyEntries = new WeakSet();
|
| 35 |
|
| 36 |
/**
|
| 37 |
* 生成完整缓存键(带前缀)
|
|
|
|
| 119 |
const fk = _fullKey(key);
|
| 120 |
const expireAt = Date.now() + ttl;
|
| 121 |
const cacheData = { schemaVersion: CACHE_SCHEMA_VERSION, value, expireAt, lastAccessAt: Date.now() };
|
| 122 |
+
memoryOnlyEntries.add(cacheData);
|
| 123 |
if (typeof metadata?.etag === "string" && API.CONDITIONAL_ETAG_PATTERN.test(metadata.etag)) cacheData.etag = metadata.etag;
|
| 124 |
|
| 125 |
// 内存缓存
|
|
|
|
| 129 |
|
| 130 |
// 持久化缓存
|
| 131 |
if (persist) {
|
| 132 |
+
let jsonStr;
|
| 133 |
+
try {
|
| 134 |
+
jsonStr = JSON.stringify(cacheData);
|
| 135 |
+
} catch {
|
| 136 |
+
console.warn('缓存无法序列化,保留内存缓存');
|
| 137 |
+
return;
|
| 138 |
+
}
|
| 139 |
+
if ((fk.length + jsonStr.length) * 2 > MAX_STORAGE_BYTES) {
|
| 140 |
+
console.warn('缓存条目超过持久化预算,保留内存缓存');
|
| 141 |
+
return;
|
| 142 |
+
}
|
| 143 |
let previousBytes = 0;
|
| 144 |
let previousLengthKnown = false;
|
| 145 |
try {
|
|
|
|
| 148 |
previousLengthKnown = true;
|
| 149 |
} catch {}
|
| 150 |
if (_safeStorageWrite(fk, jsonStr)) {
|
| 151 |
+
memoryOnlyEntries.delete(cacheData);
|
| 152 |
if (storageUsageBytes !== null && previousLengthKnown) {
|
| 153 |
storageUsageBytes += (fk.length + jsonStr.length) * 2 - previousBytes;
|
| 154 |
} else {
|
|
|
|
| 194 |
// 回填内存缓存
|
| 195 |
memoryCache.set(fk, cached);
|
| 196 |
_updateLRU(fk);
|
| 197 |
+
_enforceCacheLimit();
|
| 198 |
_touchStorageEntry(fk, cached);
|
| 199 |
return cached.value;
|
| 200 |
}
|
|
|
|
| 249 |
// 回填内存缓存
|
| 250 |
memoryCache.set(fk, cached);
|
| 251 |
_updateLRU(fk);
|
| 252 |
+
_enforceCacheLimit();
|
| 253 |
_touchStorageEntry(fk, cached);
|
| 254 |
return { value: cached.value, expired, found: true, ...(typeof cached.etag === "string" && API.CONDITIONAL_ETAG_PATTERN.test(cached.etag) ? { etag: cached.etag } : {}) };
|
| 255 |
}
|
|
|
|
| 298 |
}
|
| 299 |
|
| 300 |
function _touchStorageEntry(key, cached) {
|
| 301 |
+
if (memoryOnlyEntries.has(cached)) return;
|
| 302 |
try {
|
| 303 |
if (localStorage.getItem(key) === null) return;
|
| 304 |
const now = Date.now();
|
前端页面/post/讨论区组件.js
CHANGED
|
@@ -436,7 +436,7 @@ export function createPostsView(currentUser, keyword = "") {
|
|
| 436 |
renderPostsFromCache(allPostsData);
|
| 437 |
showToast(t('post.network_cache'), "warning");
|
| 438 |
} else {
|
| 439 |
-
renderListLoadError(postsGrid, t('post.load_failed'), () => loadPosts(1, false), t);
|
| 440 |
}
|
| 441 |
}
|
| 442 |
}
|
|
|
|
| 436 |
renderPostsFromCache(allPostsData);
|
| 437 |
showToast(t('post.network_cache'), "warning");
|
| 438 |
} else {
|
| 439 |
+
renderListLoadError(postsGrid, t('post.load_failed'), () => loadPosts(1, false), t, err);
|
| 440 |
}
|
| 441 |
}
|
| 442 |
}
|
前端页面/profile/个人中心视图.js
CHANGED
|
@@ -97,6 +97,7 @@ export function showUserProfile(initialUserData, currentUser = null, isMe = true
|
|
| 97 |
let userData = normalizeProfileData(initialUserData, initialUserData.account, isMe);
|
| 98 |
let constructing = true;
|
| 99 |
let renderGeneration = 0;
|
|
|
|
| 100 |
const componentId = `profile-detail-${++profileSequence}`;
|
| 101 |
const releaseChild = () => {
|
| 102 |
const list = container.querySelector('#profile-list-container');
|
|
@@ -215,19 +216,18 @@ export function showUserProfile(initialUserData, currentUser = null, isMe = true
|
|
| 215 |
|
| 216 |
container.querySelector("#btn-open-settings").onclick = () => { isSettingsView = true; render(); };
|
| 217 |
container.querySelector("#btn-logout").onclick = async () => {
|
| 218 |
-
if (!current()) return;
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
// 内存敏感数据与 Token/User/版本戳(与 401 强制登出共用同一函数)
|
| 225 |
logoutAndClearUserData(currentAccount);
|
| 226 |
-
|
| 227 |
-
// 2. 触发事件
|
| 228 |
window.dispatchEvent(new CustomEvent("comfy-route-back"));
|
| 229 |
window.dispatchEvent(new CustomEvent("comfy-user-logout"));
|
| 230 |
showToast(t('feedback.logged_out'), "info");
|
|
|
|
|
|
|
| 231 |
}
|
| 232 |
};
|
| 233 |
} else {
|
|
|
|
| 97 |
let userData = normalizeProfileData(initialUserData, initialUserData.account, isMe);
|
| 98 |
let constructing = true;
|
| 99 |
let renderGeneration = 0;
|
| 100 |
+
let logoutPending = false;
|
| 101 |
const componentId = `profile-detail-${++profileSequence}`;
|
| 102 |
const releaseChild = () => {
|
| 103 |
const list = container.querySelector('#profile-list-container');
|
|
|
|
| 216 |
|
| 217 |
container.querySelector("#btn-open-settings").onclick = () => { isSettingsView = true; render(); };
|
| 218 |
container.querySelector("#btn-logout").onclick = async () => {
|
| 219 |
+
if (!current() || logoutPending) return;
|
| 220 |
+
const currentAccount = getCurrentAccount();
|
| 221 |
+
logoutPending = true;
|
| 222 |
+
try {
|
| 223 |
+
if (!await showConfirm(t('confirm.logout')) || !scope.isCurrent()
|
| 224 |
+
|| getCurrentAccount() !== currentAccount) return;
|
|
|
|
| 225 |
logoutAndClearUserData(currentAccount);
|
|
|
|
|
|
|
| 226 |
window.dispatchEvent(new CustomEvent("comfy-route-back"));
|
| 227 |
window.dispatchEvent(new CustomEvent("comfy-user-logout"));
|
| 228 |
showToast(t('feedback.logged_out'), "info");
|
| 229 |
+
} finally {
|
| 230 |
+
logoutPending = false;
|
| 231 |
}
|
| 232 |
};
|
| 233 |
} else {
|
前端页面/profile/个人列表组件.js
CHANGED
|
@@ -352,7 +352,7 @@ export async function renderProfileListContent(tabId, domElement, userData, curr
|
|
| 352 |
const txCard = createTransactionCard(tx);
|
| 353 |
txListDiv.appendChild(txCard);
|
| 354 |
} catch (e) {
|
| 355 |
-
console.error("交易卡片渲染失败
|
| 356 |
}
|
| 357 |
});
|
| 358 |
}
|
|
@@ -727,7 +727,7 @@ function createPostItem(post, currentUser) {
|
|
| 727 |
const view = module.createPostDetailView(post.id, currentUser);
|
| 728 |
window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
|
| 729 |
}).catch(err => {
|
| 730 |
-
console.error("加载帖子详情组件失败
|
| 731 |
showToast(t('profile.post_detail_load_failed'), "error");
|
| 732 |
});
|
| 733 |
};
|
|
@@ -803,7 +803,7 @@ function createPromptItem(prompt, currentUser) {
|
|
| 803 |
const view = module.createPromptDetailView(prompt.id, currentUser);
|
| 804 |
window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
|
| 805 |
}).catch(err => {
|
| 806 |
-
console.error("加载提示词详情组件失败
|
| 807 |
showToast(t('profile.prompt_detail_load_failed'), "error");
|
| 808 |
});
|
| 809 |
};
|
|
|
|
| 352 |
const txCard = createTransactionCard(tx);
|
| 353 |
txListDiv.appendChild(txCard);
|
| 354 |
} catch (e) {
|
| 355 |
+
console.error("交易卡片渲染失败");
|
| 356 |
}
|
| 357 |
});
|
| 358 |
}
|
|
|
|
| 727 |
const view = module.createPostDetailView(post.id, currentUser);
|
| 728 |
window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
|
| 729 |
}).catch(err => {
|
| 730 |
+
console.error("加载帖子详情组件失败");
|
| 731 |
showToast(t('profile.post_detail_load_failed'), "error");
|
| 732 |
});
|
| 733 |
};
|
|
|
|
| 803 |
const view = module.createPromptDetailView(prompt.id, currentUser);
|
| 804 |
window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
|
| 805 |
}).catch(err => {
|
| 806 |
+
console.error("加载提示词详情组件失败");
|
| 807 |
showToast(t('profile.prompt_detail_load_failed'), "error");
|
| 808 |
});
|
| 809 |
};
|
前端页面/prompt/提示词组件.js
CHANGED
|
@@ -564,7 +564,7 @@ export function createPromptsView(currentUser, keyword = "") {
|
|
| 564 |
renderPromptsFromCache(allPromptsData);
|
| 565 |
showToast(t('prompt.network_cache'), "warning");
|
| 566 |
} else {
|
| 567 |
-
renderListLoadError(promptsGrid, t('prompt.load_failed'), () => loadPrompts(1, false), t);
|
| 568 |
}
|
| 569 |
}
|
| 570 |
}
|
|
|
|
| 564 |
renderPromptsFromCache(allPromptsData);
|
| 565 |
showToast(t('prompt.network_cache'), "warning");
|
| 566 |
} else {
|
| 567 |
+
renderListLoadError(promptsGrid, t('prompt.load_failed'), () => loadPrompts(1, false), t, err);
|
| 568 |
}
|
| 569 |
}
|
| 570 |
}
|
前端页面/social/私信聊天组件.js
CHANGED
|
@@ -139,7 +139,8 @@ export function openChatModal(currentUser, targetAccount = null) {
|
|
| 139 |
let pollTimer = null; // 🚀 P0-1:消息轮询定时器
|
| 140 |
|
| 141 |
let constructing = true, observer, listGeneration = 0, historyGeneration = 0, conversationGeneration = 0;
|
| 142 |
-
let sending = false;
|
|
|
|
| 143 |
const drafts = new Map(), clearTimes = new Map(), pendingMessages = new Map();
|
| 144 |
const componentId = `chat-${++chatSequence}`;
|
| 145 |
const sessionMatches = captureRequestSession();
|
|
@@ -153,7 +154,7 @@ export function openChatModal(currentUser, targetAccount = null) {
|
|
| 153 |
});
|
| 154 |
scope.addCleanup(() => {
|
| 155 |
clearTimeout(pollTimer); pollTimer = null; observer?.disconnect();
|
| 156 |
-
drafts.clear(); clearTimes.clear(); pendingMessages.clear(); allMessages = []; currentTargetInfo = null; currentChatTarget = null;
|
| 157 |
inputField.value = ''; messagesArea.replaceChildren(); chatListContainer.replaceChildren(); chatHeader.replaceChildren();
|
| 158 |
window.dispatchEvent(new CustomEvent('comfy-chat-unread-changed'));
|
| 159 |
});
|
|
@@ -223,12 +224,15 @@ export function openChatModal(currentUser, targetAccount = null) {
|
|
| 223 |
const loadChatList = async () => {
|
| 224 |
if (!scope.isCurrent()) return [];
|
| 225 |
const generation = ++listGeneration;
|
|
|
|
| 226 |
try {
|
| 227 |
const res = await api.getChatList(currentUser.account, options);
|
| 228 |
if (!scope.isCurrent() || generation !== listGeneration) return [];
|
| 229 |
if (res?.status !== 'success' || !Array.isArray(res.data)) throw new Error('Invalid chat list');
|
| 230 |
const list = res.data.filter(chat => chat && typeof chat === 'object' && !Array.isArray(chat)
|
| 231 |
-
&& typeof (chat.target_account || chat.account || chat.receiver || chat.sender) === 'string')
|
|
|
|
|
|
|
| 232 |
renderChatList(list); return list;
|
| 233 |
} catch (_) {
|
| 234 |
if (scope.isCurrent() && generation === listGeneration) showToast(t('chat.load_list_failed'), 'error');
|
|
@@ -395,6 +399,11 @@ export function openChatModal(currentUser, targetAccount = null) {
|
|
| 395 |
|| (msg.sender === targetAccount && msg.receiver === currentUser.account)))
|
| 396 |
.map(msg => ({ ...msg })).sort((a, b) => _normalizeTs(a.created_at) - _normalizeTs(b.created_at));
|
| 397 |
renderMsgs(messages, avatar);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
} catch (_) {
|
| 399 |
if (!current()) return;
|
| 400 |
showToast(t('chat.load_history_failed'), 'error');
|
|
@@ -456,7 +465,8 @@ export function openChatModal(currentUser, targetAccount = null) {
|
|
| 456 |
const poll = async () => {
|
| 457 |
if (!scope.isCurrent()) return;
|
| 458 |
const list = await loadChatList();
|
| 459 |
-
if (scope.isCurrent() && currentChatTarget && list.some(chat =>
|
|
|
|
| 460 |
await loadChatHistory(currentChatTarget, currentTargetInfo?.name, currentTargetInfo?.avatar);
|
| 461 |
}
|
| 462 |
if (scope.isCurrent()) pollTimer = setTimeout(poll, CACHE.MESSAGE_POLL.INTERVAL);
|
|
|
|
| 139 |
let pollTimer = null; // 🚀 P0-1:消息轮询定时器
|
| 140 |
|
| 141 |
let constructing = true, observer, listGeneration = 0, historyGeneration = 0, conversationGeneration = 0;
|
| 142 |
+
let sending = false, readRevision = 0;
|
| 143 |
+
const acknowledgedReads = new Map();
|
| 144 |
const drafts = new Map(), clearTimes = new Map(), pendingMessages = new Map();
|
| 145 |
const componentId = `chat-${++chatSequence}`;
|
| 146 |
const sessionMatches = captureRequestSession();
|
|
|
|
| 154 |
});
|
| 155 |
scope.addCleanup(() => {
|
| 156 |
clearTimeout(pollTimer); pollTimer = null; observer?.disconnect();
|
| 157 |
+
drafts.clear(); clearTimes.clear(); pendingMessages.clear(); acknowledgedReads.clear(); allMessages = []; currentTargetInfo = null; currentChatTarget = null;
|
| 158 |
inputField.value = ''; messagesArea.replaceChildren(); chatListContainer.replaceChildren(); chatHeader.replaceChildren();
|
| 159 |
window.dispatchEvent(new CustomEvent('comfy-chat-unread-changed'));
|
| 160 |
});
|
|
|
|
| 224 |
const loadChatList = async () => {
|
| 225 |
if (!scope.isCurrent()) return [];
|
| 226 |
const generation = ++listGeneration;
|
| 227 |
+
const requestedReadRevision = readRevision;
|
| 228 |
try {
|
| 229 |
const res = await api.getChatList(currentUser.account, options);
|
| 230 |
if (!scope.isCurrent() || generation !== listGeneration) return [];
|
| 231 |
if (res?.status !== 'success' || !Array.isArray(res.data)) throw new Error('Invalid chat list');
|
| 232 |
const list = res.data.filter(chat => chat && typeof chat === 'object' && !Array.isArray(chat)
|
| 233 |
+
&& typeof (chat.target_account || chat.account || chat.receiver || chat.sender) === 'string')
|
| 234 |
+
.map(chat => (acknowledgedReads.get(chat.target_account || chat.account || chat.receiver || chat.sender) || 0) > requestedReadRevision
|
| 235 |
+
? { ...chat, unread_count: 0 } : chat);
|
| 236 |
renderChatList(list); return list;
|
| 237 |
} catch (_) {
|
| 238 |
if (scope.isCurrent() && generation === listGeneration) showToast(t('chat.load_list_failed'), 'error');
|
|
|
|
| 399 |
|| (msg.sender === targetAccount && msg.receiver === currentUser.account)))
|
| 400 |
.map(msg => ({ ...msg })).sort((a, b) => _normalizeTs(a.created_at) - _normalizeTs(b.created_at));
|
| 401 |
renderMsgs(messages, avatar);
|
| 402 |
+
acknowledgedReads.set(targetAccount, ++readRevision);
|
| 403 |
+
for (const button of chatListContainer.querySelectorAll('.chat-list-item')) {
|
| 404 |
+
if (button.dataset.account === targetAccount) button.querySelector('.chat-unread')?.remove();
|
| 405 |
+
}
|
| 406 |
+
window.dispatchEvent(new CustomEvent('comfy-chat-unread-changed'));
|
| 407 |
} catch (_) {
|
| 408 |
if (!current()) return;
|
| 409 |
showToast(t('chat.load_history_failed'), 'error');
|
|
|
|
| 465 |
const poll = async () => {
|
| 466 |
if (!scope.isCurrent()) return;
|
| 467 |
const list = await loadChatList();
|
| 468 |
+
if (scope.isCurrent() && currentChatTarget && list.some(chat => (chat.target_account || chat.account || chat.receiver || chat.sender) === currentChatTarget
|
| 469 |
+
&& validUnreadCount(chat.unread_count) > 0)) {
|
| 470 |
await loadChatHistory(currentChatTarget, currentTargetInfo?.name, currentTargetInfo?.avatar);
|
| 471 |
}
|
| 472 |
if (scope.isCurrent()) pollTimer = setTimeout(poll, CACHE.MESSAGE_POLL.INTERVAL);
|
前端页面/styles/主题基础.css
CHANGED
|
@@ -1706,3 +1706,28 @@ button.ranking-rating__star { cursor: pointer; min-width: 28px; min-height: 28px
|
|
| 1706 |
@media (forced-colors: active) { .posts-card__play { background: Canvas; border: 1px solid CanvasText; } .posts-card__play-icon { border-left-color: CanvasText; } }
|
| 1707 |
|
| 1708 |
.creator-card__summary:focus-visible { outline:2px solid var(--rk-focus,#8ab4ff); outline-offset:-3px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1706 |
@media (forced-colors: active) { .posts-card__play { background: Canvas; border: 1px solid CanvasText; } .posts-card__play-icon { border-left-color: CanvasText; } }
|
| 1707 |
|
| 1708 |
.creator-card__summary:focus-visible { outline:2px solid var(--rk-focus,#8ab4ff); outline-offset:-3px; }
|
| 1709 |
+
|
| 1710 |
+
.comfy-ranking-root .comfy-ranking-request-diagnostic {
|
| 1711 |
+
box-sizing: border-box;
|
| 1712 |
+
grid-column: 1 / -1;
|
| 1713 |
+
width: 100%; max-width: 100%; min-width: 0;
|
| 1714 |
+
padding: 12px;
|
| 1715 |
+
color: var(--rk-muted);
|
| 1716 |
+
text-align: start;
|
| 1717 |
+
}
|
| 1718 |
+
.comfy-ranking-request-diagnostic summary { cursor: pointer; min-height: 24px; }
|
| 1719 |
+
.comfy-ranking-request-diagnostic__label { display: grid; gap: 6px; margin: 10px 0; }
|
| 1720 |
+
.comfy-ranking-request-diagnostic input {
|
| 1721 |
+
box-sizing: border-box; width: 100%; min-width: 0; min-height: 28px;
|
| 1722 |
+
color: var(--rk-text); background: var(--rk-panel);
|
| 1723 |
+
border: 1px solid var(--rk-border); border-radius: var(--rk-radius-sm);
|
| 1724 |
+
padding: 6px; font-family: monospace;
|
| 1725 |
+
}
|
| 1726 |
+
.comfy-ranking-request-diagnostic button {
|
| 1727 |
+
min-width: 48px; min-height: 28px; padding: 4px 10px;
|
| 1728 |
+
color: var(--rk-text); background: var(--rk-panel);
|
| 1729 |
+
border: 1px solid var(--rk-border); border-radius: var(--rk-radius-sm); cursor: pointer;
|
| 1730 |
+
}
|
| 1731 |
+
.comfy-ranking-request-diagnostic button[aria-busy="true"] { opacity: .65; }
|
| 1732 |
+
.comfy-ranking-request-diagnostic__feedback { margin: 6px 0 0; overflow-wrap: anywhere; }
|
| 1733 |
+
.comfy-ranking-request-diagnostic summary:focus-visible { outline: 2px solid var(--rk-focus); outline-offset: 3px; }
|
前端页面/task/任务榜组件.js
CHANGED
|
@@ -344,7 +344,7 @@ export function createTasksView(currentUser, keyword = "") {
|
|
| 344 |
showToast(t('task.network_cache'), "warning");
|
| 345 |
continueSearchPagination();
|
| 346 |
} else {
|
| 347 |
-
renderListLoadError(tasksList, t('task.load_failed'), () => loadTasks(1, false), t);
|
| 348 |
}
|
| 349 |
}
|
| 350 |
}
|
|
|
|
| 344 |
showToast(t('task.network_cache'), "warning");
|
| 345 |
continueSearchPagination();
|
| 346 |
} else {
|
| 347 |
+
renderListLoadError(tasksList, t('task.load_failed'), () => loadTasks(1, false), t, err);
|
| 348 |
}
|
| 349 |
}
|
| 350 |
}
|