Spaces:
Running
Running
Upload 99 files
Browse files- 前端页面/components/UI交互提示组件.js +19 -4
- 前端页面/components/互动工具函数.js +7 -3
- 前端页面/components/图表渲染组件.js +168 -72
- 前端页面/components/图表脚本加载.js +27 -0
- 前端页面/components/打赏等级工具.js +29 -39
- 前端页面/components/用户体验_国际化.js +10 -0
- 前端页面/core/侧边栏数据引擎.js +2 -2
- 前端页面/core/全局配置.js +82 -3
- 前端页面/core/榜单分页控制.js +5 -1
- 前端页面/core/榜单视图切换.js +1 -1
- 前端页面/core/网络请求_图片代理.js +5 -3
- 前端页面/market/列表卡片组件.js +5 -2
- 前端页面/market/创作者卡片组件.js +2 -25
- 前端页面/market/发布内容组件.js +121 -84
- 前端页面/profile/个人中心_UI模板.js +5 -6
- 前端页面/profile/个人中心视图.js +6 -13
- 前端页面/profile/个人列表组件.js +35 -3
- 前端页面/profile/个人设置表单组件.js +19 -11
- 前端页面/profile/个人资料工具.js +4 -1
- 前端页面/styles/主题基础.css +114 -1
- 前端页面/task/任务详情组件.js +3 -1
前端页面/components/UI交互提示组件.js
CHANGED
|
@@ -632,6 +632,7 @@ export function showRetryDialog(message, retryFn, options = {}) {
|
|
| 632 |
|
| 633 |
let loadingOverlay = null;
|
| 634 |
let loadingCount = 0;
|
|
|
|
| 635 |
|
| 636 |
/**
|
| 637 |
* 显示全局加载遮罩
|
|
@@ -639,11 +640,17 @@ let loadingCount = 0;
|
|
| 639 |
*/
|
| 640 |
export function showLoading(message = t('feedback.processing')) {
|
| 641 |
loadingCount++;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
|
| 643 |
if (loadingOverlay) {
|
| 644 |
// 更新文字
|
| 645 |
const textEl = loadingOverlay.querySelector(".loading-text");
|
| 646 |
if (textEl) textEl.textContent = message;
|
|
|
|
| 647 |
return;
|
| 648 |
}
|
| 649 |
|
|
@@ -694,8 +701,9 @@ export function showLoading(message = t('feedback.processing')) {
|
|
| 694 |
|
| 695 |
document.body.appendChild(loadingOverlay);
|
| 696 |
|
|
|
|
| 697 |
requestAnimationFrame(() => {
|
| 698 |
-
loadingOverlay.style.opacity = "1";
|
| 699 |
});
|
| 700 |
}
|
| 701 |
|
|
@@ -707,9 +715,16 @@ export function hideLoading() {
|
|
| 707 |
|
| 708 |
if (loadingCount > 0 || !loadingOverlay) return;
|
| 709 |
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
loadingOverlay = null;
|
| 714 |
}, 200);
|
| 715 |
}
|
|
|
|
| 632 |
|
| 633 |
let loadingOverlay = null;
|
| 634 |
let loadingCount = 0;
|
| 635 |
+
let loadingHideTask = null;
|
| 636 |
|
| 637 |
/**
|
| 638 |
* 显示全局加载遮罩
|
|
|
|
| 640 |
*/
|
| 641 |
export function showLoading(message = t('feedback.processing')) {
|
| 642 |
loadingCount++;
|
| 643 |
+
if (loadingHideTask) {
|
| 644 |
+
clearTimeout(loadingHideTask.timer);
|
| 645 |
+
loadingHideTask = null;
|
| 646 |
+
}
|
| 647 |
+
if (loadingOverlay && !loadingOverlay.isConnected) loadingOverlay = null;
|
| 648 |
|
| 649 |
if (loadingOverlay) {
|
| 650 |
// 更新文字
|
| 651 |
const textEl = loadingOverlay.querySelector(".loading-text");
|
| 652 |
if (textEl) textEl.textContent = message;
|
| 653 |
+
loadingOverlay.style.opacity = "1";
|
| 654 |
return;
|
| 655 |
}
|
| 656 |
|
|
|
|
| 701 |
|
| 702 |
document.body.appendChild(loadingOverlay);
|
| 703 |
|
| 704 |
+
const overlay = loadingOverlay;
|
| 705 |
requestAnimationFrame(() => {
|
| 706 |
+
if (loadingOverlay === overlay && loadingCount > 0 && overlay.isConnected) overlay.style.opacity = "1";
|
| 707 |
});
|
| 708 |
}
|
| 709 |
|
|
|
|
| 715 |
|
| 716 |
if (loadingCount > 0 || !loadingOverlay) return;
|
| 717 |
|
| 718 |
+
const overlay = loadingOverlay;
|
| 719 |
+
overlay.style.opacity = "0";
|
| 720 |
+
if (loadingHideTask) return;
|
| 721 |
+
const task = { timer: null };
|
| 722 |
+
loadingHideTask = task;
|
| 723 |
+
task.timer = setTimeout(() => {
|
| 724 |
+
if (loadingHideTask !== task) return;
|
| 725 |
+
loadingHideTask = null;
|
| 726 |
+
if (loadingOverlay !== overlay || loadingCount > 0) return;
|
| 727 |
+
overlay.remove();
|
| 728 |
loadingOverlay = null;
|
| 729 |
}, 200);
|
| 730 |
}
|
前端页面/components/互动工具函数.js
CHANGED
|
@@ -319,13 +319,17 @@ export function getCacheTTL() {
|
|
| 319 |
* @returns {string}
|
| 320 |
*/
|
| 321 |
export function formatTime(timestamp) {
|
| 322 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
const now = Date.now() / 1000;
|
| 324 |
-
const diff = now -
|
| 325 |
if (diff < 60) return t('time.just_now');
|
| 326 |
if (diff < 3600) return t('time.minutes_ago', { n: Math.floor(diff / 60) });
|
| 327 |
if (diff < 86400) return t('time.hours_ago', { n: Math.floor(diff / 3600) });
|
| 328 |
if (diff < 604800) return t('time.days_ago', { n: Math.floor(diff / 86400) });
|
| 329 |
-
const date = new Date(timestamp * 1000);
|
| 330 |
return `${date.getMonth() + 1}/${date.getDate()}`;
|
| 331 |
}
|
|
|
|
| 319 |
* @returns {string}
|
| 320 |
*/
|
| 321 |
export function formatTime(timestamp) {
|
| 322 |
+
if (timestamp === null || timestamp === undefined || timestamp === "" || timestamp === 0) return "";
|
| 323 |
+
if (typeof timestamp !== "number" && typeof timestamp !== "string") return t('common.unknown');
|
| 324 |
+
const seconds = typeof timestamp === "number" ? timestamp : timestamp.trim() === "" ? NaN : Number(timestamp);
|
| 325 |
+
if (!Number.isFinite(seconds)) return t('common.unknown');
|
| 326 |
+
const date = new Date(seconds * 1000);
|
| 327 |
+
if (!Number.isFinite(date.getTime())) return t('common.unknown');
|
| 328 |
const now = Date.now() / 1000;
|
| 329 |
+
const diff = now - seconds;
|
| 330 |
if (diff < 60) return t('time.just_now');
|
| 331 |
if (diff < 3600) return t('time.minutes_ago', { n: Math.floor(diff / 60) });
|
| 332 |
if (diff < 86400) return t('time.hours_ago', { n: Math.floor(diff / 3600) });
|
| 333 |
if (diff < 604800) return t('time.days_ago', { n: Math.floor(diff / 86400) });
|
|
|
|
| 334 |
return `${date.getMonth() + 1}/${date.getDate()}`;
|
| 335 |
}
|
前端页面/components/图表渲染组件.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
| 1 |
-
// 前端页面/components/图表渲染组件.js
|
| 2 |
-
|
| 3 |
import { t } from "./用户体验增强.js";
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
});
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
function getLast6Months() {
|
|
@@ -27,75 +28,170 @@ function getLast6Months() {
|
|
| 27 |
return res;
|
| 28 |
}
|
| 29 |
|
| 30 |
-
function
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
const months = getLast6Months();
|
| 36 |
// 提取底层的真实时间序列
|
| 37 |
const history = itemData.use_history || {};
|
| 38 |
const data = months.map(m => history[m] || 0);
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
window.
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
};
|
| 62 |
-
window.addEventListener('resize', onResize);
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
for (const entry of entries) {
|
| 81 |
-
if (entry.contentRect.width > 0) {
|
| 82 |
-
observer.disconnect();
|
| 83 |
-
_initChart(domElement, itemData);
|
| 84 |
-
return;
|
| 85 |
-
}
|
| 86 |
}
|
| 87 |
});
|
|
|
|
| 88 |
observer.observe(domElement);
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import { t } from "./用户体验增强.js";
|
| 2 |
+
import { CHART_LIFECYCLE, getSettings } from "../core/全局配置.js";
|
| 3 |
+
import { loadECharts } from "./图表脚本加载.js";
|
| 4 |
|
| 5 |
+
const owners = new Map();
|
| 6 |
+
let removalObserver = null;
|
| 7 |
+
function observeRemoval() {
|
| 8 |
+
if (removalObserver) return;
|
| 9 |
+
removalObserver = new MutationObserver(() => {
|
| 10 |
+
for (const [element, controller] of owners) {
|
| 11 |
+
if (!element.isConnected) controller.cleanup();
|
| 12 |
+
}
|
| 13 |
});
|
| 14 |
+
removalObserver.observe(document.body, { childList: true, subtree: true });
|
| 15 |
}
|
| 16 |
|
| 17 |
function getLast6Months() {
|
|
|
|
| 28 |
return res;
|
| 29 |
}
|
| 30 |
|
| 31 |
+
export function renderItemTrendChart(domElement, itemData) {
|
| 32 |
+
if (!domElement?.isConnected) return null;
|
| 33 |
+
owners.get(domElement)?.cleanup();
|
| 34 |
+
let active = true, disposed = false, generation = 0;
|
| 35 |
+
let chartInstance = null, widthObserver = null, widthTimer = null, resizeTimer = null;
|
| 36 |
+
domElement.classList.add('resource-trend');
|
| 37 |
+
let removeResize = null, removeAppearance = null;
|
| 38 |
+
const clearDescription = () => {
|
| 39 |
+
domElement.removeAttribute('role');
|
| 40 |
+
domElement.removeAttribute('aria-label');
|
| 41 |
+
};
|
| 42 |
+
const showState = (message, failed = false) => {
|
| 43 |
+
clearDescription();
|
| 44 |
+
const state = document.createElement('div');
|
| 45 |
+
state.className = 'resource-trend__state';
|
| 46 |
+
state.setAttribute('role', failed ? 'alert' : 'status');
|
| 47 |
+
state.textContent = message;
|
| 48 |
+
domElement.replaceChildren(state);
|
| 49 |
+
};
|
| 50 |
+
const clearWork = () => {
|
| 51 |
+
widthObserver?.disconnect(); widthObserver = null;
|
| 52 |
+
clearTimeout(widthTimer); widthTimer = null;
|
| 53 |
+
clearTimeout(resizeTimer); resizeTimer = null;
|
| 54 |
+
removeResize?.(); removeResize = null;
|
| 55 |
+
removeAppearance?.(); removeAppearance = null;
|
| 56 |
+
clearDescription();
|
| 57 |
+
if (chartInstance) {
|
| 58 |
+
const old = chartInstance; chartInstance = null;
|
| 59 |
+
try { if (!old.isDisposed?.()) old.dispose(); } catch (_) {}
|
| 60 |
+
}
|
| 61 |
+
};
|
| 62 |
+
const cleanup = () => {
|
| 63 |
+
if (disposed) return;
|
| 64 |
+
disposed = true; active = false; generation++;
|
| 65 |
+
clearWork();
|
| 66 |
+
if (owners.get(domElement) === controller) owners.delete(domElement);
|
| 67 |
+
if (!owners.size) { removalObserver?.disconnect(); removalObserver = null; }
|
| 68 |
+
};
|
| 69 |
+
const current = token => {
|
| 70 |
+
if (disposed || !active || generation !== token || owners.get(domElement) !== controller) return false;
|
| 71 |
+
if (!domElement.isConnected) { cleanup(); return false; }
|
| 72 |
+
return true;
|
| 73 |
+
};
|
| 74 |
+
const showError = (token, key = 'chart.render_failed') => {
|
| 75 |
+
if (!current(token)) return;
|
| 76 |
+
generation++;
|
| 77 |
+
clearWork();
|
| 78 |
+
const message = key === 'chart.load_failed' ? t('chart.load_failed')
|
| 79 |
+
: key === 'chart.size_unavailable' ? t('chart.size_unavailable') : t('chart.render_failed');
|
| 80 |
+
showState(message, true);
|
| 81 |
+
};
|
| 82 |
+
const resize = token => {
|
| 83 |
+
if (!current(token) || !chartInstance) return;
|
| 84 |
+
if (chartInstance.isDisposed?.()) { cleanup(); return; }
|
| 85 |
+
try { chartInstance.resize(); } catch (_) { showError(token); }
|
| 86 |
+
};
|
| 87 |
+
const initialize = token => {
|
| 88 |
+
let libraryReady = false;
|
| 89 |
+
loadECharts().then(echarts => {
|
| 90 |
+
if (!current(token)) return;
|
| 91 |
+
libraryReady = true;
|
| 92 |
+
if (domElement.clientWidth === 0) { waitWidth(token); return; }
|
| 93 |
+
domElement.innerHTML = "";
|
| 94 |
+
const instance = echarts.init(domElement, null, { backgroundColor: 'transparent' });
|
| 95 |
+
if (!current(token)) { instance.dispose(); return; }
|
| 96 |
+
chartInstance = instance;
|
| 97 |
const months = getLast6Months();
|
| 98 |
// 提取底层的真实时间序列
|
| 99 |
const history = itemData.use_history || {};
|
| 100 |
const data = months.map(m => history[m] || 0);
|
| 101 |
|
| 102 |
+
const describe = () => {
|
| 103 |
+
domElement.setAttribute('role', 'img');
|
| 104 |
+
domElement.setAttribute('aria-label', t('chart.data_description', {
|
| 105 |
+
series: t('chart.monthly_downloads'),
|
| 106 |
+
values: months.map((month, index) => t('chart.month_value', { month, value: data[index] })).join('; ')
|
| 107 |
+
}));
|
| 108 |
+
};
|
| 109 |
+
const appearance = () => {
|
| 110 |
+
const style = getComputedStyle(domElement);
|
| 111 |
+
const fontSize = parseFloat(style.fontSize);
|
| 112 |
+
const size = Number.isFinite(fontSize) ? fontSize : 12;
|
| 113 |
+
const color = name => style.getPropertyValue(name).trim();
|
| 114 |
+
const text = color('--resource-trend-text');
|
| 115 |
+
const border = color('--resource-trend-border');
|
| 116 |
+
const line = color('--resource-trend-line');
|
| 117 |
+
return {
|
| 118 |
+
animation: getSettings().enableAnimations !== false && !window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
| 119 |
+
textStyle: { color: text, fontFamily: style.fontFamily, fontSize: size },
|
| 120 |
+
tooltip: { trigger: 'axis', backgroundColor: color('--resource-trend-panel'), borderColor: border, textStyle: { color: text, fontSize: size } },
|
| 121 |
+
grid: { top: size, bottom: size * 1.5, left: size, right: size, containLabel: true },
|
| 122 |
+
xAxis: { type: 'category', data: months, axisLabel: { color: text, fontSize: size, hideOverlap: true, formatter: value => value.replace('-', '\n'), showMinLabel: true, showMaxLabel: true, alignMinLabel: 'left', alignMaxLabel: 'right' }, axisLine: { lineStyle: { color: border } } },
|
| 123 |
+
yAxis: { type: 'value', splitLine: { lineStyle: { color: border, type: 'dashed' } }, axisLabel: { color: text, fontSize: size }, minInterval: 1 },
|
| 124 |
+
series: [{
|
| 125 |
+
name: t('chart.monthly_downloads'), type: 'line', data: data, smooth: true,
|
| 126 |
+
itemStyle: { color: line }, lineStyle: { color: line, width: 2 },
|
| 127 |
+
areaStyle: { color: line, opacity: .12 }
|
| 128 |
+
}]
|
| 129 |
+
};
|
| 130 |
+
};
|
| 131 |
+
instance.setOption(appearance());
|
| 132 |
+
describe();
|
| 133 |
+
const onAppearance = () => {
|
| 134 |
+
if (!current(token) || chartInstance !== instance) return;
|
| 135 |
+
try { instance.setOption(appearance()); describe(); instance.resize(); }
|
| 136 |
+
catch (_) { showError(token); }
|
| 137 |
+
};
|
| 138 |
+
const observer = new MutationObserver(onAppearance);
|
| 139 |
+
for (let ancestor = domElement.parentElement; ancestor; ancestor = ancestor.parentElement) {
|
| 140 |
+
observer.observe(ancestor, { attributes: true, attributeFilter: ['class', 'style', 'data-theme', 'data-color-scheme'] });
|
| 141 |
+
}
|
| 142 |
+
const queries = ['(prefers-color-scheme: dark)', '(forced-colors: active)', '(prefers-reduced-motion: reduce)'].map(query => window.matchMedia(query));
|
| 143 |
+
for (const query of queries) query.addEventListener('change', onAppearance);
|
| 144 |
+
window.addEventListener('comfy-settings-changed', onAppearance);
|
| 145 |
+
removeAppearance = () => {
|
| 146 |
+
observer.disconnect();
|
| 147 |
+
for (const query of queries) query.removeEventListener('change', onAppearance);
|
| 148 |
+
window.removeEventListener('comfy-settings-changed', onAppearance);
|
| 149 |
};
|
|
|
|
| 150 |
|
| 151 |
+
if (!current(token) || chartInstance !== instance) return;
|
| 152 |
+
const onResize = onAppearance;
|
| 153 |
+
window.addEventListener('resize', onResize);
|
| 154 |
+
removeResize = () => window.removeEventListener('resize', onResize);
|
| 155 |
+
resizeTimer = setTimeout(() => { resizeTimer = null; resize(token); }, CHART_LIFECYCLE.RESIZE_DELAY);
|
| 156 |
+
}).catch(() => showError(token, libraryReady ? 'chart.render_failed' : 'chart.load_failed'));
|
| 157 |
+
};
|
| 158 |
+
const waitWidth = token => {
|
| 159 |
+
if (!current(token)) return;
|
| 160 |
+
if (domElement.clientWidth > 0) { initialize(token); return; }
|
| 161 |
+
const observer = new ResizeObserver(entries => {
|
| 162 |
+
if (widthObserver !== observer || !current(token)) return;
|
| 163 |
+
if (entries.some(entry => entry.contentRect.width > 0)) {
|
| 164 |
+
observer.disconnect(); widthObserver = null;
|
| 165 |
+
clearTimeout(widthTimer); widthTimer = null;
|
| 166 |
+
initialize(token);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
}
|
| 168 |
});
|
| 169 |
+
widthObserver = observer;
|
| 170 |
observer.observe(domElement);
|
| 171 |
+
const timer = setTimeout(() => {
|
| 172 |
+
if (widthTimer !== timer) return;
|
| 173 |
+
widthTimer = null;
|
| 174 |
+
if (current(token)) showError(token, 'chart.size_unavailable');
|
| 175 |
+
}, CHART_LIFECYCLE.WIDTH_WAIT_TIMEOUT);
|
| 176 |
+
widthTimer = timer;
|
| 177 |
+
};
|
| 178 |
+
const start = () => {
|
| 179 |
+
const token = ++generation;
|
| 180 |
+
clearWork();
|
| 181 |
+
showState(t('chart.loading'));
|
| 182 |
+
waitWidth(token);
|
| 183 |
+
};
|
| 184 |
+
const controller = {
|
| 185 |
+
cleanup,
|
| 186 |
+
setActive(value) {
|
| 187 |
+
if (disposed || active === !!value) return;
|
| 188 |
+
active = !!value; generation++;
|
| 189 |
+
clearWork();
|
| 190 |
+
if (active) start();
|
| 191 |
+
}
|
| 192 |
+
};
|
| 193 |
+
owners.set(domElement, controller);
|
| 194 |
+
observeRemoval();
|
| 195 |
+
start();
|
| 196 |
+
return controller;
|
| 197 |
+
}
|
前端页面/components/图表脚本加载.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { API } from "../core/全局配置.js";
|
| 2 |
+
|
| 3 |
+
let echartsLoading = null, echartsLoadFailures = 0;
|
| 4 |
+
export function loadECharts() {
|
| 5 |
+
if (typeof window.echarts?.init === 'function') return Promise.resolve(window.echarts);
|
| 6 |
+
if (echartsLoading) return echartsLoading;
|
| 7 |
+
echartsLoading = new Promise((resolve, reject) => {
|
| 8 |
+
const script = document.createElement("script");
|
| 9 |
+
let settled = false;
|
| 10 |
+
const finish = (error) => {
|
| 11 |
+
if (settled) return;
|
| 12 |
+
settled = true; clearTimeout(timer);
|
| 13 |
+
script.onload = script.onerror = null; script.remove();
|
| 14 |
+
if (error) { echartsLoadFailures++; reject(new Error('ECharts load failed')); }
|
| 15 |
+
else { echartsLoadFailures = 0; resolve(window.echarts); }
|
| 16 |
+
};
|
| 17 |
+
const timer = setTimeout(() => finish(true), API.PROFILE_RESOURCE_TIMEOUT);
|
| 18 |
+
const scriptURL = new URL(API.CREATOR_CHART_SCRIPT_URL);
|
| 19 |
+
// 移除script不保证终止原下载;失败后的读取不能复用未结束的旧URL。
|
| 20 |
+
if (echartsLoadFailures) scriptURL.searchParams.set('_ranking_retry', `${Date.now()}-${echartsLoadFailures}`);
|
| 21 |
+
script.src = scriptURL.href;
|
| 22 |
+
script.onload = () => finish(typeof window.echarts?.init !== 'function');
|
| 23 |
+
script.onerror = () => finish(true);
|
| 24 |
+
document.head.appendChild(script);
|
| 25 |
+
}).finally(() => { echartsLoading = null; });
|
| 26 |
+
return echartsLoading;
|
| 27 |
+
}
|
前端页面/components/打赏等级工具.js
CHANGED
|
@@ -69,7 +69,7 @@ export function calculateTipLevel(amount) {
|
|
| 69 |
*/
|
| 70 |
export function renderTipLevelHTML(amount, compact = false) {
|
| 71 |
if (!amount || amount <= 0) {
|
| 72 |
-
return `<span
|
| 73 |
}
|
| 74 |
|
| 75 |
const level = calculateTipLevel(amount);
|
|
@@ -79,46 +79,46 @@ export function renderTipLevelHTML(amount, compact = false) {
|
|
| 79 |
if (compact) {
|
| 80 |
// 太阳(最高级别优先显示)
|
| 81 |
if (level.suns > 0) {
|
| 82 |
-
html += `<span
|
| 83 |
}
|
| 84 |
// 月亮
|
| 85 |
if (level.moons > 0) {
|
| 86 |
-
html += `<span
|
| 87 |
}
|
| 88 |
// 星星
|
| 89 |
if (level.stars > 0) {
|
| 90 |
-
html += `<span
|
| 91 |
}
|
| 92 |
|
| 93 |
// 满级特效
|
| 94 |
if (level.isMaxLevel) {
|
| 95 |
-
html = `<span
|
| 96 |
}
|
| 97 |
|
| 98 |
-
return html || `<span
|
| 99 |
}
|
| 100 |
|
| 101 |
// ========== 完整模式:显示图标+数量 ==========
|
| 102 |
const parts = [];
|
| 103 |
|
| 104 |
if (level.suns > 0) {
|
| 105 |
-
parts.push(`<span
|
| 106 |
}
|
| 107 |
if (level.moons > 0) {
|
| 108 |
-
parts.push(`<span
|
| 109 |
}
|
| 110 |
if (level.stars > 0) {
|
| 111 |
-
parts.push(`<span
|
| 112 |
}
|
| 113 |
|
| 114 |
// 满级特效
|
| 115 |
if (level.isMaxLevel) {
|
| 116 |
-
return `<span
|
| 117 |
}
|
| 118 |
|
| 119 |
return parts.length > 0
|
| 120 |
-
? `<span
|
| 121 |
-
: `<span
|
| 122 |
}
|
| 123 |
|
| 124 |
/**
|
|
@@ -163,36 +163,26 @@ export function renderTipBoardItemHTML(tipData, rank, size = 'normal') {
|
|
| 163 |
if (!Number.isFinite(amount) || amount < 0) return '';
|
| 164 |
rank = Number.isSafeInteger(rank) && rank >= 0 ? rank : 0;
|
| 165 |
|
| 166 |
-
|
| 167 |
-
const
|
| 168 |
-
|
| 169 |
-
const badgeColor = rank < 3 ? '#000' : '#aaa';
|
| 170 |
-
|
| 171 |
-
// 尺寸配置
|
| 172 |
-
const sizeConfig = {
|
| 173 |
-
small: { badge: 16, font: 11, padding: 3, avatar: 20 },
|
| 174 |
-
normal: { badge: 20, font: 12, padding: 5, avatar: 24 },
|
| 175 |
-
large: { badge: 24, font: 14, padding: 8, avatar: 28 }
|
| 176 |
-
};
|
| 177 |
-
const cfg = sizeConfig[size] || sizeConfig.normal;
|
| 178 |
-
|
| 179 |
// 等级图标
|
| 180 |
const levelHtml = renderTipLevelHTML(amount, true);
|
| 181 |
|
| 182 |
// 排名弽章 HTML(两个分支共用)
|
| 183 |
-
const badgeHtml = `<span
|
| 184 |
|
| 185 |
// 匿名用户处理
|
| 186 |
if (is_anon) {
|
| 187 |
return `
|
| 188 |
-
<div
|
| 189 |
-
<span
|
| 190 |
${badgeHtml}
|
| 191 |
-
<span
|
| 192 |
</span>
|
| 193 |
-
<span
|
| 194 |
${levelHtml}
|
| 195 |
-
<span
|
| 196 |
</span>
|
| 197 |
</div>
|
| 198 |
`;
|
|
@@ -228,17 +218,17 @@ export function renderTipBoardItemHTML(tipData, rank, size = 'normal') {
|
|
| 228 |
// 🔒 XSS防护:昵称/头像来自用户可修改的资料,写入 HTML 前必须转义
|
| 229 |
// (上方 SWR 刷新路径用的是 .src / .textContent,本身就是安全的)
|
| 230 |
return `
|
| 231 |
-
<div id="${escapeHtml(containerId)}"
|
| 232 |
-
<span
|
| 233 |
${badgeHtml}
|
| 234 |
-
<span
|
| 235 |
-
<img class="tip-board-avatar" alt="" src="${escapeHtml(avatarUrl)}"
|
| 236 |
-
<span class="tip-board-name"
|
| 237 |
</span>
|
| 238 |
</span>
|
| 239 |
-
<span
|
| 240 |
${levelHtml}
|
| 241 |
-
<span
|
| 242 |
</span>
|
| 243 |
</div>
|
| 244 |
`;
|
|
@@ -256,7 +246,7 @@ export function renderTipBoardHTML(boardData, maxShow = 5, emptyText = t('tip.no
|
|
| 256 |
const entries = Array.isArray(boardData) ? boardData.filter(entry => entry && typeof entry === 'object'
|
| 257 |
&& !Array.isArray(entry) && Number.isFinite(Number(entry.amount)) && Number(entry.amount) >= 0) : [];
|
| 258 |
if (entries.length === 0) {
|
| 259 |
-
return `<div
|
| 260 |
}
|
| 261 |
|
| 262 |
return entries.slice(0, maxShow).map((entry, i) => renderTipBoardItemHTML(entry, i, size)).join('');
|
|
|
|
| 69 |
*/
|
| 70 |
export function renderTipLevelHTML(amount, compact = false) {
|
| 71 |
if (!amount || amount <= 0) {
|
| 72 |
+
return `<span class="tip-level tip-level--new">${t('tip.new_fan')}</span>`;
|
| 73 |
}
|
| 74 |
|
| 75 |
const level = calculateTipLevel(amount);
|
|
|
|
| 79 |
if (compact) {
|
| 80 |
// 太阳(最高级别优先显示)
|
| 81 |
if (level.suns > 0) {
|
| 82 |
+
html += `<span class="tip-level__icon tip-level__icon--sun">${ICON_SUN.repeat(Math.min(level.suns, 3))}${level.suns > 3 ? '×' + level.suns : ''}</span>`;
|
| 83 |
}
|
| 84 |
// 月亮
|
| 85 |
if (level.moons > 0) {
|
| 86 |
+
html += `<span class="tip-level__icon tip-level__icon--moon">${ICON_MOON.repeat(level.moons)}</span>`;
|
| 87 |
}
|
| 88 |
// 星星
|
| 89 |
if (level.stars > 0) {
|
| 90 |
+
html += `<span class="tip-level__icon tip-level__icon--star">${ICON_STAR.repeat(level.stars)}</span>`;
|
| 91 |
}
|
| 92 |
|
| 93 |
// 满级特效
|
| 94 |
if (level.isMaxLevel) {
|
| 95 |
+
html = `<span class="tip-level tip-level--max-compact">${html} 👑</span>`;
|
| 96 |
}
|
| 97 |
|
| 98 |
+
return html || `<span class="tip-level tip-level--fallback">⭐</span>`;
|
| 99 |
}
|
| 100 |
|
| 101 |
// ========== 完整模式:显示图标+数量 ==========
|
| 102 |
const parts = [];
|
| 103 |
|
| 104 |
if (level.suns > 0) {
|
| 105 |
+
parts.push(`<span class="tip-level__part tip-level__part--sun">${ICON_SUN}×${level.suns}</span>`);
|
| 106 |
}
|
| 107 |
if (level.moons > 0) {
|
| 108 |
+
parts.push(`<span class="tip-level__part tip-level__part--moon">${ICON_MOON}×${level.moons}</span>`);
|
| 109 |
}
|
| 110 |
if (level.stars > 0) {
|
| 111 |
+
parts.push(`<span class="tip-level__part tip-level__part--star">${ICON_STAR}×${level.stars}</span>`);
|
| 112 |
}
|
| 113 |
|
| 114 |
// 满级特效
|
| 115 |
if (level.isMaxLevel) {
|
| 116 |
+
return `<span class="tip-level tip-level--max">👑 ${t('tip.supreme_sponsor')}</span>`;
|
| 117 |
}
|
| 118 |
|
| 119 |
return parts.length > 0
|
| 120 |
+
? `<span class="tip-level tip-level--full">${parts.join(' ')}</span>`
|
| 121 |
+
: `<span class="tip-level tip-level--fallback">⭐</span>`;
|
| 122 |
}
|
| 123 |
|
| 124 |
/**
|
|
|
|
| 163 |
if (!Number.isFinite(amount) || amount < 0) return '';
|
| 164 |
rank = Number.isSafeInteger(rank) && rank >= 0 ? rank : 0;
|
| 165 |
|
| 166 |
+
const rankClass = rank < 3 ? ['gold', 'silver', 'bronze'][rank] : 'default';
|
| 167 |
+
const sizeClass = size === 'small' || size === 'large' ? size : 'normal';
|
| 168 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
// 等级图标
|
| 170 |
const levelHtml = renderTipLevelHTML(amount, true);
|
| 171 |
|
| 172 |
// 排名弽章 HTML(两个分支共用)
|
| 173 |
+
const badgeHtml = `<span class="tip-board-badge tip-board-badge--${rankClass}">${rank + 1}</span>`;
|
| 174 |
|
| 175 |
// 匿名用户处理
|
| 176 |
if (is_anon) {
|
| 177 |
return `
|
| 178 |
+
<div class="tip-board-item tip-board-item--${sizeClass}">
|
| 179 |
+
<span class="tip-board-group">
|
| 180 |
${badgeHtml}
|
| 181 |
+
<span class="tip-board-anonymous">🔒 ${t('creator.anonymous')}</span>
|
| 182 |
</span>
|
| 183 |
+
<span class="tip-board-group">
|
| 184 |
${levelHtml}
|
| 185 |
+
<span class="tip-board-amount">${amount}</span>
|
| 186 |
</span>
|
| 187 |
</div>
|
| 188 |
`;
|
|
|
|
| 218 |
// 🔒 XSS防护:昵称/头像来自用户可修改的资料,写入 HTML 前必须转义
|
| 219 |
// (上方 SWR 刷新路径用的是 .src / .textContent,本身就是安全的)
|
| 220 |
return `
|
| 221 |
+
<div id="${escapeHtml(containerId)}" class="tip-board-item tip-board-item--${sizeClass}">
|
| 222 |
+
<span class="tip-board-group">
|
| 223 |
${badgeHtml}
|
| 224 |
+
<span class="tip-board-group">
|
| 225 |
+
<img class="tip-board-avatar" alt="" src="${escapeHtml(avatarUrl)}">
|
| 226 |
+
<span class="tip-board-name">${escapeHtml(userName)}</span>
|
| 227 |
</span>
|
| 228 |
</span>
|
| 229 |
+
<span class="tip-board-group">
|
| 230 |
${levelHtml}
|
| 231 |
+
<span class="tip-board-amount">${amount}</span>
|
| 232 |
</span>
|
| 233 |
</div>
|
| 234 |
`;
|
|
|
|
| 246 |
const entries = Array.isArray(boardData) ? boardData.filter(entry => entry && typeof entry === 'object'
|
| 247 |
&& !Array.isArray(entry) && Number.isFinite(Number(entry.amount)) && Number(entry.amount) >= 0) : [];
|
| 248 |
if (entries.length === 0) {
|
| 249 |
+
return `<div class="tip-board-empty">${escapeHtml(emptyText)}</div>`;
|
| 250 |
}
|
| 251 |
|
| 252 |
return entries.slice(0, maxShow).map((entry, i) => renderTipBoardItemHTML(entry, i, size)).join('');
|
前端页面/components/用户体验_国际化.js
CHANGED
|
@@ -1287,6 +1287,11 @@ const translations = {
|
|
| 1287 |
|
| 1288 |
// 📊 图表
|
| 1289 |
'chart.loading': '正在加载图表...',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1290 |
'chart.load_failed': '图表库加载失败',
|
| 1291 |
|
| 1292 |
// 🖼️ 图片
|
|
@@ -3136,6 +3141,11 @@ const translations = {
|
|
| 3136 |
'tip.points': 'pts',
|
| 3137 |
'common.seconds': 'sec',
|
| 3138 |
'chart.loading': 'Loading chart...',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3139 |
'chart.load_failed': 'Chart failed to load',
|
| 3140 |
'image.loading': 'Loading...',
|
| 3141 |
'image.load_failed': 'Image load failed',
|
|
|
|
| 1287 |
|
| 1288 |
// 📊 图表
|
| 1289 |
'chart.loading': '正在加载图表...',
|
| 1290 |
+
'chart.monthly_downloads': '按月获取量',
|
| 1291 |
+
'chart.data_description': '{series},最近六个月:{values}',
|
| 1292 |
+
'chart.month_value': '{month}:{value}',
|
| 1293 |
+
'chart.size_unavailable': '图表区域暂时不可用',
|
| 1294 |
+
'chart.render_failed': '图表暂时无法显示',
|
| 1295 |
'chart.load_failed': '图表库加载失败',
|
| 1296 |
|
| 1297 |
// 🖼️ 图片
|
|
|
|
| 3141 |
'tip.points': 'pts',
|
| 3142 |
'common.seconds': 'sec',
|
| 3143 |
'chart.loading': 'Loading chart...',
|
| 3144 |
+
'chart.monthly_downloads': 'Monthly acquisitions',
|
| 3145 |
+
'chart.data_description': '{series}, last six months: {values}',
|
| 3146 |
+
'chart.month_value': '{month}: {value}',
|
| 3147 |
+
'chart.size_unavailable': 'Chart area is currently unavailable.',
|
| 3148 |
+
'chart.render_failed': 'Chart could not be displayed.',
|
| 3149 |
'chart.load_failed': 'Chart failed to load',
|
| 3150 |
'image.loading': 'Loading...',
|
| 3151 |
'image.load_failed': 'Image load failed',
|
前端页面/core/侧边栏数据引擎.js
CHANGED
|
@@ -209,7 +209,7 @@ export async function loadSidebarContent({
|
|
| 209 |
// 空数据处理
|
| 210 |
if (displayData.length === 0 && state.displayedCount === 0) {
|
| 211 |
const emptyState = document.createElement('div');
|
| 212 |
-
emptyState.
|
| 213 |
emptyState.textContent = t(keyword ? 'feedback.no_search_results' : 'feedback.no_list_data');
|
| 214 |
emptyState.setAttribute('role', 'status');
|
| 215 |
contentArea.replaceChildren(emptyState);
|
|
@@ -619,7 +619,7 @@ export async function loadSidebarContent({
|
|
| 619 |
displayData = searchCreatorsLocally(keyword, fallbackData);
|
| 620 |
if (displayData.length === 0) {
|
| 621 |
const emptyState = document.createElement('div');
|
| 622 |
-
emptyState.
|
| 623 |
emptyState.textContent = t('feedback.no_search_results');
|
| 624 |
emptyState.setAttribute('role', 'status');
|
| 625 |
contentArea.replaceChildren(emptyState);
|
|
|
|
| 209 |
// 空数据处理
|
| 210 |
if (displayData.length === 0 && state.displayedCount === 0) {
|
| 211 |
const emptyState = document.createElement('div');
|
| 212 |
+
emptyState.className = 'sidebar-status-message';
|
| 213 |
emptyState.textContent = t(keyword ? 'feedback.no_search_results' : 'feedback.no_list_data');
|
| 214 |
emptyState.setAttribute('role', 'status');
|
| 215 |
contentArea.replaceChildren(emptyState);
|
|
|
|
| 619 |
displayData = searchCreatorsLocally(keyword, fallbackData);
|
| 620 |
if (displayData.length === 0) {
|
| 621 |
const emptyState = document.createElement('div');
|
| 622 |
+
emptyState.className = 'sidebar-status-message';
|
| 623 |
emptyState.textContent = t('feedback.no_search_results');
|
| 624 |
emptyState.setAttribute('role', 'status');
|
| 625 |
contentArea.replaceChildren(emptyState);
|
前端页面/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, 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})$/,
|
|
@@ -91,6 +91,7 @@ export const API = {
|
|
| 91 |
SEARCH_COMPONENT_ID: "sidebar-search",
|
| 92 |
// 上传相对路径的云端图片读取入口。
|
| 93 |
IMAGE_PROXY_PATH: "/api/image_proxy",
|
|
|
|
| 94 |
// 本地广告预载要求真实图片,熔断占位图不能作为下载成功。
|
| 95 |
LOCAL_IMAGE_PROXY_PREFIX: "/community_hub/image?url=",
|
| 96 |
COMPLETE_IMAGE_QUERY_SUFFIX: "&require_complete=1",
|
|
@@ -103,6 +104,11 @@ export const API = {
|
|
| 103 |
};
|
| 104 |
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
// 发布视频的本地读取预算,不改变云端上传额度。
|
| 107 |
export const LAZY_IMAGES = Object.freeze({
|
| 108 |
ROOT_MARGIN: "200px 0px",
|
|
@@ -590,6 +596,78 @@ export function escapeHtml(str) {
|
|
| 590 |
.replace(/'/g, "'");
|
| 591 |
}
|
| 592 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
/** 统一字段映射:将后端 avatarDataUrl 映射为前端 avatar */
|
| 594 |
function _mapAvatarField(profile) {
|
| 595 |
if (profile && profile.avatarDataUrl && !profile.avatar) {
|
|
@@ -602,7 +680,8 @@ const PUBLIC_PROFILE_FIELDS = ["account", "name", "avatar", "avatarDataUrl", "in
|
|
| 602 |
"receivedLikes", "receivedFavorites", "receivedUses"];
|
| 603 |
function _publicProfile(profile) {
|
| 604 |
return Object.fromEntries(PUBLIC_PROFILE_FIELDS.filter(key => Object.prototype.hasOwnProperty.call(profile, key))
|
| 605 |
-
.map(key => [key,
|
|
|
|
| 606 |
}
|
| 607 |
|
| 608 |
function _validProfile(profile) {
|
|
@@ -708,7 +787,7 @@ export function renderAvatarWithSWR(options) {
|
|
| 708 |
|
| 709 |
// 从缓存获取
|
| 710 |
const cached = getCachedProfile(account);
|
| 711 |
-
const avatar = cached?.avatar || cached?.avatarDataUrl || fallbackAvatar
|
| 712 |
const name = cached?.name || fallbackName || account || '';
|
| 713 |
|
| 714 |
// 后台静默校对并更新 DOM
|
|
|
|
| 18 |
// 🌐 API 配置
|
| 19 |
// ==========================================
|
| 20 |
|
| 21 |
+
/** @type {{ BASE_URL: string, TIMEOUT: number, VIDEO_UPLOAD_TIMEOUT: number, VIDEO_UPLOAD_RESPONSE_MAX_CHARS: number, LOCAL_CACHE_STATS_TIMEOUT: number, LOCAL_CACHE_CLEAR_TIMEOUT: number, LOCAL_CACHE_RESPONSE_MAX_BYTES: number, SSE_MAX_RESPONSE_BYTES: number, SSE_MAX_LINE_BYTES: number, SSE_MAX_LINES: number, SSE_YIELD_LINES: number, SSE_YIELD_BYTES: number, SSE_YIELD_CHUNKS: number, MAX_RETRIES: number, RETRY_DELAY: number, LIST_RETRIES: number, SEARCH_TIMEOUT: number, SEARCH_RETRIES: number, PROFILE_RESOURCE_TIMEOUT: number, CREATOR_CHART_SCRIPT_URL: string, BACKGROUND_TIMEOUT: number, BANNER_IMAGE_TIMEOUT: number, BACKGROUND_RETRIES: number, WORKFLOW_DOWNLOAD_TIMEOUT: number, CLOUD_DOWN_COOLDOWN: number, SEARCH_COMPONENT_ID: string, IMAGE_PROXY_PATH: string, PROFILE_MEDIA_URL_MAX_BYTES: number, LOCAL_IMAGE_PROXY_PREFIX: string, COMPLETE_IMAGE_QUERY_SUFFIX: string, PRIVATE_GET_PATTERNS: RegExp[], PUBLIC_CONDITIONAL_GET_PATHS: string[], CONDITIONAL_ETAG_PATTERN: RegExp, REQUEST_ID_HEADER: string, REQUEST_ID_PATTERN: RegExp, ENDPOINT_ERROR_SCOPE_HEADER: string, ENDPOINT_ERROR_SCOPE_VALUE: string }} */
|
| 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})$/,
|
|
|
|
| 91 |
SEARCH_COMPONENT_ID: "sidebar-search",
|
| 92 |
// 上传相对路径的云端图片读取入口。
|
| 93 |
IMAGE_PROXY_PATH: "/api/image_proxy",
|
| 94 |
+
PROFILE_MEDIA_URL_MAX_BYTES: 16384,
|
| 95 |
// 本地广告预载要求真实图片,熔断占位图不能作为下载成功。
|
| 96 |
LOCAL_IMAGE_PROXY_PREFIX: "/community_hub/image?url=",
|
| 97 |
COMPLETE_IMAGE_QUERY_SUFFIX: "&require_complete=1",
|
|
|
|
| 104 |
};
|
| 105 |
|
| 106 |
|
| 107 |
+
export const CHART_LIFECYCLE = Object.freeze({
|
| 108 |
+
WIDTH_WAIT_TIMEOUT: 5000,
|
| 109 |
+
RESIZE_DELAY: 300
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
// 发布视频的本地读取预算,不改变云端上传额度。
|
| 113 |
export const LAZY_IMAGES = Object.freeze({
|
| 114 |
ROOT_MARGIN: "200px 0px",
|
|
|
|
| 596 |
.replace(/'/g, "'");
|
| 597 |
}
|
| 598 |
|
| 599 |
+
/** 资料图片只接受明确的图片表示或网络地址,不相对宿主解析业务路径。 */
|
| 600 |
+
export function normalizeProfileImageUrl(value, unwrapProxy = false) {
|
| 601 |
+
if (typeof value !== 'string' || !value || value.trim() !== value || /[\u0000-\u001f\u007f]/.test(value)) return '';
|
| 602 |
+
let source = value;
|
| 603 |
+
let layers = 0;
|
| 604 |
+
while (source.startsWith(API.LOCAL_IMAGE_PROXY_PREFIX)) {
|
| 605 |
+
if (++layers > 10) return '';
|
| 606 |
+
let encoded = source.slice(API.LOCAL_IMAGE_PROXY_PREFIX.length);
|
| 607 |
+
if (encoded.endsWith(API.COMPLETE_IMAGE_QUERY_SUFFIX)) encoded = encoded.slice(0, -API.COMPLETE_IMAGE_QUERY_SUFFIX.length);
|
| 608 |
+
try { source = decodeURIComponent(encoded); } catch (_) { return ''; }
|
| 609 |
+
if (!source || source.trim() !== source || /[\u0000-\u001f\u007f]/.test(source)) return '';
|
| 610 |
+
}
|
| 611 |
+
if (!layers && /^data:image\/[a-z0-9.+-]+(?:;[^,]*)?,.+$/i.test(source)) return value;
|
| 612 |
+
if (!layers && /^blob:https?:\/\//i.test(source)) {
|
| 613 |
+
try {
|
| 614 |
+
const blob = new URL(source);
|
| 615 |
+
if (typeof location !== 'undefined' && location.origin && blob.origin !== location.origin) return '';
|
| 616 |
+
return blob.origin === 'null' ? '' : value;
|
| 617 |
+
} catch (_) { return ''; }
|
| 618 |
+
}
|
| 619 |
+
if (!/^https?:\/\//i.test(source) || source.includes('\\')) return '';
|
| 620 |
+
try {
|
| 621 |
+
if (value.length > API.PROFILE_MEDIA_URL_MAX_BYTES
|
| 622 |
+
|| encodeURIComponent(value).replace(/%[0-9a-f]{2}/gi, 'x').length > API.PROFILE_MEDIA_URL_MAX_BYTES) return '';
|
| 623 |
+
const url = new URL(source);
|
| 624 |
+
const authority = source.match(/^https?:\/\/([^/?#]*)/i)?.[1];
|
| 625 |
+
if (!authority || authority.includes('@') || authority.includes('%') || authority.endsWith(':') || url.port === '0') return '';
|
| 626 |
+
if (url.username || url.password) return '';
|
| 627 |
+
const host = url.hostname.toLowerCase().replace(/\.$/, '');
|
| 628 |
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(host)
|
| 629 |
+
&& authority.replace(/:[0-9]+$/, '').replace(/\.$/, '').toLowerCase() !== host) return '';
|
| 630 |
+
const base = new URL(API.BASE_URL);
|
| 631 |
+
const baseHost = base.hostname.toLowerCase().replace(/\.$/, '');
|
| 632 |
+
if (host === baseHost) {
|
| 633 |
+
const expectedPath = base.pathname.replace(/\/$/, '') + API.IMAGE_PROXY_PATH;
|
| 634 |
+
const sourcePath = source.slice(source.indexOf('://') + 3 + authority.length).split(/[?#]/, 1)[0] || '/';
|
| 635 |
+
if (url.protocol !== base.protocol || url.port !== base.port || sourcePath !== expectedPath) return '';
|
| 636 |
+
return unwrapProxy ? source : value;
|
| 637 |
+
}
|
| 638 |
+
if (!host.startsWith('[') && !/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
|
| 639 |
+
const labels = host.split('.');
|
| 640 |
+
if (host.length > 253 || labels.length < 2 || !labels.every(label => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))
|
| 641 |
+
|| /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(labels.at(-1))) return '';
|
| 642 |
+
}
|
| 643 |
+
if (!host || host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local')) return '';
|
| 644 |
+
if (host.startsWith('[')) {
|
| 645 |
+
if (!/^\[[23][0-9a-f]{0,3}:/i.test(host)) return '';
|
| 646 |
+
const pieces = host.slice(1, -1).split('::');
|
| 647 |
+
const left = pieces[0] ? pieces[0].split(':').map(part => parseInt(part, 16)) : [];
|
| 648 |
+
const right = pieces[1] ? pieces[1].split(':').map(part => parseInt(part, 16)) : [];
|
| 649 |
+
const groups = pieces.length === 1 ? left : [...left, ...Array(8 - left.length - right.length).fill(0), ...right];
|
| 650 |
+
const [first, second, third] = groups;
|
| 651 |
+
if (first === 0x2002 || (first === 0x3fff && second < 0x1000) || (first === 0x2001 && second === 0xdb8)) return '';
|
| 652 |
+
if (first === 0x2001 && second < 0x200) {
|
| 653 |
+
const exception = (second === 1 && groups.slice(2, 7).every(part => part === 0) && [1, 2].includes(groups[7]))
|
| 654 |
+
|| second === 3 || (second === 4 && third === 0x112) || (second >= 0x20 && second <= 0x3f);
|
| 655 |
+
if (!exception) return '';
|
| 656 |
+
}
|
| 657 |
+
} else if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
|
| 658 |
+
const rawHost = authority.replace(/:[0-9]+$/, '').replace(/\.$/, '').toLowerCase();
|
| 659 |
+
if (rawHost !== host) return '';
|
| 660 |
+
const [a,b,c,last] = host.split('.').map(Number);
|
| 661 |
+
if (a === 0 || a === 10 || a === 127 || a >= 224 || (a === 100 && b >= 64 && b <= 127)
|
| 662 |
+
|| (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31)
|
| 663 |
+
|| (a === 192 && b === 168) || (a === 198 && (b === 18 || b === 19))
|
| 664 |
+
|| (a === 192 && b === 0 && ((c === 0 && ![9, 10].includes(last)) || c === 2))
|
| 665 |
+
|| (a === 198 && b === 51 && c === 100) || (a === 203 && b === 0 && c === 113)) return '';
|
| 666 |
+
}
|
| 667 |
+
return unwrapProxy ? source : value;
|
| 668 |
+
} catch (_) { return ''; }
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
/** 统一字段映射:将后端 avatarDataUrl 映射为前端 avatar */
|
| 672 |
function _mapAvatarField(profile) {
|
| 673 |
if (profile && profile.avatarDataUrl && !profile.avatar) {
|
|
|
|
| 680 |
"receivedLikes", "receivedFavorites", "receivedUses"];
|
| 681 |
function _publicProfile(profile) {
|
| 682 |
return Object.fromEntries(PUBLIC_PROFILE_FIELDS.filter(key => Object.prototype.hasOwnProperty.call(profile, key))
|
| 683 |
+
.map(key => [key, ['avatar', 'avatarDataUrl', 'bannerUrl'].includes(key)
|
| 684 |
+
? normalizeProfileImageUrl(profile[key]) : profile[key]]));
|
| 685 |
}
|
| 686 |
|
| 687 |
function _validProfile(profile) {
|
|
|
|
| 787 |
|
| 788 |
// 从缓存获取
|
| 789 |
const cached = getCachedProfile(account);
|
| 790 |
+
const avatar = cached?.avatar || cached?.avatarDataUrl || normalizeProfileImageUrl(fallbackAvatar);
|
| 791 |
const name = cached?.name || fallbackName || account || '';
|
| 792 |
|
| 793 |
// 后台静默校对并更新 DOM
|
前端页面/core/榜单分页控制.js
CHANGED
|
@@ -26,7 +26,7 @@ export function setupSidebarPagination(contentArea, state, pageSize, loadMoreDat
|
|
| 26 |
const isCurrent = () => !stopped && renderToken === getRenderToken();
|
| 27 |
const loadMoreBtn = document.createElement('button');
|
| 28 |
loadMoreBtn.textContent = t('task.load_more');
|
| 29 |
-
loadMoreBtn.
|
| 30 |
state.loader = { stop() {
|
| 31 |
stopped = true;
|
| 32 |
loadMoreBtn.onclick = null;
|
|
@@ -34,6 +34,7 @@ export function setupSidebarPagination(contentArea, state, pageSize, loadMoreDat
|
|
| 34 |
} };
|
| 35 |
loadMoreBtn.onclick = async () => {
|
| 36 |
if (!isCurrent()) return;
|
|
|
|
| 37 |
loadMoreBtn.disabled = true;
|
| 38 |
loadMoreBtn.textContent = t('common.loading');
|
| 39 |
try {
|
|
@@ -53,6 +54,9 @@ export function setupSidebarPagination(contentArea, state, pageSize, loadMoreDat
|
|
| 53 |
loadMoreBtn.textContent = t('task.load_failed');
|
| 54 |
}
|
| 55 |
loadMoreBtn.disabled = false;
|
|
|
|
|
|
|
|
|
|
| 56 |
};
|
| 57 |
contentArea.appendChild(loadMoreBtn);
|
| 58 |
return;
|
|
|
|
| 26 |
const isCurrent = () => !stopped && renderToken === getRenderToken();
|
| 27 |
const loadMoreBtn = document.createElement('button');
|
| 28 |
loadMoreBtn.textContent = t('task.load_more');
|
| 29 |
+
loadMoreBtn.className = 'sidebar-status-more';
|
| 30 |
state.loader = { stop() {
|
| 31 |
stopped = true;
|
| 32 |
loadMoreBtn.onclick = null;
|
|
|
|
| 34 |
} };
|
| 35 |
loadMoreBtn.onclick = async () => {
|
| 36 |
if (!isCurrent()) return;
|
| 37 |
+
const ownedFocus = document.activeElement === loadMoreBtn;
|
| 38 |
loadMoreBtn.disabled = true;
|
| 39 |
loadMoreBtn.textContent = t('common.loading');
|
| 40 |
try {
|
|
|
|
| 54 |
loadMoreBtn.textContent = t('task.load_failed');
|
| 55 |
}
|
| 56 |
loadMoreBtn.disabled = false;
|
| 57 |
+
if (ownedFocus && isCurrent() && loadMoreBtn.isConnected && document.activeElement === document.body) {
|
| 58 |
+
loadMoreBtn.focus({ preventScroll: true });
|
| 59 |
+
}
|
| 60 |
};
|
| 61 |
contentArea.appendChild(loadMoreBtn);
|
| 62 |
return;
|
前端页面/core/榜单视图切换.js
CHANGED
|
@@ -42,7 +42,7 @@ export async function mountSidebarDomainView({
|
|
| 42 |
contentArea.innerHTML = '';
|
| 43 |
const error = document.createElement('div');
|
| 44 |
error.setAttribute('role', 'alert');
|
| 45 |
-
error.
|
| 46 |
error.textContent = `❌ ${t(errorKey)}`;
|
| 47 |
contentArea.appendChild(error);
|
| 48 |
}
|
|
|
|
| 42 |
contentArea.innerHTML = '';
|
| 43 |
const error = document.createElement('div');
|
| 44 |
error.setAttribute('role', 'alert');
|
| 45 |
+
error.className = 'sidebar-status-message sidebar-status-message--error';
|
| 46 |
error.textContent = `❌ ${t(errorKey)}`;
|
| 47 |
contentArea.appendChild(error);
|
| 48 |
}
|
前端页面/core/网络请求_图片代理.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
// 作用:处理图片URL的本地代理转换,支持入口清洗和出口剥离
|
| 6 |
// ==========================================
|
| 7 |
|
| 8 |
-
import { API, IS_WEB_MODE } from './全局配置.js';
|
| 9 |
import { isOnline } from './状态管理.js'; // 🌐 网络状态统一取自 状态管理.js,不再直读 navigator.onLine
|
| 10 |
|
| 11 |
// 🟢 入口清洗:接收云端数据时,转换为本地代理,并带【自愈机制】清理被污染的历史数据
|
|
@@ -65,10 +65,12 @@ export function proxyImages(obj) {
|
|
| 65 |
for (let key in obj) {
|
| 66 |
if (IMAGE_PROXY_FIELDS.includes(key) && typeof obj[key] === 'string') {
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
| 69 |
|
| 70 |
// 只有最终剥离出来的确实是外部网络链接(包括云端代理URL),才挂上本地缓存代理
|
| 71 |
-
if (originalUrl.startsWith('http')) {
|
| 72 |
// 📱 Web 模式:同源/直链可直接加载,不挂本地代理前缀
|
| 73 |
obj[key] = IS_WEB_MODE ? originalUrl : `/community_hub/image?url=${encodeURIComponent(originalUrl)}`;
|
| 74 |
} else {
|
|
|
|
| 5 |
// 作用:处理图片URL的本地代理转换,支持入口清洗和出口剥离
|
| 6 |
// ==========================================
|
| 7 |
|
| 8 |
+
import { API, IS_WEB_MODE, normalizeProfileImageUrl } from './全局配置.js';
|
| 9 |
import { isOnline } from './状态管理.js'; // 🌐 网络状态统一取自 状态管理.js,不再直读 navigator.onLine
|
| 10 |
|
| 11 |
// 🟢 入口清洗:接收云端数据时,转换为本地代理,并带【自愈机制】清理被污染的历史数据
|
|
|
|
| 65 |
for (let key in obj) {
|
| 66 |
if (IMAGE_PROXY_FIELDS.includes(key) && typeof obj[key] === 'string') {
|
| 67 |
|
| 68 |
+
const isProfileImage = key !== 'coverUrl' && key !== 'cover_image';
|
| 69 |
+
const safeUrl = isProfileImage ? normalizeProfileImageUrl(obj[key]) : obj[key];
|
| 70 |
+
let originalUrl = _unwrapProxy(safeUrl, '/community_hub/image?url=');
|
| 71 |
|
| 72 |
// 只有最终剥离出来的确实是外部网络链接(包括云端代理URL),才挂上本地缓存代理
|
| 73 |
+
if (isProfileImage ? /^https?:\/\//i.test(originalUrl) : originalUrl.startsWith('http')) {
|
| 74 |
// 📱 Web 模式:同源/直链可直接加载,不挂本地代理前缀
|
| 75 |
obj[key] = IS_WEB_MODE ? originalUrl : `/community_hub/image?url=${encodeURIComponent(originalUrl)}`;
|
| 76 |
} else {
|
前端页面/market/列表卡片组件.js
CHANGED
|
@@ -366,6 +366,8 @@ export function createItemCard(itemData, currentUser = null, contextType = null)
|
|
| 366 |
|
| 367 |
let isRendered = false;
|
| 368 |
let commentUI = null;
|
|
|
|
|
|
|
| 369 |
const toggleButton = summaryView.querySelector('.card-summary__toggle');
|
| 370 |
const setExpanded = (expanded) => {
|
| 371 |
if (expanded) {
|
|
@@ -377,6 +379,8 @@ export function createItemCard(itemData, currentUser = null, contextType = null)
|
|
| 377 |
detailView.hidden = !expanded;
|
| 378 |
detailView.style.display = expanded ? "block" : "none";
|
| 379 |
toggleButton.setAttribute("aria-expanded", String(expanded));
|
|
|
|
|
|
|
| 380 |
};
|
| 381 |
summaryView.onclick = (event) => {
|
| 382 |
// 摘要中的作者、评分等独立控件不触发折叠;标题按钮使用浏览器原生键盘行为。
|
|
@@ -386,7 +390,6 @@ export function createItemCard(itemData, currentUser = null, contextType = null)
|
|
| 386 |
setExpanded(isHidden);
|
| 387 |
|
| 388 |
if (isHidden && !isRendered) {
|
| 389 |
-
renderItemTrendChart(detailView.querySelector(`#${chartContainerId}`), itemData);
|
| 390 |
setupImageSandboxEvents(detailView);
|
| 391 |
isRendered = true;
|
| 392 |
|
|
@@ -458,7 +461,7 @@ export function createItemCard(itemData, currentUser = null, contextType = null)
|
|
| 458 |
// 🖼️ 启用图片懒加载(跳过 data URL 和已处理的)
|
| 459 |
lazyLoadImages(card);
|
| 460 |
|
| 461 |
-
card._cleanup = () => { cleanupImageSandbox(detailView); commentUI?._cleanup?.(); };
|
| 462 |
card.appendChild(summaryView);
|
| 463 |
card.appendChild(detailView);
|
| 464 |
return card;
|
|
|
|
| 366 |
|
| 367 |
let isRendered = false;
|
| 368 |
let commentUI = null;
|
| 369 |
+
let trendChart = null;
|
| 370 |
+
let trendChartStopped = false;
|
| 371 |
const toggleButton = summaryView.querySelector('.card-summary__toggle');
|
| 372 |
const setExpanded = (expanded) => {
|
| 373 |
if (expanded) {
|
|
|
|
| 379 |
detailView.hidden = !expanded;
|
| 380 |
detailView.style.display = expanded ? "block" : "none";
|
| 381 |
toggleButton.setAttribute("aria-expanded", String(expanded));
|
| 382 |
+
if (expanded && !trendChart && !trendChartStopped) trendChart = renderItemTrendChart(detailView.querySelector(`#${chartContainerId}`), itemData);
|
| 383 |
+
trendChart?.setActive(expanded);
|
| 384 |
};
|
| 385 |
summaryView.onclick = (event) => {
|
| 386 |
// 摘要中的作者、评分等独立控件不触发折叠;标题按钮使用浏览器原生键盘行为。
|
|
|
|
| 390 |
setExpanded(isHidden);
|
| 391 |
|
| 392 |
if (isHidden && !isRendered) {
|
|
|
|
| 393 |
setupImageSandboxEvents(detailView);
|
| 394 |
isRendered = true;
|
| 395 |
|
|
|
|
| 461 |
// 🖼️ 启用图片懒加载(跳过 data URL 和已处理的)
|
| 462 |
lazyLoadImages(card);
|
| 463 |
|
| 464 |
+
card._cleanup = () => { trendChartStopped = true; trendChart?.cleanup(); cleanupImageSandbox(detailView); commentUI?._cleanup?.(); };
|
| 465 |
card.appendChild(summaryView);
|
| 466 |
card.appendChild(detailView);
|
| 467 |
return card;
|
前端页面/market/创作者卡片组件.js
CHANGED
|
@@ -17,6 +17,7 @@ import { captureRequestSession, requestCancelManager } from "../core/网络请
|
|
| 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";
|
| 22 |
|
|
@@ -115,31 +116,7 @@ async function handleTipCreator(creatorData, currentUser, onTipSuccess, parentCu
|
|
| 115 |
}, null, { isCurrent: parentCurrent });
|
| 116 |
}
|
| 117 |
|
| 118 |
-
|
| 119 |
-
function loadECharts() {
|
| 120 |
-
if (typeof window.echarts?.init === 'function') return Promise.resolve(window.echarts);
|
| 121 |
-
if (echartsLoading) return echartsLoading;
|
| 122 |
-
echartsLoading = new Promise((resolve, reject) => {
|
| 123 |
-
const script = document.createElement("script");
|
| 124 |
-
let settled = false;
|
| 125 |
-
const finish = (error) => {
|
| 126 |
-
if (settled) return;
|
| 127 |
-
settled = true; clearTimeout(timer);
|
| 128 |
-
script.onload = script.onerror = null; script.remove();
|
| 129 |
-
if (error) { echartsLoadFailures++; reject(new Error('ECharts load failed')); }
|
| 130 |
-
else { echartsLoadFailures = 0; resolve(window.echarts); }
|
| 131 |
-
};
|
| 132 |
-
const timer = setTimeout(() => finish(true), API.PROFILE_RESOURCE_TIMEOUT);
|
| 133 |
-
const scriptURL = new URL(API.CREATOR_CHART_SCRIPT_URL);
|
| 134 |
-
// 移除script不保证终止原下载;失败后的读取不能复用未结束的旧URL。
|
| 135 |
-
if (echartsLoadFailures) scriptURL.searchParams.set('_ranking_retry', `${Date.now()}-${echartsLoadFailures}`);
|
| 136 |
-
script.src = scriptURL.href;
|
| 137 |
-
script.onload = () => finish(typeof window.echarts?.init !== 'function');
|
| 138 |
-
script.onerror = () => finish(true);
|
| 139 |
-
document.head.appendChild(script);
|
| 140 |
-
}).finally(() => { echartsLoading = null; });
|
| 141 |
-
return echartsLoading;
|
| 142 |
-
}
|
| 143 |
|
| 144 |
/**
|
| 145 |
* 渲染打赏榜单条目HTML(匿名/非匿名共用外层结构)
|
|
|
|
| 17 |
import { getUser, eventBus, EVENTS } from "../core/状态管理.js";
|
| 18 |
import { createTaskViewScope, subscribeTaskSessionChanges } from "../task/任务视图生命周期.js";
|
| 19 |
import { t } from "../components/用户体验增强.js";
|
| 20 |
+
import { loadECharts } from "../components/图表脚本加载.js";
|
| 21 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 22 |
import { openTipModal } from "../profile/个人中心_赞赏组件.js";
|
| 23 |
|
|
|
|
| 116 |
}, null, { isCurrent: parentCurrent });
|
| 117 |
}
|
| 118 |
|
| 119 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
/**
|
| 122 |
* 渲染打赏榜单条目HTML(匿名/非匿名共用外层结构)
|
前端页面/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, TYPE_RULES } from "../core/全局配置.js";
|
| 10 |
|
| 11 |
// ==========================================
|
| 12 |
// 📌 模块级常量
|
|
@@ -16,9 +16,6 @@ import { escapeHtml, TYPE_RULES } from "../core/全局配置.js";
|
|
| 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;';
|
| 21 |
-
|
| 22 |
/**
|
| 23 |
* 创建单个图片预览 wrapper 元素
|
| 24 |
* @param {string} imgUrl - 图片URL或Base64
|
|
@@ -27,11 +24,12 @@ const _COVER_LABEL_STYLE = 'position: absolute; top: 2px; left: 2px; background:
|
|
| 27 |
*/
|
| 28 |
function _createImagePreviewWrapper(imgUrl, idx) {
|
| 29 |
const wrapper = document.createElement('div');
|
| 30 |
-
wrapper.
|
|
|
|
| 31 |
wrapper.innerHTML = `
|
| 32 |
-
<img src="${escapeHtml(imgUrl)}"
|
| 33 |
-
${idx === 0 ? `<span class="cover-label
|
| 34 |
-
<button data-action="remove"
|
| 35 |
`;
|
| 36 |
return wrapper;
|
| 37 |
}
|
|
@@ -49,15 +47,23 @@ function updateCoverMark(previewContainer) {
|
|
| 49 |
const wrappers = previewContainer.querySelectorAll(':scope > div');
|
| 50 |
wrappers.forEach((wrapper, idx) => {
|
| 51 |
const img = wrapper.querySelector('img');
|
| 52 |
-
const coverLabel = wrapper.querySelector('.cover-label');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
if (img) {
|
| 54 |
-
|
| 55 |
}
|
| 56 |
if (idx === 0) {
|
| 57 |
if (!coverLabel) {
|
| 58 |
const label = document.createElement('span');
|
| 59 |
-
label.className = 'cover-label';
|
| 60 |
-
label.style.cssText = _COVER_LABEL_STYLE;
|
| 61 |
label.textContent = t('post.cover');
|
| 62 |
wrapper.appendChild(label);
|
| 63 |
}
|
|
@@ -67,16 +73,44 @@ function updateCoverMark(previewContainer) {
|
|
| 67 |
});
|
| 68 |
}
|
| 69 |
|
|
|
|
|
|
|
| 70 |
// ✅ 为图片预览容器设置拖拽排序
|
| 71 |
-
function setupImageDragSort(previewContainer, fileArray) {
|
|
|
|
|
|
|
| 72 |
const wrappers = Array.from(previewContainer.querySelectorAll(':scope > div'));
|
| 73 |
-
if (wrappers.length === 0) return;
|
|
|
|
|
|
|
|
|
|
| 74 |
|
|
|
|
| 75 |
let dragSrcEl = null;
|
| 76 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
// 清除所有旧事件,防止重复绑定
|
| 79 |
wrappers.forEach(wrapper => {
|
|
|
|
|
|
|
|
|
|
| 80 |
wrapper.ondragstart = null;
|
| 81 |
wrapper.ondragover = null;
|
| 82 |
wrapper.ondragleave = null;
|
|
@@ -86,101 +120,103 @@ function setupImageDragSort(previewContainer, fileArray) {
|
|
| 86 |
|
| 87 |
wrappers.forEach(wrapper => {
|
| 88 |
wrapper.draggable = true;
|
| 89 |
-
wrapper.
|
| 90 |
|
| 91 |
// 绑定删除按钮
|
| 92 |
const removeBtn = wrapper.querySelector('button[data-action="remove"]');
|
| 93 |
if (removeBtn) {
|
| 94 |
removeBtn.onclick = null; // 清除旧事件,防止重复绑定
|
| 95 |
removeBtn.onclick = (e) => {
|
| 96 |
-
|
| 97 |
const currentIdx = Array.from(previewContainer.children).indexOf(wrapper);
|
|
|
|
|
|
|
|
|
|
| 98 |
fileArray.splice(currentIdx, 1);
|
| 99 |
wrapper.remove();
|
| 100 |
updateCoverMark(previewContainer);
|
| 101 |
-
setupImageDragSort(previewContainer, fileArray);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
};
|
| 103 |
}
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
wrapper.ondragstart = (e) => {
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
dragSrcEl = wrapper;
|
| 108 |
-
|
| 109 |
-
|
|
|
|
| 110 |
e.dataTransfer.effectAllowed = 'move';
|
| 111 |
-
e.dataTransfer.setData('text/plain', String(
|
| 112 |
};
|
| 113 |
|
| 114 |
wrapper.ondragover = (e) => {
|
|
|
|
|
|
|
| 115 |
e.preventDefault();
|
| 116 |
e.stopPropagation();
|
| 117 |
e.dataTransfer.dropEffect = 'move';
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
if (targetIndex === dragSrcIndex) {
|
| 121 |
-
Array.from(previewContainer.children).forEach(w => {
|
| 122 |
-
w.style.borderLeft = '';
|
| 123 |
-
w.style.paddingLeft = '';
|
| 124 |
-
});
|
| 125 |
-
return;
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
// 清除所有插入指示
|
| 129 |
-
Array.from(previewContainer.children).forEach(w => {
|
| 130 |
-
w.style.borderLeft = '';
|
| 131 |
-
w.style.paddingLeft = '';
|
| 132 |
-
});
|
| 133 |
-
|
| 134 |
-
// 在当前目标左侧显示绿色竖线指示插入位置
|
| 135 |
-
wrapper.style.borderLeft = '3px solid #4CAF50';
|
| 136 |
-
wrapper.style.paddingLeft = '5px';
|
| 137 |
};
|
| 138 |
|
| 139 |
wrapper.ondragleave = () => {
|
| 140 |
-
|
| 141 |
-
wrapper.
|
| 142 |
};
|
| 143 |
|
| 144 |
wrapper.ondrop = (e) => {
|
|
|
|
|
|
|
| 145 |
e.preventDefault();
|
| 146 |
e.stopPropagation();
|
| 147 |
-
const
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
// 移动 DOM 元素
|
| 155 |
-
if (targetIndex > dragSrcIndex) {
|
| 156 |
-
previewContainer.insertBefore(dragSrcEl, wrapper.nextSibling);
|
| 157 |
-
} else {
|
| 158 |
-
previewContainer.insertBefore(dragSrcEl, wrapper);
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
// 更新封面标记
|
| 162 |
updateCoverMark(previewContainer);
|
| 163 |
};
|
| 164 |
|
| 165 |
wrapper.ondragend = () => {
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
Array.from(previewContainer.children).forEach(w => {
|
| 169 |
-
w.style.opacity = '';
|
| 170 |
-
w.style.borderLeft = '';
|
| 171 |
-
w.style.paddingLeft = '';
|
| 172 |
-
});
|
| 173 |
};
|
| 174 |
});
|
| 175 |
}
|
| 176 |
|
| 177 |
export function createPublishView(currentUser, onBackCallback, onSuccessCallback, editItemData = null, initialType = null) {
|
| 178 |
const container = document.createElement("div");
|
| 179 |
-
|
| 180 |
-
display: "flex", flexDirection: "column", gap: "15px", color: "#ccc",
|
| 181 |
-
// 🔧 自适应高度:flex 填充剩余空间,内容超出时在容器内部滚动(与主界面保持一致)
|
| 182 |
-
fontSize: "14px", padding: "15px", overflowY: "auto", flex: "1", minHeight: "0", boxSizing: "border-box"
|
| 183 |
-
});
|
| 184 |
|
| 185 |
const isEditMode = !!editItemData;
|
| 186 |
const viewTitle = isEditMode ? `✏️ ${t('publish.edit_content')}` : `🚀 ${t('publish.new_content')}`;
|
|
@@ -268,7 +304,7 @@ export function createPublishView(currentUser, onBackCallback, onSuccessCallback
|
|
| 268 |
if (effectiveTime > now) {
|
| 269 |
const hoursLeft = Math.ceil((effectiveTime - now) / (1000 * 60 * 60));
|
| 270 |
pendingHint.style.display = "block";
|
| 271 |
-
pendingHint.innerHTML = `⏳ ${t('publish.current_price')} <strong>${editItemData.price}</strong> ${t('common.credits')} → ${t('publish.will_change_in')} <strong>${hoursLeft}</strong> ${t('publish.hours_later')} <strong
|
| 272 |
}
|
| 273 |
}
|
| 274 |
|
|
@@ -282,7 +318,7 @@ export function createPublishView(currentUser, onBackCallback, onSuccessCallback
|
|
| 282 |
if (editImageUrls.length > 0) {
|
| 283 |
container.querySelector("#cover-file-hint").style.display = "inline";
|
| 284 |
renderImagePreviews(editImageUrls, imagesPreview);
|
| 285 |
-
setupImageDragSort(imagesPreview, editImageUrls);
|
| 286 |
}
|
| 287 |
|
| 288 |
if (!editItemData.type.startsWith("recommend")) {
|
|
@@ -465,7 +501,7 @@ export function createPublishView(currentUser, onBackCallback, onSuccessCallback
|
|
| 465 |
const url = URL.createObjectURL(file); previewUrls.add(url);
|
| 466 |
imagesPreview.appendChild(_createImagePreviewWrapper(url, index));
|
| 467 |
}
|
| 468 |
-
updateCoverMark(imagesPreview); setupImageDragSort(imagesPreview, imageFiles);
|
| 469 |
} catch (_) {
|
| 470 |
if (generation === previewGeneration && submissionScope.isCurrent()) {
|
| 471 |
releasePreviews(); imageFiles = []; imagesPreview.replaceChildren();
|
|
@@ -503,22 +539,23 @@ export function createPublishView(currentUser, onBackCallback, onSuccessCallback
|
|
| 503 |
const showOriginalConfirmModal = (onConfirm, onCancel) => {
|
| 504 |
if (!submissionScope?.isCurrent()) return;
|
| 505 |
const contentDiv = document.createElement("div");
|
|
|
|
| 506 |
contentDiv.innerHTML = `
|
| 507 |
-
<div
|
| 508 |
-
<p
|
| 509 |
-
<p
|
| 510 |
-
<ul
|
| 511 |
-
<li
|
| 512 |
-
<li
|
| 513 |
-
<li
|
| 514 |
</ul>
|
| 515 |
-
<p
|
| 516 |
💡 ${t('publish.original_confirm_warning')}
|
| 517 |
</p>
|
| 518 |
</div>
|
| 519 |
-
<div
|
| 520 |
-
<button id="btn-original-cancel"
|
| 521 |
-
<button id="btn-original-confirm"
|
| 522 |
</div>
|
| 523 |
`;
|
| 524 |
|
|
|
|
| 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, areKeyboardShortcutsEnabled } from "../core/全局配置.js";
|
| 10 |
|
| 11 |
// ==========================================
|
| 12 |
// 📌 模块级常量
|
|
|
|
| 16 |
// UI 显隐、联动、校验统一读取此配置,避免类型分支散落多处
|
| 17 |
export { TYPE_RULES };
|
| 18 |
|
|
|
|
|
|
|
|
|
|
| 19 |
/**
|
| 20 |
* 创建单个图片预览 wrapper 元素
|
| 21 |
* @param {string} imgUrl - 图片URL或Base64
|
|
|
|
| 24 |
*/
|
| 25 |
function _createImagePreviewWrapper(imgUrl, idx) {
|
| 26 |
const wrapper = document.createElement('div');
|
| 27 |
+
wrapper.className = 'ranking-publish__image';
|
| 28 |
+
wrapper.classList.toggle('ranking-publish__image--cover', idx === 0);
|
| 29 |
wrapper.innerHTML = `
|
| 30 |
+
<img src="${escapeHtml(imgUrl)}" class="ranking-publish__thumbnail" alt="">
|
| 31 |
+
${idx === 0 ? `<span class="cover-label ranking-publish__cover-label">${t('post.cover')}</span>` : ''}
|
| 32 |
+
<button type="button" data-action="remove" class="ranking-publish__image-remove" aria-label="${escapeHtml(t('post.remove_image', { index: idx + 1 }))}">×</button>
|
| 33 |
`;
|
| 34 |
return wrapper;
|
| 35 |
}
|
|
|
|
| 47 |
const wrappers = previewContainer.querySelectorAll(':scope > div');
|
| 48 |
wrappers.forEach((wrapper, idx) => {
|
| 49 |
const img = wrapper.querySelector('img');
|
| 50 |
+
const coverLabel = wrapper.querySelector('.cover-label');
|
| 51 |
+
wrapper.tabIndex = 0;
|
| 52 |
+
wrapper.setAttribute('role', 'group');
|
| 53 |
+
wrapper.setAttribute('aria-label', t('post.image_position', { index: idx + 1, total: wrappers.length }));
|
| 54 |
+
const shortcutsEnabled = areKeyboardShortcutsEnabled();
|
| 55 |
+
wrapper.title = shortcutsEnabled ? t('post.image_move_hint') : t('post.image_drag_hint');
|
| 56 |
+
if (shortcutsEnabled) wrapper.setAttribute('aria-keyshortcuts', 'Alt+ArrowLeft Alt+ArrowRight');
|
| 57 |
+
else wrapper.removeAttribute('aria-keyshortcuts');
|
| 58 |
+
const removeBtn = wrapper.querySelector('button[data-action="remove"]');
|
| 59 |
+
if (removeBtn) removeBtn.setAttribute('aria-label', t('post.remove_image', { index: idx + 1 }));
|
| 60 |
if (img) {
|
| 61 |
+
wrapper.classList.toggle('ranking-publish__image--cover', idx === 0);
|
| 62 |
}
|
| 63 |
if (idx === 0) {
|
| 64 |
if (!coverLabel) {
|
| 65 |
const label = document.createElement('span');
|
| 66 |
+
label.className = 'cover-label ranking-publish__cover-label';
|
|
|
|
| 67 |
label.textContent = t('post.cover');
|
| 68 |
wrapper.appendChild(label);
|
| 69 |
}
|
|
|
|
| 73 |
});
|
| 74 |
}
|
| 75 |
|
| 76 |
+
const imageSortOwners = new WeakMap();
|
| 77 |
+
|
| 78 |
// ✅ 为图片预览容器设置拖拽排序
|
| 79 |
+
function setupImageDragSort(previewContainer, fileArray, options = {}) {
|
| 80 |
+
const owner = {};
|
| 81 |
+
imageSortOwners.set(previewContainer, owner);
|
| 82 |
const wrappers = Array.from(previewContainer.querySelectorAll(':scope > div'));
|
| 83 |
+
if (wrappers.length === 0) return;
|
| 84 |
+
updateCoverMark(previewContainer);
|
| 85 |
+
const canInteract = () => previewContainer.isConnected &&
|
| 86 |
+
typeof options.isCurrent === 'function' && options.isCurrent();
|
| 87 |
|
| 88 |
+
const isActiveBinding = () => imageSortOwners.get(previewContainer) === owner && canInteract();
|
| 89 |
let dragSrcEl = null;
|
| 90 |
+
let dragSrcFile = null;
|
| 91 |
+
const resetDrag = () => {
|
| 92 |
+
dragSrcEl = null;
|
| 93 |
+
dragSrcFile = null;
|
| 94 |
+
Array.from(previewContainer.children).forEach(w => {
|
| 95 |
+
w.classList.remove('ranking-publish__image--dragging');
|
| 96 |
+
w.classList.remove('ranking-publish__image--target');
|
| 97 |
+
});
|
| 98 |
+
};
|
| 99 |
+
const getDragState = (target) => {
|
| 100 |
+
if (!isActiveBinding() || !dragSrcEl) return null;
|
| 101 |
+
const current = Array.from(previewContainer.children);
|
| 102 |
+
const from = current.indexOf(dragSrcEl);
|
| 103 |
+
const to = current.indexOf(target);
|
| 104 |
+
if (current.length !== fileArray.length || from < 0 || to < 0 ||
|
| 105 |
+
from >= fileArray.length || to >= fileArray.length || fileArray[from] !== dragSrcFile) return null;
|
| 106 |
+
return { current, from, to };
|
| 107 |
+
};
|
| 108 |
|
| 109 |
// 清除所有旧事件,防止重复绑定
|
| 110 |
wrappers.forEach(wrapper => {
|
| 111 |
+
wrapper.classList.remove('ranking-publish__image--dragging');
|
| 112 |
+
wrapper.classList.remove('ranking-publish__image--target');
|
| 113 |
+
wrapper.onkeydown = null;
|
| 114 |
wrapper.ondragstart = null;
|
| 115 |
wrapper.ondragover = null;
|
| 116 |
wrapper.ondragleave = null;
|
|
|
|
| 120 |
|
| 121 |
wrappers.forEach(wrapper => {
|
| 122 |
wrapper.draggable = true;
|
| 123 |
+
wrapper.classList.add('ranking-publish__image--sortable');
|
| 124 |
|
| 125 |
// 绑定删除按钮
|
| 126 |
const removeBtn = wrapper.querySelector('button[data-action="remove"]');
|
| 127 |
if (removeBtn) {
|
| 128 |
removeBtn.onclick = null; // 清除旧事件,防止重复绑定
|
| 129 |
removeBtn.onclick = (e) => {
|
| 130 |
+
if (!isActiveBinding() || removeBtn.disabled) return;
|
| 131 |
const currentIdx = Array.from(previewContainer.children).indexOf(wrapper);
|
| 132 |
+
if (currentIdx < 0 || currentIdx >= fileArray.length) return;
|
| 133 |
+
const ownedFocus = document.activeElement === removeBtn || document.activeElement === wrapper;
|
| 134 |
+
e.stopPropagation();
|
| 135 |
fileArray.splice(currentIdx, 1);
|
| 136 |
wrapper.remove();
|
| 137 |
updateCoverMark(previewContainer);
|
| 138 |
+
setupImageDragSort(previewContainer, fileArray, options);
|
| 139 |
+
if (ownedFocus && canInteract() && document.activeElement === document.body) {
|
| 140 |
+
const remaining = Array.from(previewContainer.children);
|
| 141 |
+
const next = remaining[Math.min(currentIdx, remaining.length - 1)];
|
| 142 |
+
if (next) next.focus({ preventScroll: true });
|
| 143 |
+
else options.focusEmpty?.();
|
| 144 |
+
}
|
| 145 |
};
|
| 146 |
}
|
| 147 |
|
| 148 |
+
wrapper.onkeydown = (e) => {
|
| 149 |
+
if (!areKeyboardShortcutsEnabled() || e.defaultPrevented || e.keyCode === 229 ||
|
| 150 |
+
e.target !== wrapper || !e.altKey || e.ctrlKey || e.metaKey || e.shiftKey ||
|
| 151 |
+
e.repeat || e.isComposing || !isActiveBinding() ||
|
| 152 |
+
!['ArrowLeft', 'ArrowRight'].includes(e.key)) return;
|
| 153 |
+
const current = Array.from(previewContainer.children);
|
| 154 |
+
const from = current.indexOf(wrapper);
|
| 155 |
+
const to = from + (e.key === 'ArrowLeft' ? -1 : 1);
|
| 156 |
+
e.preventDefault();
|
| 157 |
+
e.stopPropagation();
|
| 158 |
+
if (from < 0 || to < 0 || to >= current.length) return;
|
| 159 |
+
const [moved] = fileArray.splice(from, 1);
|
| 160 |
+
fileArray.splice(to, 0, moved);
|
| 161 |
+
previewContainer.insertBefore(wrapper, to > from ? current[to].nextSibling : current[to]);
|
| 162 |
+
updateCoverMark(previewContainer);
|
| 163 |
+
wrapper.focus({ preventScroll: true });
|
| 164 |
+
};
|
| 165 |
+
|
| 166 |
wrapper.ondragstart = (e) => {
|
| 167 |
+
if (!isActiveBinding() || e.defaultPrevented || !e.dataTransfer ||
|
| 168 |
+
typeof e.dataTransfer.setData !== 'function') return;
|
| 169 |
+
const current = Array.from(previewContainer.children);
|
| 170 |
+
const index = current.indexOf(wrapper);
|
| 171 |
+
if (current.length !== fileArray.length || index < 0 || index >= fileArray.length) return;
|
| 172 |
+
resetDrag();
|
| 173 |
dragSrcEl = wrapper;
|
| 174 |
+
dragSrcFile = fileArray[index];
|
| 175 |
+
e.stopPropagation();
|
| 176 |
+
wrapper.classList.add('ranking-publish__image--dragging');
|
| 177 |
e.dataTransfer.effectAllowed = 'move';
|
| 178 |
+
e.dataTransfer.setData('text/plain', String(index));
|
| 179 |
};
|
| 180 |
|
| 181 |
wrapper.ondragover = (e) => {
|
| 182 |
+
const state = getDragState(wrapper);
|
| 183 |
+
if (!state || e.defaultPrevented || !e.dataTransfer) return;
|
| 184 |
e.preventDefault();
|
| 185 |
e.stopPropagation();
|
| 186 |
e.dataTransfer.dropEffect = 'move';
|
| 187 |
+
state.current.forEach(w => w.classList.remove('ranking-publish__image--target'));
|
| 188 |
+
if (state.from !== state.to) wrapper.classList.add('ranking-publish__image--target');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
};
|
| 190 |
|
| 191 |
wrapper.ondragleave = () => {
|
| 192 |
+
if (!isActiveBinding() || !dragSrcEl) return;
|
| 193 |
+
wrapper.classList.remove('ranking-publish__image--target');
|
| 194 |
};
|
| 195 |
|
| 196 |
wrapper.ondrop = (e) => {
|
| 197 |
+
const state = getDragState(wrapper);
|
| 198 |
+
if (!state || e.defaultPrevented || !e.dataTransfer) return;
|
| 199 |
e.preventDefault();
|
| 200 |
e.stopPropagation();
|
| 201 |
+
const source = dragSrcEl;
|
| 202 |
+
resetDrag();
|
| 203 |
+
if (state.from === state.to) return;
|
| 204 |
+
const [moved] = fileArray.splice(state.from, 1);
|
| 205 |
+
fileArray.splice(state.to, 0, moved);
|
| 206 |
+
previewContainer.insertBefore(source, state.to > state.from ? state.current[state.to].nextSibling : wrapper);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
updateCoverMark(previewContainer);
|
| 208 |
};
|
| 209 |
|
| 210 |
wrapper.ondragend = () => {
|
| 211 |
+
if (!isActiveBinding()) return;
|
| 212 |
+
resetDrag();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
};
|
| 214 |
});
|
| 215 |
}
|
| 216 |
|
| 217 |
export function createPublishView(currentUser, onBackCallback, onSuccessCallback, editItemData = null, initialType = null) {
|
| 218 |
const container = document.createElement("div");
|
| 219 |
+
container.className = 'ranking-publish-view';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
const isEditMode = !!editItemData;
|
| 222 |
const viewTitle = isEditMode ? `✏️ ${t('publish.edit_content')}` : `🚀 ${t('publish.new_content')}`;
|
|
|
|
| 304 |
if (effectiveTime > now) {
|
| 305 |
const hoursLeft = Math.ceil((effectiveTime - now) / (1000 * 60 * 60));
|
| 306 |
pendingHint.style.display = "block";
|
| 307 |
+
pendingHint.innerHTML = `⏳ ${t('publish.current_price')} <strong>${escapeHtml(String(editItemData.price))}</strong> ${t('common.credits')} → ${t('publish.will_change_in')} <strong>${hoursLeft}</strong> ${t('publish.hours_later')} <strong class="ranking-publish__pending-value">${escapeHtml(String(editItemData.pending_price))}</strong> ${t('common.credits')}`;
|
| 308 |
}
|
| 309 |
}
|
| 310 |
|
|
|
|
| 318 |
if (editImageUrls.length > 0) {
|
| 319 |
container.querySelector("#cover-file-hint").style.display = "inline";
|
| 320 |
renderImagePreviews(editImageUrls, imagesPreview);
|
| 321 |
+
setupImageDragSort(imagesPreview, editImageUrls, { isCurrent: () => submissionScope?.isCurrent(), focusEmpty: () => { if (submissionScope?.isCurrent() && imagesInput.isConnected) imagesInput.focus({ preventScroll: true }); } });
|
| 322 |
}
|
| 323 |
|
| 324 |
if (!editItemData.type.startsWith("recommend")) {
|
|
|
|
| 501 |
const url = URL.createObjectURL(file); previewUrls.add(url);
|
| 502 |
imagesPreview.appendChild(_createImagePreviewWrapper(url, index));
|
| 503 |
}
|
| 504 |
+
updateCoverMark(imagesPreview); setupImageDragSort(imagesPreview, imageFiles, { isCurrent: () => submissionScope?.isCurrent(), focusEmpty: () => { if (submissionScope?.isCurrent() && imagesInput.isConnected) imagesInput.focus({ preventScroll: true }); } });
|
| 505 |
} catch (_) {
|
| 506 |
if (generation === previewGeneration && submissionScope.isCurrent()) {
|
| 507 |
releasePreviews(); imageFiles = []; imagesPreview.replaceChildren();
|
|
|
|
| 539 |
const showOriginalConfirmModal = (onConfirm, onCancel) => {
|
| 540 |
if (!submissionScope?.isCurrent()) return;
|
| 541 |
const contentDiv = document.createElement("div");
|
| 542 |
+
contentDiv.className = "ranking-original-confirm";
|
| 543 |
contentDiv.innerHTML = `
|
| 544 |
+
<div class="ranking-original-confirm__copy">
|
| 545 |
+
<p class="ranking-original-confirm__description">${t('publish.original_confirm_desc')}</p>
|
| 546 |
+
<p class="ranking-original-confirm__subtitle">${t('publish.original_confirm_subtitle')}</p>
|
| 547 |
+
<ul class="ranking-original-confirm__list">
|
| 548 |
+
<li>${t('publish.original_confirm_item1')}</li>
|
| 549 |
+
<li>${t('publish.original_confirm_item2')}</li>
|
| 550 |
+
<li>${t('publish.original_confirm_item3')}</li>
|
| 551 |
</ul>
|
| 552 |
+
<p class="ranking-original-confirm__warning">
|
| 553 |
💡 ${t('publish.original_confirm_warning')}
|
| 554 |
</p>
|
| 555 |
</div>
|
| 556 |
+
<div class="ranking-original-confirm__actions">
|
| 557 |
+
<button type="button" id="btn-original-cancel" class="ranking-original-confirm__button">${t('common.cancel')}</button>
|
| 558 |
+
<button type="button" id="btn-original-confirm" class="ranking-original-confirm__button ranking-original-confirm__button--confirm">${t('common.confirm')}</button>
|
| 559 |
</div>
|
| 560 |
`;
|
| 561 |
|
前端页面/profile/个人中心_UI模板.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
| 8 |
// - 打赏等级工具.js (打赏榜单等级渲染)
|
| 9 |
// ==========================================
|
| 10 |
|
| 11 |
-
import { getBannerCacheKey, PLACEHOLDERS } from "../core/全局配置.js";
|
| 12 |
import { t } from "../components/用户体验增强.js";
|
| 13 |
import { escapeHtml } from "../components/互动工具函数.js"; // 🧹 P2归一:局部副本已移除
|
| 14 |
|
|
@@ -21,17 +21,16 @@ export function buildProfileHTML(userData, isMe, isSettingsView, isFollowing, fo
|
|
| 21 |
// 个人资料卡背景图:
|
| 22 |
// - 自己查看自己:优先使用本地缓存(账号区分键),否则使用云端 URL
|
| 23 |
// - 他人查看:直接使用云端 URL (userData.bannerUrl)
|
| 24 |
-
let bannerImageUrl = userData.bannerUrl;
|
| 25 |
if (isMe) {
|
| 26 |
let localBannerCache = null;
|
| 27 |
try { localBannerCache = localStorage.getItem(getBannerCacheKey(userData.account)); } catch (_) {}
|
| 28 |
-
if (localBannerCache) {
|
| 29 |
bannerImageUrl = localBannerCache;
|
| 30 |
}
|
| 31 |
}
|
| 32 |
|
| 33 |
-
const hasBannerImage =
|
| 34 |
-
&& !/[\u0000-\u001f\u007f]/.test(bannerImageUrl);
|
| 35 |
const bannerStyle = hasBannerImage
|
| 36 |
? `background-image: ${escapeHtml(`url(${JSON.stringify(bannerImageUrl)})`)}; background-size: cover; background-position: center;`
|
| 37 |
: ``;
|
|
@@ -92,7 +91,7 @@ export function buildProfileHTML(userData, isMe, isSettingsView, isFollowing, fo
|
|
| 92 |
|
| 93 |
<!-- 用户信息区域 -->
|
| 94 |
<div class="profile-identity">
|
| 95 |
-
<img alt="" class="profile-avatar" src="${escapeHtml(userData.avatarDataUrl || userData.avatar || PLACEHOLDERS.AVATAR)}">
|
| 96 |
<div class="profile-bio">
|
| 97 |
<div class="profile-name">
|
| 98 |
${escapeHtml(userData.name)}
|
|
|
|
| 8 |
// - 打赏等级工具.js (打赏榜单等级渲染)
|
| 9 |
// ==========================================
|
| 10 |
|
| 11 |
+
import { getBannerCacheKey, PLACEHOLDERS, normalizeProfileImageUrl } from "../core/全局配置.js";
|
| 12 |
import { t } from "../components/用户体验增强.js";
|
| 13 |
import { escapeHtml } from "../components/互动工具函数.js"; // 🧹 P2归一:局部副本已移除
|
| 14 |
|
|
|
|
| 21 |
// 个人资料卡背景图:
|
| 22 |
// - 自己查看自己:优先使用本地缓存(账号区分键),否则使用云端 URL
|
| 23 |
// - 他人查看:直接使用云端 URL (userData.bannerUrl)
|
| 24 |
+
let bannerImageUrl = normalizeProfileImageUrl(userData.bannerUrl);
|
| 25 |
if (isMe) {
|
| 26 |
let localBannerCache = null;
|
| 27 |
try { localBannerCache = localStorage.getItem(getBannerCacheKey(userData.account)); } catch (_) {}
|
| 28 |
+
if (normalizeProfileImageUrl(localBannerCache)) {
|
| 29 |
bannerImageUrl = localBannerCache;
|
| 30 |
}
|
| 31 |
}
|
| 32 |
|
| 33 |
+
const hasBannerImage = !!bannerImageUrl;
|
|
|
|
| 34 |
const bannerStyle = hasBannerImage
|
| 35 |
? `background-image: ${escapeHtml(`url(${JSON.stringify(bannerImageUrl)})`)}; background-size: cover; background-position: center;`
|
| 36 |
: ``;
|
|
|
|
| 91 |
|
| 92 |
<!-- 用户信息区域 -->
|
| 93 |
<div class="profile-identity">
|
| 94 |
+
<img alt="" class="profile-avatar" src="${escapeHtml(normalizeProfileImageUrl(userData.avatarDataUrl) || normalizeProfileImageUrl(userData.avatar) || PLACEHOLDERS.AVATAR)}">
|
| 95 |
<div class="profile-bio">
|
| 96 |
<div class="profile-name">
|
| 97 |
${escapeHtml(userData.name)}
|
前端页面/profile/个人中心视图.js
CHANGED
|
@@ -12,7 +12,7 @@ import { openRechargeModal } from "../market/资金与钱包_充值组件.js";
|
|
| 12 |
import { openWithdrawModal } from "../market/资金与钱包_提现组件.js";
|
| 13 |
import { buildProfileHTML } from "./个人中心_UI模板.js";
|
| 14 |
import { openTipModal } from "./个人中心_赞赏组件.js";
|
| 15 |
-
import { getBannerCacheKey, getCurrentAccount, IS_WEB_MODE, CACHE, UI, API, getCachedProfile, setCachedProfile } from "../core/全局配置.js";
|
| 16 |
import { logoutAndClearUserData, eventBus, EVENTS } from "../core/状态管理.js";
|
| 17 |
|
| 18 |
|
|
@@ -33,16 +33,14 @@ function sessionStorages() {
|
|
| 33 |
// ==========================================
|
| 34 |
// 作用:当本地无缓存但云端有背景图时,下载并缓存到本地
|
| 35 |
async function syncBannerCache(account, bannerUrl, scope, apply) {
|
| 36 |
-
|
|
|
|
|
|
|
| 37 |
const controller = new AbortController();
|
| 38 |
const release = scope.addCleanup(() => controller.abort());
|
| 39 |
const timer = setTimeout(() => controller.abort(), API.BACKGROUND_TIMEOUT);
|
| 40 |
try {
|
| 41 |
-
|
| 42 |
-
const url = new URL(bannerUrl, window.location.href);
|
| 43 |
-
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password) return;
|
| 44 |
-
fetchUrl = !IS_WEB_MODE && url.origin !== window.location.origin
|
| 45 |
-
? `/community_hub/image?url=${encodeURIComponent(url.href)}` : url.href;
|
| 46 |
const response = await fetch(fetchUrl, { signal: controller.signal });
|
| 47 |
if (!response.ok || !scope.isCurrent()) return;
|
| 48 |
const blob = await response.blob();
|
|
@@ -85,12 +83,7 @@ function _setupTabButton(container, selector, tabId, onSwitch) {
|
|
| 85 |
|
| 86 |
export function showUserProfile(initialUserData, currentUser = null, isMe = true) {
|
| 87 |
const container = document.createElement("div");
|
| 88 |
-
|
| 89 |
-
display: "flex", flexDirection: "column", gap: "15px", color: "#eee",
|
| 90 |
-
// 🔧 自适应高度:flex 填充剩余空间,内容超出时在容器内部滚动(与主界面保持一致)
|
| 91 |
-
fontSize: "14px", padding: "15px", flex: "1", minHeight: "0", boxSizing: "border-box", overflowY: "auto",
|
| 92 |
-
background: "var(--comfy-menu-bg)"
|
| 93 |
-
});
|
| 94 |
|
| 95 |
let isSettingsView = false;
|
| 96 |
let activeTab = safeGet(`Profile_ActiveTab_${isMe}`) || "published";
|
|
|
|
| 12 |
import { openWithdrawModal } from "../market/资金与钱包_提现组件.js";
|
| 13 |
import { buildProfileHTML } from "./个人中心_UI模板.js";
|
| 14 |
import { openTipModal } from "./个人中心_赞赏组件.js";
|
| 15 |
+
import { getBannerCacheKey, getCurrentAccount, IS_WEB_MODE, CACHE, UI, API, getCachedProfile, setCachedProfile, normalizeProfileImageUrl } from "../core/全局配置.js";
|
| 16 |
import { logoutAndClearUserData, eventBus, EVENTS } from "../core/状态管理.js";
|
| 17 |
|
| 18 |
|
|
|
|
| 33 |
// ==========================================
|
| 34 |
// 作用:当本地无缓存但云端有背景图时,下载并缓存到本地
|
| 35 |
async function syncBannerCache(account, bannerUrl, scope, apply) {
|
| 36 |
+
const source = normalizeProfileImageUrl(bannerUrl, true);
|
| 37 |
+
if (!/^https?:\/\//i.test(source) || !scope.isCurrent()
|
| 38 |
+
|| normalizeProfileImageUrl(safeGet(getBannerCacheKey(account)))) return;
|
| 39 |
const controller = new AbortController();
|
| 40 |
const release = scope.addCleanup(() => controller.abort());
|
| 41 |
const timer = setTimeout(() => controller.abort(), API.BACKGROUND_TIMEOUT);
|
| 42 |
try {
|
| 43 |
+
const fetchUrl = IS_WEB_MODE ? source : API.LOCAL_IMAGE_PROXY_PREFIX + encodeURIComponent(source);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
const response = await fetch(fetchUrl, { signal: controller.signal });
|
| 45 |
if (!response.ok || !scope.isCurrent()) return;
|
| 46 |
const blob = await response.blob();
|
|
|
|
| 83 |
|
| 84 |
export function showUserProfile(initialUserData, currentUser = null, isMe = true) {
|
| 85 |
const container = document.createElement("div");
|
| 86 |
+
container.className = "profile-view";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
let isSettingsView = false;
|
| 89 |
let activeTab = safeGet(`Profile_ActiveTab_${isMe}`) || "published";
|
前端页面/profile/个人列表组件.js
CHANGED
|
@@ -775,8 +775,12 @@ function createPromptItem(prompt, currentUser) {
|
|
| 775 |
* 🕒 格式化帖子时间
|
| 776 |
*/
|
| 777 |
function formatPostTime(timestamp) {
|
| 778 |
-
if (
|
| 779 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 780 |
const now = new Date();
|
| 781 |
const diff = now - date;
|
| 782 |
|
|
@@ -787,6 +791,34 @@ function formatPostTime(timestamp) {
|
|
| 787 |
return `${date.getMonth() + 1}/${date.getDate()}`;
|
| 788 |
}
|
| 789 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
/**
|
| 791 |
* 💳 创建交易记录卡片
|
| 792 |
* 根据交易类型展示不同的详细信息
|
|
@@ -796,7 +828,7 @@ function createTransactionCard(tx) {
|
|
| 796 |
txDiv.className = 'profile-transaction-card';
|
| 797 |
|
| 798 |
const isPositive = tx.amount > 0;
|
| 799 |
-
const timeStr =
|
| 800 |
|
| 801 |
// 获取交易类型配置
|
| 802 |
const typeConfig = getTransactionTypeConfig(tx.tx_type);
|
|
|
|
| 775 |
* 🕒 格式化帖子时间
|
| 776 |
*/
|
| 777 |
function formatPostTime(timestamp) {
|
| 778 |
+
if (timestamp === null || timestamp === undefined || timestamp === "" || timestamp === 0) return "";
|
| 779 |
+
if (typeof timestamp !== "number" && typeof timestamp !== "string") return t('common.unknown');
|
| 780 |
+
if (typeof timestamp === "string" && timestamp.trim() === "") return t('common.unknown');
|
| 781 |
+
const seconds = Number(timestamp);
|
| 782 |
+
const date = new Date(seconds * 1000);
|
| 783 |
+
if (!Number.isFinite(seconds) || !Number.isFinite(date.getTime())) return t('common.unknown');
|
| 784 |
const now = new Date();
|
| 785 |
const diff = now - date;
|
| 786 |
|
|
|
|
| 791 |
return `${date.getMonth() + 1}/${date.getDate()}`;
|
| 792 |
}
|
| 793 |
|
| 794 |
+
function formatTransactionTime(timestamp) {
|
| 795 |
+
if (timestamp === null || timestamp === undefined || timestamp === "" || timestamp === 0) return "";
|
| 796 |
+
let date;
|
| 797 |
+
if (typeof timestamp === "number") {
|
| 798 |
+
if (!Number.isFinite(timestamp)) return t('common.unknown');
|
| 799 |
+
date = new Date(timestamp);
|
| 800 |
+
} else if (typeof timestamp === "string") {
|
| 801 |
+
const parts = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?(Z|[+-]\d{2}:\d{2})?$/.exec(timestamp);
|
| 802 |
+
if (!parts) return t('common.unknown');
|
| 803 |
+
const [year, month, day, hour, minute, second] = parts.slice(1, 7).map(Number);
|
| 804 |
+
if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59) return t('common.unknown');
|
| 805 |
+
const milliseconds = Number((parts[7] || '').padEnd(3, '0').slice(0, 3));
|
| 806 |
+
date = new Date(0);
|
| 807 |
+
date.setUTCFullYear(year, month - 1, day);
|
| 808 |
+
date.setUTCHours(hour, minute, second, milliseconds);
|
| 809 |
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return t('common.unknown');
|
| 810 |
+
const zone = parts[8];
|
| 811 |
+
if (zone && zone !== 'Z') {
|
| 812 |
+
const hours = Number(zone.slice(1, 3)), minutes = Number(zone.slice(4, 6));
|
| 813 |
+
if (hours > 23 || minutes > 59) return t('common.unknown');
|
| 814 |
+
const offset = (hours * 60 + minutes) * (zone[0] === '+' ? 1 : -1);
|
| 815 |
+
date = new Date(date.getTime() - offset * 60000);
|
| 816 |
+
}
|
| 817 |
+
} else return t('common.unknown');
|
| 818 |
+
if (!Number.isFinite(date.getTime())) return t('common.unknown');
|
| 819 |
+
return date.toLocaleString(getLanguage(), { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
/**
|
| 823 |
* 💳 创建交易记录卡片
|
| 824 |
* 根据交易类型展示不同的详细信息
|
|
|
|
| 828 |
txDiv.className = 'profile-transaction-card';
|
| 829 |
|
| 830 |
const isPositive = tx.amount > 0;
|
| 831 |
+
const timeStr = formatTransactionTime(tx.created_at);
|
| 832 |
|
| 833 |
// 获取交易类型配置
|
| 834 |
const typeConfig = getTransactionTypeConfig(tx.tx_type);
|
前端页面/profile/个人设置表单组件.js
CHANGED
|
@@ -4,7 +4,7 @@ import { regionData, getSortedCountries } from "../auth/国家地区数据.js";
|
|
| 4 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 5 |
import { uploadFile } from "../market/发布内容_提交引擎.js";
|
| 6 |
import { openImageCropper } from "../components/图片裁剪组件.js";
|
| 7 |
-
import { CACHE, IMAGE_PROCESS, getBackgroundKey, getBannerCacheKey, PLACEHOLDERS, escapeHtml } from "../core/全局配置.js";
|
| 8 |
import { t } from "../components/用户体验增强.js";
|
| 9 |
import { normalizeProfileData, mergeStoredProfile } from "./个人资料工具.js";
|
| 10 |
|
|
@@ -17,8 +17,13 @@ function _safeLocalStorageRemove(key) {
|
|
| 17 |
}
|
| 18 |
|
| 19 |
function _cssBackgroundImage(value) {
|
| 20 |
-
|
| 21 |
-
return `url(${JSON.stringify(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
// 计算年龄工具函数
|
|
@@ -84,7 +89,7 @@ function _readAndPreviewImage(file, previewElement, cacheKey, current, readers,
|
|
| 84 |
if (settled) return;
|
| 85 |
if (!current() || signal?.aborted || !previewElement.isConnected) { finish(null); return; }
|
| 86 |
const base64 = ev.target.result;
|
| 87 |
-
if (
|
| 88 |
if (cacheKey) {
|
| 89 |
try {
|
| 90 |
localStorage.setItem(cacheKey, base64);
|
|
@@ -155,7 +160,7 @@ export function createSettingsForm(initialUserData, onCancelCallback, onSaveSucc
|
|
| 155 |
<div class="settings-heading">⚙️ ${t('settings_form.title')}</div>
|
| 156 |
<div class="settings-fields">
|
| 157 |
<div class="settings-avatar-panel">
|
| 158 |
-
<img alt="" class="settings-avatar" id="setting-avatar-preview" src="${escapeHtml(userData.avatarDataUrl || PLACEHOLDERS.AVATAR)}">
|
| 159 |
<div>
|
| 160 |
<button type="button" class="settings-avatar-action" id="btn-trigger-avatar">${t('settings_form.change_avatar')}</button>
|
| 161 |
<div class="settings-hint">${t('settings_form.avatar_hint')}</div>
|
|
@@ -285,8 +290,8 @@ export function createSettingsForm(initialUserData, onCancelCallback, onSaveSucc
|
|
| 285 |
}
|
| 286 |
const birthdayInput = container.querySelector("#setting-birthday");
|
| 287 |
const ageDisplay = container.querySelector("#setting-age-display");
|
| 288 |
-
let avatarDataUrl = userData.avatarDataUrl;
|
| 289 |
-
let bannerUrl = userData.bannerUrl
|
| 290 |
|
| 291 |
// 🔧 初始化地区下拉列表:根据当前国家加载所有地区选项
|
| 292 |
const currentCountry = userData.country;
|
|
@@ -422,8 +427,10 @@ export function createSettingsForm(initialUserData, onCancelCallback, onSaveSucc
|
|
| 422 |
btnTrigger.innerText = t('settings_form.uploading');
|
| 423 |
btnTrigger.disabled = true;
|
| 424 |
try {
|
| 425 |
-
const
|
| 426 |
if (!current()) return;
|
|
|
|
|
|
|
| 427 |
if (cloudImageUrl) {
|
| 428 |
avatarDataUrl = cloudImageUrl;
|
| 429 |
container.querySelector("#setting-avatar-preview").src = cloudImageUrl;
|
|
@@ -465,7 +472,7 @@ export function createSettingsForm(initialUserData, onCancelCallback, onSaveSucc
|
|
| 465 |
// 「本次没改这个字段」而被跳过,云端旧图残留 —— 表现为点清除并保存后提示成功,
|
| 466 |
// 但自己和他人在创作者榜单里看到的背景图依然存在。空串则会真实落库,
|
| 467 |
// 且下游 hasBanner / bannerImageUrl 等判断都按 falsy 正确处理。
|
| 468 |
-
//
|
| 469 |
// 的语义,可防止 userData 恰好缺 bannerUrl 时把云端图误清掉。
|
| 470 |
cancelBannerSelection();
|
| 471 |
bannerUrl = "";
|
|
@@ -549,9 +556,10 @@ export function createSettingsForm(initialUserData, onCancelCallback, onSaveSucc
|
|
| 549 |
if (pendingBannerFile) {
|
| 550 |
btn.innerHTML = `⏳ ${escapeHtml(t('settings_form.uploading_banner'))}`;
|
| 551 |
try {
|
| 552 |
-
const
|
| 553 |
if (!current()) return;
|
| 554 |
-
|
|
|
|
| 555 |
bannerUrl = cloudImageUrl;
|
| 556 |
updateData.bannerUrl = cloudImageUrl;
|
| 557 |
pendingBannerFile = null;
|
|
|
|
| 4 |
import { showToast } from "../components/UI交互提示组件.js";
|
| 5 |
import { uploadFile } from "../market/发布内容_提交引擎.js";
|
| 6 |
import { openImageCropper } from "../components/图片裁剪组件.js";
|
| 7 |
+
import { CACHE, IMAGE_PROCESS, getBackgroundKey, getBannerCacheKey, PLACEHOLDERS, escapeHtml, normalizeProfileImageUrl } from "../core/全局配置.js";
|
| 8 |
import { t } from "../components/用户体验增强.js";
|
| 9 |
import { normalizeProfileData, mergeStoredProfile } from "./个人资料工具.js";
|
| 10 |
|
|
|
|
| 17 |
}
|
| 18 |
|
| 19 |
function _cssBackgroundImage(value) {
|
| 20 |
+
const image = normalizeProfileImageUrl(value);
|
| 21 |
+
return image ? `url(${JSON.stringify(image)})` : "none";
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
function _cloudImageValue(value) {
|
| 25 |
+
const image = normalizeProfileImageUrl(value, true);
|
| 26 |
+
return /^https?:\/\//i.test(image) ? image : null;
|
| 27 |
}
|
| 28 |
|
| 29 |
// 计算年龄工具函数
|
|
|
|
| 89 |
if (settled) return;
|
| 90 |
if (!current() || signal?.aborted || !previewElement.isConnected) { finish(null); return; }
|
| 91 |
const base64 = ev.target.result;
|
| 92 |
+
if (!normalizeProfileImageUrl(base64) || !base64.startsWith('data:image/')) { fail(); return; }
|
| 93 |
if (cacheKey) {
|
| 94 |
try {
|
| 95 |
localStorage.setItem(cacheKey, base64);
|
|
|
|
| 160 |
<div class="settings-heading">⚙️ ${t('settings_form.title')}</div>
|
| 161 |
<div class="settings-fields">
|
| 162 |
<div class="settings-avatar-panel">
|
| 163 |
+
<img alt="" class="settings-avatar" id="setting-avatar-preview" src="${escapeHtml(normalizeProfileImageUrl(userData.avatarDataUrl) || PLACEHOLDERS.AVATAR)}">
|
| 164 |
<div>
|
| 165 |
<button type="button" class="settings-avatar-action" id="btn-trigger-avatar">${t('settings_form.change_avatar')}</button>
|
| 166 |
<div class="settings-hint">${t('settings_form.avatar_hint')}</div>
|
|
|
|
| 290 |
}
|
| 291 |
const birthdayInput = container.querySelector("#setting-birthday");
|
| 292 |
const ageDisplay = container.querySelector("#setting-age-display");
|
| 293 |
+
let avatarDataUrl = _cloudImageValue(userData.avatarDataUrl);
|
| 294 |
+
let bannerUrl = _cloudImageValue(userData.bannerUrl);
|
| 295 |
|
| 296 |
// 🔧 初始化地区下拉列表:根据当前国家加载所有地区选项
|
| 297 |
const currentCountry = userData.country;
|
|
|
|
| 427 |
btnTrigger.innerText = t('settings_form.uploading');
|
| 428 |
btnTrigger.disabled = true;
|
| 429 |
try {
|
| 430 |
+
const uploadedUrl = await uploadFile(croppedFile, "avatar");
|
| 431 |
if (!current()) return;
|
| 432 |
+
const cloudImageUrl = _cloudImageValue(uploadedUrl);
|
| 433 |
+
if (!cloudImageUrl) throw new Error("Invalid avatar upload");
|
| 434 |
if (cloudImageUrl) {
|
| 435 |
avatarDataUrl = cloudImageUrl;
|
| 436 |
container.querySelector("#setting-avatar-preview").src = cloudImageUrl;
|
|
|
|
| 472 |
// 「本次没改这个字段」而被跳过,云端旧图残留 —— 表现为点清除并保存后提示成功,
|
| 473 |
// 但自己和他人在创作者榜单里看到的背景图依然存在。空串则会真实落库,
|
| 474 |
// 且下游 hasBanner / bannerImageUrl 等判断都按 falsy 正确处理。
|
| 475 |
+
// 初值只保留网络图片;临时预览或缺省值为 null,表示「本次未涉及该字段」
|
| 476 |
// 的语义,可防止 userData 恰好缺 bannerUrl 时把云端图误清掉。
|
| 477 |
cancelBannerSelection();
|
| 478 |
bannerUrl = "";
|
|
|
|
| 556 |
if (pendingBannerFile) {
|
| 557 |
btn.innerHTML = `⏳ ${escapeHtml(t('settings_form.uploading_banner'))}`;
|
| 558 |
try {
|
| 559 |
+
const uploadedUrl = await uploadFile(pendingBannerFile, "banner", { isCurrent: current, silent: true });
|
| 560 |
if (!current()) return;
|
| 561 |
+
const cloudImageUrl = _cloudImageValue(uploadedUrl);
|
| 562 |
+
if (!cloudImageUrl) throw new Error("Invalid banner upload");
|
| 563 |
bannerUrl = cloudImageUrl;
|
| 564 |
updateData.bannerUrl = cloudImageUrl;
|
| 565 |
pendingBannerFile = null;
|
前端页面/profile/个人资料工具.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
/** 用户资料与 SQL 钱包响应分别校验,未知内部字段不进入页面。 */
|
| 2 |
const PUBLIC_FIELDS = ['name', 'avatar', 'avatarDataUrl', 'bannerUrl', 'intro', 'gender', 'country', 'region'];
|
| 3 |
const WALLET_FIELDS = ['balance', 'earn_balance', 'tip_balance', 'task_balance', 'frozen_balance', 'total_withdrawn'];
|
|
@@ -7,7 +9,8 @@ export function normalizeProfileData(value, account, own = false) {
|
|
| 7 |
|| typeof account !== 'string' || !account.trim()) throw new Error('Invalid profile');
|
| 8 |
const result = { account };
|
| 9 |
for (const key of [...PUBLIC_FIELDS, ...(own ? ['email', 'phone', 'birthday'] : [])]) {
|
| 10 |
-
if (typeof value[key] === 'string') result[key] =
|
|
|
|
| 11 |
}
|
| 12 |
for (const key of ['age', 'created_at', 'receivedLikes', 'receivedFavorites', 'receivedUses']) {
|
| 13 |
if (typeof value[key] === 'number' && Number.isFinite(value[key]) && value[key] >= 0) result[key] = value[key];
|
|
|
|
| 1 |
+
import { normalizeProfileImageUrl } from '../core/全局配置.js';
|
| 2 |
+
|
| 3 |
/** 用户资料与 SQL 钱包响应分别校验,未知内部字段不进入页面。 */
|
| 4 |
const PUBLIC_FIELDS = ['name', 'avatar', 'avatarDataUrl', 'bannerUrl', 'intro', 'gender', 'country', 'region'];
|
| 5 |
const WALLET_FIELDS = ['balance', 'earn_balance', 'tip_balance', 'task_balance', 'frozen_balance', 'total_withdrawn'];
|
|
|
|
| 9 |
|| typeof account !== 'string' || !account.trim()) throw new Error('Invalid profile');
|
| 10 |
const result = { account };
|
| 11 |
for (const key of [...PUBLIC_FIELDS, ...(own ? ['email', 'phone', 'birthday'] : [])]) {
|
| 12 |
+
if (typeof value[key] === 'string') result[key] = ['avatar', 'avatarDataUrl', 'bannerUrl'].includes(key)
|
| 13 |
+
? normalizeProfileImageUrl(value[key]) : value[key];
|
| 14 |
}
|
| 15 |
for (const key of ['age', 'created_at', 'receivedLikes', 'receivedFavorites', 'receivedUses']) {
|
| 16 |
if (typeof value[key] === 'number' && Number.isFinite(value[key]) && value[key] >= 0) result[key] = value[key];
|
前端页面/styles/主题基础.css
CHANGED
|
@@ -1205,7 +1205,7 @@
|
|
| 1205 |
.ranking-modal-overlay[hidden] { display: none; }
|
| 1206 |
.ranking-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: var(--ranking-modal-width, 500px); max-width: calc(100vw - 24px); max-height: calc(100vh - 24px); max-height: calc(100dvh - 24px); box-sizing: border-box; display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: var(--rk-radius-md, 12px); background: var(--rk-bg, var(--comfy-menu-bg, #202020)); color: var(--rk-text, var(--fg-color, #ddd)); box-shadow: 0 10px 30px rgba(0,0,0,.4); }
|
| 1207 |
.ranking-modal__header { display: flex; flex-shrink: 0; justify-content: space-between; align-items: center; gap: 12px; padding: 15px 20px; border-bottom: 1px solid var(--rk-border, var(--border-color, #555)); background: var(--rk-panel, var(--comfy-input-bg, #292929)); }
|
| 1208 |
-
.ranking-modal__title { min-width: 0; overflow-wrap: anywhere; font-weight: bold; font-size:
|
| 1209 |
.ranking-modal__close { flex-shrink: 0; min-width: 32px; min-height: 32px; border: 0; border-radius: 4px; background: transparent; color: inherit; cursor: pointer; font-size: 14px; }
|
| 1210 |
.ranking-modal__body { min-height: 0; padding: 20px; overflow: auto; overflow-wrap: anywhere; }
|
| 1211 |
.ranking-modal :focus-visible { outline: 2px solid var(--rk-focus, #8ab4ff); outline-offset: 2px; }
|
|
@@ -2639,3 +2639,116 @@ button.ranking-rating__star { cursor: pointer; min-width: 28px; min-height: 28px
|
|
| 2639 |
.wallet-modal__danger, .wallet-modal__success { text-decoration-color: CanvasText; }
|
| 2640 |
.wallet-withdraw__summary, .wallet-withdraw__fee, .wallet-recharge__qr { border-color: CanvasText; }
|
| 2641 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1205 |
.ranking-modal-overlay[hidden] { display: none; }
|
| 1206 |
.ranking-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: var(--ranking-modal-width, 500px); max-width: calc(100vw - 24px); max-height: calc(100vh - 24px); max-height: calc(100dvh - 24px); box-sizing: border-box; display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: var(--rk-radius-md, 12px); background: var(--rk-bg, var(--comfy-menu-bg, #202020)); color: var(--rk-text, var(--fg-color, #ddd)); box-shadow: 0 10px 30px rgba(0,0,0,.4); }
|
| 1207 |
.ranking-modal__header { display: flex; flex-shrink: 0; justify-content: space-between; align-items: center; gap: 12px; padding: 15px 20px; border-bottom: 1px solid var(--rk-border, var(--border-color, #555)); background: var(--rk-panel, var(--comfy-input-bg, #292929)); }
|
| 1208 |
+
.ranking-modal__title { min-width: 0; overflow-wrap: anywhere; font-weight: bold; font-size: 1rem; }
|
| 1209 |
.ranking-modal__close { flex-shrink: 0; min-width: 32px; min-height: 32px; border: 0; border-radius: 4px; background: transparent; color: inherit; cursor: pointer; font-size: 14px; }
|
| 1210 |
.ranking-modal__body { min-height: 0; padding: 20px; overflow: auto; overflow-wrap: anywhere; }
|
| 1211 |
.ranking-modal :focus-visible { outline: 2px solid var(--rk-focus, #8ab4ff); outline-offset: 2px; }
|
|
|
|
| 2639 |
.wallet-modal__danger, .wallet-modal__success { text-decoration-color: CanvasText; }
|
| 2640 |
.wallet-withdraw__summary, .wallet-withdraw__fee, .wallet-recharge__qr { border-color: CanvasText; }
|
| 2641 |
}
|
| 2642 |
+
|
| 2643 |
+
.tip-level { max-width: 100%; overflow-wrap: anywhere; }
|
| 2644 |
+
.tip-level--new, .tip-level--fallback { color: var(--rk-text, var(--fg-color, #ddd)); font-size: .6875rem; }
|
| 2645 |
+
.tip-level__icon { overflow-wrap: anywhere; }
|
| 2646 |
+
.tip-level__icon--sun { font-size: .75rem; }
|
| 2647 |
+
.tip-level__icon--moon { font-size: .6875rem; }
|
| 2648 |
+
.tip-level__icon--star, .tip-level--fallback { font-size: .625rem; }
|
| 2649 |
+
.tip-level--full { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; font-size: .6875rem; }
|
| 2650 |
+
.tip-level__part { color: var(--rk-text, var(--fg-color, #ddd)); }
|
| 2651 |
+
.tip-level__part--sun { font-weight: bold; }
|
| 2652 |
+
.tip-level--max-compact { color: var(--rk-text, var(--fg-color, #ddd)); font-weight: bold; padding-inline: 2px; border-bottom: 2px solid #c38a00; }
|
| 2653 |
+
.tip-level--max { display: inline-block; box-sizing: border-box; padding: 2px 6px; border: 1px solid #a87500; border-radius: 4px; background: #ffdf80; color: #241c00; font-size: .6875rem; font-weight: bold; }
|
| 2654 |
+
.tip-board-item { --tip-badge-size: 1.25rem; --tip-avatar-size: 1.5rem; --tip-font-size: .75rem; --tip-badge-font: .625rem; --tip-amount-font: .6875rem; --tip-row-padding: 5px; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 4px 8px; min-width: 0; max-width: 100%; box-sizing: border-box; padding: var(--tip-row-padding) 0; border-bottom: 1px dashed var(--rk-border, var(--border-color, #454545)); color: var(--rk-text, var(--fg-color, #ddd)); font-size: var(--tip-font-size); line-height: 1.5; }
|
| 2655 |
+
.tip-board-item--small { --tip-badge-size: 1rem; --tip-avatar-size: 1.25rem; --tip-font-size: .6875rem; --tip-badge-font: .5625rem; --tip-amount-font: .625rem; --tip-row-padding: 3px; }
|
| 2656 |
+
.tip-board-item--large { --tip-badge-size: 1.5rem; --tip-avatar-size: 1.75rem; --tip-font-size: .875rem; --tip-badge-font: .75rem; --tip-amount-font: .8125rem; --tip-row-padding: 8px; }
|
| 2657 |
+
.tip-board-group { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
|
| 2658 |
+
.tip-board-item > .tip-board-group:first-child { flex: 1 1 8rem; }
|
| 2659 |
+
.tip-board-item > .tip-board-group:last-child { justify-content: flex-end; margin-inline-start: auto; }
|
| 2660 |
+
.tip-board-badge { display: inline-flex; justify-content: center; align-items: center; flex: 0 0 auto; min-width: var(--tip-badge-size); min-height: var(--tip-badge-size); box-sizing: border-box; padding: 1px 3px; border: 1px solid var(--rk-border, var(--border-color, #454545)); border-radius: 50%; background: var(--rk-panel, var(--comfy-input-bg, #292929)); color: var(--rk-text, var(--fg-color, #ddd)); font-size: var(--tip-badge-font); font-weight: bold; }
|
| 2661 |
+
.tip-board-badge--gold { background: #ffd700; color: #211900; border-color: #b28a00; }
|
| 2662 |
+
.tip-board-badge--silver { background: #c0c0c0; color: #191919; border-color: #777; }
|
| 2663 |
+
.tip-board-badge--bronze { background: #cd7f32; color: #1c1000; border-color: #8d511b; }
|
| 2664 |
+
.tip-board-avatar { width: var(--tip-avatar-size); height: var(--tip-avatar-size); max-width: 100%; border-radius: 50%; object-fit: cover; flex-shrink: 0; background: var(--rk-panel, var(--comfy-input-bg, #292929)); }
|
| 2665 |
+
.tip-board-name, .tip-board-anonymous { min-width: 0; max-width: 100%; color: var(--rk-text, var(--fg-color, #ddd)); overflow-wrap: anywhere; }
|
| 2666 |
+
.tip-board-amount { max-width: 100%; color: var(--rk-text, var(--fg-color, #ddd)); color: color-mix(in srgb, var(--rk-text, var(--fg-color, #ddd)) 80%, var(--rk-success, #4caf50)); font-weight: bold; font-size: var(--tip-amount-font); overflow-wrap: anywhere; }
|
| 2667 |
+
.tip-board-empty { padding: 12px 0; color: var(--rk-text, var(--fg-color, #ddd)); font-size: .75rem; text-align: center; overflow-wrap: anywhere; }
|
| 2668 |
+
@media (forced-colors: active) {
|
| 2669 |
+
.tip-board-item { border-color: CanvasText; color: CanvasText; }
|
| 2670 |
+
.tip-board-badge, .tip-level--max { border-color: CanvasText; background: Canvas; color: CanvasText; }
|
| 2671 |
+
.tip-level--max-compact { color: CanvasText; border-color: CanvasText; }
|
| 2672 |
+
.tip-level__part, .tip-level--new, .tip-level--fallback, .tip-board-name, .tip-board-anonymous, .tip-board-amount, .tip-board-empty { color: CanvasText; }
|
| 2673 |
+
}
|
| 2674 |
+
|
| 2675 |
+
|
| 2676 |
+
.ranking-publish-view { display:flex; flex-direction:column; flex:1; gap:15px; min-width:0; min-height:0; padding:15px; box-sizing:border-box; overflow-y:auto; color:var(--rk-text,var(--fg-color,#eee)); font:inherit; font-size:.875rem; line-height:1.5; color-scheme:normal; }
|
| 2677 |
+
.ranking-publish__image { position:relative; flex:0 0 96px; width:96px; height:96px; min-width:0; box-sizing:border-box; }
|
| 2678 |
+
.ranking-publish__image--sortable { cursor:grab; }
|
| 2679 |
+
.ranking-publish__image:focus-visible { outline:3px solid var(--rk-focus,#8ab4ff); outline-offset:2px; }
|
| 2680 |
+
.ranking-publish__thumbnail { display:block; width:100%; height:100%; object-fit:cover; box-sizing:border-box; border:2px solid var(--rk-border,var(--border-color,#777)); border-radius:6px; }
|
| 2681 |
+
.ranking-publish__image--cover .ranking-publish__thumbnail { border-color:var(--rk-success,#3a9360); }
|
| 2682 |
+
.ranking-publish__cover-label { position:absolute; top:2px; left:2px; max-width:calc(100% - 4px); padding:1px 4px; box-sizing:border-box; border:1px solid var(--rk-success,#3a9360); border-radius:3px; background:var(--rk-panel,var(--comfy-input-bg,#292929)); color:var(--rk-text,var(--fg-color,#eee)); font-size:.625rem; line-height:1.3; font-weight:700; overflow-wrap:anywhere; pointer-events:none; }
|
| 2683 |
+
.ranking-publish .ranking-publish__image-remove { position:absolute; right:2px; bottom:2px; width:36px; height:36px; min-height:36px; padding:0; display:grid; place-items:center; border:2px solid var(--rk-danger,#d14b4b); border-radius:6px; background:var(--rk-panel,var(--comfy-input-bg,#292929)); color:var(--rk-text,var(--fg-color,#eee)); font:inherit; font-size:.875rem; line-height:1; cursor:pointer; }
|
| 2684 |
+
.ranking-publish .ranking-publish__image-remove:hover:not(:disabled) { border-color:var(--rk-focus,#8ab4ff); }
|
| 2685 |
+
.ranking-publish .ranking-publish__image-remove:focus-visible { outline:3px solid var(--rk-focus,#8ab4ff); outline-offset:2px; }
|
| 2686 |
+
.ranking-publish__image--dragging { opacity:.5; }
|
| 2687 |
+
.ranking-publish__image--target::before { content:""; position:absolute; left:-5px; top:0; bottom:0; width:3px; border-radius:2px; background:var(--rk-success,#3a9360); pointer-events:none; }
|
| 2688 |
+
.ranking-publish__pending-value { padding-inline:3px; border-bottom:2px solid var(--rk-success,#3a9360); color:inherit; }
|
| 2689 |
+
.ranking-original-confirm { min-width:0; max-width:100%; color:var(--rk-text,var(--fg-color,#eee)); font:inherit; font-size:.875rem; line-height:1.6; overflow-wrap:anywhere; color-scheme:normal; }
|
| 2690 |
+
.ranking-original-confirm *, .ranking-original-confirm *::before, .ranking-original-confirm *::after { box-sizing:border-box; }
|
| 2691 |
+
.ranking-original-confirm__description { margin:0 0 15px; }
|
| 2692 |
+
.ranking-original-confirm__subtitle { margin:0 0 10px; font-weight:700; }
|
| 2693 |
+
.ranking-original-confirm__list { margin:0 0 15px; padding-inline-start:20px; }
|
| 2694 |
+
.ranking-original-confirm__list li { margin-bottom:5px; }
|
| 2695 |
+
.ranking-original-confirm__warning { margin:15px 0 0; padding:10px; border:1px solid var(--rk-border,var(--border-color,#777)); border-inline-start:3px solid var(--rk-warning,#d97706); border-radius:4px; background:var(--rk-bg,var(--comfy-menu-bg,#202020)); color:inherit; font-size:.75rem; }
|
| 2696 |
+
.ranking-original-confirm__actions { display:flex; flex-wrap:wrap; gap:10px; margin-top:20px; justify-content:flex-end; }
|
| 2697 |
+
.ranking-original-confirm__button { flex:0 1 auto; min-width:0; min-height:36px; max-width:100%; padding:8px 16px; border:1px solid var(--rk-border,var(--border-color,#777)); border-radius:6px; background:var(--rk-panel,var(--comfy-input-bg,#292929)); color:inherit; font:inherit; line-height:1.4; white-space:normal; overflow-wrap:anywhere; cursor:pointer; }
|
| 2698 |
+
.ranking-original-confirm__button--confirm { border-inline-start:3px solid var(--rk-success,#3a9360); font-weight:700; }
|
| 2699 |
+
.ranking-original-confirm__button:hover:not(:disabled) { border-color:var(--rk-focus,#8ab4ff); }
|
| 2700 |
+
.ranking-original-confirm__button:focus-visible { outline:3px solid var(--rk-focus,#8ab4ff); outline-offset:2px; }
|
| 2701 |
+
.ranking-original-confirm__button:disabled { opacity:.7; cursor:default; }
|
| 2702 |
+
@media (forced-colors:active) {
|
| 2703 |
+
.ranking-publish__thumbnail { border-color:CanvasText; }
|
| 2704 |
+
.ranking-publish__image--cover .ranking-publish__thumbnail { border-color:Highlight; }
|
| 2705 |
+
.ranking-publish__image--target::before { background:Highlight; }
|
| 2706 |
+
.ranking-publish__cover-label, .ranking-publish .ranking-publish__image-remove, .ranking-original-confirm__button { background:ButtonFace; color:ButtonText; border-color:ButtonText; }
|
| 2707 |
+
.ranking-publish__image:focus-visible, .ranking-publish .ranking-publish__image-remove:focus-visible, .ranking-original-confirm__button:focus-visible { outline-color:Highlight; }
|
| 2708 |
+
.ranking-publish__pending-value, .ranking-original-confirm__warning, .ranking-original-confirm__button--confirm { border-color:Highlight; }
|
| 2709 |
+
}
|
| 2710 |
+
|
| 2711 |
+
.comfy-ranking-root .sidebar-status-message {
|
| 2712 |
+
box-sizing: border-box;
|
| 2713 |
+
min-width: 0;
|
| 2714 |
+
max-width: 100%;
|
| 2715 |
+
padding: 2.5rem 1.25rem;
|
| 2716 |
+
color: var(--rk-text, var(--fg-color, #f1f1f1));
|
| 2717 |
+
font: inherit;
|
| 2718 |
+
line-height: 1.5;
|
| 2719 |
+
text-align: center;
|
| 2720 |
+
overflow-wrap: anywhere;
|
| 2721 |
+
}
|
| 2722 |
+
.comfy-ranking-root .sidebar-status-more {
|
| 2723 |
+
box-sizing: border-box;
|
| 2724 |
+
width: 100%;
|
| 2725 |
+
min-width: 0;
|
| 2726 |
+
min-height: 2.25rem;
|
| 2727 |
+
margin-top: .625rem;
|
| 2728 |
+
padding: .625rem;
|
| 2729 |
+
border: 1px solid var(--rk-border, var(--border-color, #555));
|
| 2730 |
+
border-radius: .375rem;
|
| 2731 |
+
background: transparent;
|
| 2732 |
+
color: var(--rk-text, var(--fg-color, #f1f1f1));
|
| 2733 |
+
font: inherit;
|
| 2734 |
+
line-height: 1.5;
|
| 2735 |
+
text-align: center;
|
| 2736 |
+
white-space: normal;
|
| 2737 |
+
overflow-wrap: anywhere;
|
| 2738 |
+
cursor: pointer;
|
| 2739 |
+
}
|
| 2740 |
+
.comfy-ranking-root .sidebar-status-more:focus-visible { outline: 2px solid var(--rk-text, var(--fg-color, #f1f1f1)); outline-offset: -3px; }
|
| 2741 |
+
.comfy-ranking-root .sidebar-status-more:disabled { cursor: wait; }
|
| 2742 |
+
@media (forced-colors: active) {
|
| 2743 |
+
.comfy-ranking-root .sidebar-status-message { color: CanvasText; }
|
| 2744 |
+
.comfy-ranking-root .sidebar-status-more { color: ButtonText; border-color: ButtonText; background: ButtonFace; }
|
| 2745 |
+
.comfy-ranking-root .sidebar-status-more:focus-visible { outline-color: Highlight; }
|
| 2746 |
+
.comfy-ranking-root .sidebar-status-more:disabled { color: GrayText; border-color: GrayText; }
|
| 2747 |
+
}
|
| 2748 |
+
|
| 2749 |
+
.comfy-ranking-root .profile-view { display:flex; flex-direction:column; gap:.9375rem; padding:.9375rem; flex:1; min-width:0; min-height:0; box-sizing:border-box; overflow-y:auto; background:var(--rk-bg,var(--comfy-menu-bg,#202020)); color:var(--rk-text,var(--fg-color,#eee)); font-size:.875rem; overflow-wrap:anywhere; }
|
| 2750 |
+
|
| 2751 |
+
.comfy-ranking-root .resource-trend { --resource-trend-text: var(--rk-text); --resource-trend-border: var(--rk-border); --resource-trend-line: var(--rk-text); --resource-trend-panel: var(--rk-panel); min-width:0; min-height:10rem; max-width:100%; transition-property:none; font-family:inherit; font-size:.75rem; color:var(--rk-text); }
|
| 2752 |
+
.comfy-ranking-root .resource-trend__state { display:flex; justify-content:center; align-items:center; min-height:160px; box-sizing:border-box; padding:1rem; text-align:center; font:inherit; line-height:1.5; overflow-wrap:anywhere; color:var(--rk-text); }
|
| 2753 |
+
.comfy-ranking-root .resource-trend__state[role=alert] { border-inline-start:2px solid var(--rk-danger); }
|
| 2754 |
+
@media (forced-colors:active) { .comfy-ranking-root .resource-trend { --resource-trend-text:CanvasText; --resource-trend-border:CanvasText; --resource-trend-line:CanvasText; --resource-trend-panel:Canvas; } .comfy-ranking-root .resource-trend__state { color:CanvasText; } .comfy-ranking-root .resource-trend__state[role=alert] { border-color:CanvasText; } }
|
前端页面/task/任务详情组件.js
CHANGED
|
@@ -1300,7 +1300,9 @@ async function loadTaskComments(container, taskId, currentUser) {
|
|
| 1300 |
* 🕐 格式化时间
|
| 1301 |
*/
|
| 1302 |
function formatTime(timestamp) {
|
| 1303 |
-
if (
|
|
|
|
|
|
|
| 1304 |
const seconds = Number(timestamp);
|
| 1305 |
const date = new Date(seconds * 1000);
|
| 1306 |
if (!Number.isFinite(seconds) || seconds < 0 || !Number.isFinite(date.getTime())) return t('common.unknown');
|
|
|
|
| 1300 |
* 🕐 格式化时间
|
| 1301 |
*/
|
| 1302 |
function formatTime(timestamp) {
|
| 1303 |
+
if (timestamp === null || timestamp === undefined || timestamp === "" || timestamp === 0) return "";
|
| 1304 |
+
if (typeof timestamp !== "number" && typeof timestamp !== "string") return t('common.unknown');
|
| 1305 |
+
if (typeof timestamp === "string" && timestamp.trim() === "") return t('common.unknown');
|
| 1306 |
const seconds = Number(timestamp);
|
| 1307 |
const date = new Date(seconds * 1000);
|
| 1308 |
if (!Number.isFinite(seconds) || seconds < 0 || !Number.isFinite(date.getTime())) return t('common.unknown');
|