ZHIWEI666 commited on
Commit
082b9cb
·
verified ·
1 Parent(s): 2411edd

Upload 101 files

Browse files
前端页面/auth/找回账号表单组件.js CHANGED
@@ -22,12 +22,12 @@ export function renderRecoverForm(container, switchView) {
22
  <p class="auth-form__description">${t('auth.recover_desc')}</p>
23
  <fieldset class="auth-form__security">
24
  <legend>${t('auth.security_verify')}</legend>
25
- <label for="recover-email">${t('auth.bound_email')} <span class="auth-form__required" aria-hidden="true">*</span></label>
26
  <div class="auth-form__code-row">
27
  <input type="email" id="recover-email" name="email" autocomplete="email" required placeholder="${t('auth.enter_bound_email')}" class="auth-form__input">
28
  <button type="button" id="btn-recover-send-code" class="auth-form__button">${t('auth.get_code')}</button>
29
  </div>
30
- <label for="recover-code">${t('auth.verification_code')} <span class="auth-form__required" aria-hidden="true">*</span></label>
31
  <input type="text" id="recover-code" name="verification-code" autocomplete="one-time-code" inputmode="numeric" required placeholder="${t('auth.enter_6_digit_code')}" maxlength="6" class="auth-form__input">
32
  </fieldset>
33
  <button type="button" id="btn-submit-recover" class="auth-form__button auth-form__primary">${t('auth.recover_account')}</button>
 
22
  <p class="auth-form__description">${t('auth.recover_desc')}</p>
23
  <fieldset class="auth-form__security">
24
  <legend>${t('auth.security_verify')}</legend>
25
+ <label for="recover-email">${t('auth.bound_email')} <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label>
26
  <div class="auth-form__code-row">
27
  <input type="email" id="recover-email" name="email" autocomplete="email" required placeholder="${t('auth.enter_bound_email')}" class="auth-form__input">
28
  <button type="button" id="btn-recover-send-code" class="auth-form__button">${t('auth.get_code')}</button>
29
  </div>
30
+ <label for="recover-code">${t('auth.verification_code')} <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label>
31
  <input type="text" id="recover-code" name="verification-code" autocomplete="one-time-code" inputmode="numeric" required placeholder="${t('auth.enter_6_digit_code')}" maxlength="6" class="auth-form__input">
32
  </fieldset>
33
  <button type="button" id="btn-submit-recover" class="auth-form__button auth-form__primary">${t('auth.recover_account')}</button>
前端页面/auth/注册表单组件.js CHANGED
@@ -29,21 +29,21 @@ export function renderRegisterForm(container, switchView, onSuccessCallback) {
29
  container.innerHTML = `
30
  <div class="auth-form">
31
  <div class="auth-form__grid">
32
- <div class="auth-form__field"><label for="reg-account">${t('auth.account')} (>5${t('common.chars')}) <span class="auth-form__required" aria-hidden="true">*</span></label><input type="text" id="reg-account" class="auth-form__input" name="username" autocomplete="username" required></div>
33
- <div class="auth-form__field"><label for="reg-name">${t('auth.display_name')} <span class="auth-form__required" aria-hidden="true">*</span></label><input type="text" id="reg-name" class="auth-form__input" name="nickname" autocomplete="nickname" required></div>
34
  </div>
35
  <div class="auth-form__grid">
36
- <div class="auth-form__field"><label for="reg-password">${t('auth.set_password')} (≥6${t('common.chars')}) <span class="auth-form__required" aria-hidden="true">*</span></label><input type="password" id="reg-password" class="auth-form__input" name="password" autocomplete="new-password" required></div>
37
- <div class="auth-form__field"><label for="reg-password-confirm">${t('auth.confirm_password')} <span class="auth-form__required" aria-hidden="true">*</span></label><input type="password" id="reg-password-confirm" class="auth-form__input" name="confirm-password" autocomplete="new-password" required></div>
38
  </div>
39
  <fieldset class="auth-form__security">
40
  <legend>${t('auth.security_verify')}</legend>
41
- <label for="reg-email">${t('auth.security_email')} (${t('auth.security_email_desc')}) <span class="auth-form__required" aria-hidden="true">*</span></label>
42
  <div class="auth-form__code-row">
43
  <input type="email" id="reg-email" name="email" autocomplete="email" required class="auth-form__input">
44
  <button type="button" id="btn-reg-send-code" class="auth-form__button">${t('auth.get_code')}</button>
45
  </div>
46
- <label for="reg-code">${t('auth.verification_code')} <span class="auth-form__required" aria-hidden="true">*</span></label>
47
  <input type="text" id="reg-code" name="verification-code" autocomplete="one-time-code" inputmode="numeric" placeholder="6${t('common.digits')}" maxlength="6" required class="auth-form__input">
48
  </fieldset>
49
  <div class="auth-form__grid auth-form__profile">
@@ -52,7 +52,7 @@ export function renderRegisterForm(container, switchView, onSuccessCallback) {
52
  <div class="auth-form__avatar-row"><img id="avatar-preview" src="${DEFAULT_AVATAR}" alt="" class="auth-form__avatar"><input type="file" id="reg-avatar" accept="image/*" class="auth-form__file"></div>
53
  <div id="avatar-error" class="auth-form__error" hidden>${t('auth.file_too_large')}</div>
54
  </div>
55
- <div class="auth-form__field"><label for="reg-gender">${t('auth.gender')} <span class="auth-form__required" aria-hidden="true">*</span></label><select id="reg-gender" required class="auth-form__input"><option value="male">${t('auth.male')}</option><option value="female">${t('auth.female')}</option></select></div>
56
  </div>
57
  <div class="auth-form__grid">
58
  <div class="auth-form__field">
 
29
  container.innerHTML = `
30
  <div class="auth-form">
31
  <div class="auth-form__grid">
32
+ <div class="auth-form__field"><label for="reg-account">${t('auth.account')} (>5${t('common.chars')}) <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label><input type="text" id="reg-account" class="auth-form__input" name="username" autocomplete="username" required></div>
33
+ <div class="auth-form__field"><label for="reg-name">${t('auth.display_name')} <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label><input type="text" id="reg-name" class="auth-form__input" name="nickname" autocomplete="nickname" required></div>
34
  </div>
35
  <div class="auth-form__grid">
36
+ <div class="auth-form__field"><label for="reg-password">${t('auth.set_password')} (≥6${t('common.chars')}) <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label><input type="password" id="reg-password" class="auth-form__input" name="password" autocomplete="new-password" required></div>
37
+ <div class="auth-form__field"><label for="reg-password-confirm">${t('auth.confirm_password')} <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label><input type="password" id="reg-password-confirm" class="auth-form__input" name="confirm-password" autocomplete="new-password" required></div>
38
  </div>
39
  <fieldset class="auth-form__security">
40
  <legend>${t('auth.security_verify')}</legend>
41
+ <label for="reg-email">${t('auth.security_email')} (${t('auth.security_email_desc')}) <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label>
42
  <div class="auth-form__code-row">
43
  <input type="email" id="reg-email" name="email" autocomplete="email" required class="auth-form__input">
44
  <button type="button" id="btn-reg-send-code" class="auth-form__button">${t('auth.get_code')}</button>
45
  </div>
46
+ <label for="reg-code">${t('auth.verification_code')} <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label>
47
  <input type="text" id="reg-code" name="verification-code" autocomplete="one-time-code" inputmode="numeric" placeholder="6${t('common.digits')}" maxlength="6" required class="auth-form__input">
48
  </fieldset>
49
  <div class="auth-form__grid auth-form__profile">
 
52
  <div class="auth-form__avatar-row"><img id="avatar-preview" src="${DEFAULT_AVATAR}" alt="" class="auth-form__avatar"><input type="file" id="reg-avatar" accept="image/*" class="auth-form__file"></div>
53
  <div id="avatar-error" class="auth-form__error" hidden>${t('auth.file_too_large')}</div>
54
  </div>
55
+ <div class="auth-form__field"><label for="reg-gender">${t('auth.gender')} <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label><select id="reg-gender" required class="auth-form__input"><option value="male">${t('auth.male')}</option><option value="female">${t('auth.female')}</option></select></div>
56
  </div>
57
  <div class="auth-form__grid">
58
  <div class="auth-form__field">
前端页面/auth/登录表单组件.js CHANGED
@@ -4,8 +4,8 @@ import { t } from "../components/用户体验增强.js";
4
  export function renderLoginForm(container, switchView, onSuccessCallback) {
5
  container.innerHTML = `
6
  <div class="auth-login">
7
- <div class="auth-login__field"><label for="login-account">${t('auth.account')} <span class="auth-login__required" aria-hidden="true">*</span></label><input type="text" id="login-account" name="username" autocomplete="username" required class="auth-login__input"></div>
8
- <div class="auth-login__field"><label for="login-password">${t('auth.password')} <span class="auth-login__required" aria-hidden="true">*</span></label><input type="password" id="login-password" name="password" autocomplete="current-password" required class="auth-login__input"></div>
9
  <div class="auth-login__options">
10
  <div class="auth-login__remember">
11
  <input type="checkbox" id="login-remember" checked>
 
4
  export function renderLoginForm(container, switchView, onSuccessCallback) {
5
  container.innerHTML = `
6
  <div class="auth-login">
7
+ <div class="auth-login__field"><label for="login-account">${t('auth.account')} <span class="auth-login__required ranking-required-marker" aria-hidden="true"></span></label><input type="text" id="login-account" name="username" autocomplete="username" required class="auth-login__input"></div>
8
+ <div class="auth-login__field"><label for="login-password">${t('auth.password')} <span class="auth-login__required ranking-required-marker" aria-hidden="true"></span></label><input type="password" id="login-password" name="password" autocomplete="current-password" required class="auth-login__input"></div>
9
  <div class="auth-login__options">
10
  <div class="auth-login__remember">
11
  <input type="checkbox" id="login-remember" checked>
前端页面/auth/重置密码表单组件.js CHANGED
@@ -6,20 +6,20 @@ import { t } from "../components/用户体验增强.js";
6
  export function renderResetForm(container, switchView, onSuccessCallback, prefill = null) {
7
  container.innerHTML = `
8
  <div class="auth-form">
9
- <div class="auth-form__field"><label for="reset-account">${t('auth.current_account')} <span class="auth-form__required" aria-hidden="true">*</span></label><input type="text" id="reset-account" class="auth-form__input" name="username" autocomplete="username" required></div>
10
  <fieldset class="auth-form__security">
11
  <legend>${t('auth.security_verify')}</legend>
12
- <label for="reset-verify">${t('auth.bound_email')} <span class="auth-form__required" aria-hidden="true">*</span></label>
13
  <div class="auth-form__code-row">
14
  <input type="email" id="reset-verify" name="email" autocomplete="email" required placeholder="${t('auth.enter_bound_email')}" class="auth-form__input">
15
  <button type="button" id="btn-send-code" class="auth-form__button">${t('auth.get_code')}</button>
16
  </div>
17
- <label for="reset-code">${t('auth.verification_code')} <span class="auth-form__required" aria-hidden="true">*</span></label>
18
  <input type="text" id="reset-code" name="verification-code" autocomplete="one-time-code" inputmode="numeric" required placeholder="${t('auth.enter_6_digit_code')}" maxlength="6" class="auth-form__input">
19
  </fieldset>
20
  <div class="auth-form__field"><label for="reset-old-password">${t('auth.old_password')} (${t('auth.old_password_tip')})</label><input type="password" id="reset-old-password" class="auth-form__input" name="old-password" autocomplete="current-password" placeholder="${t('auth.old_password_placeholder')}"></div>
21
- <div class="auth-form__field"><label for="reset-new-password">${t('auth.new_password')} (≥6${t('common.chars')}) <span class="auth-form__required" aria-hidden="true">*</span></label><input type="password" id="reset-new-password" class="auth-form__input" name="new-password" autocomplete="new-password" required></div>
22
- <div class="auth-form__field"><label for="reset-confirm-password">${t('auth.confirm_new_password')} <span class="auth-form__required" aria-hidden="true">*</span></label><input type="password" id="reset-confirm-password" class="auth-form__input" name="confirm-password" autocomplete="new-password" required></div>
23
  <button type="button" id="btn-submit-reset" class="auth-form__button auth-form__primary">${t('auth.change_password')}</button>
24
  <div class="auth-form__footer"><button type="button" id="toggle-to-login" class="auth-form__link">${t('auth.back_to_login')}</button></div>
25
  </div>
 
6
  export function renderResetForm(container, switchView, onSuccessCallback, prefill = null) {
7
  container.innerHTML = `
8
  <div class="auth-form">
9
+ <div class="auth-form__field"><label for="reset-account">${t('auth.current_account')} <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label><input type="text" id="reset-account" class="auth-form__input" name="username" autocomplete="username" required></div>
10
  <fieldset class="auth-form__security">
11
  <legend>${t('auth.security_verify')}</legend>
12
+ <label for="reset-verify">${t('auth.bound_email')} <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label>
13
  <div class="auth-form__code-row">
14
  <input type="email" id="reset-verify" name="email" autocomplete="email" required placeholder="${t('auth.enter_bound_email')}" class="auth-form__input">
15
  <button type="button" id="btn-send-code" class="auth-form__button">${t('auth.get_code')}</button>
16
  </div>
17
+ <label for="reset-code">${t('auth.verification_code')} <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label>
18
  <input type="text" id="reset-code" name="verification-code" autocomplete="one-time-code" inputmode="numeric" required placeholder="${t('auth.enter_6_digit_code')}" maxlength="6" class="auth-form__input">
19
  </fieldset>
20
  <div class="auth-form__field"><label for="reset-old-password">${t('auth.old_password')} (${t('auth.old_password_tip')})</label><input type="password" id="reset-old-password" class="auth-form__input" name="old-password" autocomplete="current-password" placeholder="${t('auth.old_password_placeholder')}"></div>
21
+ <div class="auth-form__field"><label for="reset-new-password">${t('auth.new_password')} (≥6${t('common.chars')}) <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label><input type="password" id="reset-new-password" class="auth-form__input" name="new-password" autocomplete="new-password" required></div>
22
+ <div class="auth-form__field"><label for="reset-confirm-password">${t('auth.confirm_new_password')} <span class="auth-form__required ranking-required-marker" aria-hidden="true"></span></label><input type="password" id="reset-confirm-password" class="auth-form__input" name="confirm-password" autocomplete="new-password" required></div>
23
  <button type="button" id="btn-submit-reset" class="auth-form__button auth-form__primary">${t('auth.change_password')}</button>
24
  <div class="auth-form__footer"><button type="button" id="toggle-to-login" class="auth-form__link">${t('auth.back_to_login')}</button></div>
25
  </div>
前端页面/components/互动工具函数.js CHANGED
@@ -37,9 +37,13 @@ const _pendingActions = new WeakMap();
37
  */
38
  export async function recordView(apiMethod, contentId, cachePrefix, onSuccess = null) {
39
  // 🚀 登录检查:未登录时跳过浏览量记录,避免 401 错误
40
- const token = localStorage.getItem("ComfyCommunity_Token") || sessionStorage.getItem("ComfyCommunity_Token");
 
 
 
 
41
  // 🔒 P0同步:旧版 mock_token 已被后端拒绝,仅标准 JWT 格式才记录浏览量
42
- if (!token || token.split(".").length !== 3) {
43
  return null;
44
  }
45
 
 
37
  */
38
  export async function recordView(apiMethod, contentId, cachePrefix, onSuccess = null) {
39
  // 🚀 登录检查:未登录时跳过浏览量记录,避免 401 错误
40
+ let token = null;
41
+ for (const readStorage of [() => localStorage, () => sessionStorage]) {
42
+ try { token = readStorage()?.getItem("ComfyCommunity_Token"); } catch {}
43
+ if (token) break;
44
+ }
45
  // 🔒 P0同步:旧版 mock_token 已被后端拒绝,仅标准 JWT 格式才记录浏览量
46
+ if (typeof token !== "string" || token.split(".").length !== 3) {
47
  return null;
48
  }
49
 
前端页面/components/性能优化工具.js CHANGED
@@ -671,7 +671,7 @@ export function endTimer(label) {
671
  // 🎨 骨架屏生成器
672
  // ==========================================
673
  // 特点:
674
- // - 适配深色主题
675
  // - 多种卡片类型
676
  // - 流畅的动画效果
677
 
@@ -685,42 +685,6 @@ export function createSkeleton(type = "card", count = 1) {
685
  const container = document.createElement("div");
686
  container.className = "skeleton-container";
687
 
688
- // 🎨 深色主题骨架屏样式
689
- const skeletonStyles = `
690
- .skeleton-container {
691
- padding: 8px;
692
- }
693
- .skeleton-item {
694
- background: linear-gradient(90deg, #2a2a2a 25%, #3a3a3a 50%, #2a2a2a 75%);
695
- background-size: 200% 100%;
696
- animation: skeleton-shimmer 1.5s ease-in-out infinite;
697
- border-radius: 6px;
698
- }
699
- .skeleton-card {
700
- background: var(--comfy-menu-bg);
701
- border: 1px solid var(--border-color, #333);
702
- border-radius: 8px;
703
- padding: 12px;
704
- margin-bottom: 12px;
705
- }
706
- @keyframes skeleton-shimmer {
707
- 0% { background-position: 200% 0; }
708
- 100% { background-position: -200% 0; }
709
- }
710
- @keyframes skeleton-pulse {
711
- 0%, 100% { opacity: 0.4; }
712
- 50% { opacity: 0.8; }
713
- }
714
- `;
715
-
716
- // 注入样式(仅一次)
717
- if (!document.getElementById("skeleton-styles-v2")) {
718
- const styleEl = document.createElement("style");
719
- styleEl.id = "skeleton-styles-v2";
720
- styleEl.textContent = skeletonStyles;
721
- document.head.appendChild(styleEl);
722
- }
723
-
724
  for (let i = 0; i < count; i++) {
725
  const item = document.createElement("div");
726
 
 
671
  // 🎨 骨架屏生成器
672
  // ==========================================
673
  // 特点:
674
+ // - 跟随界面主题
675
  // - 多种卡片类型
676
  // - 流畅的动画效果
677
 
 
685
  const container = document.createElement("div");
686
  container.className = "skeleton-container";
687
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
  for (let i = 0; i < count; i++) {
689
  const item = document.createElement("div");
690
 
前端页面/components/用户体验_国际化.js CHANGED
@@ -586,6 +586,10 @@ const translations = {
586
  'feedback.network_unavailable': '网络连接失败,请检查网络',
587
  'feedback.http_request_failed': '请求失败(HTTP {status})',
588
  'feedback.invalid_request_data': '数据格式不正确,请检查填写内容。',
 
 
 
 
589
  'feedback.response_format_error': '服务器响应格式错误',
590
  'feedback.saved': '已保存',
591
  'feedback.logged_out': '已退出登录',
@@ -1786,13 +1790,13 @@ const translations = {
1786
  'about.warning_account_desc': '可能是你的浏览器自动帮你填入别的网站账号密码,账号是字符(字母、数字、符号)没有中文。',
1787
  'about.guide_title': '使用指南',
1788
  'about.guide_install_title': '安装插件和Skill',
1789
- 'about.guide_install_desc': '打开喜欢的作品详情页,点「立即获取使用」按钮:免费作品一键装好,付费作品买完后就能装。装完后重启 ComfyUI,新插件就会出现在节点列表里。',
1790
  'about.guide_path_title': '文件下载到哪里',
1791
- 'about.guide_path_desc': '插件和Skill统一装进 ComfyUI 安装目录里的 custom_nodes 文件夹,不用自己搬文件;工作流下载后自动加载到画布上,打开就能用。',
1792
  'about.guide_update_title': '更新到新版本',
1793
  'about.guide_update_desc': '作者发布新版本后,作品详情页会出现「发现新版本」按钮,点一下自动完成更新,不用先卸载旧版本。',
1794
  'about.guide_publish_title': '发布自己的作品',
1795
- 'about.guide_publish_desc': '点榜单顶部的「发布」按钮,填好名称、介绍和效果图就能提交。插件和Skill填代码仓库的网址,工作流直接上传工作流文件。',
1796
  'about.guide_buy_title': '购买付费作品',
1797
  'about.guide_buy_desc': '买付费作品要先让账户里有足够余额:进「个人中心-钱包」用支付宝扫码充值。充好后回详情页点「立即获取使用」即完成购买,一次购买终身有效。',
1798
  'about.guide_refund_title': '申请退款',
@@ -2378,6 +2382,10 @@ const translations = {
2378
  'feedback.network_unavailable': 'Network connection failed. Check your connection.',
2379
  'feedback.http_request_failed': 'Request failed (HTTP {status})',
2380
  'feedback.invalid_request_data': 'The submitted data has an invalid format. Check the form.',
 
 
 
 
2381
  'feedback.response_format_error': 'The server response format is invalid',
2382
  'feedback.saved': 'Saved',
2383
  'feedback.logged_out': 'Logged out',
@@ -3540,13 +3548,13 @@ const translations = {
3540
  'about.warning_account_desc': 'Your browser may have auto-filled credentials from another site. Account names only contain letters, numbers, and symbols — no Chinese characters.',
3541
  'about.guide_title': 'Usage Guide',
3542
  'about.guide_install_title': 'Installing Plugins & Skills',
3543
- 'about.guide_install_desc': 'Open the detail page of an item you like and click the "Get Now" button: free items install with one click, and paid items install right after purchase. When the installation finishes, restart ComfyUI and the new plugin will appear in your node list.',
3544
  'about.guide_path_title': 'Where Files Are Downloaded',
3545
- 'about.guide_path_desc': 'Plugins and Skills are installed automatically into the custom_nodes folder inside your ComfyUI directory, so you never need to move files yourself. Workflows are loaded straight onto the canvas after download, ready to use.',
3546
  'about.guide_update_title': 'Updating to New Versions',
3547
  'about.guide_update_desc': 'When an author releases a new version, an "Update available" button appears on the item\'s detail page. Click it to update automatically — no need to remove the old version first.',
3548
  'about.guide_publish_title': 'Publishing Your Own Work',
3549
- 'about.guide_publish_desc': 'Click the "Publish" button at the top of the ranking, fill in the name, description and preview images, then submit. For plugins and Skills, paste the web address of your code repository; for workflows, upload the workflow file directly.',
3550
  'about.guide_buy_title': 'Buying Paid Items',
3551
  'about.guide_buy_desc': 'Paid items need enough balance in your account first: open "Profile - Wallet" and top up by scanning the Alipay QR code. Then return to the detail page and click "Get Now" to finish the purchase — one purchase, yours forever.',
3552
  'about.guide_refund_title': 'Requesting a Refund',
 
586
  'feedback.network_unavailable': '网络连接失败,请检查网络',
587
  'feedback.http_request_failed': '请求失败(HTTP {status})',
588
  'feedback.invalid_request_data': '数据格式不正确,请检查填写内容。',
589
+ 'feedback.request_too_large': '提交内容过大,请缩小内容后重试。',
590
+ 'feedback.request_receive_timeout': '数据接收超时,本次操作尚未提交,请重试。',
591
+ 'feedback.request_busy': '当前请求较多,本次操作尚未提交,请稍后重试。',
592
+ 'feedback.request_encoding_unsupported': '暂不支持压缩提交,请取消压缩后重试。',
593
  'feedback.response_format_error': '服务器响应格式错误',
594
  'feedback.saved': '已保存',
595
  'feedback.logged_out': '已退出登录',
 
1790
  'about.warning_account_desc': '可能是你的浏览器自动帮你填入别的网站账号密码,账号是字符(字母、数字、符号)没有中文。',
1791
  'about.guide_title': '使用指南',
1792
  'about.guide_install_title': '安装插件和Skill',
1793
+ 'about.guide_install_desc': '打开作品详情,点「立即获取」并按提示操作;付费作品先完成购买。Skill 需选择接收目录,预览后确认安装。插件装好后重启 ComfyUI。',
1794
  'about.guide_path_title': '文件下载到哪里',
1795
+ 'about.guide_path_desc': '插件装在 ComfyUI 的 custom_nodes 文件夹。Skill 放在你选择的接收目录下,并保留仓库同名文件夹。工作流获取后会加载到画布。',
1796
  'about.guide_update_title': '更新到新版本',
1797
  'about.guide_update_desc': '作者发布新版本后,作品详情页会出现「发现新版本」按钮,点一下自动完成更新,不用先卸载旧版本。',
1798
  'about.guide_publish_title': '发布自己的作品',
1799
+ 'about.guide_publish_desc': '点榜单顶部的「发布」,填写名称和介绍。插件可填写代码仓库地址;Skill 请填写仓库根地址,不支持子目录地址;工作流上传 JSON 文件。',
1800
  'about.guide_buy_title': '购买付费作品',
1801
  'about.guide_buy_desc': '买付费作品要先让账户里有足够余额:进「个人中心-钱包」用支付宝扫码充值。充好后回详情页点「立即获取使用」即完成购买,一次购买终身有效。',
1802
  'about.guide_refund_title': '申请退款',
 
2382
  'feedback.network_unavailable': 'Network connection failed. Check your connection.',
2383
  'feedback.http_request_failed': 'Request failed (HTTP {status})',
2384
  'feedback.invalid_request_data': 'The submitted data has an invalid format. Check the form.',
2385
+ 'feedback.request_too_large': 'The submitted content is too large. Reduce its size and try again.',
2386
+ 'feedback.request_receive_timeout': 'Receiving the data timed out. The operation was not submitted. Try again.',
2387
+ 'feedback.request_busy': 'Too many requests are active. The operation was not submitted. Try again later.',
2388
+ 'feedback.request_encoding_unsupported': 'Compressed submissions are not supported. Submit the data without compression.',
2389
  'feedback.response_format_error': 'The server response format is invalid',
2390
  'feedback.saved': 'Saved',
2391
  'feedback.logged_out': 'Logged out',
 
3548
  'about.warning_account_desc': 'Your browser may have auto-filled credentials from another site. Account names only contain letters, numbers, and symbols — no Chinese characters.',
3549
  'about.guide_title': 'Usage Guide',
3550
  'about.guide_install_title': 'Installing Plugins & Skills',
3551
+ 'about.guide_install_desc': 'Open an item, click "Get Now" and follow the instructions. Paid items require purchase first. For a Skill, choose a destination folder, review the preview and confirm installation. Restart ComfyUI after installing a plugin.',
3552
  'about.guide_path_title': 'Where Files Are Downloaded',
3553
+ 'about.guide_path_desc': 'Plugins go into the custom_nodes folder inside ComfyUI. A Skill keeps its repository folder inside the destination you choose. Workflows load onto the canvas after download.',
3554
  'about.guide_update_title': 'Updating to New Versions',
3555
  'about.guide_update_desc': 'When an author releases a new version, an "Update available" button appears on the item\'s detail page. Click it to update automatically — no need to remove the old version first.',
3556
  'about.guide_publish_title': 'Publishing Your Own Work',
3557
+ 'about.guide_publish_desc': 'Click "Publish" at the top of a board and enter a name and description. Plugins can use a code repository URL. Skills require a repository root URL, not a subfolder URL. Upload a JSON file for a workflow.',
3558
  'about.guide_buy_title': 'Buying Paid Items',
3559
  'about.guide_buy_desc': 'Paid items need enough balance in your account first: open "Profile - Wallet" and top up by scanning the Alipay QR code. Then return to the detail page and click "Get Now" to finish the purchase — one purchase, yours forever.',
3560
  'about.guide_refund_title': 'Requesting a Refund',
前端页面/core/全局配置.js CHANGED
@@ -18,12 +18,20 @@
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})$/,
25
  ENDPOINT_ERROR_SCOPE_HEADER: "X-Ranking-Error-Scope",
26
  ENDPOINT_ERROR_SCOPE_VALUE: "endpoint",
 
 
 
 
 
 
 
 
27
  // 仅公开榜单使用由正文派生的条件验证器,私有响应仍禁止留存。
28
  PUBLIC_CONDITIONAL_GET_PATHS: ['/api/items', '/api/creators', '/api/creators/search', '/api/posts', '/api/prompts', '/api/tasks'],
29
  CONDITIONAL_ETAG_PATTERN: /^W\/"rk-[a-f0-9]{64}"$/,
 
18
  // 🌐 API 配置
19
  // ==========================================
20
 
21
+ /** @type {{ BASE_URL: string, TIMEOUT: number, VIDEO_UPLOAD_TIMEOUT: number, VIDEO_UPLOAD_RESPONSE_MAX_CHARS: number, LOCAL_CACHE_STATS_TIMEOUT: number, LOCAL_CACHE_CLEAR_TIMEOUT: number, LOCAL_CACHE_RESPONSE_MAX_BYTES: number, SSE_MAX_RESPONSE_BYTES: number, SSE_MAX_LINE_BYTES: number, SSE_MAX_LINES: number, SSE_YIELD_LINES: number, SSE_YIELD_BYTES: number, SSE_YIELD_CHUNKS: number, MAX_RETRIES: number, RETRY_DELAY: number, LIST_RETRIES: number, SEARCH_TIMEOUT: number, SEARCH_RETRIES: number, PROFILE_RESOURCE_TIMEOUT: number, CREATOR_CHART_SCRIPT_URL: string, BACKGROUND_TIMEOUT: number, BANNER_IMAGE_TIMEOUT: number, BACKGROUND_RETRIES: number, WORKFLOW_DOWNLOAD_TIMEOUT: number, CLOUD_DOWN_COOLDOWN: number, SEARCH_COMPONENT_ID: string, IMAGE_PROXY_PATH: string, PROFILE_MEDIA_URL_MAX_BYTES: number, LOCAL_IMAGE_PROXY_PREFIX: string, COMPLETE_IMAGE_QUERY_SUFFIX: string, PRIVATE_GET_PATTERNS: RegExp[], PUBLIC_CONDITIONAL_GET_PATHS: string[], CONDITIONAL_ETAG_PATTERN: RegExp, REQUEST_ID_HEADER: string, REQUEST_ID_PATTERN: RegExp, ENDPOINT_ERROR_SCOPE_HEADER: string, ENDPOINT_ERROR_SCOPE_VALUE: string, REQUEST_BODY_ERRORS: Record<string, { status: number, msgKey: string }> }} */
22
  export const API = {
23
  REQUEST_ID_HEADER: "X-Ranking-Request-Id",
24
  REQUEST_ID_PATTERN: /^(?:[a-f0-9]{32}|ERR_[0-9]{10,13}_[a-f0-9]{1,8})$/,
25
  ENDPOINT_ERROR_SCOPE_HEADER: "X-Ranking-Error-Scope",
26
  ENDPOINT_ERROR_SCOPE_VALUE: "endpoint",
27
+ REQUEST_BODY_ERRORS: {
28
+ request_body_invalid: { status: 400, msgKey: 'feedback.invalid_request_data' },
29
+ request_body_disconnected: { status: 400, msgKey: 'feedback.network_unavailable' },
30
+ request_body_too_large: { status: 413, msgKey: 'feedback.request_too_large' },
31
+ request_body_timeout: { status: 408, msgKey: 'feedback.request_receive_timeout' },
32
+ request_body_busy: { status: 429, msgKey: 'feedback.request_busy' },
33
+ request_body_encoding: { status: 415, msgKey: 'feedback.request_encoding_unsupported' },
34
+ },
35
  // 仅公开榜单使用由正文派生的条件验证器,私有响应仍禁止留存。
36
  PUBLIC_CONDITIONAL_GET_PATHS: ['/api/items', '/api/creators', '/api/creators/search', '/api/posts', '/api/prompts', '/api/tasks'],
37
  CONDITIONAL_ETAG_PATTERN: /^W\/"rk-[a-f0-9]{64}"$/,
前端页面/core/网络请求_基础设施.js CHANGED
@@ -47,6 +47,10 @@ function withRequestDiagnostic(error, response, data = null) {
47
  }
48
 
49
  function httpErrorMessage(data, status) {
 
 
 
 
50
  if (Array.isArray(data.detail)) return t('feedback.invalid_request_data');
51
  for (const value of [data.detail, data.message, data.error]) {
52
  if (typeof value === 'string' && value.trim()) return value;
@@ -524,6 +528,10 @@ async function request(endpoint, options = {}) {
524
  const error = withRequestDiagnostic(new Error(errorMsg), response, responseData);
525
  error.status = response.status;
526
  if (typeof responseData.detail === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(responseData.detail)) error.code = responseData.detail;
 
 
 
 
527
  throw error;
528
  }
529
 
 
47
  }
48
 
49
  function httpErrorMessage(data, status) {
50
+ if (typeof data.detail === 'string' && Object.hasOwn(API.REQUEST_BODY_ERRORS, data.detail)) {
51
+ const rule = API.REQUEST_BODY_ERRORS[data.detail];
52
+ if (rule.status === status) return t(rule.msgKey);
53
+ }
54
  if (Array.isArray(data.detail)) return t('feedback.invalid_request_data');
55
  for (const value of [data.detail, data.message, data.error]) {
56
  if (typeof value === 'string' && value.trim()) return value;
 
528
  const error = withRequestDiagnostic(new Error(errorMsg), response, responseData);
529
  error.status = response.status;
530
  if (typeof responseData.detail === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(responseData.detail)) error.code = responseData.detail;
531
+ if (result.bodyConfirmed && Object.hasOwn(API.REQUEST_BODY_ERRORS, error.code)
532
+ && API.REQUEST_BODY_ERRORS[error.code].status === error.status) {
533
+ error.requestBodyRejected = true;
534
+ }
535
  throw error;
536
  }
537
 
前端页面/core/网络请求_缓存管理.js CHANGED
@@ -302,9 +302,14 @@ function _touchStorageEntry(key, cached) {
302
  try {
303
  const now = Date.now();
304
  if (Number.isFinite(cached.lastAccessAt) && now - cached.lastAccessAt < 60_000) return;
305
- if (localStorage.getItem(key) === null) return;
 
 
 
306
  cached.lastAccessAt = now;
307
- localStorage.setItem(key, JSON.stringify(cached));
 
 
308
  } catch {}
309
  }
310
 
 
302
  try {
303
  const now = Date.now();
304
  if (Number.isFinite(cached.lastAccessAt) && now - cached.lastAccessAt < 60_000) return;
305
+ const stored = localStorage.getItem(key);
306
+ if (stored === null) return;
307
+ const persisted = JSON.parse(stored);
308
+ if (!_isValidCacheEntry(persisted)) return;
309
  cached.lastAccessAt = now;
310
+ if (Number.isFinite(persisted.lastAccessAt) && now - persisted.lastAccessAt < 60_000) return;
311
+ persisted.lastAccessAt = now;
312
+ localStorage.setItem(key, JSON.stringify(persisted));
313
  } catch {}
314
  }
315
 
前端页面/market/列表卡片组件.js CHANGED
@@ -173,196 +173,233 @@ export function createItemCard(itemData, currentUser = null, contextType = null)
173
  detailView.className = "resource-detail";
174
  detailView.style.display = "none";
175
 
176
- const authorInfo = document.createElement("div");
177
- authorInfo.className = "resource-detail__author";
 
 
 
 
178
 
179
- // 🔄 P7后悔模式:检查是否有待生效价格
180
- const priceHtml = _generatePriceHTML(itemData, t);
181
 
182
- authorInfo.innerHTML = `${t('market.author')}: <button type="button" class="author-name-link card-author-link" ${itemData.author ? '' : 'disabled'}>${t('common.loading')}...</button> ${priceHtml}`;
183
- detailView.appendChild(authorInfo);
184
 
185
- const nameDOM = authorInfo.querySelector('.author-name-link');
186
 
187
- // 使用统一辅助函数加载作者名称
188
- _loadAuthorProfile(itemData.author, {
189
- onName: (name) => { nameDOM.textContent = name; }
190
- });
191
 
192
- nameDOM.onclick = (e) => { e.stopPropagation(); openOtherUserProfileModal(itemData.author, currentUser); };
193
-
194
- // 原创内容标识
195
- if (itemData.is_original) {
196
- const originalBadge = document.createElement("div");
197
- originalBadge.className = "resource-detail__badge resource-detail__badge--original";
198
- originalBadge.textContent = "🎨 " + t('publish.mark_original');
199
- detailView.appendChild(originalBadge);
200
- }
201
 
202
- // 支持退款标识(仅工具、Skill和应用类型显示,且允许退款时)
203
- if (itemData.allow_refund !== false && (itemData.type === 'tool' || itemData.type === 'skill' || itemData.type === 'app')) {
204
- const refundBadge = document.createElement("div");
205
- refundBadge.className = "resource-detail__badge resource-detail__badge--refundable";
206
- refundBadge.textContent = `🛡️ ${t('item.refundable')}`;
207
- detailView.appendChild(refundBadge);
208
- }
209
 
210
- const actionArea = document.createElement("div");
211
- actionArea.className = "resource-detail__actions";
212
- const btnLike = document.createElement("button");
213
- btnLike.type = "button";
214
- const btnFav = document.createElement("button");
215
- btnFav.type = "button";
216
 
217
- // 🚀 根据安装状态显示不同的按钮样式(installStatus 已在上方声明)
218
- const btnUse = document.createElement("button");
219
- btnUse.type = "button";
220
- btnUse.className = "resource-detail__button resource-detail__use";
221
 
222
- if (installStatus.hasUpdate) {
223
- // 有更新可用
224
- btnUse.classList.add("resource-detail__button--warning");
225
- btnUse.innerHTML = `♻️ ${t('market.update_available')}`;
226
- } else if (installStatus.installed) {
227
- // 已安装
228
- btnUse.classList.add("resource-detail__button--success");
229
- btnUse.innerHTML = `✅ ${t('market.installed')}`;
230
- } else {
231
- // 未安装
232
- btnUse.classList.add("resource-detail__button--primary");
233
- btnUse.textContent = `🚀 ${t('market.get_now')} ${itemData.uses ? '(' + itemData.uses + ')' : ''}`;
234
- }
235
-
236
- actionArea.appendChild(btnLike); actionArea.appendChild(btnFav); actionArea.appendChild(btnUse);
237
- detailView.appendChild(actionArea);
238
 
239
- // ⭐ 展开详情区完整评分组件(互动按钮栏下方、打赏榜单上方)
240
- const ratingSection = createRatingStars({
241
- ratingData: {
242
- rating_avg: itemData.rating_avg || 0,
243
- rating_count: itemData.rating_count || 0,
244
- rating_dist: itemData.rating_dist || {"1":0,"2":0,"3":0,"4":0,"5":0},
245
- rated_by: itemData.rated_by || {}
246
- },
247
- currentUser: currentUser,
248
- authorAccount: itemData.author,
249
- isCurrent: ratingCurrent,
250
- onRate: async (score, requestContext) => {
251
- const current = () => ratingCurrent() && (!requestContext || requestContext.isCurrent());
252
- if (!current()) return null;
253
- const res = await api.rateItem(itemData.id, score);
254
- if (!current()) return null;
255
- if (isValidRatingResponse(res, score)) {
256
- itemData.rating_avg = res.rating_avg;
257
- itemData.rating_count = res.rating_count;
258
- itemData.rating_dist = res.rating_dist;
259
- if (!itemData.rated_by) itemData.rated_by = {};
260
- itemData.rated_by[currentUser.account] = { score: res.user_score, time: Date.now() / 1000 };
261
-
262
- // 同步更新摘要区紧凑评分
263
- const oldCompact = summaryView.querySelector(".card-rating-compact");
264
- if (oldCompact) {
265
- const newCompact = createRatingStars({
266
- ratingData: {
267
- rating_avg: itemData.rating_avg,
268
- rating_count: itemData.rating_count,
269
- rating_dist: itemData.rating_dist,
270
- rated_by: itemData.rated_by
271
- },
272
- compact: true
273
- });
274
- newCompact.classList.add("card-rating-compact");
275
- oldCompact.parentNode.replaceChild(newCompact, oldCompact);
 
 
 
 
276
  }
277
- }
278
- return res;
279
- },
280
- compact: false
281
- });
282
- detailView.appendChild(ratingSection);
283
 
284
- const isLiked = currentUser && Array.isArray(itemData.liked_by) && itemData.liked_by.includes(currentUser.account);
285
- const isFav = currentUser && Array.isArray(itemData.favorited_by) && itemData.favorited_by.includes(currentUser.account);
286
 
287
- const handleInteraction = async (type, isActive) => {
288
- if (!currentUser?.account) {
289
- showToast(t('feedback.login_required'), "warning");
290
- return { status: "cancelled" };
291
- }
292
- const response = await api.toggleInteraction(itemData.id, currentUser.account, type, isActive);
293
- if (response?.status === "success" && Number.isSafeInteger(response.new_count) && response.new_count >= 0) {
294
- const membershipKey = type === "like" ? "liked_by" : "favorited_by";
295
- const countKey = type === "like" ? "likes" : "favorites";
296
- const previousMembers = Array.isArray(itemData[membershipKey]) ? itemData[membershipKey] : [];
297
- const members = previousMembers.filter(account => account !== currentUser.account);
298
- if (isActive) members.push(currentUser.account);
299
- itemData[membershipKey] = members;
300
- itemData[countKey] = response.new_count;
301
- const summaryCount = summaryView.querySelector(type === "like" ? '[data-stat="likes"]' : '[data-stat="favorites"]');
302
- if (summaryCount) summaryCount.textContent = `${type === "like" ? "👍" : "🔖"} ${response.new_count}`;
303
- }
304
- return response ?? { status: "error" };
305
- };
306
 
307
- setupToggleButton(btnLike, isLiked, itemData.likes || 0, `👍 ${t('market.liked')}`, `👍 ${t('market.like')}`, "#FF5722", (isActive) => handleInteraction("like", isActive));
308
- setupToggleButton(btnFav, isFav, itemData.favorites || 0, `🔖 ${t('market.favorited')}`, `🔖 ${t('market.favorite')}`, "#FFC107", (isActive) => handleInteraction("favorite", isActive));
309
 
310
- const inlineStatusBox = document.createElement("div");
311
- inlineStatusBox.className = "resource-detail__status";
312
- inlineStatusBox.style.display = "none";
313
- detailView.appendChild(inlineStatusBox);
314
- setupResourceInstall(btnUse, itemData, currentUser, inlineStatusBox);
315
 
316
- const mediaArea = document.createElement("div");
317
- const chartContainerId = `chart-${Math.random().toString(36).substr(2, 9)}`;
318
- const chartHtml = `<div class="resource-detail__heading">📈 ${t('market.usage_trend')}</div><div id="${chartContainerId}" class="resource-detail__chart"></div>`;
319
 
320
- mediaArea.innerHTML = `
321
- ${chartHtml}
322
- ${getCoverSandboxHTML(itemData.imageUrls?.length > 0 ? itemData.imageUrls : itemData.coverUrl)}
323
- <div class="resource-detail__heading">📝 ${t('market.full_description')}</div>
324
- <div class="resource-detail__description">${escapeHtml(itemData.fullDesc)}</div>
325
- `;
326
- detailView.appendChild(mediaArea);
327
 
328
- // 🚀 新增:赞赏贡献榜单与打赏按钮(带等级显示)
329
- const tipArea = document.createElement("div");
330
- tipArea.className = "resource-detail__tips";
331
 
332
- // 使用统一的打赏榜单渲染工具(带星星/月亮/太阳等级)
333
- const boardData = Array.isArray(itemData.tip_board)
334
- ? itemData.tip_board.filter(entry => entry && typeof entry === "object" && !Array.isArray(entry))
335
- : [];
336
- const boardHtml = renderTipBoardHTML(boardData, 5, t('market.no_tips_yet'), "small");
337
 
338
- tipArea.innerHTML = `
339
- <div class="resource-detail__tip-header">
340
- <div class="resource-detail__heading resource-detail__tip-heading">💖 ${t('market.tip_board')} (TOP 5)</div>
341
- <button type="button" class="btn-tip-item resource-detail__button resource-detail__button--tip">💰 ${t('market.tip')}</button>
342
- </div>
343
- ${boardHtml}
344
- `;
345
- detailView.appendChild(tipArea);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
347
- // 🚀 绑定打赏按钮事件
348
- tipArea.querySelector('.btn-tip-item').onclick = (e) => {
349
- e.stopPropagation();
350
- if (!currentUser) return showToast(t('feedback.login_required'), "warning");
351
- // 🐛 Bug修复:防御性校验,防止 itemId 为空
352
- if (!itemData.id) {
353
- console.warn('⚠️ 列表卡片: itemData.id 为空,已拦截无效打赏请求');
354
- return showToast(t('item.invalid_id_tip'), "error");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  }
356
- openTipModal(currentUser, { account: itemData.author }, undefined, itemData.id,
357
- { isCurrent: () => card.isConnected });
358
- };
359
 
360
- // 🔄 P7后悔模式:退款区域(仅在"我购买的"列表中显示)
361
- const refundArea = document.createElement("div");
362
- refundArea.id = `refund-area-${itemData.id}`;
363
- refundArea.className = "resource-detail__refund";
364
- refundArea.style.display = "none";
365
- detailView.appendChild(refundArea);
366
 
367
  let isRendered = false;
368
  let commentUI = null;
@@ -370,7 +407,9 @@ export function createItemCard(itemData, currentUser = null, contextType = null)
370
  let trendChartStopped = false;
371
  const toggleButton = summaryView.querySelector('.card-summary__toggle');
372
  const setExpanded = (expanded) => {
 
373
  if (expanded) {
 
374
  mountComments();
375
  if (isRendered) setupImageSandboxEvents(detailView);
376
  } else cleanupImageSandbox(detailView);
@@ -383,6 +422,7 @@ export function createItemCard(itemData, currentUser = null, contextType = null)
383
  trendChart?.setActive(expanded);
384
  };
385
  summaryView.onclick = (event) => {
 
386
  // 摘要中的作者、评分等独立控件不触发折叠;标题按钮使用浏览器原生键盘行为。
387
  const control = event.target.closest('button, a, input, select, textarea, [role="button"]');
388
  if (control && control !== toggleButton) return;
@@ -418,11 +458,6 @@ export function createItemCard(itemData, currentUser = null, contextType = null)
418
  toggleButton.focus();
419
  });
420
 
421
- const commentsContainer = document.createElement("div");
422
- commentsContainer.className = "resource-detail__comments";
423
-
424
- // 🚀 核心修改:传入一个回调函数,当组件内部发送/删除留言时,外部的数字能实时响应刷新!
425
- // 🚀 P0安全修复:传递 itemData.author 作为 contentAuthor,使内容作者可以删除其内容下的评论
426
  function mountComments() {
427
  if (commentUI) return;
428
  commentUI = createCommentSection(itemData.id, itemData.commentsData || [], currentUser, (newCount) => {
@@ -431,32 +466,6 @@ export function createItemCard(itemData, currentUser = null, contextType = null)
431
  }, itemData.author);
432
  commentsContainer.appendChild(commentUI);
433
  }
434
- detailView.appendChild(commentsContainer);
435
-
436
- if (currentUser && currentUser.account === itemData.author) {
437
- const authorActionArea = document.createElement("div");
438
- authorActionArea.className = "resource-detail__manage";
439
-
440
- authorActionArea.innerHTML = `
441
- <div class="resource-detail__manage-title">👑 ${t('market.creator_manage')}</div>
442
- ${IS_WEB_MODE ? '' : `<button type="button" id="btn-edit-item" class="resource-detail__button resource-detail__button--primary">✏️ ${t('market.edit_content')}</button>`}
443
- <button type="button" id="btn-del-item" class="resource-detail__button resource-detail__button--danger">🗑️ ${t('market.delete_permanently')}</button>
444
- `;
445
-
446
- // 📱 Web 模式不渲染编辑按钮,此处需空值保护
447
- const btnEditItem = authorActionArea.querySelector("#btn-edit-item");
448
- if (btnEditItem) btnEditItem.onclick = (e) => {
449
- e.stopPropagation();
450
- window.dispatchEvent(new CustomEvent("comfy-route-edit-publish", { detail: { itemData, currentUser } }));
451
- };
452
-
453
- authorActionArea.querySelector("#btn-del-item").onclick = async (e) => {
454
- e.stopPropagation();
455
- await showResourceDeleteDialog({ itemData, currentUser, isCurrent: () => card.isConnected,
456
- onDeleted: () => window.dispatchEvent(new CustomEvent("comfy-trigger-sidebar-reload")) });
457
- };
458
- detailView.appendChild(authorActionArea);
459
- }
460
 
461
  // 🖼️ 启用图片懒加载(跳过 data URL 和已处理的)
462
  lazyLoadImages(card);
 
173
  detailView.className = "resource-detail";
174
  detailView.style.display = "none";
175
 
176
+ let commentsContainer, refundArea, chartContainerId;
177
+ let detailsMounted = false;
178
+ function mountDetails() {
179
+ if (detailsMounted) return;
180
+ const authorInfo = document.createElement("div");
181
+ authorInfo.className = "resource-detail__author";
182
 
183
+ // 🔄 P7后悔模式:检查是否有待生效价格
184
+ const priceHtml = _generatePriceHTML(itemData, t);
185
 
186
+ authorInfo.innerHTML = `${t('market.author')}: <button type="button" class="author-name-link card-author-link" ${itemData.author ? '' : 'disabled'}>${t('common.loading')}...</button> ${priceHtml}`;
187
+ detailView.appendChild(authorInfo);
188
 
189
+ const nameDOM = authorInfo.querySelector('.author-name-link');
190
 
191
+ // 使用统一辅助函数加载作者名称
192
+ _loadAuthorProfile(itemData.author, {
193
+ onName: (name) => { nameDOM.textContent = name; }
194
+ });
195
 
196
+ nameDOM.onclick = (e) => { e.stopPropagation(); openOtherUserProfileModal(itemData.author, currentUser); };
197
+
198
+ // 原创内容标识
199
+ if (itemData.is_original) {
200
+ const originalBadge = document.createElement("div");
201
+ originalBadge.className = "resource-detail__badge resource-detail__badge--original";
202
+ originalBadge.textContent = "🎨 " + t('publish.mark_original');
203
+ detailView.appendChild(originalBadge);
204
+ }
205
 
206
+ // 支持退款标识(仅工具、Skill和应用类型显示,且允许退款时)
207
+ if (itemData.allow_refund !== false && (itemData.type === 'tool' || itemData.type === 'skill' || itemData.type === 'app')) {
208
+ const refundBadge = document.createElement("div");
209
+ refundBadge.className = "resource-detail__badge resource-detail__badge--refundable";
210
+ refundBadge.textContent = `🛡️ ${t('item.refundable')}`;
211
+ detailView.appendChild(refundBadge);
212
+ }
213
 
214
+ const actionArea = document.createElement("div");
215
+ actionArea.className = "resource-detail__actions";
216
+ const btnLike = document.createElement("button");
217
+ btnLike.type = "button";
218
+ const btnFav = document.createElement("button");
219
+ btnFav.type = "button";
220
 
221
+ // 🚀 根据安装状态显示不同的按钮样式(installStatus 已在上方声明)
222
+ const btnUse = document.createElement("button");
223
+ btnUse.type = "button";
224
+ btnUse.className = "resource-detail__button resource-detail__use";
225
 
226
+ if (installStatus.hasUpdate) {
227
+ // 有更新可用
228
+ btnUse.classList.add("resource-detail__button--warning");
229
+ btnUse.innerHTML = `♻️ ${t('market.update_available')}`;
230
+ } else if (installStatus.installed) {
231
+ // 已安装
232
+ btnUse.classList.add("resource-detail__button--success");
233
+ btnUse.innerHTML = `✅ ${t('market.installed')}`;
234
+ } else {
235
+ // 未安装
236
+ btnUse.classList.add("resource-detail__button--primary");
237
+ btnUse.textContent = `🚀 ${t('market.get_now')} ${itemData.uses ? '(' + itemData.uses + ')' : ''}`;
238
+ }
 
 
 
239
 
240
+ actionArea.appendChild(btnLike); actionArea.appendChild(btnFav); actionArea.appendChild(btnUse);
241
+ detailView.appendChild(actionArea);
242
+
243
+ // ⭐ 展开详情区完整评分组件(互动按钮栏下方、打赏榜单上方)
244
+ const ratingSection = createRatingStars({
245
+ ratingData: {
246
+ rating_avg: itemData.rating_avg || 0,
247
+ rating_count: itemData.rating_count || 0,
248
+ rating_dist: itemData.rating_dist || {"1":0,"2":0,"3":0,"4":0,"5":0},
249
+ rated_by: itemData.rated_by || {}
250
+ },
251
+ currentUser: currentUser,
252
+ authorAccount: itemData.author,
253
+ isCurrent: ratingCurrent,
254
+ onRate: async (score, requestContext) => {
255
+ const current = () => ratingCurrent() && (!requestContext || requestContext.isCurrent());
256
+ if (!current()) return null;
257
+ const res = await api.rateItem(itemData.id, score);
258
+ if (!current()) return null;
259
+ if (isValidRatingResponse(res, score)) {
260
+ itemData.rating_avg = res.rating_avg;
261
+ itemData.rating_count = res.rating_count;
262
+ itemData.rating_dist = res.rating_dist;
263
+ if (!itemData.rated_by) itemData.rated_by = {};
264
+ itemData.rated_by[currentUser.account] = { score: res.user_score, time: Date.now() / 1000 };
265
+
266
+ // 同步更新摘要区紧凑评分
267
+ const oldCompact = summaryView.querySelector(".card-rating-compact");
268
+ if (oldCompact) {
269
+ const newCompact = createRatingStars({
270
+ ratingData: {
271
+ rating_avg: itemData.rating_avg,
272
+ rating_count: itemData.rating_count,
273
+ rating_dist: itemData.rating_dist,
274
+ rated_by: itemData.rated_by
275
+ },
276
+ compact: true
277
+ });
278
+ newCompact.classList.add("card-rating-compact");
279
+ oldCompact.parentNode.replaceChild(newCompact, oldCompact);
280
+ }
281
  }
282
+ return res;
283
+ },
284
+ compact: false
285
+ });
286
+ detailView.appendChild(ratingSection);
 
287
 
288
+ const isLiked = currentUser && Array.isArray(itemData.liked_by) && itemData.liked_by.includes(currentUser.account);
289
+ const isFav = currentUser && Array.isArray(itemData.favorited_by) && itemData.favorited_by.includes(currentUser.account);
290
 
291
+ const handleInteraction = async (type, isActive) => {
292
+ if (!currentUser?.account) {
293
+ showToast(t('feedback.login_required'), "warning");
294
+ return { status: "cancelled" };
295
+ }
296
+ const response = await api.toggleInteraction(itemData.id, currentUser.account, type, isActive);
297
+ if (response?.status === "success" && Number.isSafeInteger(response.new_count) && response.new_count >= 0) {
298
+ const membershipKey = type === "like" ? "liked_by" : "favorited_by";
299
+ const countKey = type === "like" ? "likes" : "favorites";
300
+ const previousMembers = Array.isArray(itemData[membershipKey]) ? itemData[membershipKey] : [];
301
+ const members = previousMembers.filter(account => account !== currentUser.account);
302
+ if (isActive) members.push(currentUser.account);
303
+ itemData[membershipKey] = members;
304
+ itemData[countKey] = response.new_count;
305
+ const summaryCount = summaryView.querySelector(type === "like" ? '[data-stat="likes"]' : '[data-stat="favorites"]');
306
+ if (summaryCount) summaryCount.textContent = `${type === "like" ? "👍" : "🔖"} ${response.new_count}`;
307
+ }
308
+ return response ?? { status: "error" };
309
+ };
310
 
311
+ setupToggleButton(btnLike, isLiked, itemData.likes || 0, `👍 ${t('market.liked')}`, `👍 ${t('market.like')}`, "#FF5722", (isActive) => handleInteraction("like", isActive));
312
+ setupToggleButton(btnFav, isFav, itemData.favorites || 0, `🔖 ${t('market.favorited')}`, `🔖 ${t('market.favorite')}`, "#FFC107", (isActive) => handleInteraction("favorite", isActive));
313
 
314
+ const inlineStatusBox = document.createElement("div");
315
+ inlineStatusBox.className = "resource-detail__status";
316
+ inlineStatusBox.style.display = "none";
317
+ detailView.appendChild(inlineStatusBox);
318
+ setupResourceInstall(btnUse, itemData, currentUser, inlineStatusBox);
319
 
320
+ const mediaArea = document.createElement("div");
321
+ chartContainerId = `chart-${Math.random().toString(36).substr(2, 9)}`;
322
+ const chartHtml = `<div class="resource-detail__heading">📈 ${t('market.usage_trend')}</div><div id="${chartContainerId}" class="resource-detail__chart"></div>`;
323
 
324
+ mediaArea.innerHTML = `
325
+ ${chartHtml}
326
+ ${getCoverSandboxHTML(itemData.imageUrls?.length > 0 ? itemData.imageUrls : itemData.coverUrl)}
327
+ <div class="resource-detail__heading">📝 ${t('market.full_description')}</div>
328
+ <div class="resource-detail__description">${escapeHtml(itemData.fullDesc)}</div>
329
+ `;
330
+ detailView.appendChild(mediaArea);
331
 
332
+ // 🚀 新增:赞赏贡献榜单与打赏按钮(带等级显示)
333
+ const tipArea = document.createElement("div");
334
+ tipArea.className = "resource-detail__tips";
335
 
336
+ // 使用统一的打赏榜单渲染工具(带星星/月亮/太阳等级)
337
+ const boardData = Array.isArray(itemData.tip_board)
338
+ ? itemData.tip_board.filter(entry => entry && typeof entry === "object" && !Array.isArray(entry))
339
+ : [];
340
+ const boardHtml = renderTipBoardHTML(boardData, 5, t('market.no_tips_yet'), "small");
341
 
342
+ tipArea.innerHTML = `
343
+ <div class="resource-detail__tip-header">
344
+ <div class="resource-detail__heading resource-detail__tip-heading">💖 ${t('market.tip_board')} (TOP 5)</div>
345
+ <button type="button" class="btn-tip-item resource-detail__button resource-detail__button--tip">💰 ${t('market.tip')}</button>
346
+ </div>
347
+ ${boardHtml}
348
+ `;
349
+ detailView.appendChild(tipArea);
350
+
351
+ // 🚀 绑定打赏按钮事件
352
+ tipArea.querySelector('.btn-tip-item').onclick = (e) => {
353
+ e.stopPropagation();
354
+ if (!currentUser) return showToast(t('feedback.login_required'), "warning");
355
+ // 🐛 Bug修复:防御性校验,防止 itemId 为空
356
+ if (!itemData.id) {
357
+ console.warn('⚠️ 列表卡片: itemData.id 为空,已拦截无效打赏请求');
358
+ return showToast(t('item.invalid_id_tip'), "error");
359
+ }
360
+ openTipModal(currentUser, { account: itemData.author }, undefined, itemData.id,
361
+ { isCurrent: () => card.isConnected });
362
+ };
363
+
364
+ // 🔄 P7后悔模式:退款区域(仅在"我购买的"列表中显示)
365
+ refundArea = document.createElement("div");
366
+ refundArea.id = `refund-area-${itemData.id}`;
367
+ refundArea.className = "resource-detail__refund";
368
+ refundArea.style.display = "none";
369
+ detailView.appendChild(refundArea);
370
+
371
+ commentsContainer = document.createElement("div");
372
+ commentsContainer.className = "resource-detail__comments";
373
 
374
+ detailView.appendChild(commentsContainer);
375
+
376
+ if (currentUser && currentUser.account === itemData.author) {
377
+ const authorActionArea = document.createElement("div");
378
+ authorActionArea.className = "resource-detail__manage";
379
+
380
+ authorActionArea.innerHTML = `
381
+ <div class="resource-detail__manage-title">👑 ${t('market.creator_manage')}</div>
382
+ ${IS_WEB_MODE ? '' : `<button type="button" id="btn-edit-item" class="resource-detail__button resource-detail__button--primary">✏️ ${t('market.edit_content')}</button>`}
383
+ <button type="button" id="btn-del-item" class="resource-detail__button resource-detail__button--danger">🗑️ ${t('market.delete_permanently')}</button>
384
+ `;
385
+
386
+ // 📱 Web 模式不渲染编辑按钮,此处需空值保护
387
+ const btnEditItem = authorActionArea.querySelector("#btn-edit-item");
388
+ if (btnEditItem) btnEditItem.onclick = (e) => {
389
+ e.stopPropagation();
390
+ window.dispatchEvent(new CustomEvent("comfy-route-edit-publish", { detail: { itemData, currentUser } }));
391
+ };
392
+
393
+ authorActionArea.querySelector("#btn-del-item").onclick = async (e) => {
394
+ e.stopPropagation();
395
+ await showResourceDeleteDialog({ itemData, currentUser, isCurrent: () => card.isConnected,
396
+ onDeleted: () => window.dispatchEvent(new CustomEvent("comfy-trigger-sidebar-reload")) });
397
+ };
398
+ detailView.appendChild(authorActionArea);
399
  }
 
 
 
400
 
401
+ detailsMounted = true;
402
+ }
 
 
 
 
403
 
404
  let isRendered = false;
405
  let commentUI = null;
 
407
  let trendChartStopped = false;
408
  const toggleButton = summaryView.querySelector('.card-summary__toggle');
409
  const setExpanded = (expanded) => {
410
+ if (trendChartStopped) return;
411
  if (expanded) {
412
+ mountDetails();
413
  mountComments();
414
  if (isRendered) setupImageSandboxEvents(detailView);
415
  } else cleanupImageSandbox(detailView);
 
422
  trendChart?.setActive(expanded);
423
  };
424
  summaryView.onclick = (event) => {
425
+ if (trendChartStopped || !ratingCurrent()) return;
426
  // 摘要中的作者、评分等独立控件不触发折叠;标题按钮使用浏览器原生键盘行为。
427
  const control = event.target.closest('button, a, input, select, textarea, [role="button"]');
428
  if (control && control !== toggleButton) return;
 
458
  toggleButton.focus();
459
  });
460
 
 
 
 
 
 
461
  function mountComments() {
462
  if (commentUI) return;
463
  commentUI = createCommentSection(itemData.id, itemData.commentsData || [], currentUser, (newCount) => {
 
466
  }, itemData.author);
467
  commentsContainer.appendChild(commentUI);
468
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
 
470
  // 🖼️ 启用图片懒加载(跳过 data URL 和已处理的)
471
  lazyLoadImages(card);
前端页面/market/发布内容_UI模板.js CHANGED
@@ -13,7 +13,7 @@ export function generatePublishHTML(isEditMode, viewTitle, submitBtnText, hasExi
13
 
14
  <div class="ranking-publish__row">
15
  <div class="ranking-publish__field">
16
- <label for="pub-type">${escapeHtml(t('publish.main_category'))} <span class="ranking-publish__required" aria-hidden="true">*</span></label>
17
  <select id="pub-type" class="form-input" aria-required="true">
18
  <option value="tool">🧰 ${escapeHtml(t('publish.type_tool'))}</option>
19
  <option value="skill">⚡ ${escapeHtml(t('publish.type_skill'))}</option>
@@ -22,7 +22,7 @@ export function generatePublishHTML(isEditMode, viewTitle, submitBtnText, hasExi
22
  </select>
23
  </div>
24
  <div id="box-recommend-type" class="ranking-publish__field" style="display: none;">
25
- <label for="pub-recommend-type">${escapeHtml(t('publish.recommend_form'))} <span class="ranking-publish__required" aria-hidden="true">*</span></label>
26
  <select id="pub-recommend-type" class="form-input" aria-required="true">
27
  <option value="recommend_tool">🧰 ${escapeHtml(t('publish.recommend_as_tool'))}</option>
28
  <option value="recommend_skill">⚡ ${escapeHtml(t('publish.recommend_as_skill'))}</option>
@@ -31,19 +31,19 @@ export function generatePublishHTML(isEditMode, viewTitle, submitBtnText, hasExi
31
  </select>
32
  </div>
33
  <div class="ranking-publish__field ranking-publish__field--wide">
34
- <label for="pub-title">${escapeHtml(t('publish.name'))} <span class="ranking-publish__required" aria-hidden="true">*</span></label>
35
  <input type="text" id="pub-title" placeholder="${escapeHtml(t('publish.name_placeholder'))}" required class="form-input">
36
  </div>
37
  </div>
38
 
39
  <div class="ranking-publish__field">
40
- <label for="pub-short">${escapeHtml(t('publish.short_desc'))} <span class="ranking-publish__required" aria-hidden="true">*</span></label>
41
  <input type="text" id="pub-short" placeholder="${escapeHtml(t('publish.short_desc_placeholder'))}" maxlength="50" required class="form-input">
42
  </div>
43
 
44
  <div class="ranking-publish__row ranking-publish__panel">
45
  <div class="ranking-publish__field" id="box-resource-select">
46
- <label for="pub-resource-type">${escapeHtml(t('publish.resource_type'))} <span class="ranking-publish__required" aria-hidden="true">*</span></label>
47
  <select id="pub-resource-type" class="form-input" aria-required="true">
48
  <option value="link">🔗 ${escapeHtml(t('publish.resource_link'))}</option>
49
  <option value="json">📄 ${escapeHtml(t('publish.resource_json'))}</option>
@@ -51,15 +51,15 @@ export function generatePublishHTML(isEditMode, viewTitle, submitBtnText, hasExi
51
  </select>
52
  </div>
53
  <div class="ranking-publish__field ranking-publish__field--wide" id="box-link">
54
- <label for="pub-link">${escapeHtml(t('publish.source_url'))} <span class="ranking-publish__required" aria-hidden="true">*</span></label>
55
  <input type="text" id="pub-link" placeholder="${escapeHtml(t('publish.source_url_placeholder'))}" class="form-input" aria-required="true">
56
  </div>
57
  <div class="ranking-publish__field ranking-publish__field--wide" id="box-json" style="display: none;">
58
- <label for="pub-json">${escapeHtml(t('publish.select_json'))} <span class="ranking-publish__required" aria-hidden="true">*</span> <span id="json-file-hint" class="ranking-publish__file-hint" style="display: none;">(${escapeHtml(t('publish.has_cloud_file'))})</span></label>
59
  <input type="file" id="pub-json" accept=".json" aria-describedby="json-file-hint" class="ranking-publish__file">
60
  </div>
61
  <div class="ranking-publish__field ranking-publish__field--wide" id="box-netdisk" style="display: none;">
62
- <label for="pub-netdisk-link">${escapeHtml(t('publish.netdisk_link'))} <span class="ranking-publish__required" aria-hidden="true">*</span></label>
63
  <input type="text" id="pub-netdisk-link" placeholder="${escapeHtml(t('publish.netdisk_placeholder'))}" class="form-input" aria-required="true">
64
  </div>
65
  </div>
@@ -80,7 +80,7 @@ export function generatePublishHTML(isEditMode, viewTitle, submitBtnText, hasExi
80
  <span>${escapeHtml(t('publish.private_repo_hint'))}</span>
81
  </label>
82
  <div id="github-token-container" class="ranking-publish__token" style="display: ${hasExistingToken ? 'block' : 'none'};">
83
- <label for="pub-github-token" class="ranking-publish__security-label">🔒 ${escapeHtml(t('publish.pat_label'))} <span class="ranking-publish__required" aria-hidden="true">*</span></label>
84
  <div id="publish-pat-hint" class="ranking-publish__hint">${escapeHtml(t('publish.pat_hint'))} <strong class="ranking-publish__security-emphasis">${escapeHtml(t('publish.pat_privacy'))}</strong></div>
85
  <input type="password" id="pub-github-token" value="${escapeHtml(isEditMode ? (safeData.github_token || '') : '')}" placeholder="${hasExistingToken && !safeData.github_token ? escapeHtml(t('publish.token_keep_hint')) : escapeHtml(t('publish.token_placeholder'))}" class="form-input" aria-describedby="publish-pat-hint">
86
  </div>
@@ -95,7 +95,7 @@ export function generatePublishHTML(isEditMode, viewTitle, submitBtnText, hasExi
95
 
96
  <div id="box-price" class="ranking-publish__row">
97
  <div class="ranking-publish__field">
98
- <label for="pub-price">${escapeHtml(t('publish.price'))} <span class="ranking-publish__required" aria-hidden="true">*</span></label>
99
  <input type="number" id="pub-price" value="0" min="0" class="form-input ranking-publish__price" aria-required="true" aria-describedby="publish-price-hint price-pending-hint">
100
  <div id="price-pending-hint" class="ranking-publish__pending" role="status" style="display: none;"></div>
101
  </div>
 
13
 
14
  <div class="ranking-publish__row">
15
  <div class="ranking-publish__field">
16
+ <label for="pub-type">${escapeHtml(t('publish.main_category'))} <span class="ranking-publish__required ranking-required-marker" aria-hidden="true"></span></label>
17
  <select id="pub-type" class="form-input" aria-required="true">
18
  <option value="tool">🧰 ${escapeHtml(t('publish.type_tool'))}</option>
19
  <option value="skill">⚡ ${escapeHtml(t('publish.type_skill'))}</option>
 
22
  </select>
23
  </div>
24
  <div id="box-recommend-type" class="ranking-publish__field" style="display: none;">
25
+ <label for="pub-recommend-type">${escapeHtml(t('publish.recommend_form'))} <span class="ranking-publish__required ranking-required-marker" aria-hidden="true"></span></label>
26
  <select id="pub-recommend-type" class="form-input" aria-required="true">
27
  <option value="recommend_tool">🧰 ${escapeHtml(t('publish.recommend_as_tool'))}</option>
28
  <option value="recommend_skill">⚡ ${escapeHtml(t('publish.recommend_as_skill'))}</option>
 
31
  </select>
32
  </div>
33
  <div class="ranking-publish__field ranking-publish__field--wide">
34
+ <label for="pub-title">${escapeHtml(t('publish.name'))} <span class="ranking-publish__required ranking-required-marker" aria-hidden="true"></span></label>
35
  <input type="text" id="pub-title" placeholder="${escapeHtml(t('publish.name_placeholder'))}" required class="form-input">
36
  </div>
37
  </div>
38
 
39
  <div class="ranking-publish__field">
40
+ <label for="pub-short">${escapeHtml(t('publish.short_desc'))} <span class="ranking-publish__required ranking-required-marker" aria-hidden="true"></span></label>
41
  <input type="text" id="pub-short" placeholder="${escapeHtml(t('publish.short_desc_placeholder'))}" maxlength="50" required class="form-input">
42
  </div>
43
 
44
  <div class="ranking-publish__row ranking-publish__panel">
45
  <div class="ranking-publish__field" id="box-resource-select">
46
+ <label for="pub-resource-type">${escapeHtml(t('publish.resource_type'))} <span class="ranking-publish__required ranking-required-marker" aria-hidden="true"></span></label>
47
  <select id="pub-resource-type" class="form-input" aria-required="true">
48
  <option value="link">🔗 ${escapeHtml(t('publish.resource_link'))}</option>
49
  <option value="json">📄 ${escapeHtml(t('publish.resource_json'))}</option>
 
51
  </select>
52
  </div>
53
  <div class="ranking-publish__field ranking-publish__field--wide" id="box-link">
54
+ <label for="pub-link">${escapeHtml(t('publish.source_url'))} <span class="ranking-publish__required ranking-required-marker" aria-hidden="true"></span></label>
55
  <input type="text" id="pub-link" placeholder="${escapeHtml(t('publish.source_url_placeholder'))}" class="form-input" aria-required="true">
56
  </div>
57
  <div class="ranking-publish__field ranking-publish__field--wide" id="box-json" style="display: none;">
58
+ <label for="pub-json">${escapeHtml(t('publish.select_json'))} <span class="ranking-publish__required ranking-required-marker" aria-hidden="true"></span> <span id="json-file-hint" class="ranking-publish__file-hint" style="display: none;">(${escapeHtml(t('publish.has_cloud_file'))})</span></label>
59
  <input type="file" id="pub-json" accept=".json" aria-describedby="json-file-hint" class="ranking-publish__file">
60
  </div>
61
  <div class="ranking-publish__field ranking-publish__field--wide" id="box-netdisk" style="display: none;">
62
+ <label for="pub-netdisk-link">${escapeHtml(t('publish.netdisk_link'))} <span class="ranking-publish__required ranking-required-marker" aria-hidden="true"></span></label>
63
  <input type="text" id="pub-netdisk-link" placeholder="${escapeHtml(t('publish.netdisk_placeholder'))}" class="form-input" aria-required="true">
64
  </div>
65
  </div>
 
80
  <span>${escapeHtml(t('publish.private_repo_hint'))}</span>
81
  </label>
82
  <div id="github-token-container" class="ranking-publish__token" style="display: ${hasExistingToken ? 'block' : 'none'};">
83
+ <label for="pub-github-token" class="ranking-publish__security-label">🔒 ${escapeHtml(t('publish.pat_label'))} <span class="ranking-publish__required ranking-required-marker" aria-hidden="true"></span></label>
84
  <div id="publish-pat-hint" class="ranking-publish__hint">${escapeHtml(t('publish.pat_hint'))} <strong class="ranking-publish__security-emphasis">${escapeHtml(t('publish.pat_privacy'))}</strong></div>
85
  <input type="password" id="pub-github-token" value="${escapeHtml(isEditMode ? (safeData.github_token || '') : '')}" placeholder="${hasExistingToken && !safeData.github_token ? escapeHtml(t('publish.token_keep_hint')) : escapeHtml(t('publish.token_placeholder'))}" class="form-input" aria-describedby="publish-pat-hint">
86
  </div>
 
95
 
96
  <div id="box-price" class="ranking-publish__row">
97
  <div class="ranking-publish__field">
98
+ <label for="pub-price">${escapeHtml(t('publish.price'))} <span class="ranking-publish__required ranking-required-marker" aria-hidden="true"></span></label>
99
  <input type="number" id="pub-price" value="0" min="0" class="form-input ranking-publish__price" aria-required="true" aria-describedby="publish-price-hint price-pending-hint">
100
  <div id="price-pending-hint" class="ranking-publish__pending" role="status" style="display: none;"></div>
101
  </div>
前端页面/market/发布内容_提交引擎.js CHANGED
@@ -294,7 +294,7 @@ export async function handlePublishSubmit(params) {
294
  return;
295
  }
296
  // 已发出的保存可能在服务端提交;仅已知前置拒绝允许原表单重试。
297
- const rejected = [400, 401, 403, 404, 409, 422, 429].includes(err?.status);
298
  outcomeUnknown = saveAttempted && !rejected;
299
  if (outcomeUnknown) {
300
  showToast(t('publish.save_unconfirmed'), "warning");
 
294
  return;
295
  }
296
  // 已发出的保存可能在服务端提交;仅已知前置拒绝允许原表单重试。
297
+ const rejected = (err?.requestBodyRejected === true || [400, 401, 403, 404, 409, 422, 429].includes(err?.status));
298
  outcomeUnknown = saveAttempted && !rejected;
299
  if (outcomeUnknown) {
300
  showToast(t('publish.save_unconfirmed'), "warning");
前端页面/market/资金与钱包_充值组件.js CHANGED
@@ -234,7 +234,7 @@ export async function openRechargeModal(currentUser, onBalanceChange) {
234
  pollingTimer = setTimeout(poll, UI.RECHARGE_POLL_INTERVAL);
235
  } catch (error) {
236
  if (!scope.isCurrent()) return;
237
- const rejected = [400, 401, 403, 404, 422, 429].includes(error.status);
238
  qrLoading.textContent = t(rejected ? 'wallet.recharge.order_failed' : 'wallet.recharge.order_uncertain');
239
  if (rejected) {
240
  submitting = false;
 
234
  pollingTimer = setTimeout(poll, UI.RECHARGE_POLL_INTERVAL);
235
  } catch (error) {
236
  if (!scope.isCurrent()) return;
237
+ const rejected = (error.requestBodyRejected === true || [400, 401, 403, 404, 422, 429].includes(error.status));
238
  qrLoading.textContent = t(rejected ? 'wallet.recharge.order_failed' : 'wallet.recharge.order_uncertain');
239
  if (rejected) {
240
  submitting = false;
前端页面/market/资金与钱包_提现组件.js CHANGED
@@ -99,7 +99,7 @@ export async function openWithdrawModal(currentUser, onSuccess) {
99
  </div>
100
 
101
  <div class="wallet-modal__field">
102
- <label for="withdraw-amount" class="wallet-modal__label">${t('wallet.withdraw.amount_label')} <span class="wallet-modal__danger">*</span></label>
103
  <input type="number" id="withdraw-amount" placeholder="${t('wallet.withdraw.amount_placeholder')}" max="${maxWithdraw}" min="1" step="1" class="wallet-modal__input">
104
 
105
  <div id="fee-calc-box" class="wallet-withdraw__fee" style="display:none;">
@@ -108,17 +108,17 @@ export async function openWithdrawModal(currentUser, onSuccess) {
108
  </div>
109
 
110
  <div class="wallet-modal__field">
111
- <label for="withdraw-account" class="wallet-modal__label">${t('wallet.withdraw.alipay_account')} <span class="wallet-modal__danger">*</span></label>
112
  <input type="text" id="withdraw-account" placeholder="${t('wallet.withdraw.alipay_placeholder')}" class="wallet-modal__input">
113
  </div>
114
 
115
  <div class="wallet-modal__field">
116
- <label for="withdraw-name" class="wallet-modal__label">${t('wallet.withdraw.real_name')} <span class="wallet-modal__danger">*</span></label>
117
  <input type="text" id="withdraw-name" placeholder="${t('wallet.withdraw.real_name_placeholder')}" class="wallet-modal__input">
118
  </div>
119
 
120
  <div class="wallet-modal__field wallet-modal__field--last">
121
- <label for="withdraw-code" class="wallet-modal__label">${t('wallet.withdraw.security_code')} <span class="wallet-modal__danger">*</span></label>
122
  <div class="wallet-withdraw__code-row">
123
  <input type="text" id="withdraw-code" placeholder="${t('wallet.withdraw.code_placeholder')}" maxlength="6" class="wallet-modal__input wallet-withdraw__code-input">
124
  <button type="button" id="btn-send-code" class="wallet-modal__button wallet-modal__button--code">${t('wallet.withdraw.get_code')}</button>
@@ -230,7 +230,7 @@ export async function openWithdrawModal(currentUser, onSuccess) {
230
  } catch (error) {
231
  clearTimeout(deadline);
232
  if (!scope.isCurrent()) return;
233
- const rejected = [400, 401, 403, 404, 422, 429].includes(error.status);
234
  showToast(t(rejected ? 'wallet.withdraw.submit_failed' : 'wallet.withdraw.timeout'), 'warning');
235
  if (rejected) {
236
  isSubmitting = false;
 
99
  </div>
100
 
101
  <div class="wallet-modal__field">
102
+ <label for="withdraw-amount" class="wallet-modal__label">${t('wallet.withdraw.amount_label')} <span class="wallet-modal__danger ranking-required-marker"></span></label>
103
  <input type="number" id="withdraw-amount" placeholder="${t('wallet.withdraw.amount_placeholder')}" max="${maxWithdraw}" min="1" step="1" class="wallet-modal__input">
104
 
105
  <div id="fee-calc-box" class="wallet-withdraw__fee" style="display:none;">
 
108
  </div>
109
 
110
  <div class="wallet-modal__field">
111
+ <label for="withdraw-account" class="wallet-modal__label">${t('wallet.withdraw.alipay_account')} <span class="wallet-modal__danger ranking-required-marker"></span></label>
112
  <input type="text" id="withdraw-account" placeholder="${t('wallet.withdraw.alipay_placeholder')}" class="wallet-modal__input">
113
  </div>
114
 
115
  <div class="wallet-modal__field">
116
+ <label for="withdraw-name" class="wallet-modal__label">${t('wallet.withdraw.real_name')} <span class="wallet-modal__danger ranking-required-marker"></span></label>
117
  <input type="text" id="withdraw-name" placeholder="${t('wallet.withdraw.real_name_placeholder')}" class="wallet-modal__input">
118
  </div>
119
 
120
  <div class="wallet-modal__field wallet-modal__field--last">
121
+ <label for="withdraw-code" class="wallet-modal__label">${t('wallet.withdraw.security_code')} <span class="wallet-modal__danger ranking-required-marker"></span></label>
122
  <div class="wallet-withdraw__code-row">
123
  <input type="text" id="withdraw-code" placeholder="${t('wallet.withdraw.code_placeholder')}" maxlength="6" class="wallet-modal__input wallet-withdraw__code-input">
124
  <button type="button" id="btn-send-code" class="wallet-modal__button wallet-modal__button--code">${t('wallet.withdraw.get_code')}</button>
 
230
  } catch (error) {
231
  clearTimeout(deadline);
232
  if (!scope.isCurrent()) return;
233
+ const rejected = (error.requestBodyRejected === true || [400, 401, 403, 404, 422, 429].includes(error.status));
234
  showToast(t(rejected ? 'wallet.withdraw.submit_failed' : 'wallet.withdraw.timeout'), 'warning');
235
  if (rejected) {
236
  isSubmitting = false;
前端页面/post/发布帖子组件.js CHANGED
@@ -403,7 +403,7 @@ export function createPublishPostView(currentUser, editPostData = null) {
403
  <!-- 图片上传区 -->
404
  <div id="image-upload-section" class="post-publish__upload-section">
405
  <label for="images-input" class="post-publish__field-label">
406
- 🖼️ ${t('post.upload_images')} <span class="post-publish__required">*</span>
407
  <span class="post-publish__hint">(${t('post.max_9_images')})</span>
408
  </label>
409
  <input type="file" id="images-input" accept="image/*" multiple style="display: none;">
@@ -418,7 +418,7 @@ export function createPublishPostView(currentUser, editPostData = null) {
418
  <!-- 视频上传区 -->
419
  <div id="video-upload-section" style="display: none;" class="post-publish__upload-section">
420
  <label for="video-input" class="post-publish__field-label">
421
- 🎬 ${t('post.upload_video')} <span class="post-publish__required">*</span>
422
  <span class="post-publish__hint">${t('post.video_limits_hint', { megabytes: PUBLISH_MEDIA_LIMITS.VIDEO_MAX_BYTES / (1024 * 1024), minutes: PUBLISH_MEDIA_LIMITS.VIDEO_MAX_SECONDS / 60 })}</span>
423
  </label>
424
  <input type="file" id="video-input" accept="video/mp4,video/webm,video/quicktime" style="display: none;">
@@ -462,7 +462,7 @@ export function createPublishPostView(currentUser, editPostData = null) {
462
  <!-- 标题输入 -->
463
  <div class="post-publish__field-group">
464
  <label for="title-input" class="post-publish__field-label">
465
- 📝 ${t('post.title_label')} <span class="post-publish__required">*</span>
466
  </label>
467
  <input type="text" id="title-input" class="ranking-publish-input" value="${escapeHtml(editTitle)}" placeholder="${t('post.title_placeholder')}" maxlength="50">
468
  <div class="post-publish__counter">
@@ -1291,7 +1291,7 @@ export function createPublishPostView(currentUser, editPostData = null) {
1291
 
1292
  } catch (err) {
1293
  if (!scope.isCurrent() || committed) return;
1294
- outcomeUnknown = saveAttempted && ![400, 401, 403, 404, 409, 422, 429].includes(err?.status);
1295
  showToast(t(outcomeUnknown ? 'publish.save_unconfirmed' : (isEditMode ? 'post.edit_failed' : 'post.publish_failed')),
1296
  outcomeUnknown ? 'warning' : 'error');
1297
  } finally {
@@ -1379,7 +1379,7 @@ export function createPublishPostView(currentUser, editPostData = null) {
1379
 
1380
  } catch (err) {
1381
  if (!scope.isCurrent() || committed) return;
1382
- outcomeUnknown = saveAttempted && ![400, 401, 403, 404, 409, 422, 429].includes(err?.status);
1383
  showToast(t(outcomeUnknown ? 'publish.save_unconfirmed' : (isEditMode ? 'post.edit_failed' : 'post.publish_failed')),
1384
  outcomeUnknown ? 'warning' : 'error');
1385
  } finally {
 
403
  <!-- 图片上传区 -->
404
  <div id="image-upload-section" class="post-publish__upload-section">
405
  <label for="images-input" class="post-publish__field-label">
406
+ 🖼️ ${t('post.upload_images')} <span class="post-publish__required ranking-required-marker"></span>
407
  <span class="post-publish__hint">(${t('post.max_9_images')})</span>
408
  </label>
409
  <input type="file" id="images-input" accept="image/*" multiple style="display: none;">
 
418
  <!-- 视频上传区 -->
419
  <div id="video-upload-section" style="display: none;" class="post-publish__upload-section">
420
  <label for="video-input" class="post-publish__field-label">
421
+ 🎬 ${t('post.upload_video')} <span class="post-publish__required ranking-required-marker"></span>
422
  <span class="post-publish__hint">${t('post.video_limits_hint', { megabytes: PUBLISH_MEDIA_LIMITS.VIDEO_MAX_BYTES / (1024 * 1024), minutes: PUBLISH_MEDIA_LIMITS.VIDEO_MAX_SECONDS / 60 })}</span>
423
  </label>
424
  <input type="file" id="video-input" accept="video/mp4,video/webm,video/quicktime" style="display: none;">
 
462
  <!-- 标题输入 -->
463
  <div class="post-publish__field-group">
464
  <label for="title-input" class="post-publish__field-label">
465
+ 📝 ${t('post.title_label')} <span class="post-publish__required ranking-required-marker"></span>
466
  </label>
467
  <input type="text" id="title-input" class="ranking-publish-input" value="${escapeHtml(editTitle)}" placeholder="${t('post.title_placeholder')}" maxlength="50">
468
  <div class="post-publish__counter">
 
1291
 
1292
  } catch (err) {
1293
  if (!scope.isCurrent() || committed) return;
1294
+ outcomeUnknown = saveAttempted && !(err?.requestBodyRejected === true || [400, 401, 403, 404, 409, 422, 429].includes(err?.status));
1295
  showToast(t(outcomeUnknown ? 'publish.save_unconfirmed' : (isEditMode ? 'post.edit_failed' : 'post.publish_failed')),
1296
  outcomeUnknown ? 'warning' : 'error');
1297
  } finally {
 
1379
 
1380
  } catch (err) {
1381
  if (!scope.isCurrent() || committed) return;
1382
+ outcomeUnknown = saveAttempted && !(err?.requestBodyRejected === true || [400, 401, 403, 404, 409, 422, 429].includes(err?.status));
1383
  showToast(t(outcomeUnknown ? 'publish.save_unconfirmed' : (isEditMode ? 'post.edit_failed' : 'post.publish_failed')),
1384
  outcomeUnknown ? 'warning' : 'error');
1385
  } finally {
前端页面/prompt/发布提示词组件.js CHANGED
@@ -395,7 +395,7 @@ export function createPublishPromptView(currentUser, editPromptData = null) {
395
 
396
  <!-- 分类选择 -->
397
  <div class="prompt-publish__field">
398
- <label for="category-select" class="prompt-publish__label">🏷️ ${t('prompt.category_label')} <span class="prompt-publish__required">*</span></label>
399
  <select id="category-select" class="ranking-publish-input prompt-publish__select">
400
  <option value="">${t('prompt.category_select')}</option>
401
  </select>
@@ -412,7 +412,7 @@ export function createPublishPromptView(currentUser, editPromptData = null) {
412
 
413
  <!-- 标题输入 -->
414
  <div class="prompt-publish__field">
415
- <label for="title-input" class="prompt-publish__label">📝 ${t('prompt.title_label')} <span class="prompt-publish__required">*</span></label>
416
  <input type="text" id="title-input" class="ranking-publish-input" value="${escapeAttr(editTitle)}" placeholder="${t('prompt.title_placeholder')}" maxlength="50">
417
  <div class="prompt-publish__count">
418
  <span id="title-count">${editTitle.length}</span>/50
@@ -421,7 +421,7 @@ export function createPublishPromptView(currentUser, editPromptData = null) {
421
 
422
  <!-- 文案/简介输入 -->
423
  <div class="prompt-publish__field">
424
- <label for="content-input" class="prompt-publish__label">✍️ ${t('prompt.content_label')} <span class="prompt-publish__required">*</span></label>
425
  <textarea id="content-input" class="ranking-publish-input prompt-publish__description" placeholder="${t('prompt.content_placeholder')}" maxlength="2000">${escapeHtml(editContent)}</textarea>
426
  <div class="prompt-publish__count">
427
  <span id="content-count">${editContent.length}</span>/2000
@@ -430,7 +430,7 @@ export function createPublishPromptView(currentUser, editPromptData = null) {
430
 
431
  <!-- 提示词正文(付费内容) -->
432
  <div class="prompt-publish__field">
433
- <label for="text-input" class="prompt-publish__label">🔑 ${t('prompt.text_label')} <span class="prompt-publish__required">*</span></label>
434
  <textarea id="text-input" class="ranking-publish-input prompt-publish__text" placeholder="${t('prompt.text_placeholder')}" maxlength="20000">${escapeHtml(editText)}</textarea>
435
  <div class="prompt-publish__count">
436
  <span id="text-count">${editText.length}</span>/20000
@@ -457,7 +457,7 @@ export function createPublishPromptView(currentUser, editPromptData = null) {
457
 
458
  <!-- 图片上传区(图像提示词专用:多图 + 拖拽排序,首张为封面) -->
459
  <div id="image-upload-section" class="prompt-publish__field">
460
- <label class="prompt-publish__label">🖼️ ${t('post.upload_images')} <span class="prompt-publish__required">*</span>
461
  <span class="prompt-publish__optional">(${t('post.max_9_images')})</span>
462
  </label>
463
  <input type="file" id="images-input" accept="image/*" multiple style="display: none;">
@@ -471,7 +471,7 @@ export function createPublishPromptView(currentUser, editPromptData = null) {
471
 
472
  <!-- 视频上传区(视频提示词专用:截取当前帧或手动上传封面) -->
473
  <div id="video-upload-section" style="display: none;" class="prompt-publish__media-section">
474
- <label class="prompt-publish__label">🎬 ${t('prompt.video_label')} <span id="video-required-mark" class="prompt-publish__required">*</span><span id="video-optional-mark" style="display: none;" class="prompt-publish__optional">${t('prompt.optional_mark')}</span></label>
475
  <input type="file" id="video-input" accept="video/mp4,video/webm,video/quicktime" style="display: none;">
476
  <div id="video-preview-area" class="prompt-publish__media">
477
  <div id="video-empty-state" class="prompt-publish__empty">
@@ -512,7 +512,7 @@ export function createPublishPromptView(currentUser, editPromptData = null) {
512
 
513
  <!-- 🎵 音频上传区(音乐提示词专用:图片+音频方案,视频可选) -->
514
  <div id="audio-upload-section" style="display: none;" class="prompt-publish__media-section">
515
- <label class="prompt-publish__label">🎵 ${t('prompt.audio_label')} <span class="prompt-publish__required">*</span></label>
516
  <div class="prompt-publish__media-hint">${t('prompt.music_media_hint')}</div>
517
  <input type="file" id="audio-input" accept="audio/mpeg,audio/wav,audio/x-wav,audio/mp4,audio/x-m4a,audio/ogg,.mp3,.wav,.m4a,.ogg" style="display: none;">
518
  <div id="audio-preview-area" class="prompt-publish__media">
@@ -1694,7 +1694,7 @@ export function createPublishPromptView(currentUser, editPromptData = null) {
1694
  if (!scope.isCurrent()) return;
1695
  if (committed) return;
1696
  console.error("提示词发布失败");
1697
- outcomeUnknown = saveAttempted && ![400, 401, 403, 404, 409, 422, 429].includes(err?.status);
1698
  showToast(t(outcomeUnknown ? 'publish.save_unconfirmed' : 'prompt.publish_failed'),
1699
  outcomeUnknown ? 'warning' : 'error');
1700
  } finally {
 
395
 
396
  <!-- 分类选择 -->
397
  <div class="prompt-publish__field">
398
+ <label for="category-select" class="prompt-publish__label">🏷️ ${t('prompt.category_label')} <span class="prompt-publish__required ranking-required-marker"></span></label>
399
  <select id="category-select" class="ranking-publish-input prompt-publish__select">
400
  <option value="">${t('prompt.category_select')}</option>
401
  </select>
 
412
 
413
  <!-- 标题输入 -->
414
  <div class="prompt-publish__field">
415
+ <label for="title-input" class="prompt-publish__label">📝 ${t('prompt.title_label')} <span class="prompt-publish__required ranking-required-marker"></span></label>
416
  <input type="text" id="title-input" class="ranking-publish-input" value="${escapeAttr(editTitle)}" placeholder="${t('prompt.title_placeholder')}" maxlength="50">
417
  <div class="prompt-publish__count">
418
  <span id="title-count">${editTitle.length}</span>/50
 
421
 
422
  <!-- 文案/简介输入 -->
423
  <div class="prompt-publish__field">
424
+ <label for="content-input" class="prompt-publish__label">✍️ ${t('prompt.content_label')} <span class="prompt-publish__required ranking-required-marker"></span></label>
425
  <textarea id="content-input" class="ranking-publish-input prompt-publish__description" placeholder="${t('prompt.content_placeholder')}" maxlength="2000">${escapeHtml(editContent)}</textarea>
426
  <div class="prompt-publish__count">
427
  <span id="content-count">${editContent.length}</span>/2000
 
430
 
431
  <!-- 提示词正文(付费内容) -->
432
  <div class="prompt-publish__field">
433
+ <label for="text-input" class="prompt-publish__label">🔑 ${t('prompt.text_label')} <span class="prompt-publish__required ranking-required-marker"></span></label>
434
  <textarea id="text-input" class="ranking-publish-input prompt-publish__text" placeholder="${t('prompt.text_placeholder')}" maxlength="20000">${escapeHtml(editText)}</textarea>
435
  <div class="prompt-publish__count">
436
  <span id="text-count">${editText.length}</span>/20000
 
457
 
458
  <!-- 图片上传区(图像提示词专用:多图 + 拖拽排序,首张为封面) -->
459
  <div id="image-upload-section" class="prompt-publish__field">
460
+ <label class="prompt-publish__label">🖼️ ${t('post.upload_images')} <span class="prompt-publish__required ranking-required-marker"></span>
461
  <span class="prompt-publish__optional">(${t('post.max_9_images')})</span>
462
  </label>
463
  <input type="file" id="images-input" accept="image/*" multiple style="display: none;">
 
471
 
472
  <!-- 视频上传区(视频提示词专用:截取当前帧或手动上传封面) -->
473
  <div id="video-upload-section" style="display: none;" class="prompt-publish__media-section">
474
+ <label class="prompt-publish__label">🎬 ${t('prompt.video_label')} <span id="video-required-mark" class="prompt-publish__required ranking-required-marker"></span><span id="video-optional-mark" style="display: none;" class="prompt-publish__optional">${t('prompt.optional_mark')}</span></label>
475
  <input type="file" id="video-input" accept="video/mp4,video/webm,video/quicktime" style="display: none;">
476
  <div id="video-preview-area" class="prompt-publish__media">
477
  <div id="video-empty-state" class="prompt-publish__empty">
 
512
 
513
  <!-- 🎵 音频上传区(音乐提示词专用:图片+音频方案,视频可选) -->
514
  <div id="audio-upload-section" style="display: none;" class="prompt-publish__media-section">
515
+ <label class="prompt-publish__label">🎵 ${t('prompt.audio_label')} <span class="prompt-publish__required ranking-required-marker"></span></label>
516
  <div class="prompt-publish__media-hint">${t('prompt.music_media_hint')}</div>
517
  <input type="file" id="audio-input" accept="audio/mpeg,audio/wav,audio/x-wav,audio/mp4,audio/x-m4a,audio/ogg,.mp3,.wav,.m4a,.ogg" style="display: none;">
518
  <div id="audio-preview-area" class="prompt-publish__media">
 
1694
  if (!scope.isCurrent()) return;
1695
  if (committed) return;
1696
  console.error("提示词发布失败");
1697
+ outcomeUnknown = saveAttempted && !(err?.requestBodyRejected === true || [400, 401, 403, 404, 409, 422, 429].includes(err?.status));
1698
  showToast(t(outcomeUnknown ? 'publish.save_unconfirmed' : 'prompt.publish_failed'),
1699
  outcomeUnknown ? 'warning' : 'error');
1700
  } finally {
前端页面/social/评论数据工具.js CHANGED
@@ -16,7 +16,7 @@ export function isConfirmedCommentDeletion(response) {
16
  }
17
 
18
  export function isDefiniteCommentRejection(error) {
19
- return Number.isInteger(error?.status) && error.status >= 400 && error.status < 500 && error.status !== 408;
20
  }
21
 
22
  export function isConfirmedDedicatedCommentResponse(response, account, content) {
 
16
  }
17
 
18
  export function isDefiniteCommentRejection(error) {
19
+ return error?.requestBodyRejected === true || (Number.isInteger(error?.status) && error.status >= 400 && error.status < 500 && error.status !== 408);
20
  }
21
 
22
  export function isConfirmedDedicatedCommentResponse(response, account, content) {
前端页面/styles/主题基础.css CHANGED
@@ -1,4 +1,5 @@
1
  /* RanKing UI foundation: ComfyUI theme aware defaults with local fallbacks. */
 
2
  .interaction-toggle { flex: 1; min-width: 0; min-height: 36px; padding: 6px; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: var(--rk-radius-sm, 6px); background: var(--rk-panel, var(--comfy-input-bg, #292929)); color: var(--rk-text, var(--fg-color, #ddd)); font: inherit; cursor: pointer; overflow-wrap: anywhere; }
3
  .interaction-toggle[data-active="true"] { border-color: var(--interaction-color); box-shadow: inset 0 -3px var(--interaction-color); }
4
  .interaction-toggle:focus-visible { outline: 2px solid var(--rk-focus, #8ab4ff); outline-offset: 2px; }
@@ -1351,7 +1352,7 @@ button.ranking-rating__star { cursor: pointer; min-width: 28px; min-height: 28px
1351
  .ranking-publish > .ranking-publish__field { flex: none; }
1352
  .ranking-publish__field--wide { flex: 2 1 200px; min-width: 0; }
1353
  .ranking-publish label:not(.ranking-publish__checkbox) { display: block; margin-bottom: 6px; color: inherit; font-weight: 600; overflow-wrap: anywhere; }
1354
- .ranking-publish__required { color: var(--rk-danger, #ef5350); }
1355
  .ranking-publish__security-emphasis { color: var(--rk-danger, #ef5350); }
1356
  .ranking-publish .form-input { display: block; width: 100%; min-width: 0; min-height: 36px; padding: 8px; background: var(--rk-panel, var(--comfy-input-bg, #292929)); border: 1px solid var(--rk-border, var(--border-color, #555)); color: inherit; border-radius: var(--rk-radius-sm, 6px); font: inherit; line-height: 1.5; }
1357
  .ranking-publish .form-input::placeholder { color: var(--rk-muted, var(--descrip-text, #a4a4a4)); opacity: 1; }
@@ -1722,29 +1723,29 @@ button.ranking-rating__star { cursor: pointer; min-width: 28px; min-height: 28px
1722
  @media (forced-colors: active) { .posts-card__play { background: Canvas; border: 1px solid CanvasText; } .posts-card__play-icon { border-left-color: CanvasText; } }
1723
 
1724
  .creator-card__summary:focus-visible { outline:2px solid var(--rk-focus,#8ab4ff); outline-offset:-3px; }
1725
-
1726
- .comfy-ranking-root .comfy-ranking-request-diagnostic {
1727
- box-sizing: border-box;
1728
- grid-column: 1 / -1;
1729
- width: 100%; max-width: 100%; min-width: 0;
1730
- padding: 12px;
1731
- color: var(--rk-muted);
1732
- text-align: start;
1733
- }
1734
- .comfy-ranking-request-diagnostic summary { cursor: pointer; min-height: 24px; }
1735
- .comfy-ranking-request-diagnostic__label { display: grid; gap: 6px; margin: 10px 0; }
1736
- .comfy-ranking-request-diagnostic input {
1737
- box-sizing: border-box; width: 100%; min-width: 0; min-height: 28px;
1738
- color: var(--rk-text); background: var(--rk-panel);
1739
- border: 1px solid var(--rk-border); border-radius: var(--rk-radius-sm);
1740
- padding: 6px; font-family: monospace;
1741
- }
1742
- .comfy-ranking-request-diagnostic button {
1743
- min-width: 48px; min-height: 28px; padding: 4px 10px;
1744
- color: var(--rk-text); background: var(--rk-panel);
1745
- border: 1px solid var(--rk-border); border-radius: var(--rk-radius-sm); cursor: pointer;
1746
- }
1747
- .comfy-ranking-request-diagnostic button[aria-busy="true"] { opacity: .65; }
1748
  .comfy-ranking-request-diagnostic__feedback { margin: 6px 0 0; overflow-wrap: anywhere; }
1749
  .comfy-ranking-request-diagnostic summary:focus-visible { outline: 2px solid var(--rk-focus); outline-offset: 3px; }
1750
 
@@ -1898,60 +1899,60 @@ button.ranking-rating__star { cursor: pointer; min-width: 28px; min-height: 28px
1898
  }
1899
 
1900
  .prompt-detail__author-name, .prompt-detail__author-time { display: block; }
1901
-
1902
- /* Settings follow the shared host palette within their own container. */
1903
- .ranking-settings { container: ranking-settings / inline-size; display: flex; flex-direction: column; gap: 0; flex: 1; min-width: 0; min-height: 0; overflow-y: auto; box-sizing: border-box; color: var(--rk-text, var(--fg-color, #ddd)); background: var(--comfy-menu-bg, #222); font: inherit; font-size: 14px; line-height: 1.5; overflow-wrap: anywhere; }
1904
- .ranking-settings *, .ranking-settings *::before, .ranking-settings *::after { box-sizing: border-box; }
1905
- .ranking-settings__header { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 16px; border-bottom: 1px solid var(--rk-border, var(--border-color, #555)); }
1906
- .ranking-settings__title { margin: 0; font-size: 16px; font-weight: 700; }
1907
- .ranking-settings__body { padding: 0; flex: 1; min-width: 0; }
1908
- .ranking-settings .setting-item { display: flex; justify-content: space-between; align-items: center; gap: 16px; min-width: 0; padding: 16px; border-bottom: 1px solid var(--rk-border, var(--border-color, #555)); }
1909
- .ranking-settings__copy, .ranking-settings .setting-shortcuts__copy { flex: 1; min-width: 0; }
1910
- .ranking-settings__copy-wide { width: 100%; min-width: 0; }
1911
- .ranking-settings__label, .ranking-settings__stats-label, .ranking-settings .setting-shortcuts__label { margin-bottom: 4px; font-size: 14px; font-weight: 600; }
1912
- .ranking-settings__description, .ranking-settings__stats-hint, .ranking-settings .setting-shortcuts__desc, .ranking-settings .setting-shortcuts__note, .ranking-settings__preview-copy, .ranking-settings__footer-copy { margin: 0; color: var(--rk-muted, var(--descrip-text, #aaa)); font-size: 12px; line-height: 1.6; }
1913
- .ranking-settings__heading-general, .ranking-settings__heading-display, .ranking-settings__heading-animation, .ranking-settings__heading-cache, .ranking-settings .setting-shortcuts__heading { margin: 20px 0 0; padding: 12px 16px; font-size: 13px; font-weight: 700; color: inherit; background: var(--rk-panel, var(--comfy-input-bg, #292929)); border-block-end: 1px solid var(--rk-border, var(--border-color, #555)); border-inline-start: 3px solid var(--settings-section-color, var(--rk-accent, #3b82f6)); }
1914
- .ranking-settings__heading-general { margin-top: 0; --settings-section-color: var(--rk-accent, #3b82f6); }
1915
- .ranking-settings__heading-display { --settings-section-color: var(--rk-success, #4caf50); }
1916
- .ranking-settings__heading-animation { --settings-section-color: var(--rk-warning, #e5a642); }
1917
- .ranking-settings__heading-cache { --settings-section-color: #a35cba; }
1918
- .ranking-settings .setting-shortcuts__heading { --settings-section-color: var(--rk-focus, #8ab4ff); }
1919
- .ranking-settings .setting-shortcuts__list { margin: 0; padding: 4px 16px; }
1920
- .ranking-settings .setting-shortcuts__entry { display: grid; grid-template-columns: minmax(0, 1fr); gap: 4px; padding: 10px 0; border-bottom: 1px solid var(--rk-border, var(--border-color, #555)); }
1921
- .ranking-settings .setting-shortcuts__entry dt, .ranking-settings .setting-shortcuts__entry dd { margin: 0; font-size: 12px; line-height: 1.6; }
1922
- .ranking-settings .setting-shortcuts__entry dd { color: var(--rk-muted, var(--descrip-text, #aaa)); }
1923
- .ranking-settings .setting-shortcuts__entry kbd { font: inherit; font-weight: 600; }
1924
- .ranking-settings .setting-shortcuts__note { padding: 10px 16px 16px; }
1925
- .ranking-settings .setting-switch { position: relative; appearance: none; flex: 0 0 44px; width: 44px; height: 24px; min-height: 24px; margin: 0; padding: 0; border: 1px solid var(--rk-border, var(--border-color, #777)); border-radius: 12px; background: var(--rk-panel, var(--comfy-input-bg, #292929)); color: inherit; cursor: pointer; transition: background-color .15s; }
1926
- .ranking-settings .setting-switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: var(--rk-text, var(--fg-color, #ddd)); transition: left .15s; }
1927
- .ranking-settings .setting-switch.active { background: #277a39; border-color: #277a39; }
1928
- .ranking-settings .setting-switch.active::after { left: 22px; background: #fff; }
1929
- .ranking-settings .setting-select, .ranking-settings .setting-number-input, .ranking-settings__back, .ranking-settings .cache-clear-btn { min-height: 36px; max-width: 100%; margin: 0; padding: 8px 12px; background: var(--rk-panel, var(--comfy-input-bg, #292929)); color: inherit; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: var(--rk-radius-sm, 6px); font: inherit; font-size: 13px; line-height: 1.4; }
1930
- .ranking-settings .setting-select { flex: 0 1 260px; min-width: 0; width: 100%; cursor: pointer; }
1931
- .ranking-settings .setting-select option { background: var(--rk-panel, var(--comfy-input-bg, #292929)); color: var(--rk-text, var(--fg-color, #ddd)); }
1932
- .ranking-settings .setting-number-input { width: 98px; min-width: 0; text-align: center; }
1933
- .ranking-settings__back, .ranking-settings .cache-clear-btn { cursor: pointer; }
1934
- .ranking-settings__back { flex: 0 1 auto; font-weight: 600; }
1935
- .ranking-settings :is(button, select, input):focus-visible { outline: 2px solid var(--rk-focus, #8ab4ff); outline-offset: 3px; }
1936
- .ranking-settings :is(button, select, input):hover:not(:disabled) { border-color: var(--rk-focus, #8ab4ff); }
1937
- .ranking-settings button:disabled { opacity: .65; cursor: not-allowed; }
1938
- .ranking-settings .ranking-settings__stack { flex-direction: column; align-items: stretch; gap: 12px; }
1939
- .ranking-settings__controls, .ranking-settings__stats { display: flex; flex-direction: column; gap: 10px; width: 100%; min-width: 0; }
1940
- .ranking-settings__range-row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; width: 100%; min-width: 0; }
1941
- .ranking-settings .setting-range-slider { appearance: none; flex: 1 1 120px; width: 100%; min-width: 0; height: 6px; margin: 10px 0; border: 0; border-radius: 3px; cursor: pointer; background: linear-gradient(to right, var(--rk-success, #4caf50) var(--settings-range-progress, 0%), var(--rk-border, var(--border-color, #777)) var(--settings-range-progress, 0%)); }
1942
- .ranking-settings .setting-range-slider::-webkit-slider-thumb { appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--rk-success, #4caf50); border: 1px solid var(--rk-text, var(--fg-color, #ddd)); }
1943
- .ranking-settings .setting-range-slider::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: var(--rk-success, #4caf50); border: 1px solid var(--rk-text, var(--fg-color, #ddd)); }
1944
- .ranking-settings__time { min-width: 55px; font-size: 12px; font-variant-numeric: tabular-nums; }
1945
- .ranking-settings__hint, .ranking-settings__stat, .ranking-settings__stat-muted { padding: 10px 12px; background: var(--rk-panel, var(--comfy-input-bg, #292929)); border-inline-start: 3px solid var(--rk-border, var(--border-color, #555)); border-radius: 4px; font-size: 12px; line-height: 1.6; }
1946
- .ranking-settings__hint, .ranking-settings__stat-muted { color: var(--rk-muted, var(--descrip-text, #aaa)); }
1947
- .ranking-settings__stat-title { font-weight: 600; }
1948
- .ranking-settings__actions { display: flex; flex-wrap: wrap; gap: 10px; width: 100%; }
1949
- .ranking-settings .cache-clear-btn { flex: 1 1 140px; min-width: 0; white-space: normal; overflow-wrap: anywhere; }
1950
- .ranking-settings__preview { padding: 16px; margin: 16px; border: 1px dashed var(--rk-border, var(--border-color, #555)); border-radius: 8px; }
1951
- .ranking-settings__preview-title { margin-bottom: 8px; font-size: 12px; font-weight: 600; }
1952
- .ranking-settings__footer { padding: 15px; border-top: 1px solid var(--rk-border, var(--border-color, #555)); text-align: center; }
1953
- @container ranking-settings (max-width: 420px) { .ranking-settings .setting-item:has(.setting-select) { flex-wrap: wrap; } .ranking-settings .setting-select { flex-basis: 100%; } .ranking-settings .setting-range-slider { flex-basis: 100%; } }
1954
- @media (prefers-reduced-motion: reduce) { .ranking-settings .setting-switch, .ranking-settings .setting-switch::after { transition: none; } }
1955
  @media (forced-colors: active) { .ranking-settings .setting-switch { background: ButtonFace; border-color: ButtonText; } .ranking-settings .setting-switch::after { background: ButtonText; } .ranking-settings .setting-switch.active { border: 2px solid Highlight; background: Highlight; } .ranking-settings .setting-switch.active::after { top: 1px; left: 21px; background: HighlightText; } .ranking-settings .setting-range-slider { appearance: auto; background: Canvas; } .ranking-settings__heading-general, .ranking-settings__heading-display, .ranking-settings__heading-animation, .ranking-settings__heading-cache, .ranking-settings .setting-shortcuts__heading { border-inline-start-color: Highlight; } }
1956
  .ranking-settings .setting-number-input { width: max(98px, 8ch); }
1957
  .ranking-settings { color-scheme: normal; }
@@ -2271,7 +2272,7 @@ button.ranking-rating__star { cursor: pointer; min-width: 28px; min-height: 28px
2271
  .discussion-detail .image-sandbox .img-counter { top:134px;bottom:auto;max-width:calc(100% - 4px);padding:2px 4px; }
2272
  .discussion-detail .image-sandbox .slider-y { top:192px;height:72px; }
2273
  }
2274
- .discussion-detail .post-detail-author-button { flex-wrap:wrap; }
2275
  .discussion-detail .post-detail__identity { flex:1 1 100px; }
2276
 
2277
  .prompt-publish .prompt-publish__field { margin-bottom: 15px; }
@@ -2491,55 +2492,55 @@ button.ranking-rating__star { cursor: pointer; min-width: 28px; min-height: 28px
2491
  @media (max-width:360px) { .image-crop-overlay { padding:8px; } .image-crop-dialog { padding:12px; gap:12px; } }
2492
  @media (forced-colors:active) { .image-crop-dialog .image-crop-button { color:ButtonText; background:ButtonFace; border-color:ButtonText; } .image-crop-dialog .image-crop-button:disabled { color:GrayText; border-color:GrayText; } .image-crop-dialog .image-crop-viewport { border-color:Highlight; } .image-crop-dialog .image-crop-button:focus-visible, .image-crop-dialog .image-crop-range:focus-visible { outline-color:Highlight; } }
2493
 
2494
- .profile-content-list,.profile-transactions,.profile-transaction-list{display:flex;flex-direction:column;min-width:0;gap:10px}
2495
- .profile-transactions{gap:15px;container-type:inline-size}
2496
- .profile-transaction-list{gap:8px}
2497
- .profile-list-section-title{font-size:13px;font-weight:700;color:var(--rk-text,var(--fg-color,#eee));padding:5px 0;margin:0;overflow-wrap:anywhere}
2498
- .profile-list-section-title:not(:first-child){margin-top:15px}
2499
- .profile-stats-card,.profile-task-card,.profile-entry-card,.profile-transaction-card{box-sizing:border-box;min-width:0;max-width:100%;padding:12px;background:var(--rk-panel,var(--comfy-input-bg,#292929));color:var(--rk-text,var(--fg-color,#eee));border:1px solid var(--rk-border,var(--border-color,#777));border-radius:8px;font-family:inherit;line-height:1.5;overflow-wrap:anywhere;color-scheme:normal}
2500
- .profile-stats-card{padding:14px}
2501
- .profile-stats-card--spaced{margin-bottom:15px}
2502
- .profile-stats-card__title{font-size:14px;font-weight:700;margin-bottom:12px}
2503
- .profile-stats-card__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
2504
- .profile-stats-card__cell{min-width:0;text-align:center;padding:8px 4px;border:1px solid var(--rk-border,var(--border-color,#777));border-radius:6px;border-top:3px solid var(--rk-accent,#3b82f6)}
2505
- .profile-stats-card__cell--income{border-top-color:var(--rk-success,#3a9360)}
2506
- .profile-stats-card__cell--expense{border-top-color:var(--rk-danger,#d14b4b)}
2507
- .profile-stats-card__value{font-size:20px;font-weight:700;overflow-wrap:anywhere}
2508
- .profile-stats-card__label{font-size:11px;color:var(--rk-muted,var(--descrip-text,#b8b8b8));overflow-wrap:anywhere}
2509
- .profile-task-card,.profile-entry-card{display:block;width:100%;text-align:start;appearance:none;cursor:pointer;transition:border-color .15s}
2510
- .profile-task-card:hover,.profile-entry-card:hover{border-color:var(--rk-accent,var(--fg-color,#eee))}
2511
- .profile-task-card:focus-visible,.profile-entry-card:focus-visible{outline:2px solid var(--rk-accent,var(--fg-color,#eee));outline-offset:-3px}
2512
- .profile-task-card__summary{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px}
2513
- .profile-task-card__status,.profile-list-badge{border:1px solid var(--rk-border,var(--border-color,#777));padding:2px 7px;border-radius:4px;font-size:11px;max-width:100%;box-sizing:border-box}
2514
- .profile-task-card__status--open{border-color:var(--rk-success,#3a9360)}
2515
- .profile-task-card__status--active,.profile-task-card__status--pending{border-color:var(--rk-accent,#3b82f6)}
2516
- .profile-task-card__status--disputed{border-color:var(--rk-danger,#d14b4b)}
2517
- .profile-task-card__price{font-weight:700;min-width:0;overflow-wrap:anywhere}
2518
- .profile-list-card__title{font-size:14px;font-weight:700;margin-bottom:6px;line-height:1.4;overflow-wrap:anywhere}
2519
- .profile-list-card__meta{display:flex;align-items:center;flex-wrap:wrap;gap:6px 12px;font-size:11px;color:var(--rk-muted,var(--descrip-text,#b8b8b8));min-width:0}
2520
- .profile-list-card__meta>*{min-width:0;max-width:100%;overflow-wrap:anywhere}
2521
- .profile-list-card__time{margin-inline-start:auto}
2522
- .profile-entry-card__row,.profile-transaction-card__summary{display:flex;align-items:flex-start;gap:10px;min-width:0}
2523
- .profile-entry-card__media{position:relative;flex-shrink:0;max-width:100%}
2524
- .profile-entry-card__image{width:60px;height:60px;max-width:100%;object-fit:cover;border-radius:6px;display:block}
2525
- .profile-entry-card__play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:28px;height:28px;background:rgba(0,0,0,.6);border-radius:50%;display:flex;align-items:center;justify-content:center;pointer-events:none}
2526
- .profile-entry-card__play-triangle{width:0;height:0;border-left:10px solid #fff;border-top:6px solid transparent;border-bottom:6px solid transparent;margin-left:2px}
2527
- .profile-list-card__body{flex:1;min-width:0}
2528
- .profile-list-badge--price{border-color:var(--rk-accent,#3b82f6)}
2529
- .profile-list-badge--free{border-color:var(--rk-success,#3a9360)}
2530
- .profile-transaction-card{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px}
2531
- .profile-transaction-card__summary{flex:1 1 140px}
2532
- .profile-transaction-card__icon{font-size:20px;flex-shrink:0}
2533
- .profile-transaction-card__heading{display:flex;align-items:center;flex-wrap:wrap;min-width:0}
2534
- .profile-transaction-card__title{font-size:13px;font-weight:700}
2535
- .profile-transaction-card__item{color:var(--rk-muted,var(--descrip-text,#b8b8b8));margin-inline-start:6px;min-width:0;max-width:calc(100% - 6px);overflow-wrap:anywhere}
2536
- .profile-transaction-card__description,.profile-transaction-card__detail,.profile-transaction-card__net{font-size:11px;color:var(--rk-muted,var(--descrip-text,#b8b8b8));margin-top:3px;overflow-wrap:anywhere}
2537
- .profile-transaction-card__money{min-width:0;max-width:100%;margin-inline-start:auto;text-align:end}
2538
- .profile-transaction-card__amount{font-weight:700;font-size:15px;overflow-wrap:anywhere;border-inline-start:3px solid var(--rk-danger,#d14b4b);padding-inline-start:7px}
2539
- .profile-transaction-card__amount--income{border-inline-start-color:var(--rk-success,#3a9360)}
2540
- @container (max-width:340px){.profile-stats-card__grid{grid-template-columns:minmax(0,1fr)}.profile-stats-card__cell{text-align:start;padding:8px}.profile-stats-card__value{font-size:18px}}
2541
- @media(prefers-reduced-motion:reduce){.profile-task-card,.profile-entry-card{transition:none}}
2542
- @media(forced-colors:active){.profile-task-card,.profile-entry-card,.profile-stats-card,.profile-transaction-card,.profile-stats-card__cell,.profile-task-card__status,.profile-list-badge{border-color:CanvasText}.profile-task-card:focus-visible,.profile-entry-card:focus-visible{outline-color:Highlight}}
2543
 
2544
  .task-publish { display:flex; flex-direction:column; flex:1; min-width:0; min-height:0; box-sizing:border-box; overflow-y:auto; color:var(--rk-text,var(--fg-color,#eee)); background:var(--rk-bg,var(--comfy-menu-bg,#202020)); font:inherit; font-size:14px; line-height:1.5; overflow-wrap:anywhere; color-scheme:normal; container-type:inline-size; }
2545
  .task-publish *, .task-publish *::before, .task-publish *::after { box-sizing:border-box; }
@@ -2639,115 +2640,115 @@ 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
  }
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
  .skill-directory { display:flex; flex-direction:column; gap:.75rem; min-width:0; color:inherit; overflow-wrap:anywhere; }
2752
  .skill-directory p { margin:0; }
2753
  .skill-directory__label { display:flex; flex-direction:column; gap:.375rem; min-width:0; }
@@ -2763,6 +2764,16 @@ button.ranking-rating__star { cursor: pointer; min-width: 28px; min-height: 28px
2763
  @media (forced-colors:active) { .skill-directory :is(input,button) { border-color:ButtonText; } .skill-directory :is(input,button):focus-visible { outline-color:Highlight; } }
2764
 
2765
  .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); }
2766
- .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); }
2767
- .comfy-ranking-root .resource-trend__state[role=alert] { border-inline-start:2px solid var(--rk-danger); }
2768
- @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; } }
 
 
 
 
 
 
 
 
 
 
 
1
  /* RanKing UI foundation: ComfyUI theme aware defaults with local fallbacks. */
2
+ .ranking-required-marker::before { content: "*"; }
3
  .interaction-toggle { flex: 1; min-width: 0; min-height: 36px; padding: 6px; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: var(--rk-radius-sm, 6px); background: var(--rk-panel, var(--comfy-input-bg, #292929)); color: var(--rk-text, var(--fg-color, #ddd)); font: inherit; cursor: pointer; overflow-wrap: anywhere; }
4
  .interaction-toggle[data-active="true"] { border-color: var(--interaction-color); box-shadow: inset 0 -3px var(--interaction-color); }
5
  .interaction-toggle:focus-visible { outline: 2px solid var(--rk-focus, #8ab4ff); outline-offset: 2px; }
 
1352
  .ranking-publish > .ranking-publish__field { flex: none; }
1353
  .ranking-publish__field--wide { flex: 2 1 200px; min-width: 0; }
1354
  .ranking-publish label:not(.ranking-publish__checkbox) { display: block; margin-bottom: 6px; color: inherit; font-weight: 600; overflow-wrap: anywhere; }
1355
+ .ranking-publish__required { color: var(--rk-danger, #ef5350); }
1356
  .ranking-publish__security-emphasis { color: var(--rk-danger, #ef5350); }
1357
  .ranking-publish .form-input { display: block; width: 100%; min-width: 0; min-height: 36px; padding: 8px; background: var(--rk-panel, var(--comfy-input-bg, #292929)); border: 1px solid var(--rk-border, var(--border-color, #555)); color: inherit; border-radius: var(--rk-radius-sm, 6px); font: inherit; line-height: 1.5; }
1358
  .ranking-publish .form-input::placeholder { color: var(--rk-muted, var(--descrip-text, #a4a4a4)); opacity: 1; }
 
1723
  @media (forced-colors: active) { .posts-card__play { background: Canvas; border: 1px solid CanvasText; } .posts-card__play-icon { border-left-color: CanvasText; } }
1724
 
1725
  .creator-card__summary:focus-visible { outline:2px solid var(--rk-focus,#8ab4ff); outline-offset:-3px; }
1726
+
1727
+ .comfy-ranking-root .comfy-ranking-request-diagnostic {
1728
+ box-sizing: border-box;
1729
+ grid-column: 1 / -1;
1730
+ width: 100%; max-width: 100%; min-width: 0;
1731
+ padding: 12px;
1732
+ color: var(--rk-muted);
1733
+ text-align: start;
1734
+ }
1735
+ .comfy-ranking-request-diagnostic summary { cursor: pointer; min-height: 24px; }
1736
+ .comfy-ranking-request-diagnostic__label { display: grid; gap: 6px; margin: 10px 0; }
1737
+ .comfy-ranking-request-diagnostic input {
1738
+ box-sizing: border-box; width: 100%; min-width: 0; min-height: 28px;
1739
+ color: var(--rk-text); background: var(--rk-panel);
1740
+ border: 1px solid var(--rk-border); border-radius: var(--rk-radius-sm);
1741
+ padding: 6px; font-family: monospace;
1742
+ }
1743
+ .comfy-ranking-request-diagnostic button {
1744
+ min-width: 48px; min-height: 28px; padding: 4px 10px;
1745
+ color: var(--rk-text); background: var(--rk-panel);
1746
+ border: 1px solid var(--rk-border); border-radius: var(--rk-radius-sm); cursor: pointer;
1747
+ }
1748
+ .comfy-ranking-request-diagnostic button[aria-busy="true"] { opacity: .65; }
1749
  .comfy-ranking-request-diagnostic__feedback { margin: 6px 0 0; overflow-wrap: anywhere; }
1750
  .comfy-ranking-request-diagnostic summary:focus-visible { outline: 2px solid var(--rk-focus); outline-offset: 3px; }
1751
 
 
1899
  }
1900
 
1901
  .prompt-detail__author-name, .prompt-detail__author-time { display: block; }
1902
+
1903
+ /* Settings follow the shared host palette within their own container. */
1904
+ .ranking-settings { container: ranking-settings / inline-size; display: flex; flex-direction: column; gap: 0; flex: 1; min-width: 0; min-height: 0; overflow-y: auto; box-sizing: border-box; color: var(--rk-text, var(--fg-color, #ddd)); background: var(--comfy-menu-bg, #222); font: inherit; font-size: 14px; line-height: 1.5; overflow-wrap: anywhere; }
1905
+ .ranking-settings *, .ranking-settings *::before, .ranking-settings *::after { box-sizing: border-box; }
1906
+ .ranking-settings__header { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 16px; border-bottom: 1px solid var(--rk-border, var(--border-color, #555)); }
1907
+ .ranking-settings__title { margin: 0; font-size: 16px; font-weight: 700; }
1908
+ .ranking-settings__body { padding: 0; flex: 1; min-width: 0; }
1909
+ .ranking-settings .setting-item { display: flex; justify-content: space-between; align-items: center; gap: 16px; min-width: 0; padding: 16px; border-bottom: 1px solid var(--rk-border, var(--border-color, #555)); }
1910
+ .ranking-settings__copy, .ranking-settings .setting-shortcuts__copy { flex: 1; min-width: 0; }
1911
+ .ranking-settings__copy-wide { width: 100%; min-width: 0; }
1912
+ .ranking-settings__label, .ranking-settings__stats-label, .ranking-settings .setting-shortcuts__label { margin-bottom: 4px; font-size: 14px; font-weight: 600; }
1913
+ .ranking-settings__description, .ranking-settings__stats-hint, .ranking-settings .setting-shortcuts__desc, .ranking-settings .setting-shortcuts__note, .ranking-settings__preview-copy, .ranking-settings__footer-copy { margin: 0; color: var(--rk-muted, var(--descrip-text, #aaa)); font-size: 12px; line-height: 1.6; }
1914
+ .ranking-settings__heading-general, .ranking-settings__heading-display, .ranking-settings__heading-animation, .ranking-settings__heading-cache, .ranking-settings .setting-shortcuts__heading { margin: 20px 0 0; padding: 12px 16px; font-size: 13px; font-weight: 700; color: inherit; background: var(--rk-panel, var(--comfy-input-bg, #292929)); border-block-end: 1px solid var(--rk-border, var(--border-color, #555)); border-inline-start: 3px solid var(--settings-section-color, var(--rk-accent, #3b82f6)); }
1915
+ .ranking-settings__heading-general { margin-top: 0; --settings-section-color: var(--rk-accent, #3b82f6); }
1916
+ .ranking-settings__heading-display { --settings-section-color: var(--rk-success, #4caf50); }
1917
+ .ranking-settings__heading-animation { --settings-section-color: var(--rk-warning, #e5a642); }
1918
+ .ranking-settings__heading-cache { --settings-section-color: #a35cba; }
1919
+ .ranking-settings .setting-shortcuts__heading { --settings-section-color: var(--rk-focus, #8ab4ff); }
1920
+ .ranking-settings .setting-shortcuts__list { margin: 0; padding: 4px 16px; }
1921
+ .ranking-settings .setting-shortcuts__entry { display: grid; grid-template-columns: minmax(0, 1fr); gap: 4px; padding: 10px 0; border-bottom: 1px solid var(--rk-border, var(--border-color, #555)); }
1922
+ .ranking-settings .setting-shortcuts__entry dt, .ranking-settings .setting-shortcuts__entry dd { margin: 0; font-size: 12px; line-height: 1.6; }
1923
+ .ranking-settings .setting-shortcuts__entry dd { color: var(--rk-muted, var(--descrip-text, #aaa)); }
1924
+ .ranking-settings .setting-shortcuts__entry kbd { font: inherit; font-weight: 600; }
1925
+ .ranking-settings .setting-shortcuts__note { padding: 10px 16px 16px; }
1926
+ .ranking-settings .setting-switch { position: relative; appearance: none; flex: 0 0 44px; width: 44px; height: 24px; min-height: 24px; margin: 0; padding: 0; border: 1px solid var(--rk-border, var(--border-color, #777)); border-radius: 12px; background: var(--rk-panel, var(--comfy-input-bg, #292929)); color: inherit; cursor: pointer; transition: background-color .15s; }
1927
+ .ranking-settings .setting-switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: var(--rk-text, var(--fg-color, #ddd)); transition: left .15s; }
1928
+ .ranking-settings .setting-switch.active { background: #277a39; border-color: #277a39; }
1929
+ .ranking-settings .setting-switch.active::after { left: 22px; background: #fff; }
1930
+ .ranking-settings .setting-select, .ranking-settings .setting-number-input, .ranking-settings__back, .ranking-settings .cache-clear-btn { min-height: 36px; max-width: 100%; margin: 0; padding: 8px 12px; background: var(--rk-panel, var(--comfy-input-bg, #292929)); color: inherit; border: 1px solid var(--rk-border, var(--border-color, #555)); border-radius: var(--rk-radius-sm, 6px); font: inherit; font-size: 13px; line-height: 1.4; }
1931
+ .ranking-settings .setting-select { flex: 0 1 260px; min-width: 0; width: 100%; cursor: pointer; }
1932
+ .ranking-settings .setting-select option { background: var(--rk-panel, var(--comfy-input-bg, #292929)); color: var(--rk-text, var(--fg-color, #ddd)); }
1933
+ .ranking-settings .setting-number-input { width: 98px; min-width: 0; text-align: center; }
1934
+ .ranking-settings__back, .ranking-settings .cache-clear-btn { cursor: pointer; }
1935
+ .ranking-settings__back { flex: 0 1 auto; font-weight: 600; }
1936
+ .ranking-settings :is(button, select, input):focus-visible { outline: 2px solid var(--rk-focus, #8ab4ff); outline-offset: 3px; }
1937
+ .ranking-settings :is(button, select, input):hover:not(:disabled) { border-color: var(--rk-focus, #8ab4ff); }
1938
+ .ranking-settings button:disabled { opacity: .65; cursor: not-allowed; }
1939
+ .ranking-settings .ranking-settings__stack { flex-direction: column; align-items: stretch; gap: 12px; }
1940
+ .ranking-settings__controls, .ranking-settings__stats { display: flex; flex-direction: column; gap: 10px; width: 100%; min-width: 0; }
1941
+ .ranking-settings__range-row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; width: 100%; min-width: 0; }
1942
+ .ranking-settings .setting-range-slider { appearance: none; flex: 1 1 120px; width: 100%; min-width: 0; height: 6px; margin: 10px 0; border: 0; border-radius: 3px; cursor: pointer; background: linear-gradient(to right, var(--rk-success, #4caf50) var(--settings-range-progress, 0%), var(--rk-border, var(--border-color, #777)) var(--settings-range-progress, 0%)); }
1943
+ .ranking-settings .setting-range-slider::-webkit-slider-thumb { appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--rk-success, #4caf50); border: 1px solid var(--rk-text, var(--fg-color, #ddd)); }
1944
+ .ranking-settings .setting-range-slider::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: var(--rk-success, #4caf50); border: 1px solid var(--rk-text, var(--fg-color, #ddd)); }
1945
+ .ranking-settings__time { min-width: 55px; font-size: 12px; font-variant-numeric: tabular-nums; }
1946
+ .ranking-settings__hint, .ranking-settings__stat, .ranking-settings__stat-muted { padding: 10px 12px; background: var(--rk-panel, var(--comfy-input-bg, #292929)); border-inline-start: 3px solid var(--rk-border, var(--border-color, #555)); border-radius: 4px; font-size: 12px; line-height: 1.6; }
1947
+ .ranking-settings__hint, .ranking-settings__stat-muted { color: var(--rk-muted, var(--descrip-text, #aaa)); }
1948
+ .ranking-settings__stat-title { font-weight: 600; }
1949
+ .ranking-settings__actions { display: flex; flex-wrap: wrap; gap: 10px; width: 100%; }
1950
+ .ranking-settings .cache-clear-btn { flex: 1 1 140px; min-width: 0; white-space: normal; overflow-wrap: anywhere; }
1951
+ .ranking-settings__preview { padding: 16px; margin: 16px; border: 1px dashed var(--rk-border, var(--border-color, #555)); border-radius: 8px; }
1952
+ .ranking-settings__preview-title { margin-bottom: 8px; font-size: 12px; font-weight: 600; }
1953
+ .ranking-settings__footer { padding: 15px; border-top: 1px solid var(--rk-border, var(--border-color, #555)); text-align: center; }
1954
+ @container ranking-settings (max-width: 420px) { .ranking-settings .setting-item:has(.setting-select) { flex-wrap: wrap; } .ranking-settings .setting-select { flex-basis: 100%; } .ranking-settings .setting-range-slider { flex-basis: 100%; } }
1955
+ @media (prefers-reduced-motion: reduce) { .ranking-settings .setting-switch, .ranking-settings .setting-switch::after { transition: none; } }
1956
  @media (forced-colors: active) { .ranking-settings .setting-switch { background: ButtonFace; border-color: ButtonText; } .ranking-settings .setting-switch::after { background: ButtonText; } .ranking-settings .setting-switch.active { border: 2px solid Highlight; background: Highlight; } .ranking-settings .setting-switch.active::after { top: 1px; left: 21px; background: HighlightText; } .ranking-settings .setting-range-slider { appearance: auto; background: Canvas; } .ranking-settings__heading-general, .ranking-settings__heading-display, .ranking-settings__heading-animation, .ranking-settings__heading-cache, .ranking-settings .setting-shortcuts__heading { border-inline-start-color: Highlight; } }
1957
  .ranking-settings .setting-number-input { width: max(98px, 8ch); }
1958
  .ranking-settings { color-scheme: normal; }
 
2272
  .discussion-detail .image-sandbox .img-counter { top:134px;bottom:auto;max-width:calc(100% - 4px);padding:2px 4px; }
2273
  .discussion-detail .image-sandbox .slider-y { top:192px;height:72px; }
2274
  }
2275
+ .discussion-detail .post-detail-author-button { flex-wrap:wrap; }
2276
  .discussion-detail .post-detail__identity { flex:1 1 100px; }
2277
 
2278
  .prompt-publish .prompt-publish__field { margin-bottom: 15px; }
 
2492
  @media (max-width:360px) { .image-crop-overlay { padding:8px; } .image-crop-dialog { padding:12px; gap:12px; } }
2493
  @media (forced-colors:active) { .image-crop-dialog .image-crop-button { color:ButtonText; background:ButtonFace; border-color:ButtonText; } .image-crop-dialog .image-crop-button:disabled { color:GrayText; border-color:GrayText; } .image-crop-dialog .image-crop-viewport { border-color:Highlight; } .image-crop-dialog .image-crop-button:focus-visible, .image-crop-dialog .image-crop-range:focus-visible { outline-color:Highlight; } }
2494
 
2495
+ .profile-content-list,.profile-transactions,.profile-transaction-list{display:flex;flex-direction:column;min-width:0;gap:10px}
2496
+ .profile-transactions{gap:15px;container-type:inline-size}
2497
+ .profile-transaction-list{gap:8px}
2498
+ .profile-list-section-title{font-size:13px;font-weight:700;color:var(--rk-text,var(--fg-color,#eee));padding:5px 0;margin:0;overflow-wrap:anywhere}
2499
+ .profile-list-section-title:not(:first-child){margin-top:15px}
2500
+ .profile-stats-card,.profile-task-card,.profile-entry-card,.profile-transaction-card{box-sizing:border-box;min-width:0;max-width:100%;padding:12px;background:var(--rk-panel,var(--comfy-input-bg,#292929));color:var(--rk-text,var(--fg-color,#eee));border:1px solid var(--rk-border,var(--border-color,#777));border-radius:8px;font-family:inherit;line-height:1.5;overflow-wrap:anywhere;color-scheme:normal}
2501
+ .profile-stats-card{padding:14px}
2502
+ .profile-stats-card--spaced{margin-bottom:15px}
2503
+ .profile-stats-card__title{font-size:14px;font-weight:700;margin-bottom:12px}
2504
+ .profile-stats-card__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
2505
+ .profile-stats-card__cell{min-width:0;text-align:center;padding:8px 4px;border:1px solid var(--rk-border,var(--border-color,#777));border-radius:6px;border-top:3px solid var(--rk-accent,#3b82f6)}
2506
+ .profile-stats-card__cell--income{border-top-color:var(--rk-success,#3a9360)}
2507
+ .profile-stats-card__cell--expense{border-top-color:var(--rk-danger,#d14b4b)}
2508
+ .profile-stats-card__value{font-size:20px;font-weight:700;overflow-wrap:anywhere}
2509
+ .profile-stats-card__label{font-size:11px;color:var(--rk-muted,var(--descrip-text,#b8b8b8));overflow-wrap:anywhere}
2510
+ .profile-task-card,.profile-entry-card{display:block;width:100%;text-align:start;appearance:none;cursor:pointer;transition:border-color .15s}
2511
+ .profile-task-card:hover,.profile-entry-card:hover{border-color:var(--rk-accent,var(--fg-color,#eee))}
2512
+ .profile-task-card:focus-visible,.profile-entry-card:focus-visible{outline:2px solid var(--rk-accent,var(--fg-color,#eee));outline-offset:-3px}
2513
+ .profile-task-card__summary{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px}
2514
+ .profile-task-card__status,.profile-list-badge{border:1px solid var(--rk-border,var(--border-color,#777));padding:2px 7px;border-radius:4px;font-size:11px;max-width:100%;box-sizing:border-box}
2515
+ .profile-task-card__status--open{border-color:var(--rk-success,#3a9360)}
2516
+ .profile-task-card__status--active,.profile-task-card__status--pending{border-color:var(--rk-accent,#3b82f6)}
2517
+ .profile-task-card__status--disputed{border-color:var(--rk-danger,#d14b4b)}
2518
+ .profile-task-card__price{font-weight:700;min-width:0;overflow-wrap:anywhere}
2519
+ .profile-list-card__title{font-size:14px;font-weight:700;margin-bottom:6px;line-height:1.4;overflow-wrap:anywhere}
2520
+ .profile-list-card__meta{display:flex;align-items:center;flex-wrap:wrap;gap:6px 12px;font-size:11px;color:var(--rk-muted,var(--descrip-text,#b8b8b8));min-width:0}
2521
+ .profile-list-card__meta>*{min-width:0;max-width:100%;overflow-wrap:anywhere}
2522
+ .profile-list-card__time{margin-inline-start:auto}
2523
+ .profile-entry-card__row,.profile-transaction-card__summary{display:flex;align-items:flex-start;gap:10px;min-width:0}
2524
+ .profile-entry-card__media{position:relative;flex-shrink:0;max-width:100%}
2525
+ .profile-entry-card__image{width:60px;height:60px;max-width:100%;object-fit:cover;border-radius:6px;display:block}
2526
+ .profile-entry-card__play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:28px;height:28px;background:rgba(0,0,0,.6);border-radius:50%;display:flex;align-items:center;justify-content:center;pointer-events:none}
2527
+ .profile-entry-card__play-triangle{width:0;height:0;border-left:10px solid #fff;border-top:6px solid transparent;border-bottom:6px solid transparent;margin-left:2px}
2528
+ .profile-list-card__body{flex:1;min-width:0}
2529
+ .profile-list-badge--price{border-color:var(--rk-accent,#3b82f6)}
2530
+ .profile-list-badge--free{border-color:var(--rk-success,#3a9360)}
2531
+ .profile-transaction-card{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px}
2532
+ .profile-transaction-card__summary{flex:1 1 140px}
2533
+ .profile-transaction-card__icon{font-size:20px;flex-shrink:0}
2534
+ .profile-transaction-card__heading{display:flex;align-items:center;flex-wrap:wrap;min-width:0}
2535
+ .profile-transaction-card__title{font-size:13px;font-weight:700}
2536
+ .profile-transaction-card__item{color:var(--rk-muted,var(--descrip-text,#b8b8b8));margin-inline-start:6px;min-width:0;max-width:calc(100% - 6px);overflow-wrap:anywhere}
2537
+ .profile-transaction-card__description,.profile-transaction-card__detail,.profile-transaction-card__net{font-size:11px;color:var(--rk-muted,var(--descrip-text,#b8b8b8));margin-top:3px;overflow-wrap:anywhere}
2538
+ .profile-transaction-card__money{min-width:0;max-width:100%;margin-inline-start:auto;text-align:end}
2539
+ .profile-transaction-card__amount{font-weight:700;font-size:15px;overflow-wrap:anywhere;border-inline-start:3px solid var(--rk-danger,#d14b4b);padding-inline-start:7px}
2540
+ .profile-transaction-card__amount--income{border-inline-start-color:var(--rk-success,#3a9360)}
2541
+ @container (max-width:340px){.profile-stats-card__grid{grid-template-columns:minmax(0,1fr)}.profile-stats-card__cell{text-align:start;padding:8px}.profile-stats-card__value{font-size:18px}}
2542
+ @media(prefers-reduced-motion:reduce){.profile-task-card,.profile-entry-card{transition:none}}
2543
+ @media(forced-colors:active){.profile-task-card,.profile-entry-card,.profile-stats-card,.profile-transaction-card,.profile-stats-card__cell,.profile-task-card__status,.profile-list-badge{border-color:CanvasText}.profile-task-card:focus-visible,.profile-entry-card:focus-visible{outline-color:Highlight}}
2544
 
2545
  .task-publish { display:flex; flex-direction:column; flex:1; min-width:0; min-height:0; box-sizing:border-box; overflow-y:auto; color:var(--rk-text,var(--fg-color,#eee)); background:var(--rk-bg,var(--comfy-menu-bg,#202020)); font:inherit; font-size:14px; line-height:1.5; overflow-wrap:anywhere; color-scheme:normal; container-type:inline-size; }
2546
  .task-publish *, .task-publish *::before, .task-publish *::after { box-sizing:border-box; }
 
2640
  .wallet-modal__danger, .wallet-modal__success { text-decoration-color: CanvasText; }
2641
  .wallet-withdraw__summary, .wallet-withdraw__fee, .wallet-recharge__qr { border-color: CanvasText; }
2642
  }
2643
+
2644
+ .tip-level { max-width: 100%; overflow-wrap: anywhere; }
2645
+ .tip-level--new, .tip-level--fallback { color: var(--rk-text, var(--fg-color, #ddd)); font-size: .6875rem; }
2646
+ .tip-level__icon { overflow-wrap: anywhere; }
2647
+ .tip-level__icon--sun { font-size: .75rem; }
2648
+ .tip-level__icon--moon { font-size: .6875rem; }
2649
+ .tip-level__icon--star, .tip-level--fallback { font-size: .625rem; }
2650
+ .tip-level--full { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; font-size: .6875rem; }
2651
+ .tip-level__part { color: var(--rk-text, var(--fg-color, #ddd)); }
2652
+ .tip-level__part--sun { font-weight: bold; }
2653
+ .tip-level--max-compact { color: var(--rk-text, var(--fg-color, #ddd)); font-weight: bold; padding-inline: 2px; border-bottom: 2px solid #c38a00; }
2654
+ .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; }
2655
+ .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; }
2656
+ .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; }
2657
+ .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; }
2658
+ .tip-board-group { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
2659
+ .tip-board-item > .tip-board-group:first-child { flex: 1 1 8rem; }
2660
+ .tip-board-item > .tip-board-group:last-child { justify-content: flex-end; margin-inline-start: auto; }
2661
+ .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; }
2662
+ .tip-board-badge--gold { background: #ffd700; color: #211900; border-color: #b28a00; }
2663
+ .tip-board-badge--silver { background: #c0c0c0; color: #191919; border-color: #777; }
2664
+ .tip-board-badge--bronze { background: #cd7f32; color: #1c1000; border-color: #8d511b; }
2665
+ .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)); }
2666
+ .tip-board-name, .tip-board-anonymous { min-width: 0; max-width: 100%; color: var(--rk-text, var(--fg-color, #ddd)); overflow-wrap: anywhere; }
2667
+ .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; }
2668
+ .tip-board-empty { padding: 12px 0; color: var(--rk-text, var(--fg-color, #ddd)); font-size: .75rem; text-align: center; overflow-wrap: anywhere; }
2669
+ @media (forced-colors: active) {
2670
+ .tip-board-item { border-color: CanvasText; color: CanvasText; }
2671
+ .tip-board-badge, .tip-level--max { border-color: CanvasText; background: Canvas; color: CanvasText; }
2672
+ .tip-level--max-compact { color: CanvasText; border-color: CanvasText; }
2673
+ .tip-level__part, .tip-level--new, .tip-level--fallback, .tip-board-name, .tip-board-anonymous, .tip-board-amount, .tip-board-empty { color: CanvasText; }
2674
+ }
2675
+
2676
+
2677
+ .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; }
2678
+ .ranking-publish__image { position:relative; flex:0 0 96px; width:96px; height:96px; min-width:0; box-sizing:border-box; }
2679
+ .ranking-publish__image--sortable { cursor:grab; }
2680
+ .ranking-publish__image:focus-visible { outline:3px solid var(--rk-focus,#8ab4ff); outline-offset:2px; }
2681
+ .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; }
2682
+ .ranking-publish__image--cover .ranking-publish__thumbnail { border-color:var(--rk-success,#3a9360); }
2683
+ .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; }
2684
+ .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; }
2685
+ .ranking-publish .ranking-publish__image-remove:hover:not(:disabled) { border-color:var(--rk-focus,#8ab4ff); }
2686
+ .ranking-publish .ranking-publish__image-remove:focus-visible { outline:3px solid var(--rk-focus,#8ab4ff); outline-offset:2px; }
2687
+ .ranking-publish__image--dragging { opacity:.5; }
2688
+ .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; }
2689
+ .ranking-publish__pending-value { padding-inline:3px; border-bottom:2px solid var(--rk-success,#3a9360); color:inherit; }
2690
+ .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; }
2691
+ .ranking-original-confirm *, .ranking-original-confirm *::before, .ranking-original-confirm *::after { box-sizing:border-box; }
2692
+ .ranking-original-confirm__description { margin:0 0 15px; }
2693
+ .ranking-original-confirm__subtitle { margin:0 0 10px; font-weight:700; }
2694
+ .ranking-original-confirm__list { margin:0 0 15px; padding-inline-start:20px; }
2695
+ .ranking-original-confirm__list li { margin-bottom:5px; }
2696
+ .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; }
2697
+ .ranking-original-confirm__actions { display:flex; flex-wrap:wrap; gap:10px; margin-top:20px; justify-content:flex-end; }
2698
+ .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; }
2699
+ .ranking-original-confirm__button--confirm { border-inline-start:3px solid var(--rk-success,#3a9360); font-weight:700; }
2700
+ .ranking-original-confirm__button:hover:not(:disabled) { border-color:var(--rk-focus,#8ab4ff); }
2701
+ .ranking-original-confirm__button:focus-visible { outline:3px solid var(--rk-focus,#8ab4ff); outline-offset:2px; }
2702
+ .ranking-original-confirm__button:disabled { opacity:.7; cursor:default; }
2703
+ @media (forced-colors:active) {
2704
+ .ranking-publish__thumbnail { border-color:CanvasText; }
2705
+ .ranking-publish__image--cover .ranking-publish__thumbnail { border-color:Highlight; }
2706
+ .ranking-publish__image--target::before { background:Highlight; }
2707
+ .ranking-publish__cover-label, .ranking-publish .ranking-publish__image-remove, .ranking-original-confirm__button { background:ButtonFace; color:ButtonText; border-color:ButtonText; }
2708
+ .ranking-publish__image:focus-visible, .ranking-publish .ranking-publish__image-remove:focus-visible, .ranking-original-confirm__button:focus-visible { outline-color:Highlight; }
2709
+ .ranking-publish__pending-value, .ranking-original-confirm__warning, .ranking-original-confirm__button--confirm { border-color:Highlight; }
2710
+ }
2711
+
2712
+ .comfy-ranking-root .sidebar-status-message {
2713
+ box-sizing: border-box;
2714
+ min-width: 0;
2715
+ max-width: 100%;
2716
+ padding: 2.5rem 1.25rem;
2717
+ color: var(--rk-text, var(--fg-color, #f1f1f1));
2718
+ font: inherit;
2719
+ line-height: 1.5;
2720
+ text-align: center;
2721
+ overflow-wrap: anywhere;
2722
+ }
2723
+ .comfy-ranking-root .sidebar-status-more {
2724
+ box-sizing: border-box;
2725
+ width: 100%;
2726
+ min-width: 0;
2727
+ min-height: 2.25rem;
2728
+ margin-top: .625rem;
2729
+ padding: .625rem;
2730
+ border: 1px solid var(--rk-border, var(--border-color, #555));
2731
+ border-radius: .375rem;
2732
+ background: transparent;
2733
+ color: var(--rk-text, var(--fg-color, #f1f1f1));
2734
+ font: inherit;
2735
+ line-height: 1.5;
2736
+ text-align: center;
2737
+ white-space: normal;
2738
+ overflow-wrap: anywhere;
2739
+ cursor: pointer;
2740
+ }
2741
+ .comfy-ranking-root .sidebar-status-more:focus-visible { outline: 2px solid var(--rk-text, var(--fg-color, #f1f1f1)); outline-offset: -3px; }
2742
+ .comfy-ranking-root .sidebar-status-more:disabled { cursor: wait; }
2743
+ @media (forced-colors: active) {
2744
+ .comfy-ranking-root .sidebar-status-message { color: CanvasText; }
2745
+ .comfy-ranking-root .sidebar-status-more { color: ButtonText; border-color: ButtonText; background: ButtonFace; }
2746
+ .comfy-ranking-root .sidebar-status-more:focus-visible { outline-color: Highlight; }
2747
+ .comfy-ranking-root .sidebar-status-more:disabled { color: GrayText; border-color: GrayText; }
2748
+ }
2749
+
2750
+ .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; }
2751
+
2752
  .skill-directory { display:flex; flex-direction:column; gap:.75rem; min-width:0; color:inherit; overflow-wrap:anywhere; }
2753
  .skill-directory p { margin:0; }
2754
  .skill-directory__label { display:flex; flex-direction:column; gap:.375rem; min-width:0; }
 
2764
  @media (forced-colors:active) { .skill-directory :is(input,button) { border-color:ButtonText; } .skill-directory :is(input,button):focus-visible { outline-color:Highlight; } }
2765
 
2766
  .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); }
2767
+ .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); }
2768
+ .comfy-ranking-root .resource-trend__state[role=alert] { border-inline-start:2px solid var(--rk-danger); }
2769
+ @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; } }
2770
+
2771
+ .skeleton-container { padding: 8px; }
2772
+ .skeleton-container .skeleton-item {
2773
+ background: linear-gradient(90deg, var(--rk-panel, var(--comfy-input-bg, #292929)) 25%, var(--rk-border, var(--border-color, #454545)) 50%, var(--rk-panel, var(--comfy-input-bg, #292929)) 75%);
2774
+ background-size: 200% 100%; animation: ranking-skeleton-shimmer 1.5s ease-in-out infinite; border-radius: 6px;
2775
+ }
2776
+ .skeleton-container .skeleton-card { background: var(--rk-panel, var(--comfy-input-bg, #292929)); border: 1px solid var(--rk-border, var(--border-color, #454545)); border-radius: 8px; padding: 12px; margin-bottom: 12px; }
2777
+ @keyframes ranking-skeleton-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
2778
+ @media (prefers-reduced-motion: reduce) { .skeleton-container .skeleton-item { animation: none; } }
2779
+ @media (forced-colors: active) { .skeleton-container .skeleton-card { background: Canvas; border-color: CanvasText; } .skeleton-container .skeleton-item { background: GrayText; animation: none; } }
前端页面/task/任务详情组件.js CHANGED
@@ -487,7 +487,7 @@ function _showConfirmDialog({ scope, title, icon, message, description, confirmT
487
  await onSuccess(content);
488
  } catch (err) {
489
  if (!taskModalCurrent(content) || phase === "closed" || phase === "committed") return;
490
- const rejected = [400, 401, 403, 404, 409, 422, 429].includes(err?.status);
491
  phase = rejected ? "idle" : "unconfirmed";
492
  showToast(rejected ? errorText : t('task.operation_unconfirmed'), rejected ? "error" : "warning");
493
  } finally {
@@ -559,7 +559,7 @@ function bindTaskTextSubmission({ content, taskId, input, button, confirmText, e
559
  await onSuccess();
560
  } catch (err) {
561
  if (!taskModalCurrent(content) || phase === "closed" || phase === "committed") return;
562
- const rejected = [400, 401, 403, 404, 409, 422, 429].includes(err?.status);
563
  phase = rejected ? "idle" : "unconfirmed";
564
  showToast(rejected ? errorText : t('task.operation_unconfirmed'), rejected ? "error" : "warning");
565
  } finally {
@@ -764,7 +764,7 @@ function showDeleteCommentConfirmDialog(taskId, comment, container, currentUser)
764
  await loadTaskComments(container, taskId, currentUser);
765
  } catch (err) {
766
  if (!taskModalCurrent(content) || phase === "closed" || phase === "committed") return;
767
- const rejected = [400, 401, 403, 404, 409, 422, 429].includes(err?.status);
768
  phase = rejected ? "idle" : "unconfirmed";
769
  showToast(t(rejected ? 'post.delete_comment_failed' : 'task.comment_unconfirmed'), rejected ? "error" : "warning");
770
  } finally {
@@ -919,7 +919,7 @@ function showSubmitDialog(task, currentUser) {
919
  content.classList.add("task-modal");
920
  content.innerHTML = `
921
  <div class="task-detail__spaced">
922
- <label for="deliverable-files" class="task-detail__file-label">${t('task.deliverable_images')} <span aria-hidden="true">*</span></label>
923
  <input type="file" aria-required="true" aria-describedby="deliverable-hint" id="deliverable-files" multiple accept="image/*" class="task-detail__file-input">
924
  <div id="deliverable-hint" class="task-detail__hint">${t('task.support_multiple_images')}</div>
925
  </div>
@@ -990,7 +990,7 @@ function showSubmitDialog(task, currentUser) {
990
  if (container && currentUser) await loadTaskDetail(container, taskId, currentUser);
991
  } catch (err) {
992
  if (!taskModalCurrent(content) || phase === "closed" || phase === "committed") return;
993
- const unknown = saveAttempted && ![400, 401, 403, 404, 409, 422, 429].includes(err?.status);
994
  phase = unknown ? "unconfirmed" : "idle";
995
  showToast(t(unknown ? 'task.operation_unconfirmed' : (saveAttempted ? 'task.submit_failed' : 'task.upload_failed_retry')),
996
  unknown ? "warning" : "error");
@@ -1277,7 +1277,7 @@ async function loadTaskComments(container, taskId, currentUser) {
1277
  if (isCurrent()) state.phase = "idle";
1278
  } catch (err) {
1279
  if (!isCurrent() || state.phase === "committed") return;
1280
- const rejected = [400, 401, 403, 404, 409, 422, 429].includes(err?.status);
1281
  state.phase = rejected ? "idle" : "unconfirmed";
1282
  showToast(t(rejected ? 'post.comment_failed' : 'task.comment_unconfirmed'), rejected ? "error" : "warning");
1283
  } finally {
@@ -1374,7 +1374,7 @@ function showDeleteTaskConfirm(task, contentEl) {
1374
  window.dispatchEvent(new CustomEvent("comfy-route-back"));
1375
  } catch (err) {
1376
  if (!taskModalCurrent(content) || phase === "closed" || phase === "committed") return;
1377
- const rejected = [400, 401, 403, 404, 409, 422, 429].includes(err?.status);
1378
  phase = rejected ? "idle" : "unconfirmed";
1379
  showToast(t(rejected ? 'task.delete_failed' : 'task.delete_unconfirmed'), rejected ? "error" : "warning");
1380
  } finally {
@@ -1396,7 +1396,7 @@ function showDisputeDialog(task, currentUser) {
1396
  content.className = "task-dispute";
1397
  content.innerHTML = `
1398
  <div class="task-dispute__field">
1399
- <label for="dispute-reason" class="task-dispute__label">${t('dispute.reason')} <span aria-hidden="true" class="task-dispute__required">*</span></label>
1400
  <textarea id="dispute-reason" aria-required="true" placeholder="${t('dispute.reason_placeholder')}" class="task-dispute__reason"></textarea>
1401
  </div>
1402
 
 
487
  await onSuccess(content);
488
  } catch (err) {
489
  if (!taskModalCurrent(content) || phase === "closed" || phase === "committed") return;
490
+ const rejected = (err?.requestBodyRejected === true || [400, 401, 403, 404, 409, 422, 429].includes(err?.status));
491
  phase = rejected ? "idle" : "unconfirmed";
492
  showToast(rejected ? errorText : t('task.operation_unconfirmed'), rejected ? "error" : "warning");
493
  } finally {
 
559
  await onSuccess();
560
  } catch (err) {
561
  if (!taskModalCurrent(content) || phase === "closed" || phase === "committed") return;
562
+ const rejected = (err?.requestBodyRejected === true || [400, 401, 403, 404, 409, 422, 429].includes(err?.status));
563
  phase = rejected ? "idle" : "unconfirmed";
564
  showToast(rejected ? errorText : t('task.operation_unconfirmed'), rejected ? "error" : "warning");
565
  } finally {
 
764
  await loadTaskComments(container, taskId, currentUser);
765
  } catch (err) {
766
  if (!taskModalCurrent(content) || phase === "closed" || phase === "committed") return;
767
+ const rejected = (err?.requestBodyRejected === true || [400, 401, 403, 404, 409, 422, 429].includes(err?.status));
768
  phase = rejected ? "idle" : "unconfirmed";
769
  showToast(t(rejected ? 'post.delete_comment_failed' : 'task.comment_unconfirmed'), rejected ? "error" : "warning");
770
  } finally {
 
919
  content.classList.add("task-modal");
920
  content.innerHTML = `
921
  <div class="task-detail__spaced">
922
+ <label for="deliverable-files" class="task-detail__file-label">${t('task.deliverable_images')} <span aria-hidden="true" class="ranking-required-marker"></span></label>
923
  <input type="file" aria-required="true" aria-describedby="deliverable-hint" id="deliverable-files" multiple accept="image/*" class="task-detail__file-input">
924
  <div id="deliverable-hint" class="task-detail__hint">${t('task.support_multiple_images')}</div>
925
  </div>
 
990
  if (container && currentUser) await loadTaskDetail(container, taskId, currentUser);
991
  } catch (err) {
992
  if (!taskModalCurrent(content) || phase === "closed" || phase === "committed") return;
993
+ const unknown = saveAttempted && !(err?.requestBodyRejected === true || [400, 401, 403, 404, 409, 422, 429].includes(err?.status));
994
  phase = unknown ? "unconfirmed" : "idle";
995
  showToast(t(unknown ? 'task.operation_unconfirmed' : (saveAttempted ? 'task.submit_failed' : 'task.upload_failed_retry')),
996
  unknown ? "warning" : "error");
 
1277
  if (isCurrent()) state.phase = "idle";
1278
  } catch (err) {
1279
  if (!isCurrent() || state.phase === "committed") return;
1280
+ const rejected = (err?.requestBodyRejected === true || [400, 401, 403, 404, 409, 422, 429].includes(err?.status));
1281
  state.phase = rejected ? "idle" : "unconfirmed";
1282
  showToast(t(rejected ? 'post.comment_failed' : 'task.comment_unconfirmed'), rejected ? "error" : "warning");
1283
  } finally {
 
1374
  window.dispatchEvent(new CustomEvent("comfy-route-back"));
1375
  } catch (err) {
1376
  if (!taskModalCurrent(content) || phase === "closed" || phase === "committed") return;
1377
+ const rejected = (err?.requestBodyRejected === true || [400, 401, 403, 404, 409, 422, 429].includes(err?.status));
1378
  phase = rejected ? "idle" : "unconfirmed";
1379
  showToast(t(rejected ? 'task.delete_failed' : 'task.delete_unconfirmed'), rejected ? "error" : "warning");
1380
  } finally {
 
1396
  content.className = "task-dispute";
1397
  content.innerHTML = `
1398
  <div class="task-dispute__field">
1399
+ <label for="dispute-reason" class="task-dispute__label">${t('dispute.reason')} <span aria-hidden="true" class="task-dispute__required ranking-required-marker"></span></label>
1400
  <textarea id="dispute-reason" aria-required="true" placeholder="${t('dispute.reason_placeholder')}" class="task-dispute__reason"></textarea>
1401
  </div>
1402
 
前端页面/task/发布任务组件.js CHANGED
@@ -130,7 +130,7 @@ export function createPublishTaskView(currentUser, editTaskData = null) {
130
  <!-- 任务标题 -->
131
  <div>
132
  <label for="task-title" class="task-publish__label">
133
- 📌 ${t('task.task_title')} <span aria-hidden="true" class="task-publish__required">*</span>
134
  </label>
135
  <input type="text" id="task-title" required maxlength="50" placeholder="${t('task.title_placeholder')}" value="${escapeHtml(editTitle)}"
136
  class="task-publish__input">
@@ -139,7 +139,7 @@ export function createPublishTaskView(currentUser, editTaskData = null) {
139
  <!-- 任务描述 -->
140
  <div>
141
  <label for="task-description" class="task-publish__label">
142
- 📄 ${t('task.description')} <span aria-hidden="true" class="task-publish__required">*</span>
143
  </label>
144
  <textarea id="task-description" required aria-describedby="desc-count" rows="6" maxlength="2000" placeholder="${t('task.description_placeholder')}"
145
  class="task-publish__textarea">${escapeHtml(editDescription)}</textarea>
@@ -175,7 +175,7 @@ export function createPublishTaskView(currentUser, editTaskData = null) {
175
  <!-- 总价格 -->
176
  <div>
177
  <label for="total-price" class="task-publish__label">
178
- 💰 ${t('task.total_price')} <span aria-hidden="true" class="task-publish__required">*</span>
179
  </label>
180
  <div class="task-publish__amount-wrap">
181
  <input type="number" id="total-price" required aria-describedby="task-price-hint" min="0" max="${TASK_LIMITS.MAX_PRICE}" step="1" placeholder="100" value="${escapeHtml(editTotalPrice)}" ${isLockedStatus ? 'disabled' : ''}
@@ -188,7 +188,7 @@ export function createPublishTaskView(currentUser, editTaskData = null) {
188
  <!-- 订金比例 -->
189
  <div>
190
  <label for="deposit-ratio" class="task-publish__label">
191
- 📊 ${t('task.deposit_ratio')} <span aria-hidden="true" class="task-publish__required">*</span>
192
  </label>
193
  <select id="deposit-ratio" required aria-describedby="task-deposit-hint" ${isLockedStatus ? 'disabled' : ''} class="task-publish__select">
194
  ${TASK_LIMITS.DEPOSIT_RATIOS.map(ratio => `<option value="${ratio}" ${editDepositRatio == ratio ? 'selected' : ''}>${ratio}%</option>`).join('')}
@@ -210,7 +210,7 @@ export function createPublishTaskView(currentUser, editTaskData = null) {
210
  <!-- 截止日期 -->
211
  <div>
212
  <label for="deadline" class="task-publish__label">
213
- ⏰ ${t('task.deadline')} <span aria-hidden="true" class="task-publish__required">*</span>
214
  </label>
215
  <input type="date" id="deadline" required ${isLockedStatus ? 'aria-describedby="task-deadline-hint"' : ''} min="${minDate}" value="${defaultDate}" ${isLockedStatus ? 'disabled' : ''}
216
  class="task-publish__date">
@@ -511,7 +511,7 @@ export function createPublishTaskView(currentUser, editTaskData = null) {
511
  } catch (err) {
512
  if (!scope.isCurrent() || committed) return;
513
  console.error(isEditMode ? "修改任务失败" : "发布任务失败");
514
- outcomeUnknown = saveAttempted && ![400, 401, 403, 404, 409, 422, 429].includes(err?.status);
515
  showToast(t(outcomeUnknown ? 'publish.save_unconfirmed' : (isEditMode ? 'task.edit_failed' : 'task.publish_failed')),
516
  outcomeUnknown ? 'warning' : 'error');
517
  } finally {
 
130
  <!-- 任务标题 -->
131
  <div>
132
  <label for="task-title" class="task-publish__label">
133
+ 📌 ${t('task.task_title')} <span aria-hidden="true" class="task-publish__required ranking-required-marker"></span>
134
  </label>
135
  <input type="text" id="task-title" required maxlength="50" placeholder="${t('task.title_placeholder')}" value="${escapeHtml(editTitle)}"
136
  class="task-publish__input">
 
139
  <!-- 任务描述 -->
140
  <div>
141
  <label for="task-description" class="task-publish__label">
142
+ 📄 ${t('task.description')} <span aria-hidden="true" class="task-publish__required ranking-required-marker"></span>
143
  </label>
144
  <textarea id="task-description" required aria-describedby="desc-count" rows="6" maxlength="2000" placeholder="${t('task.description_placeholder')}"
145
  class="task-publish__textarea">${escapeHtml(editDescription)}</textarea>
 
175
  <!-- 总价格 -->
176
  <div>
177
  <label for="total-price" class="task-publish__label">
178
+ 💰 ${t('task.total_price')} <span aria-hidden="true" class="task-publish__required ranking-required-marker"></span>
179
  </label>
180
  <div class="task-publish__amount-wrap">
181
  <input type="number" id="total-price" required aria-describedby="task-price-hint" min="0" max="${TASK_LIMITS.MAX_PRICE}" step="1" placeholder="100" value="${escapeHtml(editTotalPrice)}" ${isLockedStatus ? 'disabled' : ''}
 
188
  <!-- 订金比例 -->
189
  <div>
190
  <label for="deposit-ratio" class="task-publish__label">
191
+ 📊 ${t('task.deposit_ratio')} <span aria-hidden="true" class="task-publish__required ranking-required-marker"></span>
192
  </label>
193
  <select id="deposit-ratio" required aria-describedby="task-deposit-hint" ${isLockedStatus ? 'disabled' : ''} class="task-publish__select">
194
  ${TASK_LIMITS.DEPOSIT_RATIOS.map(ratio => `<option value="${ratio}" ${editDepositRatio == ratio ? 'selected' : ''}>${ratio}%</option>`).join('')}
 
210
  <!-- 截止日期 -->
211
  <div>
212
  <label for="deadline" class="task-publish__label">
213
+ ⏰ ${t('task.deadline')} <span aria-hidden="true" class="task-publish__required ranking-required-marker"></span>
214
  </label>
215
  <input type="date" id="deadline" required ${isLockedStatus ? 'aria-describedby="task-deadline-hint"' : ''} min="${minDate}" value="${defaultDate}" ${isLockedStatus ? 'disabled' : ''}
216
  class="task-publish__date">
 
511
  } catch (err) {
512
  if (!scope.isCurrent() || committed) return;
513
  console.error(isEditMode ? "修改任务失败" : "发布任务失败");
514
+ outcomeUnknown = saveAttempted && !(err?.requestBodyRejected === true || [400, 401, 403, 404, 409, 422, 429].includes(err?.status));
515
  showToast(t(outcomeUnknown ? 'publish.save_unconfirmed' : (isEditMode ? 'task.edit_failed' : 'task.publish_failed')),
516
  outcomeUnknown ? 'warning' : 'error');
517
  } finally {