Spaces:
Running
Running
Upload 98 files
Browse files- 前端页面/components/UI交互提示组件.js +133 -37
- 前端页面/components/全局设置组件.js +2 -40
- 前端页面/components/动画音效引擎.js +1 -1
- 前端页面/components/性能优化工具.js +11 -4
- 前端页面/components/用户体验_国际化.js +22 -2
- 前端页面/components/顶部导航组件.js +70 -10
- 前端页面/core/侧边栏主程序.js +112 -84
- 前端页面/core/全局配置.js +70 -1
- 前端页面/core/网络请求_基础设施.js +89 -15
- 前端页面/market/创作者卡片组件.js +1 -2
- 前端页面/market/发布内容_UI模板.js +1 -1
- 前端页面/market/发布内容_提交引擎.js +1 -2
- 前端页面/market/发布内容组件.js +2 -20
- 前端页面/market/资源安装引擎.js +28 -15
- 前端页面/market/资源详情页面组件.js +25 -32
- 前端页面/profile/个人设置表单组件.js +85 -51
- 前端页面/prompt/提示词组件.js +27 -42
- 前端页面/social/未读角标工具.js +12 -1
- 前端页面/social/通知中心组件.js +2 -12
- 前端页面/styles/主题基础.css +52 -0
前端页面/components/UI交互提示组件.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
| 15 |
// ===========================================
|
| 16 |
|
| 17 |
import { t } from "./用户体验增强.js";
|
| 18 |
-
import { escapeHtml } from "../core/全局配置.js";
|
| 19 |
|
| 20 |
// ==========================================
|
| 21 |
// 📢 Toast 消息提示
|
|
@@ -24,16 +24,58 @@ import { escapeHtml } from "../core/全局配置.js";
|
|
| 24 |
|
| 25 |
// Toast 队列管理(避免重叠)
|
| 26 |
let toastQueue = [];
|
| 27 |
-
let
|
|
|
|
| 28 |
let dialogInstanceId = 0;
|
| 29 |
const dialogStack = [];
|
| 30 |
|
| 31 |
function repositionToasts() {
|
|
|
|
|
|
|
| 32 |
toastQueue.forEach((toast, index) => {
|
| 33 |
-
toast.style.top = `${
|
|
|
|
| 34 |
});
|
| 35 |
}
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
/**
|
| 38 |
* 显示 Toast 消息提示
|
| 39 |
* @param {string} message - 提示内容
|
|
@@ -41,16 +83,17 @@ function repositionToasts() {
|
|
| 41 |
* @param {number} duration - 显示时长(毫秒),默认3000
|
| 42 |
*/
|
| 43 |
export function showToast(message, type = "info", duration = 3000) {
|
|
|
|
| 44 |
const toast = document.createElement("div");
|
| 45 |
const safeType = ["success", "error", "warning", "info"].includes(type) ? type : "info";
|
| 46 |
toast.className = `comfy-ranking-toast comfy-ranking-toast--${safeType}`;
|
| 47 |
-
toast.setAttribute("role",
|
| 48 |
toast.setAttribute("aria-live", safeType === "error" ? "assertive" : "polite");
|
| 49 |
toast.setAttribute("aria-atomic", "true");
|
| 50 |
const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
| 51 |
if (reduceMotion) toast.classList.add("comfy-ranking-toast--reduced-motion");
|
| 52 |
// 新提示从目标位置上方滑入;关闭后再按当前队列位置重排。
|
| 53 |
-
toast.style.top = `${
|
| 54 |
|
| 55 |
// 提示文本统一转义:message 常携带昵称/标题/后端报错等用户可控内容,
|
| 56 |
// 全库无任何调用方故意向 showToast 传 HTML 标签,故无条件转义不会改变现有显示效果。
|
|
@@ -61,34 +104,74 @@ export function showToast(message, type = "info", duration = 3000) {
|
|
| 61 |
<button type="button" class="toast-close comfy-ranking-toast__close" aria-label="${escapeHtml(t('common.close'))}">×</button>
|
| 62 |
`;
|
| 63 |
|
|
|
|
| 64 |
document.body.appendChild(toast);
|
| 65 |
toastQueue.push(toast);
|
|
|
|
|
|
|
| 66 |
|
| 67 |
// 点击关闭
|
| 68 |
-
toast.querySelector(".toast-close")
|
|
|
|
|
|
|
| 69 |
|
| 70 |
// 入场动画
|
| 71 |
-
requestAnimationFrame(() => {
|
|
|
|
|
|
|
| 72 |
toast.style.opacity = "1";
|
| 73 |
repositionToasts();
|
| 74 |
});
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
function closeToast(toast) {
|
| 82 |
-
if (!toast
|
| 83 |
-
if (toast.
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
toast.classList.add("comfy-ranking-toast--closing");
|
| 86 |
|
| 87 |
-
setTimeout(() => {
|
|
|
|
|
|
|
| 88 |
toast.remove();
|
| 89 |
-
|
| 90 |
-
if (
|
| 91 |
-
repositionToasts();
|
| 92 |
}, window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? 0 : 300);
|
| 93 |
}
|
| 94 |
|
|
@@ -716,6 +799,8 @@ export function createInstallProgress(container) {
|
|
| 716 |
const progressTrack = wrapper.querySelector(".install-progress__track");
|
| 717 |
const startedAt = Date.now();
|
| 718 |
let cancelButton = null;
|
|
|
|
|
|
|
| 719 |
const updateElapsed = () => {
|
| 720 |
const totalSeconds = Math.floor((Date.now() - startedAt) / 1000);
|
| 721 |
const time = `${Math.floor(totalSeconds / 60)}:${String(totalSeconds % 60).padStart(2, "0")}`;
|
|
@@ -755,28 +840,37 @@ export function createInstallProgress(container) {
|
|
| 755 |
stage.textContent = message || `❌ ${t('install.failed')}`;
|
| 756 |
progressTrack.setAttribute("aria-valuetext", stage.textContent);
|
| 757 |
},
|
| 758 |
-
showDiagnostics(entries) {
|
| 759 |
-
if (
|
| 760 |
-
const
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
const
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 777 |
}
|
| 778 |
-
|
| 779 |
-
|
|
|
|
|
|
|
|
|
|
| 780 |
},
|
| 781 |
addCancelButton(label, onCancel) {
|
| 782 |
if (cancelButton || typeof onCancel !== "function") return;
|
|
@@ -793,6 +887,8 @@ export function createInstallProgress(container) {
|
|
| 793 |
wrapper.appendChild(cancelButton);
|
| 794 |
},
|
| 795 |
destroy() {
|
|
|
|
|
|
|
| 796 |
stopElapsedTimer();
|
| 797 |
wrapper.remove();
|
| 798 |
}
|
|
|
|
| 15 |
// ===========================================
|
| 16 |
|
| 17 |
import { t } from "./用户体验增强.js";
|
| 18 |
+
import { escapeHtml, UI } from "../core/全局配置.js";
|
| 19 |
|
| 20 |
// ==========================================
|
| 21 |
// 📢 Toast 消息提示
|
|
|
|
| 24 |
|
| 25 |
// Toast 队列管理(避免重叠)
|
| 26 |
let toastQueue = [];
|
| 27 |
+
let toastObserver = null;
|
| 28 |
+
let toastSizeObserver = null;
|
| 29 |
let dialogInstanceId = 0;
|
| 30 |
const dialogStack = [];
|
| 31 |
|
| 32 |
function repositionToasts() {
|
| 33 |
+
const heights = toastQueue.map(toast => toast.offsetHeight);
|
| 34 |
+
let top = UI.TOAST.OFFSET;
|
| 35 |
toastQueue.forEach((toast, index) => {
|
| 36 |
+
toast.style.top = `${top}px`;
|
| 37 |
+
top += heights[index] + UI.TOAST.GAP;
|
| 38 |
});
|
| 39 |
}
|
| 40 |
|
| 41 |
+
function observeToastSize(toast) {
|
| 42 |
+
if (typeof ResizeObserver !== "function") return;
|
| 43 |
+
if (!toastSizeObserver) toastSizeObserver = new ResizeObserver(repositionToasts);
|
| 44 |
+
toastSizeObserver.observe(toast);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
function releaseToast(toast) {
|
| 49 |
+
toast._toastReleased = true;
|
| 50 |
+
toastSizeObserver?.unobserve(toast);
|
| 51 |
+
if (toast._autoCloseTimer != null) clearTimeout(toast._autoCloseTimer);
|
| 52 |
+
if (toast._closeTimer != null) clearTimeout(toast._closeTimer);
|
| 53 |
+
if (toast._entryFrame != null) cancelAnimationFrame(toast._entryFrame);
|
| 54 |
+
toast._autoCloseTimer = null;
|
| 55 |
+
toast._closeTimer = null;
|
| 56 |
+
toast._entryFrame = null;
|
| 57 |
+
toast._previousFocus = null;
|
| 58 |
+
toast._cleanupToastEvents?.();
|
| 59 |
+
toast._cleanupToastEvents = null;
|
| 60 |
+
const index = toastQueue.indexOf(toast);
|
| 61 |
+
if (index > -1) toastQueue.splice(index, 1);
|
| 62 |
+
repositionToasts();
|
| 63 |
+
if (!toastQueue.length) {
|
| 64 |
+
toastObserver?.disconnect();
|
| 65 |
+
toastObserver = null;
|
| 66 |
+
toastSizeObserver?.disconnect();
|
| 67 |
+
toastSizeObserver = null;
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function observeToastRemoval() {
|
| 72 |
+
if (toastObserver) return;
|
| 73 |
+
toastObserver = new MutationObserver(() => {
|
| 74 |
+
for (const toast of [...toastQueue]) if (!toast.isConnected) releaseToast(toast);
|
| 75 |
+
});
|
| 76 |
+
toastObserver.observe(document.body, { childList: true });
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
/**
|
| 80 |
* 显示 Toast 消息提示
|
| 81 |
* @param {string} message - 提示内容
|
|
|
|
| 83 |
* @param {number} duration - 显示时长(毫秒),默认3000
|
| 84 |
*/
|
| 85 |
export function showToast(message, type = "info", duration = 3000) {
|
| 86 |
+
const previousFocus = document.activeElement;
|
| 87 |
const toast = document.createElement("div");
|
| 88 |
const safeType = ["success", "error", "warning", "info"].includes(type) ? type : "info";
|
| 89 |
toast.className = `comfy-ranking-toast comfy-ranking-toast--${safeType}`;
|
| 90 |
+
toast.setAttribute("role", safeType === "error" ? "alert" : "status");
|
| 91 |
toast.setAttribute("aria-live", safeType === "error" ? "assertive" : "polite");
|
| 92 |
toast.setAttribute("aria-atomic", "true");
|
| 93 |
const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
| 94 |
if (reduceMotion) toast.classList.add("comfy-ranking-toast--reduced-motion");
|
| 95 |
// 新提示从目标位置上方滑入;关闭后再按当前队列位置重排。
|
| 96 |
+
toast.style.top = `${UI.TOAST.OFFSET - 30}px`;
|
| 97 |
|
| 98 |
// 提示文本统一转义:message 常携带昵称/标题/后端报错等用户可控内容,
|
| 99 |
// 全库无任何调用方故意向 showToast 传 HTML 标签,故无条件转义不会改变现有显示效果。
|
|
|
|
| 104 |
<button type="button" class="toast-close comfy-ranking-toast__close" aria-label="${escapeHtml(t('common.close'))}">×</button>
|
| 105 |
`;
|
| 106 |
|
| 107 |
+
toast._previousFocus = previousFocus;
|
| 108 |
document.body.appendChild(toast);
|
| 109 |
toastQueue.push(toast);
|
| 110 |
+
observeToastRemoval();
|
| 111 |
+
observeToastSize(toast);
|
| 112 |
|
| 113 |
// 点击关闭
|
| 114 |
+
const closeButton = toast.querySelector(".toast-close");
|
| 115 |
+
const handleClose = () => closeToast(toast);
|
| 116 |
+
closeButton.addEventListener("click", handleClose);
|
| 117 |
|
| 118 |
// 入场动画
|
| 119 |
+
toast._entryFrame = requestAnimationFrame(() => {
|
| 120 |
+
toast._entryFrame = null;
|
| 121 |
+
if (toast._toastReleased || !toast.isConnected || toast.classList.contains("comfy-ranking-toast--closing")) return;
|
| 122 |
toast.style.opacity = "1";
|
| 123 |
repositionToasts();
|
| 124 |
});
|
| 125 |
|
| 126 |
+
let remaining = Number(duration);
|
| 127 |
+
let deadline = 0;
|
| 128 |
+
let hovered = false;
|
| 129 |
+
const pauseTimer = () => {
|
| 130 |
+
if (toast._autoCloseTimer == null) return;
|
| 131 |
+
clearTimeout(toast._autoCloseTimer);
|
| 132 |
+
toast._autoCloseTimer = null;
|
| 133 |
+
remaining = Math.max(0, deadline - performance.now());
|
| 134 |
+
};
|
| 135 |
+
const resumeTimer = () => {
|
| 136 |
+
if (toast._toastReleased || hovered || toast.contains(document.activeElement) || !toast.isConnected ||
|
| 137 |
+
toast.classList.contains("comfy-ranking-toast--closing") || toast._autoCloseTimer != null) return;
|
| 138 |
+
deadline = performance.now() + remaining;
|
| 139 |
+
toast._autoCloseTimer = setTimeout(() => closeToast(toast), remaining);
|
| 140 |
+
};
|
| 141 |
+
const handleEnter = () => { hovered = true; pauseTimer(); };
|
| 142 |
+
const handleLeave = () => { hovered = false; resumeTimer(); };
|
| 143 |
+
const handleFocusOut = event => {
|
| 144 |
+
if (!toast.contains(event.relatedTarget)) queueMicrotask(resumeTimer);
|
| 145 |
+
};
|
| 146 |
+
toast.addEventListener("mouseenter", handleEnter);
|
| 147 |
+
toast.addEventListener("mouseleave", handleLeave);
|
| 148 |
+
toast.addEventListener("focusin", pauseTimer);
|
| 149 |
+
toast.addEventListener("focusout", handleFocusOut);
|
| 150 |
+
toast._cleanupToastEvents = () => {
|
| 151 |
+
closeButton.removeEventListener("click", handleClose);
|
| 152 |
+
toast.removeEventListener("mouseenter", handleEnter);
|
| 153 |
+
toast.removeEventListener("mouseleave", handleLeave);
|
| 154 |
+
toast.removeEventListener("focusin", pauseTimer);
|
| 155 |
+
toast.removeEventListener("focusout", handleFocusOut);
|
| 156 |
+
};
|
| 157 |
+
resumeTimer();
|
| 158 |
}
|
| 159 |
|
| 160 |
function closeToast(toast) {
|
| 161 |
+
if (!toast || toast._toastReleased) return;
|
| 162 |
+
if (!toast.isConnected) { releaseToast(toast); return; }
|
| 163 |
+
if (toast.classList.contains("comfy-ranking-toast--closing")) return;
|
| 164 |
+
if (toast._autoCloseTimer != null) clearTimeout(toast._autoCloseTimer);
|
| 165 |
+
toast._autoCloseTimer = null;
|
| 166 |
|
| 167 |
toast.classList.add("comfy-ranking-toast--closing");
|
| 168 |
|
| 169 |
+
toast._closeTimer = setTimeout(() => {
|
| 170 |
+
const restore = toast.contains(document.activeElement);
|
| 171 |
+
const previousFocus = toast._previousFocus;
|
| 172 |
toast.remove();
|
| 173 |
+
releaseToast(toast);
|
| 174 |
+
if (restore && previousFocus?.isConnected) previousFocus.focus();
|
|
|
|
| 175 |
}, window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? 0 : 300);
|
| 176 |
}
|
| 177 |
|
|
|
|
| 799 |
const progressTrack = wrapper.querySelector(".install-progress__track");
|
| 800 |
const startedAt = Date.now();
|
| 801 |
let cancelButton = null;
|
| 802 |
+
let destroyed = false;
|
| 803 |
+
let diagnosticGeneration = 0;
|
| 804 |
const updateElapsed = () => {
|
| 805 |
const totalSeconds = Math.floor((Date.now() - startedAt) / 1000);
|
| 806 |
const time = `${Math.floor(totalSeconds / 60)}:${String(totalSeconds % 60).padStart(2, "0")}`;
|
|
|
|
| 840 |
stage.textContent = message || `❌ ${t('install.failed')}`;
|
| 841 |
progressTrack.setAttribute("aria-valuetext", stage.textContent);
|
| 842 |
},
|
| 843 |
+
showDiagnostics(entries, error = null) {
|
| 844 |
+
if (destroyed) return;
|
| 845 |
+
const generation = ++diagnosticGeneration;
|
| 846 |
+
wrapper.querySelectorAll('.install-progress__diagnostics, .comfy-ranking-request-diagnostic').forEach(node => node.remove());
|
| 847 |
+
if (Array.isArray(entries) && entries.length) {
|
| 848 |
+
const details = document.createElement("details");
|
| 849 |
+
details.className = "install-progress__diagnostics";
|
| 850 |
+
const summary = document.createElement("summary");
|
| 851 |
+
summary.textContent = t('install.diagnostics');
|
| 852 |
+
details.appendChild(summary);
|
| 853 |
+
|
| 854 |
+
const list = document.createElement("ol");
|
| 855 |
+
for (const entry of entries.slice(-50)) {
|
| 856 |
+
const item = document.createElement("li");
|
| 857 |
+
const stageKey = entry.stageKey;
|
| 858 |
+
const stageLabel = stageKey ? t(stageKey) : (entry.stage || t('install.stage_unknown'));
|
| 859 |
+
item.textContent = t('install.diagnostic_entry', {
|
| 860 |
+
time: entry.time,
|
| 861 |
+
stage: stageLabel,
|
| 862 |
+
progress: Number.isFinite(entry.progress) ? `${entry.progress}%` : "—"
|
| 863 |
+
});
|
| 864 |
+
list.appendChild(item);
|
| 865 |
+
}
|
| 866 |
+
details.appendChild(list);
|
| 867 |
+
wrapper.appendChild(details);
|
| 868 |
}
|
| 869 |
+
if (typeof error?.requestId !== 'string') return;
|
| 870 |
+
import('./性能优化工具.js').then(({ appendRequestDiagnostic }) => {
|
| 871 |
+
const isCurrent = () => !destroyed && generation === diagnosticGeneration && wrapper.isConnected;
|
| 872 |
+
if (isCurrent()) appendRequestDiagnostic(wrapper, error, t, isCurrent);
|
| 873 |
+
}).catch(() => { console.warn('请求诊断界面加载失败'); });
|
| 874 |
},
|
| 875 |
addCancelButton(label, onCancel) {
|
| 876 |
if (cancelButton || typeof onCancel !== "function") return;
|
|
|
|
| 887 |
wrapper.appendChild(cancelButton);
|
| 888 |
},
|
| 889 |
destroy() {
|
| 890 |
+
destroyed = true;
|
| 891 |
+
diagnosticGeneration++;
|
| 892 |
stopElapsedTimer();
|
| 893 |
wrapper.remove();
|
| 894 |
}
|
前端页面/components/全局设置组件.js
CHANGED
|
@@ -11,54 +11,16 @@
|
|
| 11 |
import { showToast } from "./UI交互提示组件.js";
|
| 12 |
import { setLanguage, getLanguage, t } from "./用户体验增强.js";
|
| 13 |
import { clearAllCache } from "./性能优化工具.js";
|
| 14 |
-
import { IS_WEB_MODE } from "../core/全局配置.js";
|
|
|
|
| 15 |
import { getDiskCacheStats, clearDiskCache } from './缓存管理请求.js';
|
| 16 |
|
| 17 |
-
// 🔧 设置项 localStorage Key
|
| 18 |
-
const SETTINGS_KEY = "ComfyCommunity_Settings";
|
| 19 |
-
|
| 20 |
-
// 🔧 默认设置值
|
| 21 |
-
const DEFAULT_SETTINGS = {
|
| 22 |
-
showCreatorBanner: true, // 创作者卡片显示背景图
|
| 23 |
-
enableAnimations: true, // ✨ 榜单切换动画
|
| 24 |
-
enableSoundEffects: true, // 🔊 动画音效
|
| 25 |
-
language: 'zh-CN', // 🌐 界面语言
|
| 26 |
-
cacheExpireSeconds: 7200, // ⏱️ 缓存刷新间隔(秒),默认2小时
|
| 27 |
-
// 🚀 后续可扩展更多设置项
|
| 28 |
-
// compactMode: false, // 紧凑模式
|
| 29 |
-
// darkMode: true, // 深色模式
|
| 30 |
-
};
|
| 31 |
-
|
| 32 |
// 🌐 语言选项(双语显示)
|
| 33 |
const LANGUAGE_OPTIONS = [
|
| 34 |
{ value: 'zh-CN', label: '简体中文 / Simplified Chinese' },
|
| 35 |
{ value: 'en-US', label: 'English / 英语' }
|
| 36 |
];
|
| 37 |
|
| 38 |
-
/**
|
| 39 |
-
* 📦 获取当前设置
|
| 40 |
-
*/
|
| 41 |
-
export function getSettings() {
|
| 42 |
-
try {
|
| 43 |
-
const saved = localStorage.getItem(SETTINGS_KEY);
|
| 44 |
-
if (saved) {
|
| 45 |
-
return { ...DEFAULT_SETTINGS, ...JSON.parse(saved) };
|
| 46 |
-
}
|
| 47 |
-
} catch (e) {}
|
| 48 |
-
return { ...DEFAULT_SETTINGS };
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
/**
|
| 52 |
-
* 💾 保存设置
|
| 53 |
-
*/
|
| 54 |
-
export function saveSettings(settings) {
|
| 55 |
-
try {
|
| 56 |
-
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
| 57 |
-
// 🔔 广播设置变更事件,让其他组件响应
|
| 58 |
-
window.dispatchEvent(new CustomEvent("comfy-settings-changed", { detail: settings }));
|
| 59 |
-
} catch (e) {}
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
/**
|
| 63 |
* 📊 格式化文件大小
|
| 64 |
*/
|
|
|
|
| 11 |
import { showToast } from "./UI交互提示组件.js";
|
| 12 |
import { setLanguage, getLanguage, t } from "./用户体验增强.js";
|
| 13 |
import { clearAllCache } from "./性能优化工具.js";
|
| 14 |
+
import { IS_WEB_MODE, getSettings, saveSettings } from "../core/全局配置.js";
|
| 15 |
+
export { getSettings, saveSettings };
|
| 16 |
import { getDiskCacheStats, clearDiskCache } from './缓存管理请求.js';
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
// 🌐 语言选项(双语显示)
|
| 19 |
const LANGUAGE_OPTIONS = [
|
| 20 |
{ value: 'zh-CN', label: '简体中文 / Simplified Chinese' },
|
| 21 |
{ value: 'en-US', label: 'English / 英语' }
|
| 22 |
];
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
/**
|
| 25 |
* 📊 格式化文件大小
|
| 26 |
*/
|
前端页面/components/动画音效引擎.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
| 11 |
// - tetris: 提示词 - 俄罗斯方块式逐个下坠堆积
|
| 12 |
// ==========================================
|
| 13 |
|
| 14 |
-
import { getSettings } from "./全局
|
| 15 |
|
| 16 |
function prefersReducedMotion() {
|
| 17 |
try { return window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true; }
|
|
|
|
| 11 |
// - tetris: 提示词 - 俄罗斯方块式逐个下坠堆积
|
| 12 |
// ==========================================
|
| 13 |
|
| 14 |
+
import { getSettings } from "../core/全局配置.js";
|
| 15 |
|
| 16 |
function prefersReducedMotion() {
|
| 17 |
try { return window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true; }
|
前端页面/components/性能优化工具.js
CHANGED
|
@@ -1411,6 +1411,12 @@ export function renderListLoadError(container, message, onRetry, t, error = null
|
|
| 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 |
|
|
@@ -1433,24 +1439,25 @@ export function renderListLoadError(container, message, onRetry, t, error = null
|
|
| 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 |
/**
|
|
|
|
| 1411 |
|
| 1412 |
status.append(text, retry);
|
| 1413 |
container.replaceChildren(status);
|
| 1414 |
+
appendRequestDiagnostic(container, error, t);
|
| 1415 |
+
}
|
| 1416 |
+
|
| 1417 |
+
|
| 1418 |
+
export function appendRequestDiagnostic(container, error, t, isCurrent = () => true) {
|
| 1419 |
+
if (!container || !isCurrent()) return;
|
| 1420 |
const requestId = error?.requestId;
|
| 1421 |
if (typeof requestId !== 'string' || !API.REQUEST_ID_PATTERN.test(requestId)) return;
|
| 1422 |
|
|
|
|
| 1439 |
feedback.setAttribute('aria-live', 'polite');
|
| 1440 |
let copying = false;
|
| 1441 |
copy.addEventListener('click', async () => {
|
| 1442 |
+
if (copying || !details.isConnected || !isCurrent()) return;
|
| 1443 |
copying = true; copy.setAttribute('aria-busy', 'true');
|
| 1444 |
try {
|
| 1445 |
if (typeof navigator.clipboard?.writeText !== 'function') throw new Error();
|
| 1446 |
await navigator.clipboard.writeText(requestId);
|
| 1447 |
+
if (details.isConnected && isCurrent()) feedback.textContent = t('feedback.copied');
|
| 1448 |
} catch {
|
| 1449 |
+
if (details.isConnected && isCurrent()) {
|
| 1450 |
feedback.textContent = t('feedback.copy_id_manually');
|
| 1451 |
if (document.activeElement === copy) { input.focus(); input.select(); }
|
| 1452 |
}
|
| 1453 |
} finally {
|
| 1454 |
copying = false;
|
| 1455 |
+
if (details.isConnected && isCurrent()) copy.setAttribute('aria-busy', 'false');
|
| 1456 |
}
|
| 1457 |
});
|
| 1458 |
details.append(summary, label, copy, feedback);
|
| 1459 |
container.append(details);
|
| 1460 |
+
return details;
|
| 1461 |
}
|
| 1462 |
|
| 1463 |
/**
|
前端页面/components/用户体验_国际化.js
CHANGED
|
@@ -808,6 +808,11 @@ const translations = {
|
|
| 808 |
'feedback.search_local_result': '⚠️ 网络不可用,显示本地搜索结果',
|
| 809 |
// 🔍 搜索请求被新关键词取代时抛出的错误消息(详见 网络请求_基础设施.js 的主动取消分支)
|
| 810 |
'feedback.request_cancelled': '请求已取消',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
|
| 812 |
// 通用
|
| 813 |
'common.processing': '处理中',
|
|
@@ -1458,7 +1463,8 @@ const translations = {
|
|
| 1458 |
'publish.password_encrypted_hint': '密码加密存储于云端,仅在用户成功购买后所解密显示。未购买用户无法通过任何接口获取。',
|
| 1459 |
'publish.private_repo_hint': '这是一个私有 GitHub 仓库(需配置访问密钥)',
|
| 1460 |
'publish.pat_label': '私有库专属访问密钥 (PAT)',
|
| 1461 |
-
'publish.pat_hint': '系统使用此 Token 替购买者拉取源码。该密钥仅存储于云端后台,
|
|
|
|
| 1462 |
'publish.token_placeholder': 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
| 1463 |
'publish.token_keep_hint': '已配置,留空保留原密钥',
|
| 1464 |
'publish.effect_images': '效果展示图 (最多6张,可选)',
|
|
@@ -1656,6 +1662,10 @@ const translations = {
|
|
| 1656 |
'settings_form.moderation_skipped': '上传图片将跳过审核',
|
| 1657 |
'settings_form.moderation_enabled': '图像审核已启用',
|
| 1658 |
'settings_form.moderation_disabled': '图像审核已关闭',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1659 |
// 视频播放器
|
| 1660 |
'video.play': '播放',
|
| 1661 |
'video.pause': '暂停',
|
|
@@ -2520,6 +2530,11 @@ const translations = {
|
|
| 2520 |
'feedback.cloud_unreachable_no_cache': 'Cloud service is unreachable, and there is no local content to show',
|
| 2521 |
'feedback.search_local_result': '⚠️ Network unavailable, showing local search results',
|
| 2522 |
'feedback.request_cancelled': 'Request cancelled',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2523 |
'common.processing': 'Processing',
|
| 2524 |
'common.got_it': 'Got it',
|
| 2525 |
'confirm.delete_title': 'Confirm Delete',
|
|
@@ -3147,7 +3162,8 @@ const translations = {
|
|
| 3147 |
'publish.password_encrypted_hint': 'Password encrypted in cloud, shown only after successful purchase.',
|
| 3148 |
'publish.private_repo_hint': 'This is a private GitHub repo (requires access key)',
|
| 3149 |
'publish.pat_label': 'Private Repo PAT',
|
| 3150 |
-
'publish.pat_hint': 'System uses this token to pull source for buyers. Only stored on backend,
|
|
|
|
| 3151 |
'publish.token_placeholder': 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
| 3152 |
'publish.token_keep_hint': 'Already configured, leave empty to keep',
|
| 3153 |
'publish.effect_images': 'Preview Images (max 6, optional)',
|
|
@@ -3342,6 +3358,10 @@ const translations = {
|
|
| 3342 |
'settings_form.moderation_skipped': 'Uploaded images will skip moderation',
|
| 3343 |
'settings_form.moderation_enabled': 'Image moderation enabled',
|
| 3344 |
'settings_form.moderation_disabled': 'Image moderation disabled',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3345 |
|
| 3346 |
// Video player
|
| 3347 |
'video.play': 'Play',
|
|
|
|
| 808 |
'feedback.search_local_result': '⚠️ 网络不可用,显示本地搜索结果',
|
| 809 |
// 🔍 搜索请求被新关键词取代时抛出的错误消息(详见 网络请求_基础设施.js 的主动取消分支)
|
| 810 |
'feedback.request_cancelled': '请求已取消',
|
| 811 |
+
'feedback.stream_idle_timeout': '连接长时间未收到数据,操作结果暂时无法确认。',
|
| 812 |
+
'feedback.stream_missing_final': '连接已结束,但未收到最终结果,请先核对操作状态。',
|
| 813 |
+
'feedback.stream_invalid_encoding': '流数据编码异常,操作结果暂时无法确认。',
|
| 814 |
+
'feedback.stream_limit_exceeded': '流数据超出安全读取范围,操作结果暂时无法确认。',
|
| 815 |
+
'feedback.stream_timeout_unconfirmed': '连接超时,操作结果暂时无法确认。',
|
| 816 |
|
| 817 |
// 通用
|
| 818 |
'common.processing': '处理中',
|
|
|
|
| 1463 |
'publish.password_encrypted_hint': '密码加密存储于云端,仅在用户成功购买后所解密显示。未购买用户无法通过任何接口获取。',
|
| 1464 |
'publish.private_repo_hint': '这是一个私有 GitHub 仓库(需配置访问密钥)',
|
| 1465 |
'publish.pat_label': '私有库专属访问密钥 (PAT)',
|
| 1466 |
+
'publish.pat_hint': '系统使用此 Token 替购买者拉取源码。该密钥仅存储于云端后台,',
|
| 1467 |
+
'publish.pat_privacy': '绝对不会在前端接口中公开暴露。',
|
| 1468 |
'publish.token_placeholder': 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
| 1469 |
'publish.token_keep_hint': '已配置,留空保留原密钥',
|
| 1470 |
'publish.effect_images': '效果展示图 (最多6张,可选)',
|
|
|
|
| 1662 |
'settings_form.moderation_skipped': '上传图片将跳过审核',
|
| 1663 |
'settings_form.moderation_enabled': '图像审核已启用',
|
| 1664 |
'settings_form.moderation_disabled': '图像审核已关闭',
|
| 1665 |
+
'settings_form.moderation_read_failed': '审核状态读取失败,请重试后再修改。',
|
| 1666 |
+
'settings_form.moderation_confirming': '正在保存并确认审核状态…',
|
| 1667 |
+
'settings_form.moderation_unconfirmed': '审核设置结果尚未确认,请重试读取状态后再操作。',
|
| 1668 |
+
'settings_form.moderation_changed': '审核状态与本次设置不同,已显示当前读取结果。',
|
| 1669 |
// 视频播放器
|
| 1670 |
'video.play': '播放',
|
| 1671 |
'video.pause': '暂停',
|
|
|
|
| 2530 |
'feedback.cloud_unreachable_no_cache': 'Cloud service is unreachable, and there is no local content to show',
|
| 2531 |
'feedback.search_local_result': '⚠️ Network unavailable, showing local search results',
|
| 2532 |
'feedback.request_cancelled': 'Request cancelled',
|
| 2533 |
+
'feedback.stream_idle_timeout': 'No data has arrived for a while. The operation result is not yet confirmed.',
|
| 2534 |
+
'feedback.stream_missing_final': 'The connection ended without a final result. Check the operation status before continuing.',
|
| 2535 |
+
'feedback.stream_invalid_encoding': 'The stream contains invalid text encoding. The operation result is not yet confirmed.',
|
| 2536 |
+
'feedback.stream_limit_exceeded': 'The stream exceeds the safe reading limits. The operation result is not yet confirmed.',
|
| 2537 |
+
'feedback.stream_timeout_unconfirmed': 'The connection timed out. The operation result is not yet confirmed.',
|
| 2538 |
'common.processing': 'Processing',
|
| 2539 |
'common.got_it': 'Got it',
|
| 2540 |
'confirm.delete_title': 'Confirm Delete',
|
|
|
|
| 3162 |
'publish.password_encrypted_hint': 'Password encrypted in cloud, shown only after successful purchase.',
|
| 3163 |
'publish.private_repo_hint': 'This is a private GitHub repo (requires access key)',
|
| 3164 |
'publish.pat_label': 'Private Repo PAT',
|
| 3165 |
+
'publish.pat_hint': 'System uses this token to pull source for buyers. Only stored on backend,',
|
| 3166 |
+
'publish.pat_privacy': 'never exposed in frontend APIs.',
|
| 3167 |
'publish.token_placeholder': 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
| 3168 |
'publish.token_keep_hint': 'Already configured, leave empty to keep',
|
| 3169 |
'publish.effect_images': 'Preview Images (max 6, optional)',
|
|
|
|
| 3358 |
'settings_form.moderation_skipped': 'Uploaded images will skip moderation',
|
| 3359 |
'settings_form.moderation_enabled': 'Image moderation enabled',
|
| 3360 |
'settings_form.moderation_disabled': 'Image moderation disabled',
|
| 3361 |
+
'settings_form.moderation_read_failed': 'Could not read moderation status. Retry before changing it.',
|
| 3362 |
+
'settings_form.moderation_confirming': 'Saving and confirming moderation status…',
|
| 3363 |
+
'settings_form.moderation_unconfirmed': 'Moderation settings are not yet confirmed. Retry reading the status before making changes.',
|
| 3364 |
+
'settings_form.moderation_changed': 'Moderation status differs from this change. The current readback is shown.',
|
| 3365 |
|
| 3366 |
// Video player
|
| 3367 |
'video.play': 'Play',
|
前端页面/components/顶部导航组件.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
| 1 |
// 前端页面/components/顶部导航组件.js
|
| 2 |
-
import { createAuthView } from "../auth/用户注册登录组件.js";
|
| 3 |
import { openUserProfileModal } from "../profile/个人中心视图.js";
|
| 4 |
import { openChatModal, loadChatUnreadCount } from "../social/私信聊天组件.js";
|
| 5 |
import { api, captureRequestSession } from "../core/网络请求API.js";
|
| 6 |
import { showToast } from "./UI交互提示组件.js";
|
| 7 |
import { showAboutInfo } from "./关于插件组件.js";
|
| 8 |
-
import {
|
| 9 |
-
import { openSettingsPage } from "./全局设置组件.js"; // ⚙️ 新增
|
| 10 |
import { setUser, updateUser, initUserState } from "../core/状态管理.js";
|
| 11 |
import { readUserSession, sessionProfile } from "../core/用户会话存储.js";
|
| 12 |
import { API, CACHE, PLACEHOLDERS } from "../core/全局配置.js";
|
|
@@ -58,7 +56,13 @@ export function createTopNav() {
|
|
| 58 |
settingsBtn.textContent = "⚙️";
|
| 59 |
settingsBtn.title = t('nav.settings');
|
| 60 |
settingsBtn.setAttribute("aria-label", t('nav.settings'));
|
| 61 |
-
settingsBtn.onclick = (e) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
const titleWrapper = document.createElement("div");
|
| 64 |
titleWrapper.className = "top-nav__heading";
|
|
@@ -84,13 +88,69 @@ export function createTopNav() {
|
|
| 84 |
bellBtn.setAttribute("aria-label", t('social.notifications'));
|
| 85 |
|
| 86 |
let currentUser = null;
|
|
|
|
| 87 |
chatEntryBtn.onclick = () => openChatModal(currentUser);
|
| 88 |
-
bellBtn.onclick = () =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
const sessionStorages = () => ['localStorage', 'sessionStorage'].map(name => { try { return window[name]; } catch (_) { return null; } });
|
| 90 |
const restoredSession = readUserSession(sessionStorages(), CACHE.LEGACY_KEYS);
|
| 91 |
currentUser = restoredSession?.user || null;
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
const updateUserButtonState = () => {
|
|
|
|
| 94 |
if (currentUser) {
|
| 95 |
const avatar = document.createElement("img");
|
| 96 |
avatar.className = "top-nav__avatar";
|
|
@@ -121,8 +181,9 @@ export function createTopNav() {
|
|
| 121 |
bellBtn.querySelector("#unread-badge").style.display = "none";
|
| 122 |
chatEntryBtn.querySelector("#chat-unread-badge").style.display = "none";
|
| 123 |
|
| 124 |
-
userActionBtn.onclick = () => {
|
| 125 |
-
const
|
|
|
|
| 126 |
const originalSession = captureRequestSession();
|
| 127 |
const current = () => context.isCurrent() && userHeader.isConnected
|
| 128 |
&& _navInstance?.dom === userHeader && originalSession();
|
|
@@ -157,7 +218,7 @@ export function createTopNav() {
|
|
| 157 |
showToast(t(isRegister ? 'auth.register_success' : 'auth.login_success'), 'success');
|
| 158 |
// 凭证已发布即完成登录,资料补充不阻塞页面返回。
|
| 159 |
window.dispatchEvent(new CustomEvent('comfy-route-back'));
|
| 160 |
-
|
| 161 |
const applyProfile = data => {
|
| 162 |
if (!sameLogin() || data?.account !== account) return;
|
| 163 |
currentUser = sessionProfile({ ...currentUser, ...data });
|
|
@@ -195,8 +256,7 @@ export function createTopNav() {
|
|
| 195 |
}
|
| 196 |
}
|
| 197 |
});
|
| 198 |
-
|
| 199 |
-
};
|
| 200 |
}
|
| 201 |
};
|
| 202 |
|
|
|
|
| 1 |
// 前端页面/components/顶部导航组件.js
|
|
|
|
| 2 |
import { openUserProfileModal } from "../profile/个人中心视图.js";
|
| 3 |
import { openChatModal, loadChatUnreadCount } from "../social/私信聊天组件.js";
|
| 4 |
import { api, captureRequestSession } from "../core/网络请求API.js";
|
| 5 |
import { showToast } from "./UI交互提示组件.js";
|
| 6 |
import { showAboutInfo } from "./关于插件组件.js";
|
| 7 |
+
import { loadUnreadCount } from "../social/未读角标工具.js";
|
|
|
|
| 8 |
import { setUser, updateUser, initUserState } from "../core/状态管理.js";
|
| 9 |
import { readUserSession, sessionProfile } from "../core/用户会话存储.js";
|
| 10 |
import { API, CACHE, PLACEHOLDERS } from "../core/全局配置.js";
|
|
|
|
| 56 |
settingsBtn.textContent = "⚙️";
|
| 57 |
settingsBtn.title = t('nav.settings');
|
| 58 |
settingsBtn.setAttribute("aria-label", t('nav.settings'));
|
| 59 |
+
settingsBtn.onclick = (e) => {
|
| 60 |
+
e.stopPropagation();
|
| 61 |
+
return openDeferredView(async () => {
|
| 62 |
+
const { createSettingsView } = await import('./全局设置组件.js');
|
| 63 |
+
return createSettingsView;
|
| 64 |
+
}, settingsBtn);
|
| 65 |
+
};
|
| 66 |
|
| 67 |
const titleWrapper = document.createElement("div");
|
| 68 |
titleWrapper.className = "top-nav__heading";
|
|
|
|
| 88 |
bellBtn.setAttribute("aria-label", t('social.notifications'));
|
| 89 |
|
| 90 |
let currentUser = null;
|
| 91 |
+
let navigationGeneration = 0;
|
| 92 |
chatEntryBtn.onclick = () => openChatModal(currentUser);
|
| 93 |
+
bellBtn.onclick = () => {
|
| 94 |
+
if (!currentUser?.account) return showToast(t('notif.login_required'), 'warning');
|
| 95 |
+
const notificationUser = currentUser;
|
| 96 |
+
return openDeferredView(async () => {
|
| 97 |
+
const { openNotificationCenter } = await import('../social/通知中心组件.js');
|
| 98 |
+
return () => openNotificationCenter(notificationUser, bellBtn);
|
| 99 |
+
}, bellBtn, { ownsRoute: true });
|
| 100 |
+
};
|
| 101 |
const sessionStorages = () => ['localStorage', 'sessionStorage'].map(name => { try { return window[name]; } catch (_) { return null; } });
|
| 102 |
const restoredSession = readUserSession(sessionStorages(), CACHE.LEGACY_KEYS);
|
| 103 |
currentUser = restoredSession?.user || null;
|
| 104 |
|
| 105 |
+
async function openDeferredView(loadFactory, trigger, { ownsRoute = false } = {}) {
|
| 106 |
+
const generation = navigationGeneration;
|
| 107 |
+
const matchesSession = captureRequestSession();
|
| 108 |
+
const loadingView = document.createElement('div');
|
| 109 |
+
loadingView.className = 'hub-inline-load-state';
|
| 110 |
+
const message = document.createElement('span');
|
| 111 |
+
message.textContent = t('common.loading');
|
| 112 |
+
message.setAttribute('role', 'status');
|
| 113 |
+
message.setAttribute('aria-live', 'polite');
|
| 114 |
+
const back = document.createElement('button');
|
| 115 |
+
back.type = 'button'; back.className = 'ranking-publish-back';
|
| 116 |
+
back.textContent = t('common.back');
|
| 117 |
+
let active = true;
|
| 118 |
+
loadingView._cleanup = () => { active = false; back.onclick = null; };
|
| 119 |
+
loadingView.appendChild(message); loadingView.appendChild(back);
|
| 120 |
+
const currentLoad = () => active && loadingView.isConnected && userHeader.isConnected
|
| 121 |
+
&& _navInstance?.dom === userHeader && navigationGeneration === generation && matchesSession();
|
| 122 |
+
back.onclick = () => {
|
| 123 |
+
if (!active || !loadingView.isConnected || _navInstance?.dom !== userHeader) return;
|
| 124 |
+
window.dispatchEvent(new CustomEvent('comfy-route-back'));
|
| 125 |
+
if (trigger.isConnected) trigger.focus();
|
| 126 |
+
};
|
| 127 |
+
window.dispatchEvent(new CustomEvent('comfy-route-view', { detail: { view: loadingView } }));
|
| 128 |
+
try {
|
| 129 |
+
const createView = await loadFactory();
|
| 130 |
+
if (!currentLoad()) return;
|
| 131 |
+
if (ownsRoute) {
|
| 132 |
+
await createView();
|
| 133 |
+
if (currentLoad()) back.onclick();
|
| 134 |
+
return;
|
| 135 |
+
}
|
| 136 |
+
const view = createView();
|
| 137 |
+
if (!currentLoad()) { try { view._cleanup?.(); } finally { view.remove(); } return; }
|
| 138 |
+
window.dispatchEvent(new CustomEvent('comfy-route-view', { detail: { view } }));
|
| 139 |
+
} catch {
|
| 140 |
+
if (currentLoad()) {
|
| 141 |
+
message.textContent = t('common.operation_failed');
|
| 142 |
+
message.setAttribute('role', 'alert'); message.setAttribute('aria-live', 'assertive');
|
| 143 |
+
}
|
| 144 |
+
} finally {
|
| 145 |
+
if (active && !currentLoad()) {
|
| 146 |
+
if (loadingView.isConnected && _navInstance?.dom === userHeader) window.dispatchEvent(new CustomEvent('comfy-route-back'));
|
| 147 |
+
else { loadingView._cleanup(); loadingView.remove(); }
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
const updateUserButtonState = () => {
|
| 153 |
+
navigationGeneration++;
|
| 154 |
if (currentUser) {
|
| 155 |
const avatar = document.createElement("img");
|
| 156 |
avatar.className = "top-nav__avatar";
|
|
|
|
| 181 |
bellBtn.querySelector("#unread-badge").style.display = "none";
|
| 182 |
chatEntryBtn.querySelector("#chat-unread-badge").style.display = "none";
|
| 183 |
|
| 184 |
+
userActionBtn.onclick = () => openDeferredView(async () => {
|
| 185 |
+
const { createAuthView } = await import('../auth/用户注册登录组件.js');
|
| 186 |
+
return () => createAuthView(async (formData, context) => {
|
| 187 |
const originalSession = captureRequestSession();
|
| 188 |
const current = () => context.isCurrent() && userHeader.isConnected
|
| 189 |
&& _navInstance?.dom === userHeader && originalSession();
|
|
|
|
| 218 |
showToast(t(isRegister ? 'auth.register_success' : 'auth.login_success'), 'success');
|
| 219 |
// 凭证已发布即完成登录,资料补充不阻塞页面返回。
|
| 220 |
window.dispatchEvent(new CustomEvent('comfy-route-back'));
|
| 221 |
+
window.dispatchEvent(new CustomEvent('comfy-user-login')); updateUserButtonState();
|
| 222 |
const applyProfile = data => {
|
| 223 |
if (!sameLogin() || data?.account !== account) return;
|
| 224 |
currentUser = sessionProfile({ ...currentUser, ...data });
|
|
|
|
| 256 |
}
|
| 257 |
}
|
| 258 |
});
|
| 259 |
+
}, userActionBtn);
|
|
|
|
| 260 |
}
|
| 261 |
};
|
| 262 |
|
前端页面/core/侧边栏主程序.js
CHANGED
|
@@ -6,13 +6,8 @@
|
|
| 6 |
// 由 侧边栏入口注册.js 动态加载
|
| 7 |
// ==========================================
|
| 8 |
|
| 9 |
-
import { createPublishView } from "../market/发布内容组件.js";
|
| 10 |
-
import { createPublishTaskView } from "../task/发布任务组件.js"; // 🎯 新增:任务发布
|
| 11 |
-
import { createPublishPostView } from "../post/发布帖子组件.js"; // 🎯 新增:帖子发布
|
| 12 |
-
import { createPublishPromptView } from "../prompt/发布提示词组件.js"; // 🎯 新增:提示词发布
|
| 13 |
import { createTopNav } from "../components/顶部导航组件.js";
|
| 14 |
import { loadSidebarContent, disposeSidebarContent } from "./侧边栏数据引擎.js";
|
| 15 |
-
import { createItemDetailView } from "../market/资源详情页面组件.js";
|
| 16 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 17 |
import { api, captureRequestSession } from "./网络请求API.js"; // 🔴 编辑模式需调用详情API获取完整数据
|
| 18 |
import { CACHE, UI, getBackgroundKey, IS_WEB_MODE } from "./全局配置.js";
|
|
@@ -117,6 +112,18 @@ function _onSidebarWindow(event, handler) {
|
|
| 117 |
_sidebarWindowListeners.push({ event, handler });
|
| 118 |
}
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
export function buildSidebarDOM() {
|
| 121 |
// 🔧 P1修复:重建前先清理上一轮的 window 监听,避免重复注册
|
| 122 |
_cleanupSidebarWindowListeners();
|
|
@@ -356,77 +363,120 @@ export function buildSidebarDOM() {
|
|
| 356 |
|
| 357 |
_onSidebarWindow("comfy-open-detail", (e) => {
|
| 358 |
const { itemData, currentUser } = e.detail;
|
| 359 |
-
|
| 360 |
-
showInlineView(view);
|
| 361 |
});
|
| 362 |
|
| 363 |
-
|
| 364 |
-
// 🔴 修复:编辑时先获取详情API数据,确保 has_private_token 等字段完整(列表缓存可能缺少该字段)
|
| 365 |
-
_onSidebarWindow("comfy-route-edit-publish", async (e) => {
|
| 366 |
-
// 📱 Web 模式中央兜底:发布族功能仅本地 ComfyUI 可用
|
| 367 |
-
if (IS_WEB_MODE) return showToast(t('web.publish_not_supported'), "warning");
|
| 368 |
-
const { itemData, currentUser } = e.detail;
|
| 369 |
const matchesSession = captureRequestSession();
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
const
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
showInlineView(loadingView);
|
| 377 |
-
|
| 378 |
-
|
| 379 |
&& activeInlineView === loadingView && loadingView.isConnected;
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
try {
|
| 382 |
-
const
|
| 383 |
-
if (!isCurrent()) return;
|
| 384 |
-
const data = detailRes?.data, revision = data?.edit_revision === undefined ? 0 : data.edit_revision;
|
| 385 |
-
if (detailRes?.status !== 'success' || data?.id !== itemData.id || data?.author !== currentUser.account
|
| 386 |
-
|| !Number.isSafeInteger(revision) || revision < 0) throw new Error('Invalid edit detail');
|
| 387 |
-
enrichedItemData = { ...data, edit_revision: revision };
|
| 388 |
-
} catch (_) {
|
| 389 |
if (!isCurrent()) return;
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
}
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
)
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
});
|
| 403 |
|
| 404 |
// 监听进入任务编辑页面的请求
|
| 405 |
-
_onSidebarWindow("comfy-route-edit-task", (e) => {
|
| 406 |
-
if (IS_WEB_MODE) return showToast(t('web.publish_not_supported'), "warning");
|
| 407 |
const { taskData, currentUser } = e.detail;
|
| 408 |
-
|
| 409 |
-
const view = createPublishTaskView(currentUser, taskData);
|
| 410 |
-
showInlineView(view);
|
| 411 |
-
} catch (err) {
|
| 412 |
-
console.error('创建编辑任务视图失败');
|
| 413 |
-
}
|
| 414 |
});
|
| 415 |
|
| 416 |
-
|
| 417 |
-
_onSidebarWindow("comfy-route-edit-post", (e) => {
|
| 418 |
-
if (IS_WEB_MODE) return showToast(t('web.publish_not_supported'), "warning");
|
| 419 |
const { postData, currentUser } = e.detail;
|
| 420 |
-
|
| 421 |
-
showInlineView(view);
|
| 422 |
});
|
| 423 |
|
| 424 |
-
|
| 425 |
-
_onSidebarWindow("comfy-route-edit-prompt", (e) => {
|
| 426 |
-
if (IS_WEB_MODE) return showToast(t('web.publish_not_supported'), "warning");
|
| 427 |
const { promptData, currentUser } = e.detail;
|
| 428 |
-
|
| 429 |
-
showInlineView(view);
|
| 430 |
});
|
| 431 |
|
| 432 |
// 🔔 通知跳转:切换到对应Tab并展开指定卡片
|
|
@@ -545,33 +595,11 @@ export function buildSidebarDOM() {
|
|
| 545 |
triggerLoad(e.detail?.force ?? false);
|
| 546 |
});
|
| 547 |
|
| 548 |
-
sortContainer.querySelector("#btn-open-publish").onclick = () => {
|
| 549 |
const currentUser = topNav.getCurrentUser();
|
| 550 |
-
if (!currentUser) return showToast(t('auth.login_required'),
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
if (currentTab === "tasks") {
|
| 554 |
-
// 任务榜 -> 打开发布任务界面
|
| 555 |
-
const view = createPublishTaskView(currentUser);
|
| 556 |
-
showInlineView(view);
|
| 557 |
-
} else if (currentTab === "posts") {
|
| 558 |
-
// 讨论区 -> 打开发布帖子界面
|
| 559 |
-
const view = createPublishPostView(currentUser);
|
| 560 |
-
showInlineView(view);
|
| 561 |
-
} else if (currentTab === "prompts") {
|
| 562 |
-
// 🧩 提示词 -> 打开发布提示词界面
|
| 563 |
-
const view = createPublishPromptView(currentUser);
|
| 564 |
-
showInlineView(view);
|
| 565 |
-
} else {
|
| 566 |
-
// 工具/Skill/应用/推荐 -> 打开发布内容界面,并自动设置对应类型
|
| 567 |
-
const publishView = createPublishView(currentUser,
|
| 568 |
-
() => hideInlineView(),
|
| 569 |
-
() => { hideInlineView(); triggerLoad(true); },
|
| 570 |
-
null, // editItemData
|
| 571 |
-
currentTab // initialType: tools/skills/apps/recommends
|
| 572 |
-
);
|
| 573 |
-
showInlineView(publishView);
|
| 574 |
-
}
|
| 575 |
};
|
| 576 |
|
| 577 |
tabs.forEach(tab => {
|
|
|
|
| 6 |
// 由 侧边栏入口注册.js 动态加载
|
| 7 |
// ==========================================
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
import { createTopNav } from "../components/顶部导航组件.js";
|
| 10 |
import { loadSidebarContent, disposeSidebarContent } from "./侧边栏数据引擎.js";
|
|
|
|
| 11 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 12 |
import { api, captureRequestSession } from "./网络请求API.js"; // 🔴 编辑模式需调用详情API获取完整数据
|
| 13 |
import { CACHE, UI, getBackgroundKey, IS_WEB_MODE } from "./全局配置.js";
|
|
|
|
| 112 |
_sidebarWindowListeners.push({ event, handler });
|
| 113 |
}
|
| 114 |
|
| 115 |
+
async function loadDetailFactory() {
|
| 116 |
+
return (await import('../market/资源详情页面组件.js')).createItemDetailView;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
async function loadPublishFactory(kind) {
|
| 120 |
+
if (kind === 'resource') return (await import('../market/发布内容组件.js')).createPublishView;
|
| 121 |
+
if (kind === 'task') return (await import('../task/发布任务组件.js')).createPublishTaskView;
|
| 122 |
+
if (kind === 'post') return (await import('../post/发布帖子组件.js')).createPublishPostView;
|
| 123 |
+
if (kind === 'prompt') return (await import('../prompt/发布提示词组件.js')).createPublishPromptView;
|
| 124 |
+
throw new Error('Invalid publish view');
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
export function buildSidebarDOM() {
|
| 128 |
// 🔧 P1修复:重建前先清理上一轮的 window 监听,避免重复注册
|
| 129 |
_cleanupSidebarWindowListeners();
|
|
|
|
| 363 |
|
| 364 |
_onSidebarWindow("comfy-open-detail", (e) => {
|
| 365 |
const { itemData, currentUser } = e.detail;
|
| 366 |
+
void openDetailView(itemData, currentUser);
|
|
|
|
| 367 |
});
|
| 368 |
|
| 369 |
+
const openInlineViewAfterLoad = async (prepareView, ownerCurrent) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
const matchesSession = captureRequestSession();
|
| 371 |
+
const renderToken = getRenderToken();
|
| 372 |
+
const loadingView = document.createElement('div');
|
| 373 |
+
loadingView.className = 'hub-inline-load-state';
|
| 374 |
+
const loadingMessage = document.createElement('span');
|
| 375 |
+
loadingMessage.setAttribute('role', 'status');
|
| 376 |
+
loadingMessage.setAttribute('aria-live', 'polite');
|
| 377 |
+
loadingMessage.textContent = t('common.loading');
|
| 378 |
+
const returnButton = document.createElement('button');
|
| 379 |
+
returnButton.type = 'button';
|
| 380 |
+
returnButton.className = 'ranking-publish-back';
|
| 381 |
+
returnButton.textContent = t('common.back');
|
| 382 |
+
const returnFocus = document.activeElement;
|
| 383 |
+
loadingView.appendChild(loadingMessage);
|
| 384 |
+
loadingView.appendChild(returnButton);
|
| 385 |
+
loadingView._cleanup = () => { returnButton.onclick = null; };
|
| 386 |
showInlineView(loadingView);
|
| 387 |
+
const isCurrent = () => !sidebarDisposed && matchesSession() && ownerCurrent()
|
| 388 |
+
&& renderToken === getRenderToken()
|
| 389 |
&& activeInlineView === loadingView && loadingView.isConnected;
|
| 390 |
+
returnButton.onclick = () => {
|
| 391 |
+
if (activeInlineView !== loadingView || sidebarDisposed) return;
|
| 392 |
+
hideInlineView();
|
| 393 |
+
if (returnFocus?.isConnected) returnFocus.focus();
|
| 394 |
+
};
|
| 395 |
try {
|
| 396 |
+
const createView = await prepareView(isCurrent);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
if (!isCurrent()) return;
|
| 398 |
+
const view = createView();
|
| 399 |
+
if (!isCurrent()) {
|
| 400 |
+
try { view._cleanup?.(); }
|
| 401 |
+
catch { console.warn('Unshown inline view cleanup failed'); }
|
| 402 |
+
try { cleanupImageSandbox(view); }
|
| 403 |
+
finally { view.remove(); }
|
| 404 |
+
return;
|
| 405 |
+
}
|
| 406 |
+
showInlineView(view);
|
| 407 |
+
} catch {
|
| 408 |
+
if (isCurrent()) {
|
| 409 |
+
loadingMessage.textContent = t('common.operation_failed');
|
| 410 |
+
loadingMessage.setAttribute('role', 'alert');
|
| 411 |
+
loadingMessage.setAttribute('aria-live', 'assertive');
|
| 412 |
+
}
|
| 413 |
+
} finally {
|
| 414 |
+
if (activeInlineView === loadingView && !isCurrent()) hideInlineView();
|
| 415 |
}
|
| 416 |
+
};
|
| 417 |
+
|
| 418 |
+
const openDetailView = async (itemData, currentUser) => {
|
| 419 |
+
const account = currentUser?.account;
|
| 420 |
+
const ownerCurrent = () => topNav.getCurrentUser()?.account === account;
|
| 421 |
+
if (!ownerCurrent()) return;
|
| 422 |
+
await openInlineViewAfterLoad(async () => {
|
| 423 |
+
const createView = await loadDetailFactory();
|
| 424 |
+
return () => createView(itemData, currentUser);
|
| 425 |
+
}, ownerCurrent);
|
| 426 |
+
};
|
| 427 |
+
|
| 428 |
+
const openPublishView = async (kind, currentUser, editData = null, initialType) => {
|
| 429 |
+
if (IS_WEB_MODE) return showToast(t('web.publish_not_supported'), 'warning');
|
| 430 |
+
const account = currentUser?.account;
|
| 431 |
+
const ownerCurrent = () => topNav.getCurrentUser()?.account === account;
|
| 432 |
+
if (!account || !ownerCurrent()) return;
|
| 433 |
+
await openInlineViewAfterLoad(async isCurrent => {
|
| 434 |
+
let preparedData = editData;
|
| 435 |
+
if (kind === 'resource' && editData) {
|
| 436 |
+
const itemId = editData.id;
|
| 437 |
+
const detailRes = await api.getItemById(itemId, { noStore: true });
|
| 438 |
+
if (!isCurrent()) return;
|
| 439 |
+
const data = detailRes?.data, revision = data?.edit_revision === undefined ? 0 : data.edit_revision;
|
| 440 |
+
if (detailRes?.status !== 'success' || data?.id !== itemId || data?.author !== account
|
| 441 |
+
|| !Number.isSafeInteger(revision) || revision < 0) throw new Error('Invalid edit detail');
|
| 442 |
+
preparedData = { ...data, edit_revision: revision };
|
| 443 |
+
}
|
| 444 |
+
const createView = await loadPublishFactory(kind);
|
| 445 |
+
if (!isCurrent()) return;
|
| 446 |
+
return () => {
|
| 447 |
+
let view;
|
| 448 |
+
if (kind === 'resource') {
|
| 449 |
+
view = createView(currentUser,
|
| 450 |
+
() => { if (activeInlineView === view) hideInlineView(); },
|
| 451 |
+
() => { if (activeInlineView === view) { hideInlineView(); triggerLoad(true); } },
|
| 452 |
+
preparedData, initialType);
|
| 453 |
+
} else {
|
| 454 |
+
view = createView(currentUser, preparedData);
|
| 455 |
+
}
|
| 456 |
+
return view;
|
| 457 |
+
};
|
| 458 |
+
}, ownerCurrent);
|
| 459 |
+
};
|
| 460 |
+
|
| 461 |
+
_onSidebarWindow("comfy-route-edit-publish", async (e) => {
|
| 462 |
+
const { itemData, currentUser } = e.detail;
|
| 463 |
+
await openPublishView('resource', currentUser, itemData);
|
| 464 |
});
|
| 465 |
|
| 466 |
// 监听进入任务编辑页面的请求
|
| 467 |
+
_onSidebarWindow("comfy-route-edit-task", async (e) => {
|
|
|
|
| 468 |
const { taskData, currentUser } = e.detail;
|
| 469 |
+
await openPublishView('task', currentUser, taskData);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
});
|
| 471 |
|
| 472 |
+
_onSidebarWindow("comfy-route-edit-post", async (e) => {
|
|
|
|
|
|
|
| 473 |
const { postData, currentUser } = e.detail;
|
| 474 |
+
await openPublishView('post', currentUser, postData);
|
|
|
|
| 475 |
});
|
| 476 |
|
| 477 |
+
_onSidebarWindow("comfy-route-edit-prompt", async (e) => {
|
|
|
|
|
|
|
| 478 |
const { promptData, currentUser } = e.detail;
|
| 479 |
+
await openPublishView('prompt', currentUser, promptData);
|
|
|
|
| 480 |
});
|
| 481 |
|
| 482 |
// 🔔 通知跳转:切换到对应Tab并展开指定卡片
|
|
|
|
| 595 |
triggerLoad(e.detail?.force ?? false);
|
| 596 |
});
|
| 597 |
|
| 598 |
+
sortContainer.querySelector("#btn-open-publish").onclick = async () => {
|
| 599 |
const currentUser = topNav.getCurrentUser();
|
| 600 |
+
if (!currentUser) return showToast(t('auth.login_required'), 'warning');
|
| 601 |
+
const kind = { tasks: 'task', posts: 'post', prompts: 'prompt' }[currentTab] || 'resource';
|
| 602 |
+
await openPublishView(kind, currentUser, null, currentTab);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
};
|
| 604 |
|
| 605 |
tabs.forEach(tab => {
|
前端页面/core/全局配置.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
| 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})$/,
|
|
@@ -38,6 +38,13 @@ export const API = {
|
|
| 38 |
LOCAL_CACHE_STATS_TIMEOUT: 10000,
|
| 39 |
LOCAL_CACHE_CLEAR_TIMEOUT: 60000,
|
| 40 |
LOCAL_CACHE_RESPONSE_MAX_BYTES: 8192,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
// GET 请求重试次数(实际尝试 MAX_RETRIES + 1 次)
|
| 43 |
// 🔧 修复:此前本值为 3 且全库无人读取,真正的重试次数硬编码在 网络请求_基础设施.js 中为 2,
|
|
@@ -300,6 +307,8 @@ export const UI = {
|
|
| 300 |
},
|
| 301 |
// Toast 消息配置
|
| 302 |
TOAST: {
|
|
|
|
|
|
|
| 303 |
DURATION: 3000, // 显示时长(毫秒)
|
| 304 |
MAX_VISIBLE: 3, // 最大同时显示数量
|
| 305 |
POSITION: "top-center" // 位置
|
|
@@ -773,3 +782,63 @@ export const PROMPT_LIMITS = Object.freeze({ MAX_PRICE: 10000 });
|
|
| 773 |
|
| 774 |
// 任务定价边界,与云端 task_publication/task_terms 对应。
|
| 775 |
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]) });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, 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})$/,
|
|
|
|
| 38 |
LOCAL_CACHE_STATS_TIMEOUT: 10000,
|
| 39 |
LOCAL_CACHE_CLEAR_TIMEOUT: 60000,
|
| 40 |
LOCAL_CACHE_RESPONSE_MAX_BYTES: 8192,
|
| 41 |
+
// 工作流终态编码上限为512 MiB,另保留64 MiB进度数据;行预算包含data前缀。
|
| 42 |
+
SSE_MAX_RESPONSE_BYTES: 576 * 1024 * 1024,
|
| 43 |
+
SSE_MAX_LINE_BYTES: 512 * 1024 * 1024,
|
| 44 |
+
SSE_MAX_LINES: 262144,
|
| 45 |
+
SSE_YIELD_LINES: 256,
|
| 46 |
+
SSE_YIELD_BYTES: 1024 * 1024,
|
| 47 |
+
SSE_YIELD_CHUNKS: 256,
|
| 48 |
|
| 49 |
// GET 请求重试次数(实际尝试 MAX_RETRIES + 1 次)
|
| 50 |
// 🔧 修复:此前本值为 3 且全库无人读取,真正的重试次数硬编码在 网络请求_基础设施.js 中为 2,
|
|
|
|
| 307 |
},
|
| 308 |
// Toast 消息配置
|
| 309 |
TOAST: {
|
| 310 |
+
OFFSET: 50,
|
| 311 |
+
GAP: 10,
|
| 312 |
DURATION: 3000, // 显示时长(毫秒)
|
| 313 |
MAX_VISIBLE: 3, // 最大同时显示数量
|
| 314 |
POSITION: "top-center" // 位置
|
|
|
|
| 782 |
|
| 783 |
// 任务定价边界,与云端 task_publication/task_terms 对应。
|
| 784 |
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]) });
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
export const TYPE_RULES = {
|
| 788 |
+
tool: {
|
| 789 |
+
recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
|
| 790 |
+
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 791 |
+
},
|
| 792 |
+
skill: {
|
| 793 |
+
recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
|
| 794 |
+
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 795 |
+
},
|
| 796 |
+
app: {
|
| 797 |
+
recommendForm: false, resType: { fixed: 'json' },
|
| 798 |
+
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 799 |
+
},
|
| 800 |
+
recommend: {
|
| 801 |
+
recommendForm: true, resType: { lockedByForm: true },
|
| 802 |
+
privateRepoWhenLink: false, price: false, refund: false, originalRequired: false,
|
| 803 |
+
formToResType: { recommend_tool: 'link', recommend_skill: 'link', recommend_app: 'json', recommend_link: 'netdisk' }
|
| 804 |
+
}
|
| 805 |
+
};
|
| 806 |
+
|
| 807 |
+
// 🔧 设置项 localStorage Key
|
| 808 |
+
const SETTINGS_KEY = "ComfyCommunity_Settings";
|
| 809 |
+
|
| 810 |
+
// 🔧 默认设置值
|
| 811 |
+
const DEFAULT_SETTINGS = {
|
| 812 |
+
showCreatorBanner: true, // 创作者卡片显示背景图
|
| 813 |
+
enableAnimations: true, // ✨ 榜单切换动画
|
| 814 |
+
enableSoundEffects: true, // 🔊 动画音效
|
| 815 |
+
language: 'zh-CN', // 🌐 界面语言
|
| 816 |
+
cacheExpireSeconds: 7200, // ⏱️ 缓存刷新间隔(秒),默认2小时
|
| 817 |
+
// 🚀 后续可扩展更多设置项
|
| 818 |
+
// compactMode: false, // 紧凑模式
|
| 819 |
+
// darkMode: true, // 深色模式
|
| 820 |
+
};
|
| 821 |
+
|
| 822 |
+
/**
|
| 823 |
+
* 📦 获取当前设置
|
| 824 |
+
*/
|
| 825 |
+
export function getSettings() {
|
| 826 |
+
try {
|
| 827 |
+
const saved = localStorage.getItem(SETTINGS_KEY);
|
| 828 |
+
if (saved) {
|
| 829 |
+
return { ...DEFAULT_SETTINGS, ...JSON.parse(saved) };
|
| 830 |
+
}
|
| 831 |
+
} catch (e) {}
|
| 832 |
+
return { ...DEFAULT_SETTINGS };
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
/**
|
| 836 |
+
* 💾 保存设置
|
| 837 |
+
*/
|
| 838 |
+
export function saveSettings(settings) {
|
| 839 |
+
try {
|
| 840 |
+
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
| 841 |
+
// 🔔 广播设置变更事件,让其他组件响应
|
| 842 |
+
window.dispatchEvent(new CustomEvent("comfy-settings-changed", { detail: settings }));
|
| 843 |
+
} catch (e) {}
|
| 844 |
+
}
|
前端页面/core/网络请求_基础设施.js
CHANGED
|
@@ -702,6 +702,7 @@ export async function requestSSE(endpoint, body, onProgress, options = {}) {
|
|
| 702 |
let currentIdleTimeout = options.idleTimeout || IDLE_TIMEOUT_DEFAULT;
|
| 703 |
let idleTimer = null;
|
| 704 |
let finalResult = null;
|
|
|
|
| 705 |
|
| 706 |
const clearIdleTimer = () => {
|
| 707 |
if (idleTimer !== null) {
|
|
@@ -727,16 +728,68 @@ export async function requestSSE(endpoint, body, onProgress, options = {}) {
|
|
| 727 |
throw error;
|
| 728 |
}
|
| 729 |
|
| 730 |
-
const reader = response.body.getReader();
|
| 731 |
-
const decoder = new TextDecoder();
|
| 732 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 733 |
const processSSELine = (line) => {
|
| 734 |
if (!line.startsWith("data: ")) return;
|
| 735 |
let data;
|
| 736 |
try {
|
| 737 |
data = JSON.parse(line.slice(6));
|
| 738 |
-
} catch (
|
| 739 |
-
console.warn("[SSE] 解析事件失败
|
| 740 |
return;
|
| 741 |
}
|
| 742 |
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
@@ -754,6 +807,22 @@ export async function requestSSE(endpoint, body, onProgress, options = {}) {
|
|
| 754 |
console.warn("[SSE] 进度回调失败");
|
| 755 |
}
|
| 756 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 757 |
while (true) {
|
| 758 |
// 使用 Promise.race 实现空闲超时:reader.read() 与空闲定时器竞争
|
| 759 |
const idlePromise = new Promise((_, reject) => {
|
|
@@ -773,7 +842,7 @@ export async function requestSSE(endpoint, body, onProgress, options = {}) {
|
|
| 773 |
// 如果已收到 finalResult,优先返回它(服务器只是没正确关闭连接)
|
| 774 |
if (finalResult) return finalResult;
|
| 775 |
// 否则返回空闲超时错误
|
| 776 |
-
return { status: "error", outcomeUnknown: true, message:
|
| 777 |
}
|
| 778 |
throw e;
|
| 779 |
}
|
|
@@ -781,19 +850,18 @@ export async function requestSSE(endpoint, body, onProgress, options = {}) {
|
|
| 781 |
|
| 782 |
const { done, value } = readResult;
|
| 783 |
if (done) {
|
| 784 |
-
|
| 785 |
-
for (const line of buffer.split("\n")) processSSELine(line);
|
| 786 |
-
buffer = "";
|
| 787 |
break;
|
| 788 |
}
|
| 789 |
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
|
|
|
| 794 |
}
|
| 795 |
|
| 796 |
-
return finalResult || { status: "error", outcomeUnknown: true, message:
|
| 797 |
} catch (err) {
|
| 798 |
// 已收到最终事件时,后续连接关闭错误不应覆盖操作结果或触发重复提交。
|
| 799 |
if (finalResult) return finalResult;
|
|
@@ -802,7 +870,7 @@ export async function requestSSE(endpoint, body, onProgress, options = {}) {
|
|
| 802 |
status: "error",
|
| 803 |
outcomeUnknown: true,
|
| 804 |
cancelRequested: !!externalSignal?.aborted,
|
| 805 |
-
message: externalSignal?.aborted ? t('feedback.request_cancelled') :
|
| 806 |
};
|
| 807 |
}
|
| 808 |
throw err;
|
|
@@ -810,6 +878,12 @@ export async function requestSSE(endpoint, body, onProgress, options = {}) {
|
|
| 810 |
clearTimeout(globalTimer);
|
| 811 |
clearIdleTimer();
|
| 812 |
externalSignal?.removeEventListener("abort", abortFromCaller);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 813 |
}
|
| 814 |
}
|
| 815 |
|
|
|
|
| 702 |
let currentIdleTimeout = options.idleTimeout || IDLE_TIMEOUT_DEFAULT;
|
| 703 |
let idleTimer = null;
|
| 704 |
let finalResult = null;
|
| 705 |
+
let streamReader = null;
|
| 706 |
|
| 707 |
const clearIdleTimer = () => {
|
| 708 |
if (idleTimer !== null) {
|
|
|
|
| 728 |
throw error;
|
| 729 |
}
|
| 730 |
|
| 731 |
+
const reader = streamReader = response.body.getReader();
|
| 732 |
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
| 733 |
+
const decodeSSEChunk = (value, options) => {
|
| 734 |
+
try {
|
| 735 |
+
return decoder.decode(value, options);
|
| 736 |
+
} catch (_) {
|
| 737 |
+
const error = new Error(t('feedback.stream_invalid_encoding'));
|
| 738 |
+
error.code = 'invalid_stream_encoding';
|
| 739 |
+
error.outcomeUnknown = true;
|
| 740 |
+
throw error;
|
| 741 |
+
}
|
| 742 |
+
};
|
| 743 |
+
let totalBytes = 0, lineBytes = 0, lineCount = 0;
|
| 744 |
+
let workBytes = 0, workLines = 0, workChunks = 0;
|
| 745 |
+
const lineParts = [];
|
| 746 |
+
const limitError = () => {
|
| 747 |
+
const error = new Error(t('feedback.stream_limit_exceeded'));
|
| 748 |
+
error.code = 'stream_limit_exceeded';
|
| 749 |
+
error.outcomeUnknown = true;
|
| 750 |
+
return error;
|
| 751 |
+
};
|
| 752 |
+
const appendLinePart = (part) => {
|
| 753 |
+
if (!part) return;
|
| 754 |
+
let level = 0;
|
| 755 |
+
while (lineParts[level] !== undefined) {
|
| 756 |
+
part = lineParts[level] + part;
|
| 757 |
+
lineParts[level++] = undefined;
|
| 758 |
+
}
|
| 759 |
+
lineParts[level] = part;
|
| 760 |
+
};
|
| 761 |
+
const takeLine = () => {
|
| 762 |
+
const line = lineParts.slice().reverse().filter(part => part !== undefined).join('');
|
| 763 |
+
lineParts.length = 0;
|
| 764 |
+
return line;
|
| 765 |
+
};
|
| 766 |
+
const yieldIfNeeded = async () => {
|
| 767 |
+
if (workBytes >= API.SSE_YIELD_BYTES || workLines >= API.SSE_YIELD_LINES || workChunks >= API.SSE_YIELD_CHUNKS) {
|
| 768 |
+
workBytes = workLines = workChunks = 0;
|
| 769 |
+
await new Promise(resolve => setTimeout(resolve, 0));
|
| 770 |
+
}
|
| 771 |
+
if (controller.signal.aborted) throw new DOMException('Aborted', 'AbortError');
|
| 772 |
+
};
|
| 773 |
+
const checkChunkBudget = (value) => {
|
| 774 |
+
totalBytes += value.byteLength;
|
| 775 |
+
if (totalBytes > API.SSE_MAX_RESPONSE_BYTES) throw limitError();
|
| 776 |
+
let start = 0, end;
|
| 777 |
+
while ((end = value.indexOf(10, start)) !== -1) {
|
| 778 |
+
lineBytes += end - start;
|
| 779 |
+
if (lineBytes > API.SSE_MAX_LINE_BYTES) throw limitError();
|
| 780 |
+
lineBytes = 0;
|
| 781 |
+
start = end + 1;
|
| 782 |
+
}
|
| 783 |
+
lineBytes += value.byteLength - start;
|
| 784 |
+
if (lineBytes > API.SSE_MAX_LINE_BYTES) throw limitError();
|
| 785 |
+
};
|
| 786 |
const processSSELine = (line) => {
|
| 787 |
if (!line.startsWith("data: ")) return;
|
| 788 |
let data;
|
| 789 |
try {
|
| 790 |
data = JSON.parse(line.slice(6));
|
| 791 |
+
} catch (_) {
|
| 792 |
+
console.warn("[SSE] 解析事件失败");
|
| 793 |
return;
|
| 794 |
}
|
| 795 |
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
|
|
| 807 |
console.warn("[SSE] 进度回调失败");
|
| 808 |
}
|
| 809 |
};
|
| 810 |
+
const processSSEText = async (text, flush = false) => {
|
| 811 |
+
let start = 0, end;
|
| 812 |
+
while ((end = text.indexOf("\n", start)) !== -1) {
|
| 813 |
+
if (++lineCount > API.SSE_MAX_LINES) throw limitError();
|
| 814 |
+
appendLinePart(text.slice(start, end));
|
| 815 |
+
processSSELine(takeLine());
|
| 816 |
+
start = end + 1;
|
| 817 |
+
workLines++;
|
| 818 |
+
await yieldIfNeeded();
|
| 819 |
+
}
|
| 820 |
+
appendLinePart(text.slice(start));
|
| 821 |
+
if (flush && lineParts.length) {
|
| 822 |
+
if (++lineCount > API.SSE_MAX_LINES) throw limitError();
|
| 823 |
+
processSSELine(takeLine());
|
| 824 |
+
}
|
| 825 |
+
};
|
| 826 |
while (true) {
|
| 827 |
// 使用 Promise.race 实现空闲超时:reader.read() 与空闲定时器竞争
|
| 828 |
const idlePromise = new Promise((_, reject) => {
|
|
|
|
| 842 |
// 如果已收到 finalResult,优先返回它(服务器只是没正确关闭连接)
|
| 843 |
if (finalResult) return finalResult;
|
| 844 |
// 否则返回空闲超时错误
|
| 845 |
+
return { status: "error", outcomeUnknown: true, message: t('feedback.stream_idle_timeout') };
|
| 846 |
}
|
| 847 |
throw e;
|
| 848 |
}
|
|
|
|
| 850 |
|
| 851 |
const { done, value } = readResult;
|
| 852 |
if (done) {
|
| 853 |
+
await processSSEText(decodeSSEChunk(), true);
|
|
|
|
|
|
|
| 854 |
break;
|
| 855 |
}
|
| 856 |
|
| 857 |
+
checkChunkBudget(value);
|
| 858 |
+
workBytes += value.byteLength;
|
| 859 |
+
workChunks++;
|
| 860 |
+
await processSSEText(decodeSSEChunk(value, { stream: true }));
|
| 861 |
+
await yieldIfNeeded();
|
| 862 |
}
|
| 863 |
|
| 864 |
+
return finalResult || { status: "error", outcomeUnknown: true, message: t('feedback.stream_missing_final') };
|
| 865 |
} catch (err) {
|
| 866 |
// 已收到最终事件时,后续连接关闭错误不应覆盖操作结果或触发重复提交。
|
| 867 |
if (finalResult) return finalResult;
|
|
|
|
| 870 |
status: "error",
|
| 871 |
outcomeUnknown: true,
|
| 872 |
cancelRequested: !!externalSignal?.aborted,
|
| 873 |
+
message: externalSignal?.aborted ? t('feedback.request_cancelled') : t('feedback.stream_timeout_unconfirmed')
|
| 874 |
};
|
| 875 |
}
|
| 876 |
throw err;
|
|
|
|
| 878 |
clearTimeout(globalTimer);
|
| 879 |
clearIdleTimer();
|
| 880 |
externalSignal?.removeEventListener("abort", abortFromCaller);
|
| 881 |
+
try { controller.abort(); } catch (_) {}
|
| 882 |
+
if (streamReader) {
|
| 883 |
+
try { Promise.resolve(streamReader.cancel()).catch(() => {}); } catch (_) {}
|
| 884 |
+
try { streamReader.releaseLock(); } catch (_) {}
|
| 885 |
+
streamReader = null;
|
| 886 |
+
}
|
| 887 |
}
|
| 888 |
}
|
| 889 |
|
前端页面/market/创作者卡片组件.js
CHANGED
|
@@ -11,12 +11,11 @@
|
|
| 11 |
import { openOtherUserProfileModal } from "../profile/个人中心视图.js";
|
| 12 |
import { createCommentSection } from "../social/评论与互动组件.js";
|
| 13 |
import { renderTipLevelHTML, getTipLevelRuleShort } from "../components/打赏等级工具.js";
|
| 14 |
-
import { getBannerCacheKey, PLACEHOLDERS, getCachedProfile, getProfileWithSWR, escapeHtml, CACHE, API } from "../core/全局配置.js";
|
| 15 |
import { api } from "../core/网络请求_业务API.js";
|
| 16 |
import { captureRequestSession, requestCancelManager } from "../core/网络请求_基础设施.js";
|
| 17 |
import { getUser, eventBus, EVENTS } from "../core/状态管理.js";
|
| 18 |
import { createTaskViewScope, subscribeTaskSessionChanges } from "../task/任务视图生命周期.js";
|
| 19 |
-
import { getSettings } from "../components/全局设置组件.js";
|
| 20 |
import { t } from "../components/用户体验增强.js";
|
| 21 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 22 |
import { openTipModal } from "../profile/个人中心_赞赏组件.js";
|
|
|
|
| 11 |
import { openOtherUserProfileModal } from "../profile/个人中心视图.js";
|
| 12 |
import { createCommentSection } from "../social/评论与互动组件.js";
|
| 13 |
import { renderTipLevelHTML, getTipLevelRuleShort } from "../components/打赏等级工具.js";
|
| 14 |
+
import { getBannerCacheKey, PLACEHOLDERS, getCachedProfile, getProfileWithSWR, escapeHtml, CACHE, API, getSettings } from "../core/全局配置.js";
|
| 15 |
import { api } from "../core/网络请求_业务API.js";
|
| 16 |
import { captureRequestSession, requestCancelManager } from "../core/网络请求_基础设施.js";
|
| 17 |
import { getUser, eventBus, EVENTS } from "../core/状态管理.js";
|
| 18 |
import { createTaskViewScope, subscribeTaskSessionChanges } from "../task/任务视图生命周期.js";
|
|
|
|
| 19 |
import { t } from "../components/用户体验增强.js";
|
| 20 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 21 |
import { openTipModal } from "../profile/个人中心_赞赏组件.js";
|
前端页面/market/发布内容_UI模板.js
CHANGED
|
@@ -81,7 +81,7 @@ export function generatePublishHTML(isEditMode, viewTitle, submitBtnText, hasExi
|
|
| 81 |
</label>
|
| 82 |
<div id="github-token-container" class="ranking-publish__token" style="display: ${hasExistingToken ? 'block' : 'none'};">
|
| 83 |
<label for="pub-github-token" class="ranking-publish__security-label">🔒 ${escapeHtml(t('publish.pat_label'))} <span class="ranking-publish__required" aria-hidden="true">*</span></label>
|
| 84 |
-
<div id="publish-pat-hint" class="ranking-publish__hint">${escapeHtml(t('publish.pat_hint'))}</div>
|
| 85 |
<input type="password" id="pub-github-token" value="${escapeHtml(isEditMode ? (safeData.github_token || '') : '')}" placeholder="${hasExistingToken && !safeData.github_token ? escapeHtml(t('publish.token_keep_hint')) : escapeHtml(t('publish.token_placeholder'))}" class="form-input" aria-describedby="publish-pat-hint">
|
| 86 |
</div>
|
| 87 |
</div>
|
|
|
|
| 81 |
</label>
|
| 82 |
<div id="github-token-container" class="ranking-publish__token" style="display: ${hasExistingToken ? 'block' : 'none'};">
|
| 83 |
<label for="pub-github-token" class="ranking-publish__security-label">🔒 ${escapeHtml(t('publish.pat_label'))} <span class="ranking-publish__required" aria-hidden="true">*</span></label>
|
| 84 |
+
<div id="publish-pat-hint" class="ranking-publish__hint">${escapeHtml(t('publish.pat_hint'))} <strong class="ranking-publish__security-emphasis">${escapeHtml(t('publish.pat_privacy'))}</strong></div>
|
| 85 |
<input type="password" id="pub-github-token" value="${escapeHtml(isEditMode ? (safeData.github_token || '') : '')}" placeholder="${hasExistingToken && !safeData.github_token ? escapeHtml(t('publish.token_keep_hint')) : escapeHtml(t('publish.token_placeholder'))}" class="form-input" aria-describedby="publish-pat-hint">
|
| 86 |
</div>
|
| 87 |
</div>
|
前端页面/market/发布内容_提交引擎.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
| 1 |
import { processImageCore as _processImageCore } from "./有界图片处理.js";
|
| 2 |
import { validateImageFile } from "./图片文件检查.js";
|
| 3 |
-
import { IMAGE_PROCESS } from "../core/全局配置.js";
|
| 4 |
// 前端页面/market/发布内容_提交引擎.js
|
| 5 |
import { createPublishSubmissionScope } from "./发布提交生命周期.js";
|
| 6 |
import { api } from "../core/网络请求API.js";
|
| 7 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 8 |
import { t } from "../components/用户体验增强.js";
|
| 9 |
import { removeCache, removeCacheByPrefix } from "../components/性能优化工具.js";
|
| 10 |
-
import { TYPE_RULES } from "./发布内容组件.js"; // 📋 四榜发布表单声明式规则(与 UI 联动同源)
|
| 11 |
|
| 12 |
/**
|
| 13 |
* 📋 获取指定主类型的提交规则(校验与字段组装统一读取,与 UI 显隐同源)
|
|
|
|
| 1 |
import { processImageCore as _processImageCore } from "./有界图片处理.js";
|
| 2 |
import { validateImageFile } from "./图片文件检查.js";
|
| 3 |
+
import { IMAGE_PROCESS, TYPE_RULES } from "../core/全局配置.js";
|
| 4 |
// 前端页面/market/发布内容_提交引擎.js
|
| 5 |
import { createPublishSubmissionScope } from "./发布提交生命周期.js";
|
| 6 |
import { api } from "../core/网络请求API.js";
|
| 7 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 8 |
import { t } from "../components/用户体验增强.js";
|
| 9 |
import { removeCache, removeCacheByPrefix } from "../components/性能优化工具.js";
|
|
|
|
| 10 |
|
| 11 |
/**
|
| 12 |
* 📋 获取指定主类型的提交规则(校验与字段组装统一读取,与 UI 显隐同源)
|
前端页面/market/发布内容组件.js
CHANGED
|
@@ -6,7 +6,7 @@ import { handlePublishSubmit } from "./发布内容_提交引擎.js";
|
|
| 6 |
import { t } from "../components/用户体验增强.js";
|
| 7 |
import { showConfirm, showToast } from "../components/UI交互提示组件.js";
|
| 8 |
import { globalModal } from "../components/全局弹窗管理器.js";
|
| 9 |
-
import { escapeHtml } from "../core/全局配置.js";
|
| 10 |
|
| 11 |
// ==========================================
|
| 12 |
// 📌 模块级常量
|
|
@@ -14,25 +14,7 @@ import { escapeHtml } from "../core/全局配置.js";
|
|
| 14 |
|
| 15 |
// 📋 四榜发布表单声明式规则:tool 插件榜 / skill Skill榜 / app 工作流 / recommend 推荐榜
|
| 16 |
// UI 显隐、联动、校验统一读取此配置,避免类型分支散落多处
|
| 17 |
-
export
|
| 18 |
-
tool: {
|
| 19 |
-
recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
|
| 20 |
-
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 21 |
-
},
|
| 22 |
-
skill: {
|
| 23 |
-
recommendForm: false, resType: { selectable: ['link', 'netdisk'] },
|
| 24 |
-
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 25 |
-
},
|
| 26 |
-
app: {
|
| 27 |
-
recommendForm: false, resType: { fixed: 'json' },
|
| 28 |
-
privateRepoWhenLink: true, price: true, refund: true, originalRequired: true
|
| 29 |
-
},
|
| 30 |
-
recommend: {
|
| 31 |
-
recommendForm: true, resType: { lockedByForm: true },
|
| 32 |
-
privateRepoWhenLink: false, price: false, refund: false, originalRequired: false,
|
| 33 |
-
formToResType: { recommend_tool: 'link', recommend_skill: 'link', recommend_app: 'json', recommend_link: 'netdisk' }
|
| 34 |
-
}
|
| 35 |
-
};
|
| 36 |
|
| 37 |
// 封面标签统一样式
|
| 38 |
const _COVER_LABEL_STYLE = 'position: absolute; top: 2px; left: 2px; background: #4CAF50; color: #fff; font-size: 10px; padding: 1px 4px; border-radius: 2px; pointer-events: none;';
|
|
|
|
| 6 |
import { t } from "../components/用户体验增强.js";
|
| 7 |
import { showConfirm, showToast } from "../components/UI交互提示组件.js";
|
| 8 |
import { globalModal } from "../components/全局弹窗管理器.js";
|
| 9 |
+
import { escapeHtml, TYPE_RULES } from "../core/全局配置.js";
|
| 10 |
|
| 11 |
// ==========================================
|
| 12 |
// 📌 模块级常量
|
|
|
|
| 14 |
|
| 15 |
// 📋 四榜发布表单声明式规则:tool 插件榜 / skill Skill榜 / app 工作流 / recommend 推荐榜
|
| 16 |
// UI 显隐、联动、校验统一读取此配置,避免类型分支散落多处
|
| 17 |
+
export { TYPE_RULES };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
// 封面标签统一样式
|
| 20 |
const _COVER_LABEL_STYLE = 'position: absolute; top: 2px; left: 2px; background: #4CAF50; color: #fff; font-size: 10px; padding: 1px 4px; border-radius: 2px; pointer-events: none;';
|
前端页面/market/资源安装引擎.js
CHANGED
|
@@ -139,6 +139,23 @@ const WORKFLOW_STAGE_KEYS = {
|
|
| 139 |
error: 'workflow.stage_failed'
|
| 140 |
};
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
function _getWorkflowErrorMessage(result) {
|
| 143 |
if (result?.outcomeUnknown) return t('workflow.connection_uncertain');
|
| 144 |
const errorKeys = {
|
|
@@ -599,8 +616,7 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 599 |
const progress = trackProgress();
|
| 600 |
const cancelController = new AbortController();
|
| 601 |
progress.addCancelButton(t('install.cancel_in_progress'), () => cancelController.abort());
|
| 602 |
-
const
|
| 603 |
-
const installStartedAt = Date.now();
|
| 604 |
|
| 605 |
try {
|
| 606 |
// 发起 SSE 流式安装请求
|
|
@@ -609,15 +625,8 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 609 |
{ url: itemData.link, id: itemData.id, account, token },
|
| 610 |
(event) => {
|
| 611 |
if (!isCurrent()) return;
|
| 612 |
-
|
| 613 |
const rawStage = typeof event.stage === "string" ? event.stage : "";
|
| 614 |
-
diagnosticEvents.push({
|
| 615 |
-
stageKey: Object.hasOwn(INSTALL_STAGE_KEYS, rawStage) ? INSTALL_STAGE_KEYS[rawStage] : "",
|
| 616 |
-
stage: /^[a-z0-9_]{1,40}$/i.test(rawStage) ? rawStage : "",
|
| 617 |
-
progress: Number.isFinite(event.progress) ? Math.max(0, Math.min(100, event.progress)) : null,
|
| 618 |
-
time: `${Math.floor(elapsedSeconds / 60)}:${String(elapsedSeconds % 60).padStart(2, "0")}`
|
| 619 |
-
});
|
| 620 |
-
if (diagnosticEvents.length > 50) diagnosticEvents.shift();
|
| 621 |
const stageKey = Object.hasOwn(INSTALL_STAGE_KEYS, rawStage) ? INSTALL_STAGE_KEYS[rawStage] : "";
|
| 622 |
progress.update(event.progress, event.status === "error"
|
| 623 |
? _getInstallErrorMessage(event)
|
|
@@ -640,7 +649,7 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 640 |
phase = "unknown"; btnUse.removeAttribute("aria-busy");
|
| 641 |
const message = t('install.connection_uncertain');
|
| 642 |
progress.error(message);
|
| 643 |
-
progress.showDiagnostics(
|
| 644 |
showToast(message, "warning");
|
| 645 |
return;
|
| 646 |
}
|
|
@@ -675,7 +684,7 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 675 |
if (result?.outcomeUnknown) { phase = "unknown"; btnUse.removeAttribute("aria-busy"); }
|
| 676 |
const message = _getInstallErrorMessage(result);
|
| 677 |
progress.error(message);
|
| 678 |
-
progress.showDiagnostics(
|
| 679 |
showToast(t('install.failed_for_item', { title: itemData.title, message }), "error");
|
| 680 |
}
|
| 681 |
} catch(err) {
|
|
@@ -684,7 +693,7 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 684 |
phase = "unknown"; btnUse.removeAttribute("aria-busy");
|
| 685 |
const message = t('install.connection_uncertain');
|
| 686 |
progress.error(message);
|
| 687 |
-
progress.showDiagnostics(
|
| 688 |
showToast(message, "error");
|
| 689 |
return;
|
| 690 |
}
|
|
@@ -764,6 +773,7 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 764 |
};
|
| 765 |
} else if (isApp) {
|
| 766 |
const progress = trackProgress();
|
|
|
|
| 767 |
|
| 768 |
// 附带 account 凭证,解决问题 2 (一键鉴权加载)
|
| 769 |
try {
|
|
@@ -772,6 +782,7 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 772 |
{ url: itemData.link, id: itemData.id, account, token },
|
| 773 |
(event) => {
|
| 774 |
if (!isCurrent()) return;
|
|
|
|
| 775 |
const stageKey = typeof event.stage === "string" && Object.hasOwn(WORKFLOW_STAGE_KEYS, event.stage)
|
| 776 |
? WORKFLOW_STAGE_KEYS[event.stage]
|
| 777 |
: "";
|
|
@@ -790,6 +801,7 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 790 |
if (!graphLoaded) {
|
| 791 |
const message = t('workflow.canvas_load_failed');
|
| 792 |
progress.error(message);
|
|
|
|
| 793 |
showToast(message, "error");
|
| 794 |
return;
|
| 795 |
}
|
|
@@ -821,8 +833,8 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 821 |
const localizedMessage = result.error_code
|
| 822 |
? message
|
| 823 |
: t('workflow.failed', { message });
|
| 824 |
-
progress.error(
|
| 825 |
-
|
| 826 |
showToast(localizedMessage, "error");
|
| 827 |
}
|
| 828 |
} catch(err) {
|
|
@@ -831,6 +843,7 @@ export function setupResourceInstall(btnUse, itemData, currentUser, inlineStatus
|
|
| 831 |
phase = "unknown"; btnUse.removeAttribute("aria-busy");
|
| 832 |
const message = t('workflow.connection_uncertain');
|
| 833 |
progress.error(message);
|
|
|
|
| 834 |
showToast(message, "error");
|
| 835 |
return;
|
| 836 |
}
|
|
|
|
| 139 |
error: 'workflow.stage_failed'
|
| 140 |
};
|
| 141 |
|
| 142 |
+
function _createProgressDiagnostics(stageKeys) {
|
| 143 |
+
const entries = [], startedAt = Date.now();
|
| 144 |
+
return {
|
| 145 |
+
entries,
|
| 146 |
+
record(event) {
|
| 147 |
+
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
|
| 148 |
+
const rawStage = typeof event.stage === 'string' ? event.stage : '';
|
| 149 |
+
entries.push({
|
| 150 |
+
stageKey: Object.hasOwn(stageKeys, rawStage) ? stageKeys[rawStage] : '',
|
| 151 |
+
progress: Number.isFinite(event.progress) ? Math.max(0, Math.min(100, event.progress)) : null,
|
| 152 |
+
time: `${Math.floor(elapsedSeconds / 60)}:${String(elapsedSeconds % 60).padStart(2, '0')}`
|
| 153 |
+
});
|
| 154 |
+
if (entries.length > 50) entries.shift();
|
| 155 |
+
}
|
| 156 |
+
};
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
function _getWorkflowErrorMessage(result) {
|
| 160 |
if (result?.outcomeUnknown) return t('workflow.connection_uncertain');
|
| 161 |
const errorKeys = {
|
|
|
|
| 616 |
const progress = trackProgress();
|
| 617 |
const cancelController = new AbortController();
|
| 618 |
progress.addCancelButton(t('install.cancel_in_progress'), () => cancelController.abort());
|
| 619 |
+
const diagnostics = _createProgressDiagnostics(INSTALL_STAGE_KEYS);
|
|
|
|
| 620 |
|
| 621 |
try {
|
| 622 |
// 发起 SSE 流式安装请求
|
|
|
|
| 625 |
{ url: itemData.link, id: itemData.id, account, token },
|
| 626 |
(event) => {
|
| 627 |
if (!isCurrent()) return;
|
| 628 |
+
diagnostics.record(event);
|
| 629 |
const rawStage = typeof event.stage === "string" ? event.stage : "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
const stageKey = Object.hasOwn(INSTALL_STAGE_KEYS, rawStage) ? INSTALL_STAGE_KEYS[rawStage] : "";
|
| 631 |
progress.update(event.progress, event.status === "error"
|
| 632 |
? _getInstallErrorMessage(event)
|
|
|
|
| 649 |
phase = "unknown"; btnUse.removeAttribute("aria-busy");
|
| 650 |
const message = t('install.connection_uncertain');
|
| 651 |
progress.error(message);
|
| 652 |
+
progress.showDiagnostics(diagnostics.entries, result);
|
| 653 |
showToast(message, "warning");
|
| 654 |
return;
|
| 655 |
}
|
|
|
|
| 684 |
if (result?.outcomeUnknown) { phase = "unknown"; btnUse.removeAttribute("aria-busy"); }
|
| 685 |
const message = _getInstallErrorMessage(result);
|
| 686 |
progress.error(message);
|
| 687 |
+
progress.showDiagnostics(diagnostics.entries, result);
|
| 688 |
showToast(t('install.failed_for_item', { title: itemData.title, message }), "error");
|
| 689 |
}
|
| 690 |
} catch(err) {
|
|
|
|
| 693 |
phase = "unknown"; btnUse.removeAttribute("aria-busy");
|
| 694 |
const message = t('install.connection_uncertain');
|
| 695 |
progress.error(message);
|
| 696 |
+
progress.showDiagnostics(diagnostics.entries, err);
|
| 697 |
showToast(message, "error");
|
| 698 |
return;
|
| 699 |
}
|
|
|
|
| 773 |
};
|
| 774 |
} else if (isApp) {
|
| 775 |
const progress = trackProgress();
|
| 776 |
+
const diagnostics = _createProgressDiagnostics(WORKFLOW_STAGE_KEYS);
|
| 777 |
|
| 778 |
// 附带 account 凭证,解决问题 2 (一键鉴权加载)
|
| 779 |
try {
|
|
|
|
| 782 |
{ url: itemData.link, id: itemData.id, account, token },
|
| 783 |
(event) => {
|
| 784 |
if (!isCurrent()) return;
|
| 785 |
+
diagnostics.record(event);
|
| 786 |
const stageKey = typeof event.stage === "string" && Object.hasOwn(WORKFLOW_STAGE_KEYS, event.stage)
|
| 787 |
? WORKFLOW_STAGE_KEYS[event.stage]
|
| 788 |
: "";
|
|
|
|
| 801 |
if (!graphLoaded) {
|
| 802 |
const message = t('workflow.canvas_load_failed');
|
| 803 |
progress.error(message);
|
| 804 |
+
progress.showDiagnostics(diagnostics.entries, result);
|
| 805 |
showToast(message, "error");
|
| 806 |
return;
|
| 807 |
}
|
|
|
|
| 833 |
const localizedMessage = result.error_code
|
| 834 |
? message
|
| 835 |
: t('workflow.failed', { message });
|
| 836 |
+
progress.error(localizedMessage);
|
| 837 |
+
progress.showDiagnostics(diagnostics.entries, result);
|
| 838 |
showToast(localizedMessage, "error");
|
| 839 |
}
|
| 840 |
} catch(err) {
|
|
|
|
| 843 |
phase = "unknown"; btnUse.removeAttribute("aria-busy");
|
| 844 |
const message = t('workflow.connection_uncertain');
|
| 845 |
progress.error(message);
|
| 846 |
+
progress.showDiagnostics(diagnostics.entries, err);
|
| 847 |
showToast(message, "error");
|
| 848 |
return;
|
| 849 |
}
|
前端页面/market/资源详情页面组件.js
CHANGED
|
@@ -51,16 +51,16 @@ async function renderRefundButton(container, itemData, currentUser, scope, purch
|
|
| 51 |
const hoursLeft = Number(statusRes.refund_hours_left);
|
| 52 |
if (!Number.isFinite(hoursLeft) || hoursLeft < 0) throw new Error("Invalid refund hours");
|
| 53 |
refundArea.innerHTML = `
|
| 54 |
-
<div
|
| 55 |
-
<div
|
| 56 |
-
<div
|
| 57 |
🔄 ${t('item.regret_mode')}${t('item.hours_left', { hours: hoursLeft.toFixed(1) })}
|
| 58 |
</div>
|
| 59 |
-
<button id="btn-refund"
|
| 60 |
💸 ${t('item.apply_refund')}
|
| 61 |
</button>
|
| 62 |
</div>
|
| 63 |
-
<div
|
| 64 |
⚠️ ${t('item.refund_warning')}
|
| 65 |
</div>
|
| 66 |
</div>
|
|
@@ -71,7 +71,7 @@ async function renderRefundButton(container, itemData, currentUser, scope, purch
|
|
| 71 |
};
|
| 72 |
} else {
|
| 73 |
refundArea.innerHTML = `
|
| 74 |
-
<div
|
| 75 |
✅ ${t('item.refund_expired')}
|
| 76 |
</div>
|
| 77 |
`;
|
|
@@ -121,12 +121,7 @@ export function createItemDetailView(itemData, currentUser) {
|
|
| 121 |
itemData = proxyImages(itemData);
|
| 122 |
|
| 123 |
const container = document.createElement("div");
|
| 124 |
-
|
| 125 |
-
display: "flex", flexDirection: "column", gap: "15px", color: "#ccc",
|
| 126 |
-
// 🔧 自适应高度:flex 填充剩余空间,内容超出时在容器内部滚动(与主界面保持一致)
|
| 127 |
-
fontSize: "14px", padding: "15px", overflowY: "auto", flex: "1", minHeight: "0", boxSizing: "border-box",
|
| 128 |
-
backgroundColor: "#13151c"
|
| 129 |
-
});
|
| 130 |
|
| 131 |
const cachedAuthor = getCachedProfile(itemData.author);
|
| 132 |
const authorName = cachedAuthor?.name || itemData.author || t('common.unknown_user');
|
|
@@ -138,34 +133,32 @@ export function createItemDetailView(itemData, currentUser) {
|
|
| 138 |
// 🗑️ 判断是否有权限删除
|
| 139 |
const showDeleteBtn = canDeleteItem(itemData, currentUser);
|
| 140 |
const deleteBtnHtml = showDeleteBtn ? `
|
| 141 |
-
<button id="btn-delete-item"
|
| 142 |
` : '';
|
| 143 |
|
| 144 |
let authorInfoHtml = `
|
| 145 |
-
<div
|
| 146 |
<div>
|
| 147 |
<strong>${t('item.author')}:</strong> <span id="detail-author-name">${escapeHtml(authorName)}</span>
|
| 148 |
<!-- 原创标识 -->
|
| 149 |
${itemData.is_original ? `
|
| 150 |
-
<div
|
| 151 |
-
background: linear-gradient(135deg, #FF6B35, #FF8F00); border-radius: 12px;
|
| 152 |
-
font-size: 11px; color: #fff; font-weight: 500; margin-left: 8px;">
|
| 153 |
🎨 ${t('item.original_notice')}
|
| 154 |
</div>
|
| 155 |
` : ''}
|
| 156 |
</div>
|
| 157 |
-
<div
|
| 158 |
-
<button id="btn-tip-item"
|
| 159 |
${deleteBtnHtml}
|
| 160 |
</div>
|
| 161 |
</div>
|
| 162 |
-
<div
|
| 163 |
|
| 164 |
<!-- 🔄 P7后悔模式:退款区域 -->
|
| 165 |
-
<div id="refund-area"></div>
|
| 166 |
|
| 167 |
-
<div
|
| 168 |
-
<div
|
| 169 |
${boardHtml}
|
| 170 |
</div>
|
| 171 |
`;
|
|
@@ -193,24 +186,23 @@ export function createItemDetailView(itemData, currentUser) {
|
|
| 193 |
actionBtnHtml = `<button type="button" id="btn-use-item" class="resource-install__button resource-install__entry" data-install-state="ready">⬇️ ${t('item.get_now')} <span class="resource-install__price">${isFree ? t('item.free') : escapeHtml(itemData.price) + ' ' + t('common.credits')}</span></button>`;
|
| 194 |
}
|
| 195 |
|
| 196 |
-
// 🚀 返回按钮位置可调整参数:margin-left 控制右移,margin-top 控制下移
|
| 197 |
container.innerHTML = `
|
| 198 |
-
<button id="btn-back-detail"
|
| 199 |
⬅ ${t('common.back')}
|
| 200 |
</button>
|
| 201 |
|
| 202 |
-
<div
|
| 203 |
-
<div
|
| 204 |
⚙️ ${t('item.detail_info')}
|
| 205 |
</div>
|
| 206 |
-
<div
|
| 207 |
</div>
|
| 208 |
|
| 209 |
-
<div
|
| 210 |
-
<div
|
| 211 |
🎉 ${t('item.other_info')}
|
| 212 |
</div>
|
| 213 |
-
<div
|
| 214 |
${authorInfoHtml}
|
| 215 |
</div>
|
| 216 |
</div>
|
|
@@ -231,7 +223,8 @@ export function createItemDetailView(itemData, currentUser) {
|
|
| 231 |
// 🚀 创建安装状态容器
|
| 232 |
const inlineStatusBox = document.createElement("div");
|
| 233 |
inlineStatusBox.id = "inline-status-box";
|
| 234 |
-
|
|
|
|
| 235 |
container.appendChild(inlineStatusBox);
|
| 236 |
if (!localVersionStatus.available) {
|
| 237 |
inlineStatusBox.style.display = "block";
|
|
|
|
| 51 |
const hoursLeft = Number(statusRes.refund_hours_left);
|
| 52 |
if (!Number.isFinite(hoursLeft) || hoursLeft < 0) throw new Error("Invalid refund hours");
|
| 53 |
refundArea.innerHTML = `
|
| 54 |
+
<div class="resource-detail__refund-panel">
|
| 55 |
+
<div class="resource-detail__refund-header">
|
| 56 |
+
<div class="resource-detail__refund-copy">
|
| 57 |
🔄 ${t('item.regret_mode')}${t('item.hours_left', { hours: hoursLeft.toFixed(1) })}
|
| 58 |
</div>
|
| 59 |
+
<button type="button" id="btn-refund" class="resource-detail__button resource-detail__button--warning">
|
| 60 |
💸 ${t('item.apply_refund')}
|
| 61 |
</button>
|
| 62 |
</div>
|
| 63 |
+
<div class="resource-detail__refund-warning">
|
| 64 |
⚠️ ${t('item.refund_warning')}
|
| 65 |
</div>
|
| 66 |
</div>
|
|
|
|
| 71 |
};
|
| 72 |
} else {
|
| 73 |
refundArea.innerHTML = `
|
| 74 |
+
<div class="resource-detail__refund-expired">
|
| 75 |
✅ ${t('item.refund_expired')}
|
| 76 |
</div>
|
| 77 |
`;
|
|
|
|
| 121 |
itemData = proxyImages(itemData);
|
| 122 |
|
| 123 |
const container = document.createElement("div");
|
| 124 |
+
container.className = "resource-page";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
const cachedAuthor = getCachedProfile(itemData.author);
|
| 127 |
const authorName = cachedAuthor?.name || itemData.author || t('common.unknown_user');
|
|
|
|
| 133 |
// 🗑️ 判断是否有权限删除
|
| 134 |
const showDeleteBtn = canDeleteItem(itemData, currentUser);
|
| 135 |
const deleteBtnHtml = showDeleteBtn ? `
|
| 136 |
+
<button type="button" id="btn-delete-item" class="resource-detail__button resource-detail__button--danger">🗑️ ${t('common.delete')}</button>
|
| 137 |
` : '';
|
| 138 |
|
| 139 |
let authorInfoHtml = `
|
| 140 |
+
<div class="resource-page__author">
|
| 141 |
<div>
|
| 142 |
<strong>${t('item.author')}:</strong> <span id="detail-author-name">${escapeHtml(authorName)}</span>
|
| 143 |
<!-- 原创标识 -->
|
| 144 |
${itemData.is_original ? `
|
| 145 |
+
<div class="resource-detail__badge resource-detail__badge--original">
|
|
|
|
|
|
|
| 146 |
🎨 ${t('item.original_notice')}
|
| 147 |
</div>
|
| 148 |
` : ''}
|
| 149 |
</div>
|
| 150 |
+
<div class="resource-page__actions">
|
| 151 |
+
<button type="button" id="btn-tip-item" class="resource-detail__button resource-detail__button--tip">💰 ${t('item.tip_encourage')}</button>
|
| 152 |
${deleteBtnHtml}
|
| 153 |
</div>
|
| 154 |
</div>
|
| 155 |
+
<div class="resource-page__thanks">${escapeHtml(t('item.sponsor_thanks'))}</div>
|
| 156 |
|
| 157 |
<!-- 🔄 P7后悔模式:退款区域 -->
|
| 158 |
+
<div id="refund-area" class="resource-page__refund"></div>
|
| 159 |
|
| 160 |
+
<div class="resource-page__tips">
|
| 161 |
+
<div class="resource-detail__heading">💖 ${t('item.tip_board_title')}</div>
|
| 162 |
${boardHtml}
|
| 163 |
</div>
|
| 164 |
`;
|
|
|
|
| 186 |
actionBtnHtml = `<button type="button" id="btn-use-item" class="resource-install__button resource-install__entry" data-install-state="ready">⬇️ ${t('item.get_now')} <span class="resource-install__price">${isFree ? t('item.free') : escapeHtml(itemData.price) + ' ' + t('common.credits')}</span></button>`;
|
| 187 |
}
|
| 188 |
|
|
|
|
| 189 |
container.innerHTML = `
|
| 190 |
+
<button type="button" id="btn-back-detail" class="resource-detail__button resource-page__back">
|
| 191 |
⬅ ${t('common.back')}
|
| 192 |
</button>
|
| 193 |
|
| 194 |
+
<div class="resource-page__section">
|
| 195 |
+
<div class="resource-page__heading">
|
| 196 |
⚙️ ${t('item.detail_info')}
|
| 197 |
</div>
|
| 198 |
+
<div class="resource-page__description">${escapeHtml(itemData.fullDesc || itemData.shortDesc)}</div>
|
| 199 |
</div>
|
| 200 |
|
| 201 |
+
<div class="resource-page__section">
|
| 202 |
+
<div class="resource-page__heading">
|
| 203 |
🎉 ${t('item.other_info')}
|
| 204 |
</div>
|
| 205 |
+
<div class="resource-page__info">
|
| 206 |
${authorInfoHtml}
|
| 207 |
</div>
|
| 208 |
</div>
|
|
|
|
| 223 |
// 🚀 创建安装状态容器
|
| 224 |
const inlineStatusBox = document.createElement("div");
|
| 225 |
inlineStatusBox.id = "inline-status-box";
|
| 226 |
+
inlineStatusBox.className = "resource-detail__status resource-install__status";
|
| 227 |
+
inlineStatusBox.style.display = "none";
|
| 228 |
container.appendChild(inlineStatusBox);
|
| 229 |
if (!localVersionStatus.available) {
|
| 230 |
inlineStatusBox.style.display = "block";
|
前端页面/profile/个人设置表单组件.js
CHANGED
|
@@ -121,11 +121,13 @@ export function createSettingsForm(initialUserData, onCancelCallback, onSaveSucc
|
|
| 121 |
container.className = 'ranking-settings-form';
|
| 122 |
const sessionMatches = captureRequestSession();
|
| 123 |
let disposed = false, saving = false;
|
|
|
|
| 124 |
const readers = new Set(), cropControllers = new Set();
|
| 125 |
const current = () => !disposed && container.isConnected && sessionMatches() && (options.isCurrent?.() ?? true);
|
| 126 |
container._cleanup = () => {
|
| 127 |
if (disposed) return;
|
| 128 |
disposed = true;
|
|
|
|
| 129 |
for (const controller of cropControllers) { try { controller.abort(); } catch (_) {} }
|
| 130 |
cropControllers.clear();
|
| 131 |
for (const reader of readers) { try { reader.abort(); } catch (_) {} }
|
|
@@ -243,15 +245,16 @@ export function createSettingsForm(initialUserData, onCancelCallback, onSaveSucc
|
|
| 243 |
<div style="background: var(--comfy-input-bg); padding: 12px; border-radius: 6px; border: 1px solid #444;">
|
| 244 |
<div style="display: flex; align-items: center; justify-content: space-between;">
|
| 245 |
<div>
|
| 246 |
-
<div style="font-size: 13px; color: #fff; margin-bottom: 4px;">🖼️
|
| 247 |
-
<div style="font-size: 11px; color: #888;">
|
| 248 |
</div>
|
| 249 |
<label class="toggle-switch" style="position: relative; display: inline-block; width: 50px; height: 26px;">
|
| 250 |
-
<input type="checkbox" id="admin-image-moderation" style="opacity: 0; width: 0; height: 0;">
|
| 251 |
<span class="toggle-slider" style="position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--comfy-input-bg); transition: .3s; border-radius: 26px;"></span>
|
| 252 |
</label>
|
| 253 |
</div>
|
| 254 |
-
<div id="moderation-status" style="margin-top: 8px; font-size: 11px; color: #
|
|
|
|
| 255 |
</div>
|
| 256 |
</div>
|
| 257 |
</div>
|
|
@@ -316,56 +319,87 @@ export function createSettingsForm(initialUserData, onCancelCallback, onSaveSucc
|
|
| 316 |
document.head.appendChild(toggleStyles);
|
| 317 |
}
|
| 318 |
|
| 319 |
-
// 🔒 管理员:初始化图像审核开关状态
|
| 320 |
if (isAdmin) {
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
try {
|
| 328 |
-
|
| 329 |
-
const res = await api.getSystemConfig('image_moderation');
|
| 330 |
if (!current()) return;
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
? `<span style="color: #4CAF50;">✅ ${escapeHtml(t('settings_form.enabled'))}</span> | ${escapeHtml(t('settings_form.moderation_quota', { aliyun: quota.aliyun || 0, tencent: quota.tencent || 0 }))}`
|
| 338 |
-
: `<span style="color: #888;">⚪ ${escapeHtml(t('settings_form.disabled'))}</span> | ${escapeHtml(t('settings_form.moderation_skipped'))}`;
|
| 339 |
-
}
|
| 340 |
-
} catch (e) {
|
| 341 |
-
console.warn('获取审核状态失败:', e?.name || 'Error');
|
| 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 |
// 加载本地界面背景预览(使用账号区分键)
|
|
@@ -503,7 +537,7 @@ export function createSettingsForm(initialUserData, onCancelCallback, onSaveSucc
|
|
| 503 |
for (const [control] of lockedControls) control.disabled = true;
|
| 504 |
const unlock = () => {
|
| 505 |
for (const [control, disabled] of lockedControls) control.disabled = disabled;
|
| 506 |
-
saving = false; refreshSaveAvailability();
|
| 507 |
};
|
| 508 |
|
| 509 |
try {
|
|
|
|
| 121 |
container.className = 'ranking-settings-form';
|
| 122 |
const sessionMatches = captureRequestSession();
|
| 123 |
let disposed = false, saving = false;
|
| 124 |
+
let refreshModerationControls = () => {}, releaseModeration = () => {};
|
| 125 |
const readers = new Set(), cropControllers = new Set();
|
| 126 |
const current = () => !disposed && container.isConnected && sessionMatches() && (options.isCurrent?.() ?? true);
|
| 127 |
container._cleanup = () => {
|
| 128 |
if (disposed) return;
|
| 129 |
disposed = true;
|
| 130 |
+
releaseModeration();
|
| 131 |
for (const controller of cropControllers) { try { controller.abort(); } catch (_) {} }
|
| 132 |
cropControllers.clear();
|
| 133 |
for (const reader of readers) { try { reader.abort(); } catch (_) {} }
|
|
|
|
| 245 |
<div style="background: var(--comfy-input-bg); padding: 12px; border-radius: 6px; border: 1px solid #444;">
|
| 246 |
<div style="display: flex; align-items: center; justify-content: space-between;">
|
| 247 |
<div>
|
| 248 |
+
<div style="font-size: 13px; color: #fff; margin-bottom: 4px;">🖼️ ${escapeHtml(t('settings_form.image_moderation'))}</div>
|
| 249 |
+
<div id="moderation-hint" style="font-size: 11px; color: #888;">${escapeHtml(t('settings_form.image_moderation_hint'))}</div>
|
| 250 |
</div>
|
| 251 |
<label class="toggle-switch" style="position: relative; display: inline-block; width: 50px; height: 26px;">
|
| 252 |
+
<input type="checkbox" id="admin-image-moderation" disabled aria-label="${escapeHtml(t('settings_form.image_moderation'))}" aria-describedby="moderation-hint moderation-status" style="opacity: 0; width: 0; height: 0;">
|
| 253 |
<span class="toggle-slider" style="position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--comfy-input-bg); transition: .3s; border-radius: 26px;"></span>
|
| 254 |
</label>
|
| 255 |
</div>
|
| 256 |
+
<div id="moderation-status" role="status" aria-live="polite" style="margin-top: 8px; font-size: 11px; color: var(--rk-text-muted, #aaa);">${escapeHtml(t('common.loading'))}</div>
|
| 257 |
+
<button id="moderation-retry" type="button" hidden>${escapeHtml(t('common.retry'))}</button>
|
| 258 |
</div>
|
| 259 |
</div>
|
| 260 |
</div>
|
|
|
|
| 319 |
document.head.appendChild(toggleStyles);
|
| 320 |
}
|
| 321 |
|
|
|
|
| 322 |
if (isAdmin) {
|
| 323 |
+
const moderationToggle = container.querySelector('#admin-image-moderation');
|
| 324 |
+
const statusDiv = container.querySelector('#moderation-status');
|
| 325 |
+
const retryButton = container.querySelector('#moderation-retry');
|
| 326 |
+
let ready = false, busy = false;
|
| 327 |
+
moderationToggle.indeterminate = true;
|
| 328 |
+
refreshModerationControls = () => {
|
| 329 |
+
moderationToggle.disabled = saving || busy || !ready;
|
| 330 |
+
retryButton.disabled = saving || busy;
|
| 331 |
+
statusDiv.setAttribute('aria-busy', String(busy));
|
| 332 |
+
};
|
| 333 |
+
const confirmState = response => {
|
| 334 |
+
if (response?.status !== 'success' || typeof response?.data?.enabled !== 'boolean') {
|
| 335 |
+
throw new Error('invalid moderation response');
|
| 336 |
+
}
|
| 337 |
+
return response.data.enabled;
|
| 338 |
+
};
|
| 339 |
+
const showState = enabled => {
|
| 340 |
+
ready = true;
|
| 341 |
+
moderationToggle.indeterminate = false;
|
| 342 |
+
moderationToggle.checked = enabled;
|
| 343 |
+
retryButton.hidden = true;
|
| 344 |
+
statusDiv.textContent = enabled ? t('settings_form.enabled')
|
| 345 |
+
: `${t('settings_form.disabled')} | ${t('settings_form.moderation_skipped')}`;
|
| 346 |
+
};
|
| 347 |
+
const showUnknown = message => {
|
| 348 |
+
ready = false;
|
| 349 |
+
moderationToggle.indeterminate = true;
|
| 350 |
+
statusDiv.textContent = message;
|
| 351 |
+
retryButton.hidden = false;
|
| 352 |
+
};
|
| 353 |
+
const loadState = async () => {
|
| 354 |
+
if (!current() || saving || busy) return;
|
| 355 |
+
ready = false; busy = true;
|
| 356 |
+
retryButton.hidden = true;
|
| 357 |
+
statusDiv.textContent = t('common.loading');
|
| 358 |
+
refreshModerationControls();
|
| 359 |
try {
|
| 360 |
+
const response = await api.getSystemConfig('image_moderation');
|
|
|
|
| 361 |
if (!current()) return;
|
| 362 |
+
showState(confirmState(response));
|
| 363 |
+
} catch (_) {
|
| 364 |
+
if (current()) showUnknown(t('settings_form.moderation_read_failed'));
|
| 365 |
+
} finally {
|
| 366 |
+
busy = false;
|
| 367 |
+
if (current()) refreshModerationControls();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
}
|
| 369 |
+
};
|
| 370 |
+
retryButton.onclick = loadState;
|
| 371 |
+
moderationToggle.onchange = async () => {
|
| 372 |
+
if (!current() || saving || busy || !ready || moderationToggle.disabled) return;
|
| 373 |
+
const requested = moderationToggle.checked;
|
| 374 |
+
ready = false; busy = true;
|
| 375 |
+
statusDiv.textContent = t('settings_form.moderation_confirming');
|
| 376 |
+
refreshModerationControls();
|
| 377 |
+
try {
|
| 378 |
+
const result = await api.setSystemConfig('image_moderation', { enabled: requested });
|
| 379 |
+
if (!current()) return;
|
| 380 |
+
if (result?.status !== 'success') throw new Error('unconfirmed moderation save');
|
| 381 |
+
const response = await api.getSystemConfig('image_moderation');
|
| 382 |
+
if (!current()) return;
|
| 383 |
+
const enabled = confirmState(response);
|
| 384 |
+
showState(enabled);
|
| 385 |
+
showToast(enabled === requested
|
| 386 |
+
? t(enabled ? 'settings_form.moderation_enabled' : 'settings_form.moderation_disabled')
|
| 387 |
+
: t('settings_form.moderation_changed'), enabled === requested ? 'success' : 'warning');
|
| 388 |
+
} catch (_) {
|
| 389 |
+
if (!current()) return;
|
| 390 |
+
showUnknown(t('settings_form.moderation_unconfirmed'));
|
| 391 |
+
showToast(t('settings_form.moderation_unconfirmed'), 'warning');
|
| 392 |
+
} finally {
|
| 393 |
+
busy = false;
|
| 394 |
+
if (current()) refreshModerationControls();
|
| 395 |
+
}
|
| 396 |
+
};
|
| 397 |
+
const initialRead = setTimeout(loadState, 100);
|
| 398 |
+
releaseModeration = () => {
|
| 399 |
+
clearTimeout(initialRead);
|
| 400 |
+
moderationToggle.onchange = retryButton.onclick = null;
|
| 401 |
+
};
|
| 402 |
+
refreshModerationControls();
|
| 403 |
}
|
| 404 |
|
| 405 |
// 加载本地界面背景预览(使用账号区分键)
|
|
|
|
| 537 |
for (const [control] of lockedControls) control.disabled = true;
|
| 538 |
const unlock = () => {
|
| 539 |
for (const [control, disabled] of lockedControls) control.disabled = disabled;
|
| 540 |
+
saving = false; refreshSaveAvailability(); refreshModerationControls();
|
| 541 |
};
|
| 542 |
|
| 543 |
try {
|
前端页面/prompt/提示词组件.js
CHANGED
|
@@ -95,14 +95,13 @@ function _renderEmptyState() {
|
|
| 95 |
/**
|
| 96 |
* 🖼️ 生成头像 HTML 字符串
|
| 97 |
*/
|
| 98 |
-
function _generateAvatarHtml(avatar, name
|
| 99 |
const initial = (name || 'U')[0].toUpperCase();
|
| 100 |
// 🔒 XSS防护:avatar / name 均来自他人可修改的资料(昵称首字母可能是 < ),写入 HTML 前必须转义
|
| 101 |
if (avatar) {
|
| 102 |
-
return `<img class="swr-avatar" alt="" src="${escapeHtml(avatar)}"
|
| 103 |
}
|
| 104 |
-
|
| 105 |
-
return `<div class="swr-avatar" style="width: ${size}px; height: ${size}px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: ${fontSize}px; font-weight: bold;">${escapeHtml(initial)}</div>`;
|
| 106 |
}
|
| 107 |
|
| 108 |
/**
|
|
@@ -741,15 +740,7 @@ export function createPromptsView(currentUser, keyword = "") {
|
|
| 741 |
*/
|
| 742 |
function createPromptCard(prompt, currentUser = currentUserCache, isCurrent = () => true, captureNavigation = () => isCurrent) {
|
| 743 |
const card = document.createElement("div");
|
| 744 |
-
card.className = "business-list-card";
|
| 745 |
-
Object.assign(card.style, {
|
| 746 |
-
background: "var(--comfy-menu-bg)",
|
| 747 |
-
borderRadius: "8px",
|
| 748 |
-
overflow: "hidden",
|
| 749 |
-
cursor: "pointer",
|
| 750 |
-
transition: "transform 0.2s, box-shadow 0.2s",
|
| 751 |
-
border: "1px solid var(--border-color, #333)"
|
| 752 |
-
});
|
| 753 |
|
| 754 |
// 格式化时间
|
| 755 |
const timeStr = formatTime(prompt.created_at);
|
|
@@ -757,59 +748,53 @@ function createPromptCard(prompt, currentUser = currentUserCache, isCurrent = ()
|
|
| 757 |
// 判断是否为视频封面
|
| 758 |
const isVideo = prompt.media_type === "video";
|
| 759 |
|
| 760 |
-
// 价格徽标
|
| 761 |
const priceHtml = (prompt.price > 0)
|
| 762 |
-
? `<span
|
| 763 |
-
: `<span
|
| 764 |
|
| 765 |
card.innerHTML = `
|
| 766 |
<!-- 封面图 -->
|
| 767 |
-
<div
|
| 768 |
-
<img src="${escapeHtml(prompt.cover_image || 'data:image/svg+xml,...')}"
|
| 769 |
-
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;"
|
| 770 |
-
alt="">
|
| 771 |
${isVideo ? `
|
| 772 |
<!-- 视频播放按钮 -->
|
| 773 |
-
<div
|
| 774 |
-
|
| 775 |
-
display: flex; align-items: center; justify-content: center; pointer-events: none;">
|
| 776 |
-
<div style="width: 0; height: 0; border-left: 16px solid rgba(255,255,255,0.9);
|
| 777 |
-
border-top: 10px solid transparent; border-bottom: 10px solid transparent;
|
| 778 |
-
margin-left: 4px;"></div>
|
| 779 |
</div>
|
| 780 |
` : ''}
|
| 781 |
<!-- 价格徽标 -->
|
| 782 |
-
<div
|
| 783 |
${priceHtml}
|
| 784 |
</div>
|
| 785 |
<!-- 🔒 付费锁标识 -->
|
| 786 |
${prompt.price > 0 ? `
|
| 787 |
-
<div
|
| 788 |
` : ''}
|
| 789 |
</div>
|
| 790 |
-
|
| 791 |
<!-- 内容区 -->
|
| 792 |
-
<div
|
| 793 |
<!-- 标题 -->
|
| 794 |
<button type="button" class="business-list-card__title">
|
| 795 |
${escapeHtml(prompt.title)}
|
| 796 |
</button>
|
| 797 |
-
|
| 798 |
<!-- 分类标签 -->
|
| 799 |
-
<div
|
| 800 |
-
<span
|
| 801 |
-
${(Array.isArray(prompt.tags) && prompt.tags.length) ? prompt.tags.slice(0, 3).map(tag => `<span
|
| 802 |
</div>
|
| 803 |
-
|
| 804 |
<!-- 作者信息(SWR 缓存头像) -->
|
| 805 |
-
<div class="business-list-card__author
|
| 806 |
-
|
| 807 |
<!-- 互动数据 -->
|
| 808 |
-
<div
|
| 809 |
<span>❤️ ${escapeHtml(prompt.likes || 0)}</span>
|
| 810 |
<span>🔖 ${escapeHtml(prompt.favorites || 0)}</span>
|
| 811 |
<span>🔥 ${escapeHtml(prompt.views || 0)}</span>
|
| 812 |
-
<span
|
| 813 |
</div>
|
| 814 |
</div>
|
| 815 |
`;
|
|
@@ -826,9 +811,9 @@ function createPromptCard(prompt, currentUser = currentUserCache, isCurrent = ()
|
|
| 826 |
const name = cached?.name || account || '';
|
| 827 |
|
| 828 |
// 渲染初始头像
|
| 829 |
-
const avatarHtml = _generateAvatarHtml(avatar, name
|
| 830 |
|
| 831 |
-
authorContainer.innerHTML = `${avatarHtml}<span class="swr-name
|
| 832 |
|
| 833 |
// 后台静默校对
|
| 834 |
getProfileWithSWR(account, api.getUserProfile, (profile) => {
|
|
@@ -839,7 +824,7 @@ function createPromptCard(prompt, currentUser = currentUserCache, isCurrent = ()
|
|
| 839 |
if (avatarEl.tagName === 'IMG') {
|
| 840 |
avatarEl.src = profile.avatar;
|
| 841 |
} else {
|
| 842 |
-
avatarEl.outerHTML = _generateAvatarHtml(profile.avatar, profile.name || name
|
| 843 |
}
|
| 844 |
}
|
| 845 |
if (nameEl && profile.name) {
|
|
|
|
| 95 |
/**
|
| 96 |
* 🖼️ 生成头像 HTML 字符串
|
| 97 |
*/
|
| 98 |
+
function _generateAvatarHtml(avatar, name) {
|
| 99 |
const initial = (name || 'U')[0].toUpperCase();
|
| 100 |
// 🔒 XSS防护:avatar / name 均来自他人可修改的资料(昵称首字母可能是 < ),写入 HTML 前必须转义
|
| 101 |
if (avatar) {
|
| 102 |
+
return `<img class="swr-avatar prompt-card__avatar" alt="" src="${escapeHtml(avatar)}">`;
|
| 103 |
}
|
| 104 |
+
return `<div class="swr-avatar prompt-card__avatar prompt-card__avatar--initial">${escapeHtml(initial)}</div>`;
|
|
|
|
| 105 |
}
|
| 106 |
|
| 107 |
/**
|
|
|
|
| 740 |
*/
|
| 741 |
function createPromptCard(prompt, currentUser = currentUserCache, isCurrent = () => true, captureNavigation = () => isCurrent) {
|
| 742 |
const card = document.createElement("div");
|
| 743 |
+
card.className = "business-list-card prompt-card";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
|
| 745 |
// 格式化时间
|
| 746 |
const timeStr = formatTime(prompt.created_at);
|
|
|
|
| 748 |
// 判断是否为视频封面
|
| 749 |
const isVideo = prompt.media_type === "video";
|
| 750 |
|
| 751 |
+
// 价格徽标
|
| 752 |
const priceHtml = (prompt.price > 0)
|
| 753 |
+
? `<span class="prompt-card__badge prompt-card__badge--paid">💎 ${escapeHtml(prompt.price)}</span>`
|
| 754 |
+
: `<span class="prompt-card__badge prompt-card__badge--free">${t('prompt.free')}</span>`;
|
| 755 |
|
| 756 |
card.innerHTML = `
|
| 757 |
<!-- 封面图 -->
|
| 758 |
+
<div class="prompt-card__cover">
|
| 759 |
+
<img class="prompt-card__image" src="${escapeHtml(prompt.cover_image || 'data:image/svg+xml,...')}" alt="">
|
|
|
|
|
|
|
| 760 |
${isVideo ? `
|
| 761 |
<!-- 视频播放按钮 -->
|
| 762 |
+
<div class="prompt-card__play">
|
| 763 |
+
<div class="prompt-card__play-icon"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
</div>
|
| 765 |
` : ''}
|
| 766 |
<!-- 价格徽标 -->
|
| 767 |
+
<div class="prompt-card__price">
|
| 768 |
${priceHtml}
|
| 769 |
</div>
|
| 770 |
<!-- 🔒 付费锁标识 -->
|
| 771 |
${prompt.price > 0 ? `
|
| 772 |
+
<div class="prompt-card__lock">🔒</div>
|
| 773 |
` : ''}
|
| 774 |
</div>
|
| 775 |
+
|
| 776 |
<!-- 内容区 -->
|
| 777 |
+
<div class="prompt-card__body">
|
| 778 |
<!-- 标题 -->
|
| 779 |
<button type="button" class="business-list-card__title">
|
| 780 |
${escapeHtml(prompt.title)}
|
| 781 |
</button>
|
| 782 |
+
|
| 783 |
<!-- 分类标签 -->
|
| 784 |
+
<div class="prompt-card__tags">
|
| 785 |
+
<span class="prompt-card__badge prompt-card__badge--category">${escapeHtml(_catLabel(prompt.category || ''))}</span>
|
| 786 |
+
${(Array.isArray(prompt.tags) && prompt.tags.length) ? prompt.tags.slice(0, 3).map(tag => `<span class="prompt-card__badge prompt-card__badge--tag">#${escapeHtml(tag)}</span>`).join('') : ''}
|
| 787 |
</div>
|
| 788 |
+
|
| 789 |
<!-- 作者信息(SWR 缓存头像) -->
|
| 790 |
+
<div class="business-list-card__author prompt-card__author"></div>
|
| 791 |
+
|
| 792 |
<!-- 互动数据 -->
|
| 793 |
+
<div class="prompt-card__stats">
|
| 794 |
<span>❤️ ${escapeHtml(prompt.likes || 0)}</span>
|
| 795 |
<span>🔖 ${escapeHtml(prompt.favorites || 0)}</span>
|
| 796 |
<span>🔥 ${escapeHtml(prompt.views || 0)}</span>
|
| 797 |
+
<span class="prompt-card__time">${escapeHtml(timeStr)}</span>
|
| 798 |
</div>
|
| 799 |
</div>
|
| 800 |
`;
|
|
|
|
| 811 |
const name = cached?.name || account || '';
|
| 812 |
|
| 813 |
// 渲染初始头像
|
| 814 |
+
const avatarHtml = _generateAvatarHtml(avatar, name);
|
| 815 |
|
| 816 |
+
authorContainer.innerHTML = `${avatarHtml}<span class="swr-name prompt-card__name">${escapeHtml(name)}</span>`;
|
| 817 |
|
| 818 |
// 后台静默校对
|
| 819 |
getProfileWithSWR(account, api.getUserProfile, (profile) => {
|
|
|
|
| 824 |
if (avatarEl.tagName === 'IMG') {
|
| 825 |
avatarEl.src = profile.avatar;
|
| 826 |
} else {
|
| 827 |
+
avatarEl.outerHTML = _generateAvatarHtml(profile.avatar, profile.name || name);
|
| 828 |
}
|
| 829 |
}
|
| 830 |
if (nameEl && profile.name) {
|
前端页面/social/未读角标工具.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { captureRequestSession } from "../core/网络请求API.js";
|
| 2 |
import { getUser } from "../core/状态管理.js";
|
| 3 |
|
| 4 |
const badgeGenerations = new WeakMap();
|
|
@@ -25,3 +25,14 @@ export function createUnreadBadgeUpdate(button, selector, account) {
|
|
| 25 |
export function validUnreadCount(value) {
|
| 26 |
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
| 27 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { api, captureRequestSession } from "../core/网络请求API.js";
|
| 2 |
import { getUser } from "../core/状态管理.js";
|
| 3 |
|
| 4 |
const badgeGenerations = new WeakMap();
|
|
|
|
| 25 |
export function validUnreadCount(value) {
|
| 26 |
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
| 27 |
}
|
| 28 |
+
|
| 29 |
+
export async function loadUnreadCount(currentUser, bellBtn) {
|
| 30 |
+
const update = createUnreadBadgeUpdate(bellBtn, '#unread-badge', currentUser?.account);
|
| 31 |
+
if (!currentUser?.account || !bellBtn?.querySelector("#unread-badge")) return;
|
| 32 |
+
try {
|
| 33 |
+
const res = await api.getUnreadCount(currentUser.account);
|
| 34 |
+
if (res?.status === "success" && Number.isSafeInteger(res.unread_count) && res.unread_count >= 0) {
|
| 35 |
+
update.render(res.unread_count);
|
| 36 |
+
}
|
| 37 |
+
} catch (_) {}
|
| 38 |
+
}
|
前端页面/social/通知中心组件.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
// 前端页面/social/通知中心组件.js
|
| 2 |
-
import {
|
|
|
|
| 3 |
import { api, captureRequestSession, requestCancelManager } from "../core/网络请求API.js";
|
| 4 |
import { openOtherUserProfileModal } from "../profile/个人中心视图.js";
|
| 5 |
import { showToast, showConfirm } from "../components/UI交互提示组件.js";
|
|
@@ -275,14 +276,3 @@ export async function openNotificationCenter(currentUser, bellBtn) {
|
|
| 275 |
} catch (_) { if (scope.isCurrent() && messages.length === 0) listArea.innerHTML = `<div class="notification-status" role="alert">${t('common.error')}</div>`; }
|
| 276 |
|
| 277 |
}
|
| 278 |
-
|
| 279 |
-
export async function loadUnreadCount(currentUser, bellBtn) {
|
| 280 |
-
const update = createUnreadBadgeUpdate(bellBtn, '#unread-badge', currentUser?.account);
|
| 281 |
-
if (!currentUser?.account || !bellBtn?.querySelector("#unread-badge")) return;
|
| 282 |
-
try {
|
| 283 |
-
const res = await api.getUnreadCount(currentUser.account);
|
| 284 |
-
if (res?.status === "success" && Number.isSafeInteger(res.unread_count) && res.unread_count >= 0) {
|
| 285 |
-
update.render(res.unread_count);
|
| 286 |
-
}
|
| 287 |
-
} catch (_) {}
|
| 288 |
-
}
|
|
|
|
| 1 |
// 前端页面/social/通知中心组件.js
|
| 2 |
+
import { loadUnreadCount } from "./未读角标工具.js";
|
| 3 |
+
export { loadUnreadCount };
|
| 4 |
import { api, captureRequestSession, requestCancelManager } from "../core/网络请求API.js";
|
| 5 |
import { openOtherUserProfileModal } from "../profile/个人中心视图.js";
|
| 6 |
import { showToast, showConfirm } from "../components/UI交互提示组件.js";
|
|
|
|
| 276 |
} catch (_) { if (scope.isCurrent() && messages.length === 0) listArea.innerHTML = `<div class="notification-status" role="alert">${t('common.error')}</div>`; }
|
| 277 |
|
| 278 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
前端页面/styles/主题基础.css
CHANGED
|
@@ -1350,6 +1350,7 @@ button.ranking-rating__star { cursor: pointer; min-width: 28px; min-height: 28px
|
|
| 1350 |
.ranking-publish__field--wide { flex: 2 1 200px; min-width: 0; }
|
| 1351 |
.ranking-publish label:not(.ranking-publish__checkbox) { display: block; margin-bottom: 6px; color: inherit; font-weight: 600; overflow-wrap: anywhere; }
|
| 1352 |
.ranking-publish__required { color: var(--rk-danger, #ef5350); }
|
|
|
|
| 1353 |
.ranking-publish .form-input { display: block; width: 100%; min-width: 0; min-height: 36px; padding: 8px; background: var(--rk-panel, var(--comfy-input-bg, #292929)); border: 1px solid var(--rk-border, var(--border-color, #555)); color: inherit; border-radius: var(--rk-radius-sm, 6px); font: inherit; line-height: 1.5; }
|
| 1354 |
.ranking-publish .form-input::placeholder { color: var(--rk-muted, var(--descrip-text, #a4a4a4)); opacity: 1; }
|
| 1355 |
.ranking-publish__panel { min-width: 0; padding: 12px; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: var(--rk-radius-sm, 6px); background: var(--rk-panel, var(--comfy-input-bg, #292929)); }
|
|
@@ -1451,6 +1452,19 @@ button.ranking-rating__star { cursor: pointer; min-width: 28px; min-height: 28px
|
|
| 1451 |
@media (hover: hover) { .resource-detail__button:hover:not(:disabled) { border-color: var(--rk-focus, #8ab4ff); } }
|
| 1452 |
@media (prefers-reduced-motion: reduce) { .resource-detail__button { transition: none; } }
|
| 1453 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1454 |
/* Install engine runtime UI also appears outside list-card details. */
|
| 1455 |
.resource-install__status { min-width: 0; color: var(--rk-text, var(--fg-color, #ddd)); overflow-wrap: anywhere; }
|
| 1456 |
.resource-install__confirmation { min-width: 0; }
|
|
@@ -1731,3 +1745,41 @@ button.ranking-rating__star { cursor: pointer; min-width: 28px; min-height: 28px
|
|
| 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; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1350 |
.ranking-publish__field--wide { flex: 2 1 200px; min-width: 0; }
|
| 1351 |
.ranking-publish label:not(.ranking-publish__checkbox) { display: block; margin-bottom: 6px; color: inherit; font-weight: 600; overflow-wrap: anywhere; }
|
| 1352 |
.ranking-publish__required { color: var(--rk-danger, #ef5350); }
|
| 1353 |
+
.ranking-publish__security-emphasis { color: var(--rk-danger, #ef5350); }
|
| 1354 |
.ranking-publish .form-input { display: block; width: 100%; min-width: 0; min-height: 36px; padding: 8px; background: var(--rk-panel, var(--comfy-input-bg, #292929)); border: 1px solid var(--rk-border, var(--border-color, #555)); color: inherit; border-radius: var(--rk-radius-sm, 6px); font: inherit; line-height: 1.5; }
|
| 1355 |
.ranking-publish .form-input::placeholder { color: var(--rk-muted, var(--descrip-text, #a4a4a4)); opacity: 1; }
|
| 1356 |
.ranking-publish__panel { min-width: 0; padding: 12px; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: var(--rk-radius-sm, 6px); background: var(--rk-panel, var(--comfy-input-bg, #292929)); }
|
|
|
|
| 1452 |
@media (hover: hover) { .resource-detail__button:hover:not(:disabled) { border-color: var(--rk-focus, #8ab4ff); } }
|
| 1453 |
@media (prefers-reduced-motion: reduce) { .resource-detail__button { transition: none; } }
|
| 1454 |
|
| 1455 |
+
.resource-page { display: flex; flex-direction: column; gap: 15px; flex: 1; min-width: 0; min-height: 0; box-sizing: border-box; padding: 15px; overflow-y: auto; color: var(--rk-text, var(--fg-color, #ddd)); background: var(--rk-bg, var(--comfy-menu-bg, #202020)); font-size: 14px; overflow-wrap: anywhere; }
|
| 1456 |
+
.resource-page__back { align-self: flex-start; }
|
| 1457 |
+
.resource-page__section { min-width: 0; box-sizing: border-box; padding: 15px; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: var(--rk-radius, 8px); background: var(--rk-panel, var(--comfy-input-bg, #292929)); }
|
| 1458 |
+
.resource-page__heading { margin-bottom: 15px; font-size: 16px; font-weight: 600; line-height: 1.4; }
|
| 1459 |
+
.resource-page__description { line-height: 1.8; font-size: 13px; white-space: pre-wrap; }
|
| 1460 |
+
.resource-page__info { line-height: 1.6; }
|
| 1461 |
+
.resource-page__author { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 10px; min-width: 0; }
|
| 1462 |
+
.resource-page__author > div { min-width: 0; max-width: 100%; }
|
| 1463 |
+
.resource-page__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; }
|
| 1464 |
+
.resource-page__thanks { margin-top: 10px; color: var(--rk-muted, var(--descrip-text, #a4a4a4)); }
|
| 1465 |
+
.resource-page__tips { margin-top: 15px; padding-top: 15px; border-top: 1px dashed var(--rk-border, var(--border-color, #555)); }
|
| 1466 |
+
.resource-page__refund:not(:empty) { margin-top: 12px; }
|
| 1467 |
+
|
| 1468 |
/* Install engine runtime UI also appears outside list-card details. */
|
| 1469 |
.resource-install__status { min-width: 0; color: var(--rk-text, var(--fg-color, #ddd)); overflow-wrap: anywhere; }
|
| 1470 |
.resource-install__confirmation { min-width: 0; }
|
|
|
|
| 1745 |
.comfy-ranking-request-diagnostic button[aria-busy="true"] { opacity: .65; }
|
| 1746 |
.comfy-ranking-request-diagnostic__feedback { margin: 6px 0 0; overflow-wrap: anywhere; }
|
| 1747 |
.comfy-ranking-request-diagnostic summary:focus-visible { outline: 2px solid var(--rk-focus); outline-offset: 3px; }
|
| 1748 |
+
|
| 1749 |
+
.comfy-ranking-root .hub-inline-load-state {
|
| 1750 |
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
| 1751 |
+
gap: 16px;
|
| 1752 |
+
flex: 1; min-height: 0; padding: 20px; overflow-wrap: anywhere;
|
| 1753 |
+
color: var(--rk-muted, var(--descrip-text, #a4a4a4));
|
| 1754 |
+
}
|
| 1755 |
+
|
| 1756 |
+
.ranking-settings-form .toggle-switch input:focus-visible + .toggle-slider { outline: 2px solid var(--rk-accent, #64b5f6); outline-offset: 3px; }
|
| 1757 |
+
|
| 1758 |
+
.prompt-card { min-width: 0; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: 8px; background: var(--rk-panel, var(--comfy-menu-bg, #292929)); color: var(--rk-text, var(--fg-color, #ddd)); overflow: hidden; cursor: pointer; transition: transform .2s, box-shadow .2s; }
|
| 1759 |
+
.prompt-card__cover { position: relative; width: 100%; padding-top: 100%; background: var(--rk-panel, var(--comfy-input-bg, #292929)); }
|
| 1760 |
+
.prompt-card__image { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
| 1761 |
+
.prompt-card__play { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 48px; height: 48px; border-radius: 50%; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; pointer-events: none; }
|
| 1762 |
+
.prompt-card__play-icon { width: 0; height: 0; border-left: 16px solid white; border-top: 10px solid transparent; border-bottom: 10px solid transparent; margin-left: 4px; }
|
| 1763 |
+
.prompt-card__price { position: absolute; top: 8px; right: 8px; max-width: calc(100% - 16px); pointer-events: none; }
|
| 1764 |
+
.prompt-card__badge { display: inline-block; box-sizing: border-box; min-width: 0; max-width: 100%; padding: 2px 8px; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: 10px; background: var(--rk-panel, var(--comfy-input-bg, #292929)); color: var(--rk-text, var(--fg-color, #ddd)); font-size: 10px; line-height: 1.4; overflow-wrap: anywhere; }
|
| 1765 |
+
.prompt-card__badge--paid { border-color: var(--rk-warning, #d97706); font-weight: bold; }
|
| 1766 |
+
.prompt-card__badge--free { border-color: var(--rk-success, #4caf50); font-weight: bold; }
|
| 1767 |
+
.prompt-card__badge--category { border-color: var(--rk-accent, #3b82f6); }
|
| 1768 |
+
.prompt-card__badge--tag { padding-inline: 6px; }
|
| 1769 |
+
.prompt-card__lock { position: absolute; bottom: 8px; right: 8px; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: 50%; background: var(--rk-panel, var(--comfy-input-bg, #292929)); font-size: 12px; pointer-events: none; }
|
| 1770 |
+
.prompt-card__body { padding: 10px; min-width: 0; }
|
| 1771 |
+
.prompt-card .business-list-card__title { min-height: 28px; }
|
| 1772 |
+
.prompt-card .business-list-card__title:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
|
| 1773 |
+
.prompt-card__tags { display: flex; flex-wrap: wrap; gap: 4px; min-width: 0; margin-bottom: 6px; }
|
| 1774 |
+
.prompt-card__author { display: flex; align-items: center; gap: 6px; min-width: 0; margin-bottom: 6px; }
|
| 1775 |
+
.prompt-card__avatar { box-sizing: border-box; width: 18px; height: 18px; flex: 0 0 18px; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: 50%; object-fit: cover; background: var(--rk-panel, var(--comfy-input-bg, #292929)); }
|
| 1776 |
+
.prompt-card__avatar--initial { display: flex; align-items: center; justify-content: center; color: var(--rk-text, var(--fg-color, #ddd)); font-size: 9px; font-weight: bold; }
|
| 1777 |
+
.prompt-card__name { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: var(--rk-muted, var(--descrip-text, #a4a4a4)); }
|
| 1778 |
+
.prompt-card__stats { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px; min-width: 0; font-size: 11px; color: var(--rk-muted, var(--descrip-text, #a4a4a4)); overflow-wrap: anywhere; }
|
| 1779 |
+
.prompt-card__stats > span { min-width: 0; max-width: 100%; }
|
| 1780 |
+
.prompt-card__time { margin-left: auto; font-size: 10px; }
|
| 1781 |
+
@media (prefers-reduced-motion: reduce) { .prompt-card { transition: none; } }
|
| 1782 |
+
@media (forced-colors: active) {
|
| 1783 |
+
.prompt-card__badge, .prompt-card__lock, .prompt-card__play { background: Canvas; color: CanvasText; border: 1px solid CanvasText; }
|
| 1784 |
+
.prompt-card__play-icon { border-left-color: CanvasText; }
|
| 1785 |
+
}
|