ZHIWEI666 commited on
Commit
752f43e
·
verified ·
1 Parent(s): 97a3b6b

Upload 3 files

Browse files
Files changed (2) hide show
  1. 管理界面/app.js +329 -63
  2. 管理界面/index.html +94 -94
管理界面/app.js CHANGED
@@ -9,6 +9,96 @@
9
  // 新增反馈集中在双语词典;管理页语言遵循文档lang。
10
  const ADMIN_MESSAGES = {
11
  zh: {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  dispute_subtitle: '查看任务纠纷申诉,审核双方证据并做出裁决。',
13
  dispute_list_loading: '正在加载申诉列表…',
14
  dispute_empty_title: '暂无申诉',
@@ -82,6 +172,96 @@
82
  banner_active: '广告展示中', banner_inactive: '无活跃广告', banner_unknown: '状态未知',
83
  },
84
  en: {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  dispute_subtitle: 'Review task disputes, consider both parties’ evidence and issue a resolution.',
86
  dispute_list_loading: 'Loading disputes…',
87
  dispute_empty_title: 'No disputes',
@@ -775,7 +955,7 @@
775
  const account = $('#login-account').value.trim();
776
  const password = $('#login-password').value;
777
  if (!account || !password) {
778
- showError(errBox, '请输入账号和密码');
779
  return;
780
  }
781
 
@@ -797,7 +977,7 @@
797
  assertAdminSession(loginSession);
798
  syncUserChip();
799
  showView('view-main');
800
- toast('登录成功', 'success');
801
  } catch (err) {
802
  if (isStaleSessionError(err) || !loginSession.isCurrent()) return;
803
  if (err instanceof ApiError && err.status === 401) return; // 网络层已清会话并回登录。
@@ -811,7 +991,7 @@
811
  }
812
  } catch (err) {
813
  if (isStaleSessionError(err) || !loginSession.isCurrent()) return;
814
- showError(errBox, err.message || '登录失败');
815
  } finally {
816
  finishBusy();
817
  }
@@ -1199,12 +1379,12 @@
1199
 
1200
  // -------------------- 通用确认对话框 --------------------
1201
  function confirmAction({ title, message, okText, danger, onConfirm }) {
1202
- $('#modal-confirm-title').textContent = title || '请确认';
1203
- $('#modal-confirm-msg').textContent = message || '确认执行此操作?';
1204
  const okBtn = $('#btn-confirm-ok');
1205
  const previous = adminBusyOperations.get(okBtn);
1206
  if (previous) previous();
1207
- $('.btn-label', okBtn).textContent = okText || '确定';
1208
  okBtn.classList.toggle('btn-danger', !!danger);
1209
  okBtn.classList.toggle('btn-primary', !danger);
1210
  state.confirmHandler = onConfirm;
@@ -1228,7 +1408,7 @@
1228
  closeModal('modal-confirm');
1229
  } catch (err) {
1230
  if (isStaleSessionError(err) || !finishBusy.isCurrent() || state.confirmHandler !== handler) return;
1231
- toast(err.message || '操作失败', 'error');
1232
  } finally {
1233
  if (finishBusy() && state.confirmHandler === handler) state.confirmHandler = null;
1234
  }
@@ -1385,11 +1565,12 @@
1385
  $('#withdraw-empty').hidden = stateName !== 'empty';
1386
  $('#withdraw-error').hidden = stateName !== 'error';
1387
  $('#withdraw-list').hidden = stateName !== 'data';
 
1388
  if (stateName === 'error' && message) $('#withdraw-error-msg').textContent = message;
1389
  if (stateName === 'empty') {
1390
  $('#withdraw-empty-msg').textContent = state.withdrawStatus === 'pending'
1391
- ? '当前没有待打款的提现申请。'
1392
- : '当前没有已完成的提现记录。';
1393
  }
1394
  }
1395
 
@@ -1403,7 +1584,7 @@
1403
  } catch (err) {
1404
  if (isStaleSessionError(err) || !read.isCurrent()) return;
1405
  if (err instanceof ApiError && err.status === 401) return; // 已自动跳回登录
1406
- setWithdrawState('error', err.message || '加载失败');
1407
  } finally {
1408
  read.release();
1409
  }
@@ -1427,51 +1608,60 @@
1427
  card.innerHTML = `
1428
  <div class="row-card-head">
1429
  <div class="row-card-title">
1430
- <span class="row-card-name">👤 ${escapeHtml(w.account || '未知用户')}</span>
1431
- <span class="badge ${isPending ? 'is-off' : 'is-on'}">${isPending ? '待打款' : '已完成'}</span>
1432
  </div>
1433
- <div class="row-card-amount">${formatNumber(amount)} 积分 <em>≈ ¥${formatNumber(amount)}</em></div>
1434
  </div>
1435
  <dl class="row-card-meta">
1436
- <div><dt>支付宝账号</dt><dd>${escapeHtml(w.alipay_account || '-')}</dd></div>
1437
- <div><dt>真实姓名</dt><dd>${escapeHtml(w.real_name || '-')}</dd></div>
1438
- <div><dt>申请时间</dt><dd>${escapeHtml(formatDate(w.created_at))}</dd></div>
1439
- <div><dt>交易 ID</dt><dd>#${escapeHtml(w.tx_id)}</dd></div>
1440
  </dl>
1441
  ${isPending ? `
1442
  <div class="row-card-action">
1443
- <input type="text" class="row-input js-order-input" placeholder="输入打款订单号(1-64 字符)" maxlength="64">
1444
  <button type="button" class="btn btn-primary btn-sm js-confirm-pay">
1445
- <span class="btn-label">确认打款</span>
1446
  <span class="btn-spinner" aria-hidden="true"></span>
1447
  </button>
1448
  </div>` : `
1449
- <div class="row-card-done">✅ 已完成打款 · 订单号:${escapeHtml(w.payment_order_id || '-')}</div>`}
1450
  `;
1451
  if (isPending) {
1452
  const btn = $('.js-confirm-pay', card);
1453
- btn.addEventListener('click', () => confirmWithdrawPayment(w, card, btn));
 
 
 
 
1454
  }
1455
  return card;
1456
  }
1457
 
1458
- function confirmWithdrawPayment(w, card, btn) {
 
1459
  const input = $('.js-order-input', card);
1460
  const orderId = input.value.trim();
1461
- if (!orderId) { toast('请先输入打款订单号', 'warning'); input.focus(); return; }
 
 
1462
  confirmAction({
1463
- title: '确认打款',
1464
- message: `确认已向 ${w.account || '该用户'} 打款 ${Math.abs(w.amount || 0)} 积分对应的金额?\n订单号:${orderId}`,
1465
- okText: '确认打款',
1466
  danger: false,
1467
  onConfirm: async (assertCurrent) => {
 
1468
  const finishBusy = startAdminBusy(btn);
1469
  if (!finishBusy) return;
1470
  try {
1471
  const response = await apiRequest('POST', '/api/admin/withdrawals/' + encodeURIComponent(w.tx_id) + '/complete', { payment_order_id: orderId });
1472
  assertCurrent();
 
1473
  confirmAdminStatusWrite(response);
1474
- toast('打款已确认', 'success');
1475
  await refreshWithdrawals();
1476
  } finally {
1477
  finishBusy();
@@ -1481,11 +1671,19 @@
1481
  }
1482
 
1483
  function bindWithdrawals() {
 
 
 
 
 
1484
  $('#btn-withdraw-refresh').addEventListener('click', () => refreshWithdrawals());
1485
  $$('#withdraw-status-tabs .filter-tab').forEach((tab) => {
1486
  tab.addEventListener('click', () => {
1487
  state.withdrawStatus = tab.dataset.status;
1488
- $$('#withdraw-status-tabs .filter-tab').forEach((t) => t.classList.toggle('is-active', t === tab));
 
 
 
1489
  refreshWithdrawals();
1490
  });
1491
  });
@@ -1758,13 +1956,16 @@
1758
  }
1759
 
1760
  // -------------------- 🗂️ 提示词分类 --------------------
1761
- const PCAT_TYPE_LABELS = { image: '图像提示词', video: '视频提示词', music: '音乐提示词' };
 
1762
 
1763
  function setPcatState(stateName, message) {
 
1764
  $('#pcat-loading').hidden = stateName !== 'loading';
1765
  $('#pcat-empty').hidden = stateName !== 'empty';
1766
  $('#pcat-error').hidden = stateName !== 'error';
1767
  $('#pcat-list').hidden = stateName !== 'data';
 
1768
  if (stateName === 'error' && message) $('#pcat-error-msg').textContent = message;
1769
  }
1770
 
@@ -1779,20 +1980,23 @@
1779
  } catch (err) {
1780
  if (isStaleSessionError(err) || !read.isCurrent()) return;
1781
  if (err instanceof ApiError && err.status === 401) return; // 已自动跳回登录
1782
- setPcatState('error', err.message || '加载失败');
1783
  } finally {
1784
  read.release();
1785
  }
1786
  }
1787
 
1788
  function renderPromptCats(list) {
 
1789
  const badge = $('#pcat-count-badge');
1790
- if (badge) badge.textContent = `共 ${list.length} 个`;
1791
  if (!list.length) { setPcatState('empty'); return; }
1792
  setPcatState('data');
1793
  const wrap = $('#pcat-list');
1794
  wrap.innerHTML = '';
1795
  const frag = document.createDocumentFragment();
 
 
1796
  list.forEach((cat) => {
1797
  const row = document.createElement('div');
1798
  row.className = 'pcat-row';
@@ -1802,8 +2006,12 @@
1802
  const del = document.createElement('button');
1803
  del.type = 'button';
1804
  del.className = 'btn btn-ghost btn-sm btn-danger-text';
1805
- del.textContent = '删除';
1806
- del.addEventListener('click', () => confirmDeletePromptCat(cat));
 
 
 
 
1807
  row.appendChild(name);
1808
  row.appendChild(del);
1809
  frag.appendChild(row);
@@ -1811,18 +2019,21 @@
1811
  wrap.appendChild(frag);
1812
  }
1813
 
1814
- function confirmDeletePromptCat(cat) {
1815
- const promptType = state.pcatType;
 
1816
  confirmAction({
1817
- title: '删除分类',
1818
- message: `确定删除${PCAT_TYPE_LABELS[promptType] || ''}分类「${cat}」?\n已使用该分类的提示词不受影响。`,
1819
- okText: '确认删除',
1820
  danger: true,
1821
  onConfirm: async (assertCurrent) => {
 
1822
  const response = await apiRequest('DELETE', '/api/prompt-categories?prompt_type=' + promptType + '&name=' + encodeURIComponent(cat));
1823
  assertCurrent();
 
1824
  confirmAdminStatusWrite(response);
1825
- toast('分类已删除', 'success');
1826
  await refreshPromptCats();
1827
  }
1828
  });
@@ -1830,38 +2041,56 @@
1830
 
1831
  function bindPromptCats() {
1832
  let categoryRevision = 0;
 
 
 
 
 
 
1833
  $('#btn-pcat-refresh').addEventListener('click', () => refreshPromptCats());
1834
  $$('#pcat-type-tabs .filter-tab').forEach((tab) => {
1835
  tab.addEventListener('click', () => {
1836
  categoryRevision++;
1837
  state.pcatType = tab.dataset.type;
1838
- $$('#pcat-type-tabs .filter-tab').forEach((t) => t.classList.toggle('is-active', t === tab));
 
 
 
1839
  refreshPromptCats();
1840
  });
1841
  });
1842
  const addBtn = $('#btn-pcat-add');
1843
  const nameInput = $('#pcat-name');
1844
- nameInput.addEventListener('input', () => { categoryRevision++; });
1845
- nameInput.addEventListener('change', () => { categoryRevision++; });
 
 
 
 
 
 
1846
  const submitAdd = async () => {
1847
  const rawName = nameInput.value;
1848
  const name = rawName.trim();
1849
  const promptType = state.pcatType;
1850
  const revision = categoryRevision;
1851
- if (!name) { toast('请输入分类名称', 'warning'); nameInput.focus(); return; }
1852
- if (name.length > 8) { toast('分类名称不能超过 8 个字', 'warning'); return; }
1853
  const finishBusy = startAdminBusy(addBtn);
1854
  if (!finishBusy) return;
1855
  try {
1856
  const response = await apiRequest('POST', '/api/prompt-categories', { prompt_type: promptType, name });
1857
  finishBusy.assertCurrent();
1858
  confirmAdminStatusWrite(response);
1859
- if (categoryRevision === revision && state.pcatType === promptType && nameInput.value === rawName) nameInput.value = '';
1860
- toast('分类已添加', 'success');
 
 
 
1861
  await refreshPromptCats();
1862
  } catch (err) {
1863
  if (isStaleSessionError(err) || !finishBusy.isCurrent()) return;
1864
- toast(err.message || '添加分类失败', 'error');
1865
  } finally {
1866
  finishBusy();
1867
  }
@@ -1901,7 +2130,16 @@
1901
  }
1902
 
1903
  // -------------------- 🏷️ 版本管理 --------------------
1904
- const STAGE_LABELS = { alpha: 'Alpha(内测)', beta: 'Beta(公测)', rc: 'RC(候选发布)', stable: 'Stable(稳定版)' };
 
 
 
 
 
 
 
 
 
1905
 
1906
  function updateVerPreview() {
1907
  const form = $('#form-version');
@@ -1910,7 +2148,9 @@
1910
  const major = form.querySelector('[name="major"]').value;
1911
  const minor = form.querySelector('[name="minor"]').value;
1912
  const patch = form.querySelector('[name="patch"]').value;
1913
- $('#ver-preview').textContent = `版本预览:V${major}.${minor}.${patch} · ${STAGE_LABELS[stage] || stage}`;
 
 
1914
  }
1915
 
1916
  let versionConfigGeneration = 0;
@@ -1986,10 +2226,12 @@
1986
  const response = await apiRequest('PUT', '/api/admin/config/project_version', payload);
1987
  finishBusy.assertCurrent();
1988
  confirmAdminStatusWrite(response);
1989
- toast(`版本已更新为 V${payload.major}.${payload.minor}.${payload.patch}(${STAGE_LABELS[payload.stage] || payload.stage})`, 'success');
 
 
1990
  } catch (err) {
1991
  if (isStaleSessionError(err) || !finishBusy.isCurrent()) return;
1992
- showError($('#form-version-error'), err.message || '版本更新失败');
1993
  } finally {
1994
  versionSavePending = false;
1995
  finishBusy();
@@ -2067,6 +2309,15 @@
2067
  }
2068
 
2069
  function bindBroadcast() {
 
 
 
 
 
 
 
 
 
2070
  const bannerSave = $('#btn-banner-save');
2071
  const bannerUploads = new Set();
2072
  let bannerSaveBeforeUpload = null;
@@ -2105,18 +2356,18 @@
2105
  const rawContent = announcement.value;
2106
  const content = rawContent.trim();
2107
  const revision = announcementRevision;
2108
- if (!content) { toast('请输入公告内容', 'warning'); return; }
2109
  confirmAction({
2110
- title: '发布全站公告',
2111
- message: `公告将推送给全体用户,确认发布?\n\n${content.slice(0, 80)}${content.length > 80 ? '…' : ''}`,
2112
- okText: '确认发布',
2113
  danger: true,
2114
  onConfirm: async (assertCurrent) => {
2115
  const response = await apiRequest('POST', '/api/system/announcement', { admin_account: adminAccount(), content });
2116
  assertCurrent();
2117
  confirmAdminStatusWrite(response);
2118
  if (announcementRevision === revision && announcement.value === rawContent) announcement.value = '';
2119
- toast('公告已发布', 'success');
2120
  }
2121
  });
2122
  });
@@ -2134,7 +2385,7 @@
2134
  const draft = readBannerDraft();
2135
  const revision = bannerRevision;
2136
  if (draft.enabled && !draft.bannerImage) {
2137
- toast('启用广告时必须上传横幅图片', 'warning');
2138
  return;
2139
  }
2140
  const config = { ...draft, title: draft.title.trim(), description: draft.description.trim(),
@@ -2146,7 +2397,7 @@
2146
  const response = await apiRequest('PUT', '/api/admin/config/banner_config', config);
2147
  finishBusy.assertCurrent();
2148
  confirmAdminStatusWrite(response);
2149
- toast('广告配置已保存', 'success');
2150
  const current = readBannerDraft();
2151
  if (bannerRevision === revision && Object.keys(draft).every(key => current[key] === draft[key])) {
2152
  $('#banner-enabled').checked = false;
@@ -2164,7 +2415,7 @@
2164
  refreshBannerStatus();
2165
  } catch (err) {
2166
  if (isStaleSessionError(err) || !finishBusy.isCurrent()) return;
2167
- toast(err.message || '保存失败', 'error');
2168
  } finally {
2169
  finishBusy();
2170
  }
@@ -2214,14 +2465,14 @@
2214
  function bindMain() {
2215
  $('#btn-logout').addEventListener('click', () => {
2216
  confirmAction({
2217
- title: '退出登录',
2218
- message: '��定要退出当前登录吗?',
2219
- okText: '退出',
2220
  danger: false,
2221
  onConfirm: async () => {
2222
  clearSession();
2223
  showView('view-login');
2224
- toast('已退出登录', 'success');
2225
  },
2226
  });
2227
  });
@@ -2269,8 +2520,23 @@
2269
  });
2270
  }
2271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2272
  // -------------------- 启动 --------------------
2273
  function bootstrap() {
 
 
2274
  bindLogin();
2275
  bindMain();
2276
  bindCreate();
 
9
  // 新增反馈集中在双语词典;管理页语言遵循文档lang。
10
  const ADMIN_MESSAGES = {
11
  zh: {
12
+ broadcast_subtitle: '发布全站系统公告,管理顶部广告横幅展示。',
13
+ announcement_title: '📢 全站系统公告', announcement_content: '公告内容',
14
+ announcement_placeholder: '输入要向全体用户推送的公告内容…', announcement_publish: '🚀 确认发布',
15
+ announcement_required: '请输入公告内容', announcement_confirm_title: '发布全站公告',
16
+ announcement_confirm_message: '公告将推送给全体用户,确认发布?\n\n{preview}',
17
+ announcement_confirm: '确认发布', announcement_published: '公告已发布',
18
+ banner_manage_title: '📰 广告横幅管理', banner_checking: '检查中…', banner_enabled: '启用广告展示',
19
+ banner_image_label: '横幅图片(支持 GIF)', banner_image_preview: '横幅预览', banner_select_image: '🖼️ 选择横幅图片',
20
+ banner_detail_image_label: '详情页大图', banner_detail_preview: '详情图预览', banner_select_detail: '🖼️ 选择详情大图',
21
+ banner_title_label: '广告标题', banner_title_placeholder: '广告主标题(可选)',
22
+ banner_description_label: '广告描述', banner_description_placeholder: '一句话描述(可选)',
23
+ banner_detail_content_label: '详情内容', banner_detail_content_placeholder: '点击广告后展示的详细内容(可选)',
24
+ banner_save: '💾 保存广告配置', banner_image_required: '启用广告时必须上传横幅图片',
25
+ banner_saved: '广告配置已保存', banner_save_failed: '保存失败', confirm_failed: '操作失败',
26
+ pcat_subtitle: '管理提示词市场图像/视频/音乐三个子界面的可选分类。',
27
+ pcat_refresh: '刷新', pcat_type_filter: '提示词类型筛选',
28
+ pcat_image: '图像提示词', pcat_video: '视频提示词', pcat_music: '音乐提示词',
29
+ pcat_image_tab: '🎨 图像提示词', pcat_video_tab: '🎬 视频提示词', pcat_music_tab: '🎵 音乐提示词',
30
+ pcat_add_title: '➕ 添加分类', admin_privilege: '管理员特权',
31
+ pcat_name: '分类名称', pcat_name_placeholder: '输入分类名称(8 字以内)…', pcat_add: '添加',
32
+ pcat_current_title: '📋 当前分类', pcat_loading: '正在加载分类列表…',
33
+ pcat_empty_title: '暂无分类', pcat_empty_description: '在上方输入框中添加第一个分类。',
34
+ pcat_load_failed: '加载分类失败', pcat_count: '共 {count} 个',
35
+ pcat_delete: '删除', pcat_delete_title: '删除分类', pcat_delete_confirm: '确认删除',
36
+ pcat_delete_message: '确定删除{type}分类「{name}」?\n已使用该分类的提示词不受影响。',
37
+ pcat_deleted: '分类已删除', pcat_name_required: '请输入分类名称',
38
+ pcat_name_too_long: '分类名称不能超过 8 个字', pcat_added: '分类已添加', pcat_add_failed: '添加分类失败',
39
+ pcat_delete_name: '删除分类 {name}',
40
+ version_subtitle: '设置全站展示的项目版本号与开发阶段。',
41
+ version_stage: '项目阶段', version_number: '版本号',
42
+ version_major: '主版本号', version_minor: '次版本号', version_patch: '修订号',
43
+ version_alpha: 'Alpha(内测)', version_beta: 'Beta(公测)',
44
+ version_rc: 'RC(候选发布)', version_stable: 'Stable(稳定版)',
45
+ version_initial: '当前版本:—', version_apply: '应用版本',
46
+ version_preview: '版本预览:V{version} · {stage}',
47
+ version_saved: '版本已更新为 V{version}({stage})', version_failed: '版本更新失败',
48
+ withdraw_subtitle: "审核用户提现申请,线下打款后回填支付订单号完成结算。",
49
+ withdraw_loading: "正在加载提现列表…",
50
+ withdraw_empty_title: "暂无提现记录",
51
+ withdraw_empty_pending: "当前没有待打款的提现申请。",
52
+ withdraw_empty_completed: "当前没有已完成的提现记录。",
53
+ withdraw_load_failed: "加载提现列表失败",
54
+ withdraw_filter: "提现状态筛选",
55
+ withdraw_pending: "待打款",
56
+ withdraw_completed: "已完成",
57
+ withdraw_pending_filter: "⏳ 待打款",
58
+ withdraw_completed_filter: "✅ 已完成",
59
+ withdraw_refresh: "刷新",
60
+ withdraw_unknown_user: "未知用户",
61
+ withdraw_points: "积分",
62
+ withdraw_alipay: "支付宝账号",
63
+ withdraw_real_name: "真实姓名",
64
+ withdraw_requested_at: "申请时间",
65
+ withdraw_transaction_id: "交易 ID",
66
+ withdraw_order_input: "打款订单号",
67
+ withdraw_order_placeholder: "输入打款订单号(1-64 字符)",
68
+ withdraw_confirm_payment: "确认打款",
69
+ withdraw_completed_order: "✅ 已完成打款 · 订单号:",
70
+ withdraw_order_required: "请先输入打款订单号",
71
+ withdraw_confirm_message: "确认已向 {account} 打款 {amount} 积分对应的金额?\n订单号:{order}",
72
+ withdraw_confirmed: "打款已确认",
73
+ session_page_title: "RanKing · 插件管理控制台",
74
+ login_headline: "第三方插件",
75
+ login_headline_emphasis: "统一管理控制台",
76
+ login_intro: "使用 RanKing 管理员账号登录,集中管理所有第三方插件的访问凭证、权限与配额。",
77
+ login_feature_keys: "Plugin Key 动态轮换",
78
+ login_feature_access: "限额与权限精细化控制",
79
+ login_feature_interface: "双主题 · 响应式 · 原生实现",
80
+ login_title: "管理员登录",
81
+ login_subtitle: "请使用具备管理员权限的 RanKing 账号",
82
+ login_account: "账号",
83
+ login_password: "密码",
84
+ login_account_placeholder: "用户名 / 邮箱 / 手机号",
85
+ login_password_placeholder: "请输入密码",
86
+ login_submit: "登 录",
87
+ login_session_notice: "仅管理员可访问 · Token 仅保存于当前会话",
88
+ login_required: "请输入账号和密码",
89
+ login_success: "登录成功",
90
+ login_failed: "登录失败",
91
+ logout_title: "退出登录",
92
+ logout_message: "确定要退出当前登录吗?",
93
+ logout_confirm: "退出",
94
+ logout_success: "已退出登录",
95
+ forbidden_title: "您不是管理员,无权访问",
96
+ forbidden_description: "当前登录账号没有插件管理权限。请使用具备管理员权限的账号重新登录。",
97
+ forbidden_return: "返回登录",
98
+ confirm_title: "请确认",
99
+ confirm_message: "确认执行此操作?",
100
+ confirm_ok: "确定",
101
+ confirm_cancel: "取消",
102
  dispute_subtitle: '查看任务纠纷申诉,审核双方证据并做出裁决。',
103
  dispute_list_loading: '正在加载申诉列表…',
104
  dispute_empty_title: '暂无申诉',
 
172
  banner_active: '广告展示中', banner_inactive: '无活跃广告', banner_unknown: '状态未知',
173
  },
174
  en: {
175
+ broadcast_subtitle: 'Publish announcements for all users and manage the top advertisement banner.',
176
+ announcement_title: '📢 Site announcements', announcement_content: 'Announcement content',
177
+ announcement_placeholder: 'Enter an announcement for all users…', announcement_publish: '🚀 Publish announcement',
178
+ announcement_required: 'Enter announcement content', announcement_confirm_title: 'Publish site announcement',
179
+ announcement_confirm_message: 'Publish this announcement to all users?\n\n{preview}',
180
+ announcement_confirm: 'Publish', announcement_published: 'Announcement published',
181
+ banner_manage_title: '📰 Advertisement banner', banner_checking: 'Checking…', banner_enabled: 'Enable advertisement',
182
+ banner_image_label: 'Banner image (GIF supported)', banner_image_preview: 'Banner preview', banner_select_image: '🖼️ Choose banner image',
183
+ banner_detail_image_label: 'Detail image', banner_detail_preview: 'Detail image preview', banner_select_detail: '🖼️ Choose detail image',
184
+ banner_title_label: 'Advertisement title', banner_title_placeholder: 'Title (optional)',
185
+ banner_description_label: 'Advertisement description', banner_description_placeholder: 'Short description (optional)',
186
+ banner_detail_content_label: 'Detail content', banner_detail_content_placeholder: 'Content shown after opening the advertisement (optional)',
187
+ banner_save: '💾 Save advertisement', banner_image_required: 'Upload a banner image before enabling the advertisement',
188
+ banner_saved: 'Advertisement settings saved', banner_save_failed: 'Could not save settings', confirm_failed: 'Operation failed',
189
+ pcat_subtitle: 'Manage the available categories for image, video and music prompts.',
190
+ pcat_refresh: 'Refresh', pcat_type_filter: 'Filter by prompt type',
191
+ pcat_image: 'Image prompts', pcat_video: 'Video prompts', pcat_music: 'Music prompts',
192
+ pcat_image_tab: '🎨 Image prompts', pcat_video_tab: '🎬 Video prompts', pcat_music_tab: '🎵 Music prompts',
193
+ pcat_add_title: '➕ Add category', admin_privilege: 'Administrator access',
194
+ pcat_name: 'Category name', pcat_name_placeholder: 'Up to 8 characters…', pcat_add: 'Add',
195
+ pcat_current_title: '📋 Current categories', pcat_loading: 'Loading categories…',
196
+ pcat_empty_title: 'No categories', pcat_empty_description: 'Use the field above to add the first category.',
197
+ pcat_load_failed: 'Could not load categories', pcat_count: '{count} categories',
198
+ pcat_delete: 'Delete', pcat_delete_title: 'Delete category', pcat_delete_confirm: 'Confirm deletion',
199
+ pcat_delete_message: 'Delete the category “{name}” from {type}?\nPrompts already using this category are unaffected.',
200
+ pcat_deleted: 'Category deleted', pcat_name_required: 'Enter a category name',
201
+ pcat_name_too_long: 'Category names cannot exceed 8 characters', pcat_added: 'Category added', pcat_add_failed: 'Could not add category',
202
+ pcat_delete_name: 'Delete category {name}',
203
+ version_subtitle: 'Set the project version and development stage displayed across the site.',
204
+ version_stage: 'Development stage', version_number: 'Version number',
205
+ version_major: 'Major version', version_minor: 'Minor version', version_patch: 'Patch version',
206
+ version_alpha: 'Alpha (internal testing)', version_beta: 'Beta (public testing)',
207
+ version_rc: 'RC (release candidate)', version_stable: 'Stable (stable release)',
208
+ version_initial: 'Current version: —', version_apply: 'Apply version',
209
+ version_preview: 'Version preview: V{version} · {stage}',
210
+ version_saved: 'Version updated to V{version} ({stage})', version_failed: 'Version update failed',
211
+ withdraw_subtitle: "Review withdrawal requests. After making the offline payment, enter its order ID to complete settlement.",
212
+ withdraw_loading: "Loading withdrawals…",
213
+ withdraw_empty_title: "No withdrawals",
214
+ withdraw_empty_pending: "There are no withdrawal requests awaiting payment.",
215
+ withdraw_empty_completed: "There are no completed withdrawals.",
216
+ withdraw_load_failed: "Could not load withdrawals",
217
+ withdraw_filter: "Filter withdrawals by status",
218
+ withdraw_pending: "Awaiting payment",
219
+ withdraw_completed: "Completed",
220
+ withdraw_pending_filter: "⏳ Awaiting payment",
221
+ withdraw_completed_filter: "✅ Completed",
222
+ withdraw_refresh: "Refresh",
223
+ withdraw_unknown_user: "Unknown user",
224
+ withdraw_points: "points",
225
+ withdraw_alipay: "Alipay account",
226
+ withdraw_real_name: "Legal name",
227
+ withdraw_requested_at: "Requested at",
228
+ withdraw_transaction_id: "Transaction ID",
229
+ withdraw_order_input: "Payment order ID",
230
+ withdraw_order_placeholder: "Enter the payment order ID (1–64 characters)",
231
+ withdraw_confirm_payment: "Confirm payment",
232
+ withdraw_completed_order: "✅ Payment completed · Order ID: ",
233
+ withdraw_order_required: "Enter the payment order ID first.",
234
+ withdraw_confirm_message: "Confirm that the amount corresponding to {amount} points has been paid to {account}?\nPayment order ID: {order}",
235
+ withdraw_confirmed: "Payment confirmed",
236
+ session_page_title: "RanKing · Plugin Admin Console",
237
+ login_headline: "Third-party plugins",
238
+ login_headline_emphasis: "Unified administration",
239
+ login_intro: "Sign in with a RanKing administrator account to manage plugin credentials, permissions and quotas.",
240
+ login_feature_keys: "Dynamic Plugin Key rotation",
241
+ login_feature_access: "Control quotas and permissions",
242
+ login_feature_interface: "Dual themes · Responsive · Native interface",
243
+ login_title: "Administrator sign-in",
244
+ login_subtitle: "Use a RanKing account with administrator access.",
245
+ login_account: "Account",
246
+ login_password: "Password",
247
+ login_account_placeholder: "Username / Email / Phone number",
248
+ login_password_placeholder: "Enter your password",
249
+ login_submit: "Sign in",
250
+ login_session_notice: "Administrators only · Token stored for this session only",
251
+ login_required: "Enter your account and password.",
252
+ login_success: "Signed in successfully",
253
+ login_failed: "Sign-in failed",
254
+ logout_title: "Sign out",
255
+ logout_message: "Sign out of the current session?",
256
+ logout_confirm: "Sign out",
257
+ logout_success: "Signed out",
258
+ forbidden_title: "Administrator access required",
259
+ forbidden_description: "This account does not have plugin administration access. Sign in with an administrator account.",
260
+ forbidden_return: "Back to sign-in",
261
+ confirm_title: "Please confirm",
262
+ confirm_message: "Perform this action?",
263
+ confirm_ok: "Confirm",
264
+ confirm_cancel: "Cancel",
265
  dispute_subtitle: 'Review task disputes, consider both parties’ evidence and issue a resolution.',
266
  dispute_list_loading: 'Loading disputes…',
267
  dispute_empty_title: 'No disputes',
 
955
  const account = $('#login-account').value.trim();
956
  const password = $('#login-password').value;
957
  if (!account || !password) {
958
+ showError(errBox, adminMessage('login_required'));
959
  return;
960
  }
961
 
 
977
  assertAdminSession(loginSession);
978
  syncUserChip();
979
  showView('view-main');
980
+ toast(adminMessage('login_success'), 'success');
981
  } catch (err) {
982
  if (isStaleSessionError(err) || !loginSession.isCurrent()) return;
983
  if (err instanceof ApiError && err.status === 401) return; // 网络层已清会话并回登录。
 
991
  }
992
  } catch (err) {
993
  if (isStaleSessionError(err) || !loginSession.isCurrent()) return;
994
+ showError(errBox, err.message || adminMessage('login_failed'));
995
  } finally {
996
  finishBusy();
997
  }
 
1379
 
1380
  // -------------------- 通用确认对话框 --------------------
1381
  function confirmAction({ title, message, okText, danger, onConfirm }) {
1382
+ $('#modal-confirm-title').textContent = title || adminMessage('confirm_title');
1383
+ $('#modal-confirm-msg').textContent = message || adminMessage('confirm_message');
1384
  const okBtn = $('#btn-confirm-ok');
1385
  const previous = adminBusyOperations.get(okBtn);
1386
  if (previous) previous();
1387
+ $('.btn-label', okBtn).textContent = okText || adminMessage('confirm_ok');
1388
  okBtn.classList.toggle('btn-danger', !!danger);
1389
  okBtn.classList.toggle('btn-primary', !danger);
1390
  state.confirmHandler = onConfirm;
 
1408
  closeModal('modal-confirm');
1409
  } catch (err) {
1410
  if (isStaleSessionError(err) || !finishBusy.isCurrent() || state.confirmHandler !== handler) return;
1411
+ toast(err.message || adminMessage('confirm_failed'), 'error');
1412
  } finally {
1413
  if (finishBusy() && state.confirmHandler === handler) state.confirmHandler = null;
1414
  }
 
1565
  $('#withdraw-empty').hidden = stateName !== 'empty';
1566
  $('#withdraw-error').hidden = stateName !== 'error';
1567
  $('#withdraw-list').hidden = stateName !== 'data';
1568
+ $('#withdraw-list').setAttribute('aria-busy', String(stateName === 'loading'));
1569
  if (stateName === 'error' && message) $('#withdraw-error-msg').textContent = message;
1570
  if (stateName === 'empty') {
1571
  $('#withdraw-empty-msg').textContent = state.withdrawStatus === 'pending'
1572
+ ? adminMessage('withdraw_empty_pending')
1573
+ : adminMessage('withdraw_empty_completed');
1574
  }
1575
  }
1576
 
 
1584
  } catch (err) {
1585
  if (isStaleSessionError(err) || !read.isCurrent()) return;
1586
  if (err instanceof ApiError && err.status === 401) return; // 已自动跳回登录
1587
+ setWithdrawState('error', err.message || adminMessage('withdraw_load_failed'));
1588
  } finally {
1589
  read.release();
1590
  }
 
1608
  card.innerHTML = `
1609
  <div class="row-card-head">
1610
  <div class="row-card-title">
1611
+ <span class="row-card-name">👤 ${escapeHtml(w.account || adminMessage('withdraw_unknown_user'))}</span>
1612
+ <span class="badge ${isPending ? 'is-off' : 'is-on'}">${adminMessage(isPending ? 'withdraw_pending' : 'withdraw_completed')}</span>
1613
  </div>
1614
+ <div class="row-card-amount">${formatNumber(amount)} ${adminMessage('withdraw_points')} <em>≈ ¥${formatNumber(amount)}</em></div>
1615
  </div>
1616
  <dl class="row-card-meta">
1617
+ <div><dt>${adminMessage('withdraw_alipay')}</dt><dd>${escapeHtml(w.alipay_account || '-')}</dd></div>
1618
+ <div><dt>${adminMessage('withdraw_real_name')}</dt><dd>${escapeHtml(w.real_name || '-')}</dd></div>
1619
+ <div><dt>${adminMessage('withdraw_requested_at')}</dt><dd>${escapeHtml(formatDate(w.created_at))}</dd></div>
1620
+ <div><dt>${adminMessage('withdraw_transaction_id')}</dt><dd>#${escapeHtml(w.tx_id)}</dd></div>
1621
  </dl>
1622
  ${isPending ? `
1623
  <div class="row-card-action">
1624
+ <input type="text" class="row-input js-order-input" aria-label="${escapeHtml(adminMessage('withdraw_order_input'))}" placeholder="${escapeHtml(adminMessage('withdraw_order_placeholder'))}" maxlength="64">
1625
  <button type="button" class="btn btn-primary btn-sm js-confirm-pay">
1626
+ <span class="btn-label">${adminMessage('withdraw_confirm_payment')}</span>
1627
  <span class="btn-spinner" aria-hidden="true"></span>
1628
  </button>
1629
  </div>` : `
1630
+ <div class="row-card-done">${adminMessage('withdraw_completed_order')}${escapeHtml(w.payment_order_id || '-')}</div>`}
1631
  `;
1632
  if (isPending) {
1633
  const btn = $('.js-confirm-pay', card);
1634
+ const session = captureAdminSession();
1635
+ const isCurrent = () => card.isConnected && session.isCurrent();
1636
+ btn.addEventListener('click', () => {
1637
+ if (isCurrent()) confirmWithdrawPayment(w, card, btn, isCurrent);
1638
+ });
1639
  }
1640
  return card;
1641
  }
1642
 
1643
+ function confirmWithdrawPayment(w, card, btn, parentCurrent = () => true) {
1644
+ if (!parentCurrent()) return;
1645
  const input = $('.js-order-input', card);
1646
  const orderId = input.value.trim();
1647
+ if (!orderId) { toast(adminMessage('withdraw_order_required'), 'warning'); input.focus(); return; }
1648
+ const values = { account: w.account || adminMessage('withdraw_unknown_user'), amount: Math.abs(w.amount || 0), order: orderId };
1649
+ const message = adminMessage('withdraw_confirm_message').replace(/\{(account|amount|order)\}/g, (_, key) => String(values[key]));
1650
  confirmAction({
1651
+ title: adminMessage('withdraw_confirm_payment'),
1652
+ message,
1653
+ okText: adminMessage('withdraw_confirm_payment'),
1654
  danger: false,
1655
  onConfirm: async (assertCurrent) => {
1656
+ if (!parentCurrent()) return;
1657
  const finishBusy = startAdminBusy(btn);
1658
  if (!finishBusy) return;
1659
  try {
1660
  const response = await apiRequest('POST', '/api/admin/withdrawals/' + encodeURIComponent(w.tx_id) + '/complete', { payment_order_id: orderId });
1661
  assertCurrent();
1662
+ if (!parentCurrent()) return;
1663
  confirmAdminStatusWrite(response);
1664
+ toast(adminMessage('withdraw_confirmed'), 'success');
1665
  await refreshWithdrawals();
1666
  } finally {
1667
  finishBusy();
 
1671
  }
1672
 
1673
  function bindWithdrawals() {
1674
+ $$('[data-admin-withdraw-text]').forEach(node => {
1675
+ node.textContent = adminMessage(node.dataset.adminWithdrawText);
1676
+ });
1677
+ $('#btn-withdraw-refresh').setAttribute('title', adminMessage('withdraw_refresh'));
1678
+ $('#withdraw-status-tabs').setAttribute('aria-label', adminMessage('withdraw_filter'));
1679
  $('#btn-withdraw-refresh').addEventListener('click', () => refreshWithdrawals());
1680
  $$('#withdraw-status-tabs .filter-tab').forEach((tab) => {
1681
  tab.addEventListener('click', () => {
1682
  state.withdrawStatus = tab.dataset.status;
1683
+ $$('#withdraw-status-tabs .filter-tab').forEach((t) => {
1684
+ t.classList.toggle('is-active', t === tab);
1685
+ t.setAttribute('aria-pressed', String(t === tab));
1686
+ });
1687
  refreshWithdrawals();
1688
  });
1689
  });
 
1956
  }
1957
 
1958
  // -------------------- 🗂️ 提示词分类 --------------------
1959
+ const PCAT_TYPE_LABELS = { image: 'pcat_image', video: 'pcat_video', music: 'pcat_music' };
1960
+ let pcatListGeneration = 0;
1961
 
1962
  function setPcatState(stateName, message) {
1963
+ if (stateName === 'loading') pcatListGeneration++;
1964
  $('#pcat-loading').hidden = stateName !== 'loading';
1965
  $('#pcat-empty').hidden = stateName !== 'empty';
1966
  $('#pcat-error').hidden = stateName !== 'error';
1967
  $('#pcat-list').hidden = stateName !== 'data';
1968
+ $('#pcat-list').setAttribute('aria-busy', String(stateName === 'loading'));
1969
  if (stateName === 'error' && message) $('#pcat-error-msg').textContent = message;
1970
  }
1971
 
 
1980
  } catch (err) {
1981
  if (isStaleSessionError(err) || !read.isCurrent()) return;
1982
  if (err instanceof ApiError && err.status === 401) return; // 已自动跳回登录
1983
+ setPcatState('error', err.message || adminMessage('pcat_load_failed'));
1984
  } finally {
1985
  read.release();
1986
  }
1987
  }
1988
 
1989
  function renderPromptCats(list) {
1990
+ const generation = ++pcatListGeneration;
1991
  const badge = $('#pcat-count-badge');
1992
+ if (badge) badge.textContent = adminMessage('pcat_count').replace('{count}', String(list.length));
1993
  if (!list.length) { setPcatState('empty'); return; }
1994
  setPcatState('data');
1995
  const wrap = $('#pcat-list');
1996
  wrap.innerHTML = '';
1997
  const frag = document.createDocumentFragment();
1998
+ const promptType = state.pcatType;
1999
+ const session = captureAdminSession();
2000
  list.forEach((cat) => {
2001
  const row = document.createElement('div');
2002
  row.className = 'pcat-row';
 
2006
  const del = document.createElement('button');
2007
  del.type = 'button';
2008
  del.className = 'btn btn-ghost btn-sm btn-danger-text';
2009
+ del.textContent = adminMessage('pcat_delete');
2010
+ del.setAttribute('aria-label', adminMessage('pcat_delete_name').replace('{name}', () => cat));
2011
+ const isCurrent = () => generation === pcatListGeneration && row.isConnected && session.isCurrent() && state.pcatType === promptType;
2012
+ del.addEventListener('click', () => {
2013
+ if (isCurrent()) confirmDeletePromptCat(cat, promptType, isCurrent);
2014
+ });
2015
  row.appendChild(name);
2016
  row.appendChild(del);
2017
  frag.appendChild(row);
 
2019
  wrap.appendChild(frag);
2020
  }
2021
 
2022
+ function confirmDeletePromptCat(cat, promptType = state.pcatType, parentCurrent = () => true) {
2023
+ if (!parentCurrent()) return;
2024
+ const values = { type: Object.hasOwn(PCAT_TYPE_LABELS, promptType) ? adminMessage(PCAT_TYPE_LABELS[promptType]) : '', name: cat };
2025
  confirmAction({
2026
+ title: adminMessage('pcat_delete_title'),
2027
+ message: adminMessage('pcat_delete_message').replace(/\{(type|name)\}/g, (_, key) => values[key]),
2028
+ okText: adminMessage('pcat_delete_confirm'),
2029
  danger: true,
2030
  onConfirm: async (assertCurrent) => {
2031
+ if (!parentCurrent()) return;
2032
  const response = await apiRequest('DELETE', '/api/prompt-categories?prompt_type=' + promptType + '&name=' + encodeURIComponent(cat));
2033
  assertCurrent();
2034
+ if (!parentCurrent()) return;
2035
  confirmAdminStatusWrite(response);
2036
+ toast(adminMessage('pcat_deleted'), 'success');
2037
  await refreshPromptCats();
2038
  }
2039
  });
 
2041
 
2042
  function bindPromptCats() {
2043
  let categoryRevision = 0;
2044
+ $$('[data-admin-pcat-text]').forEach(node => {
2045
+ node.textContent = adminMessage(node.dataset.adminPcatText);
2046
+ });
2047
+ $('#pcat-name').placeholder = adminMessage('pcat_name_placeholder');
2048
+ $('#btn-pcat-refresh').setAttribute('title', adminMessage('pcat_refresh'));
2049
+ $('#pcat-type-tabs').setAttribute('aria-label', adminMessage('pcat_type_filter'));
2050
  $('#btn-pcat-refresh').addEventListener('click', () => refreshPromptCats());
2051
  $$('#pcat-type-tabs .filter-tab').forEach((tab) => {
2052
  tab.addEventListener('click', () => {
2053
  categoryRevision++;
2054
  state.pcatType = tab.dataset.type;
2055
+ $$('#pcat-type-tabs .filter-tab').forEach((t) => {
2056
+ t.classList.toggle('is-active', t === tab);
2057
+ t.setAttribute('aria-pressed', String(t === tab));
2058
+ });
2059
  refreshPromptCats();
2060
  });
2061
  });
2062
  const addBtn = $('#btn-pcat-add');
2063
  const nameInput = $('#pcat-name');
2064
+ let categoryDraftValue = nameInput.value;
2065
+ const trackCategoryEdit = () => {
2066
+ if (nameInput.value === categoryDraftValue) return;
2067
+ categoryDraftValue = nameInput.value;
2068
+ categoryRevision++;
2069
+ };
2070
+ nameInput.addEventListener('input', trackCategoryEdit);
2071
+ nameInput.addEventListener('change', trackCategoryEdit);
2072
  const submitAdd = async () => {
2073
  const rawName = nameInput.value;
2074
  const name = rawName.trim();
2075
  const promptType = state.pcatType;
2076
  const revision = categoryRevision;
2077
+ if (!name) { toast(adminMessage('pcat_name_required'), 'warning'); nameInput.focus(); return; }
2078
+ if (name.length > 8) { toast(adminMessage('pcat_name_too_long'), 'warning'); return; }
2079
  const finishBusy = startAdminBusy(addBtn);
2080
  if (!finishBusy) return;
2081
  try {
2082
  const response = await apiRequest('POST', '/api/prompt-categories', { prompt_type: promptType, name });
2083
  finishBusy.assertCurrent();
2084
  confirmAdminStatusWrite(response);
2085
+ if (categoryRevision === revision && state.pcatType === promptType && nameInput.value === rawName) {
2086
+ nameInput.value = '';
2087
+ categoryDraftValue = '';
2088
+ }
2089
+ toast(adminMessage('pcat_added'), 'success');
2090
  await refreshPromptCats();
2091
  } catch (err) {
2092
  if (isStaleSessionError(err) || !finishBusy.isCurrent()) return;
2093
+ toast(err.message || adminMessage('pcat_add_failed'), 'error');
2094
  } finally {
2095
  finishBusy();
2096
  }
 
2130
  }
2131
 
2132
  // -------------------- 🏷️ 版本管理 --------------------
2133
+ const STAGE_LABELS = { alpha: 'version_alpha', beta: 'version_beta', rc: 'version_rc', stable: 'version_stable' };
2134
+
2135
+ function localizeAdminVersionUI() {
2136
+ $$('[data-admin-version-text]').forEach(node => {
2137
+ node.textContent = adminMessage(node.dataset.adminVersionText);
2138
+ });
2139
+ $$('[data-admin-version-label]').forEach(node => {
2140
+ node.setAttribute('aria-label', adminMessage(node.dataset.adminVersionLabel));
2141
+ });
2142
+ }
2143
 
2144
  function updateVerPreview() {
2145
  const form = $('#form-version');
 
2148
  const major = form.querySelector('[name="major"]').value;
2149
  const minor = form.querySelector('[name="minor"]').value;
2150
  const patch = form.querySelector('[name="patch"]').value;
2151
+ const label = Object.hasOwn(STAGE_LABELS, stage) ? adminMessage(STAGE_LABELS[stage]) : stage;
2152
+ $('#ver-preview').textContent = adminMessage('version_preview')
2153
+ .replace('{version}', `${major}.${minor}.${patch}`).replace('{stage}', label);
2154
  }
2155
 
2156
  let versionConfigGeneration = 0;
 
2226
  const response = await apiRequest('PUT', '/api/admin/config/project_version', payload);
2227
  finishBusy.assertCurrent();
2228
  confirmAdminStatusWrite(response);
2229
+ const label = Object.hasOwn(STAGE_LABELS, payload.stage) ? adminMessage(STAGE_LABELS[payload.stage]) : payload.stage;
2230
+ toast(adminMessage('version_saved').replace('{version}', `${payload.major}.${payload.minor}.${payload.patch}`)
2231
+ .replace('{stage}', label), 'success');
2232
  } catch (err) {
2233
  if (isStaleSessionError(err) || !finishBusy.isCurrent()) return;
2234
+ showError($('#form-version-error'), err.message || adminMessage('version_failed'));
2235
  } finally {
2236
  versionSavePending = false;
2237
  finishBusy();
 
2309
  }
2310
 
2311
  function bindBroadcast() {
2312
+ $$('[data-admin-broadcast-text]').forEach(node => {
2313
+ node.textContent = adminMessage(node.dataset.adminBroadcastText);
2314
+ });
2315
+ $$('[data-admin-broadcast-placeholder]').forEach(node => {
2316
+ node.placeholder = adminMessage(node.dataset.adminBroadcastPlaceholder);
2317
+ });
2318
+ $$('[data-admin-broadcast-alt]').forEach(node => {
2319
+ node.alt = adminMessage(node.dataset.adminBroadcastAlt);
2320
+ });
2321
  const bannerSave = $('#btn-banner-save');
2322
  const bannerUploads = new Set();
2323
  let bannerSaveBeforeUpload = null;
 
2356
  const rawContent = announcement.value;
2357
  const content = rawContent.trim();
2358
  const revision = announcementRevision;
2359
+ if (!content) { toast(adminMessage('announcement_required'), 'warning'); announcement.focus(); return; }
2360
  confirmAction({
2361
+ title: adminMessage('announcement_confirm_title'),
2362
+ message: adminMessage('announcement_confirm_message').replace('{preview}', () => content.slice(0, 80) + (content.length > 80 ? '…' : '')),
2363
+ okText: adminMessage('announcement_confirm'),
2364
  danger: true,
2365
  onConfirm: async (assertCurrent) => {
2366
  const response = await apiRequest('POST', '/api/system/announcement', { admin_account: adminAccount(), content });
2367
  assertCurrent();
2368
  confirmAdminStatusWrite(response);
2369
  if (announcementRevision === revision && announcement.value === rawContent) announcement.value = '';
2370
+ toast(adminMessage('announcement_published'), 'success');
2371
  }
2372
  });
2373
  });
 
2385
  const draft = readBannerDraft();
2386
  const revision = bannerRevision;
2387
  if (draft.enabled && !draft.bannerImage) {
2388
+ toast(adminMessage('banner_image_required'), 'warning');
2389
  return;
2390
  }
2391
  const config = { ...draft, title: draft.title.trim(), description: draft.description.trim(),
 
2397
  const response = await apiRequest('PUT', '/api/admin/config/banner_config', config);
2398
  finishBusy.assertCurrent();
2399
  confirmAdminStatusWrite(response);
2400
+ toast(adminMessage('banner_saved'), 'success');
2401
  const current = readBannerDraft();
2402
  if (bannerRevision === revision && Object.keys(draft).every(key => current[key] === draft[key])) {
2403
  $('#banner-enabled').checked = false;
 
2415
  refreshBannerStatus();
2416
  } catch (err) {
2417
  if (isStaleSessionError(err) || !finishBusy.isCurrent()) return;
2418
+ toast(err.message || adminMessage('banner_save_failed'), 'error');
2419
  } finally {
2420
  finishBusy();
2421
  }
 
2465
  function bindMain() {
2466
  $('#btn-logout').addEventListener('click', () => {
2467
  confirmAction({
2468
+ title: adminMessage('logout_title'),
2469
+ message: adminMessage('logout_message'),
2470
+ okText: adminMessage('logout_confirm'),
2471
  danger: false,
2472
  onConfirm: async () => {
2473
  clearSession();
2474
  showView('view-login');
2475
+ toast(adminMessage('logout_success'), 'success');
2476
  },
2477
  });
2478
  });
 
2520
  });
2521
  }
2522
 
2523
+ function localizeAdminSessionUI() {
2524
+ document.title = adminMessage('session_page_title');
2525
+ $$('[data-admin-session-text]').forEach(node => {
2526
+ node.textContent = adminMessage(node.dataset.adminSessionText);
2527
+ });
2528
+ $$('[data-admin-session-placeholder]').forEach(node => {
2529
+ node.placeholder = adminMessage(node.dataset.adminSessionPlaceholder);
2530
+ });
2531
+ $$('#modal-confirm .modal-close').forEach(node => {
2532
+ node.setAttribute('aria-label', adminMessage('close_dialog'));
2533
+ });
2534
+ }
2535
+
2536
  // -------------------- 启动 --------------------
2537
  function bootstrap() {
2538
+ localizeAdminSessionUI();
2539
+ localizeAdminVersionUI();
2540
  bindLogin();
2541
  bindMain();
2542
  bindCreate();
管理界面/index.html CHANGED
@@ -23,45 +23,45 @@
23
  </div>
24
  </div>
25
  <div class="login-headline">
26
- <h1>第三方插件 <br><em>统一管理控制台</em></h1>
27
- <p>使用 RanKing 管理员账号登录,集中管理所有第三方插件的访问凭证、权限与配额。</p>
28
  </div>
29
  <ul class="login-meta">
30
- <li><span class="dot dot-success"></span>Plugin Key 动态轮换</li>
31
- <li><span class="dot dot-warning"></span>限额与权限精细化控制</li>
32
- <li><span class="dot dot-primary"></span>双主题 · 响应式 · 原生实现</li>
33
  </ul>
34
  </div>
35
 
36
  <form id="login-form" class="login-card" novalidate>
37
  <header class="login-card-header">
38
- <h2>管理员登录</h2>
39
- <p>请使用具备管理员权限的 RanKing 账号</p>
40
  </header>
41
 
42
  <label class="field">
43
- <span class="field-label">账号</span>
44
  <input type="text" id="login-account" name="account"
45
- autocomplete="username" required
46
- placeholder="用户名 / 邮箱 / 手机号">
47
  </label>
48
 
49
  <label class="field">
50
- <span class="field-label">密码</span>
51
  <input type="password" id="login-password" name="password"
52
- autocomplete="current-password" required
53
- placeholder="请输入密码">
54
  </label>
55
 
56
- <div id="login-error" class="form-error" hidden></div>
57
 
58
  <button type="submit" id="login-submit" class="btn btn-primary btn-block">
59
- <span class="btn-label">登 录</span>
60
  <span class="btn-spinner" aria-hidden="true"></span>
61
  </button>
62
 
63
  <footer class="login-card-footer">
64
- <span>仅管理员可访问 · Token 仅保存于当前会话</span>
65
  </footer>
66
  </form>
67
  </div>
@@ -92,7 +92,7 @@
92
  <span class="user-avatar" id="user-avatar">A</span>
93
  <span class="user-name" id="user-name">admin</span>
94
  </div>
95
- <button type="button" id="btn-logout" class="btn btn-ghost">退出登录</button>
96
  </div>
97
  </header>
98
 
@@ -168,35 +168,35 @@
168
  <div class="tab-panel" id="panel-withdrawals" role="tabpanel" aria-labelledby="tab-withdrawals" tabindex="0" hidden>
169
  <section class="page-head">
170
  <div class="page-head-text">
171
- <h1>提现管理</h1>
172
- <p class="page-sub">审核用户提现申请,线下打款后回填支付订单号完成结算。</p>
173
  </div>
174
  <div class="page-head-actions">
175
  <button type="button" id="btn-withdraw-refresh" class="btn btn-secondary" title="刷新">
176
- <span class="btn-label">刷新</span>
177
  </button>
178
  </div>
179
  </section>
180
 
181
- <div class="filter-tabs" id="withdraw-status-tabs">
182
- <button type="button" class="filter-tab is-active" data-status="pending">⏳ 待打款</button>
183
- <button type="button" class="filter-tab" data-status="completed">✅ 已完成</button>
184
  </div>
185
 
186
  <section class="list-shell">
187
- <div id="withdraw-loading" class="state-block" hidden>
188
  <div class="spinner-lg" aria-hidden="true"></div>
189
- <p>正在加载提现列表…</p>
190
  </div>
191
- <div id="withdraw-empty" class="state-block" hidden>
192
  <div class="empty-mark">📭</div>
193
- <h3>暂无提现记录</h3>
194
- <p id="withdraw-empty-msg">当前没有待打款的提现申请。</p>
195
  </div>
196
- <div id="withdraw-error" class="state-block state-error" hidden>
197
  <div class="empty-mark">!</div>
198
- <h3>加载失败</h3>
199
- <p id="withdraw-error-msg">请稍后重试。</p>
200
  </div>
201
  <div id="withdraw-list" class="admin-list"></div>
202
  </section>
@@ -246,8 +246,8 @@
246
  <div class="tab-panel" id="panel-config" role="tabpanel" aria-labelledby="tab-config" tabindex="0" hidden>
247
  <section class="page-head">
248
  <div class="page-head-text">
249
- <h1>版本管理</h1>
250
- <p class="page-sub">设置全站展示的项目版本号与开发阶段。</p>
251
  </div>
252
  </section>
253
 
@@ -255,30 +255,30 @@
255
  <form id="form-version" class="admin-card-body" novalidate>
256
  <div class="grid-2">
257
  <label class="field">
258
- <span class="field-label">项目阶段</span>
259
  <select name="stage">
260
- <option value="alpha">Alpha(内测)</option>
261
- <option value="beta" selected>Beta(公测)</option>
262
- <option value="rc">RC(候选发布)</option>
263
- <option value="stable">Stable(稳定版)</option>
264
  </select>
265
  </label>
266
  <div class="field">
267
- <span class="field-label">版本号</span>
268
- <div class="ver-inputs">
269
- <input type="number" name="major" min="0" max="99" value="2" aria-label="主版本号">
270
  <span class="ver-dot">.</span>
271
- <input type="number" name="minor" min="0" max="99" value="0" aria-label="次版本号">
272
  <span class="ver-dot">.</span>
273
- <input type="number" name="patch" min="0" max="99" value="0" aria-label="修订号">
274
  </div>
275
  </div>
276
  </div>
277
- <div class="ver-preview" id="ver-preview">当前版本:—</div>
278
- <div class="form-error" id="form-version-error" hidden></div>
279
  <div>
280
  <button type="submit" id="btn-version-save" class="btn btn-primary">
281
- <span class="btn-label">应用版本</span>
282
  <span class="btn-spinner" aria-hidden="true"></span>
283
  </button>
284
  </div>
@@ -290,25 +290,25 @@
290
  <div class="tab-panel" id="panel-broadcast" role="tabpanel" aria-labelledby="tab-broadcast" tabindex="0" hidden>
291
  <section class="page-head">
292
  <div class="page-head-text">
293
- <h1>公告与广告</h1>
294
- <p class="page-sub">发布全站系统公告,管理顶部广告横幅展示。</p>
295
  </div>
296
  </section>
297
 
298
  <!-- 📢 系统公告 -->
299
  <div class="admin-card">
300
  <div class="admin-card-head">
301
- <h3>📢 全站系统公告</h3>
302
- <span class="badge badge-warning">管理员特权</span>
303
  </div>
304
  <div class="admin-card-body">
305
  <label class="field">
306
- <span class="field-label">公告内容</span>
307
- <textarea id="ann-content" rows="4" placeholder="输入要向全体用户推送的公告内容…"></textarea>
308
  </label>
309
  <div>
310
  <button type="button" id="btn-ann-publish" class="btn btn-primary">
311
- <span class="btn-label">🚀 确认发布</span>
312
  <span class="btn-spinner" aria-hidden="true"></span>
313
  </button>
314
  </div>
@@ -318,29 +318,29 @@
318
  <!-- 📰 广告横幅管理 -->
319
  <div class="admin-card">
320
  <div class="admin-card-head">
321
- <h3>📰 广告横幅管理</h3>
322
- <span class="badge" id="banner-status-badge">检查中…</span>
323
  </div>
324
  <div class="admin-card-body">
325
  <label class="switch-row">
326
  <input type="checkbox" id="banner-enabled">
327
- <span>启用广告展示</span>
328
  </label>
329
 
330
  <div class="grid-2">
331
  <div class="field">
332
- <span class="field-label">横幅图片(支持 GIF)</span>
333
- <img id="banner-img-preview" class="upload-preview" hidden alt="横幅预览">
334
  <button type="button" id="btn-banner-img" class="btn btn-secondary btn-upload">
335
- <span class="btn-label">🖼️ 选择横幅图片</span>
336
  </button>
337
  <input type="file" id="banner-img-file" accept="image/*" hidden>
338
  </div>
339
  <div class="field">
340
- <span class="field-label">详情页大图</span>
341
- <img id="banner-detail-img-preview" class="upload-preview" hidden alt="详情图预览">
342
  <button type="button" id="btn-detail-img" class="btn btn-secondary btn-upload">
343
- <span class="btn-label">🖼️ 选择详情大图</span>
344
  </button>
345
  <input type="file" id="banner-detail-img-file" accept="image/*" hidden>
346
  </div>
@@ -348,23 +348,23 @@
348
 
349
  <div class="grid-2">
350
  <label class="field">
351
- <span class="field-label">广告标题</span>
352
- <input type="text" id="banner-title" placeholder="广告主标题(可选)">
353
  </label>
354
  <label class="field">
355
- <span class="field-label">广告描述</span>
356
- <input type="text" id="banner-desc" placeholder="一句话描述(可选)">
357
  </label>
358
  </div>
359
 
360
  <label class="field">
361
- <span class="field-label">详情内容</span>
362
- <textarea id="banner-detail-content" rows="6" placeholder="点击广告后展示的详细内容(可选)"></textarea>
363
  </label>
364
 
365
  <div>
366
  <button type="button" id="btn-banner-save" class="btn btn-primary">
367
- <span class="btn-label">💾 保存广告配置</span>
368
  <span class="btn-spinner" aria-hidden="true"></span>
369
  </button>
370
  </div>
@@ -376,35 +376,35 @@
376
  <div class="tab-panel" id="panel-promptcats" role="tabpanel" aria-labelledby="tab-promptcats" tabindex="0" hidden>
377
  <section class="page-head">
378
  <div class="page-head-text">
379
- <h1>提示词分类</h1>
380
- <p class="page-sub">管理提示词市场图像/视频/音乐三个子界面的可选分类。</p>
381
  </div>
382
  <div class="page-head-actions">
383
  <button type="button" id="btn-pcat-refresh" class="btn btn-secondary" title="刷新">
384
- <span class="btn-label">刷新</span>
385
  </button>
386
  </div>
387
  </section>
388
 
389
- <div class="filter-tabs" id="pcat-type-tabs">
390
- <button type="button" class="filter-tab is-active" data-type="image">🎨 图像提示词</button>
391
- <button type="button" class="filter-tab" data-type="video">🎬 视频提示词</button>
392
- <button type="button" class="filter-tab" data-type="music">🎵 音乐提示词</button>
393
  </div>
394
 
395
  <div class="admin-card">
396
  <div class="admin-card-head">
397
- <h3>➕ 添加分类</h3>
398
- <span class="badge badge-warning">管理员特权</span>
399
  </div>
400
  <div class="admin-card-body">
401
  <div class="script-row">
402
  <label class="field script-field">
403
- <span class="field-label">分类名称</span>
404
  <input type="text" id="pcat-name" maxlength="8" placeholder="输入分类名称(8 字以内)…">
405
  </label>
406
  <button type="button" id="btn-pcat-add" class="btn btn-primary btn-script-run">
407
- <span class="btn-label">添加</span>
408
  <span class="btn-spinner" aria-hidden="true"></span>
409
  </button>
410
  </div>
@@ -413,25 +413,25 @@
413
 
414
  <div class="admin-card">
415
  <div class="admin-card-head">
416
- <h3>📋 当前分类</h3>
417
  <span class="badge" id="pcat-count-badge">—</span>
418
  </div>
419
  <div class="admin-card-body">
420
- <div id="pcat-loading" class="state-block" hidden>
421
  <div class="spinner-lg" aria-hidden="true"></div>
422
- <p>正在加载分类列表…</p>
423
  </div>
424
- <div id="pcat-empty" class="state-block" hidden>
425
  <div class="empty-mark">∅</div>
426
- <h3>暂无分类</h3>
427
- <p>在上方输入框中添加第一个分类。</p>
428
  </div>
429
- <div id="pcat-error" class="state-block state-error" hidden>
430
  <div class="empty-mark">!</div>
431
- <h3>加载失败</h3>
432
- <p id="pcat-error-msg">请稍后重试。</p>
433
  </div>
434
- <div id="pcat-list"></div>
435
  </div>
436
  </div>
437
  </div>
@@ -472,9 +472,9 @@
472
  <section id="view-forbidden" class="view view-forbidden" hidden>
473
  <div class="forbidden-card">
474
  <div class="forbidden-mark">⛔</div>
475
- <h2>您不是管理员,无权访问</h2>
476
- <p>当前登录账号没有插件管理权限。请使用具备管理员权限的账号重新登录。</p>
477
- <button type="button" id="btn-relogin" class="btn btn-primary">返回登录</button>
478
  </div>
479
  </section>
480
 
@@ -665,7 +665,7 @@
665
  <p id="modal-confirm-msg" class="confirm-msg">确认执行此操作?</p>
666
  </div>
667
  <footer class="modal-footer">
668
- <button type="button" class="btn btn-ghost" data-close="modal-confirm">取消</button>
669
  <button type="button" id="btn-confirm-ok" class="btn btn-danger">
670
  <span class="btn-label">确定</span>
671
  <span class="btn-spinner" aria-hidden="true"></span>
 
23
  </div>
24
  </div>
25
  <div class="login-headline">
26
+ <h1><span data-admin-session-text="login_headline">第三方插件</span> <br><em data-admin-session-text="login_headline_emphasis">统一管理控制台</em></h1>
27
+ <p data-admin-session-text="login_intro">使用 RanKing 管理员账号登录,集中管理所有第三方插件的访问凭证、权限与配额。</p>
28
  </div>
29
  <ul class="login-meta">
30
+ <li><span class="dot dot-success"></span><span data-admin-session-text="login_feature_keys">Plugin Key 动态轮换</span></li>
31
+ <li><span class="dot dot-warning"></span><span data-admin-session-text="login_feature_access">限额与权限精细化控制</span></li>
32
+ <li><span class="dot dot-primary"></span><span data-admin-session-text="login_feature_interface">双主题 · 响应式 · 原生实现</span></li>
33
  </ul>
34
  </div>
35
 
36
  <form id="login-form" class="login-card" novalidate>
37
  <header class="login-card-header">
38
+ <h2 data-admin-session-text="login_title">管理员登录</h2>
39
+ <p data-admin-session-text="login_subtitle">请使用具备管理员权限的 RanKing 账号</p>
40
  </header>
41
 
42
  <label class="field">
43
+ <span class="field-label" data-admin-session-text="login_account">账号</span>
44
  <input type="text" id="login-account" name="account"
45
+ autocomplete="username" aria-describedby="login-error" required
46
+ data-admin-session-placeholder="login_account_placeholder" placeholder="用户名 / 邮箱 / 手机号">
47
  </label>
48
 
49
  <label class="field">
50
+ <span class="field-label" data-admin-session-text="login_password">密码</span>
51
  <input type="password" id="login-password" name="password"
52
+ autocomplete="current-password" aria-describedby="login-error" required
53
+ data-admin-session-placeholder="login_password_placeholder" placeholder="请输入密码">
54
  </label>
55
 
56
+ <div id="login-error" class="form-error" role="alert" aria-atomic="true" hidden></div>
57
 
58
  <button type="submit" id="login-submit" class="btn btn-primary btn-block">
59
+ <span class="btn-label" data-admin-session-text="login_submit">登 录</span>
60
  <span class="btn-spinner" aria-hidden="true"></span>
61
  </button>
62
 
63
  <footer class="login-card-footer">
64
+ <span data-admin-session-text="login_session_notice">仅管理员可访问 · Token 仅保存于当前会话</span>
65
  </footer>
66
  </form>
67
  </div>
 
92
  <span class="user-avatar" id="user-avatar">A</span>
93
  <span class="user-name" id="user-name">admin</span>
94
  </div>
95
+ <button type="button" id="btn-logout" class="btn btn-ghost" data-admin-session-text="logout_title">退出登录</button>
96
  </div>
97
  </header>
98
 
 
168
  <div class="tab-panel" id="panel-withdrawals" role="tabpanel" aria-labelledby="tab-withdrawals" tabindex="0" hidden>
169
  <section class="page-head">
170
  <div class="page-head-text">
171
+ <h1 data-admin-withdraw-text="tab_withdrawals">提现管理</h1>
172
+ <p class="page-sub" data-admin-withdraw-text="withdraw_subtitle">审核用户提现申请,线下打款后回填支付订单号完成结算。</p>
173
  </div>
174
  <div class="page-head-actions">
175
  <button type="button" id="btn-withdraw-refresh" class="btn btn-secondary" title="刷新">
176
+ <span class="btn-label" data-admin-withdraw-text="withdraw_refresh">刷新</span>
177
  </button>
178
  </div>
179
  </section>
180
 
181
+ <div class="filter-tabs" id="withdraw-status-tabs" role="group" aria-label="提现状态筛选">
182
+ <button type="button" class="filter-tab is-active" data-status="pending" aria-pressed="true" data-admin-withdraw-text="withdraw_pending_filter">⏳ 待打款</button>
183
+ <button type="button" class="filter-tab" data-status="completed" aria-pressed="false" data-admin-withdraw-text="withdraw_completed_filter">✅ 已完成</button>
184
  </div>
185
 
186
  <section class="list-shell">
187
+ <div id="withdraw-loading" role="status" aria-atomic="true" class="state-block" hidden>
188
  <div class="spinner-lg" aria-hidden="true"></div>
189
+ <p data-admin-withdraw-text="withdraw_loading">正在加载提现列表…</p>
190
  </div>
191
+ <div id="withdraw-empty" role="status" aria-atomic="true" class="state-block" hidden>
192
  <div class="empty-mark">📭</div>
193
+ <h3 data-admin-withdraw-text="withdraw_empty_title">暂无提现记录</h3>
194
+ <p id="withdraw-empty-msg" data-admin-withdraw-text="withdraw_empty_pending">当前没有待打款的提现申请。</p>
195
  </div>
196
+ <div id="withdraw-error" role="status" aria-atomic="true" class="state-block state-error" hidden>
197
  <div class="empty-mark">!</div>
198
+ <h3 data-admin-withdraw-text="withdraw_load_failed">加载失败</h3>
199
+ <p id="withdraw-error-msg" data-admin-withdraw-text="withdraw_load_failed">请稍后重试。</p>
200
  </div>
201
  <div id="withdraw-list" class="admin-list"></div>
202
  </section>
 
246
  <div class="tab-panel" id="panel-config" role="tabpanel" aria-labelledby="tab-config" tabindex="0" hidden>
247
  <section class="page-head">
248
  <div class="page-head-text">
249
+ <h1 data-admin-version-text="tab_config">版本管理</h1>
250
+ <p class="page-sub" data-admin-version-text="version_subtitle">设置全站展示的项目版本号与开发阶段。</p>
251
  </div>
252
  </section>
253
 
 
255
  <form id="form-version" class="admin-card-body" novalidate>
256
  <div class="grid-2">
257
  <label class="field">
258
+ <span class="field-label" data-admin-version-text="version_stage">项目阶段</span>
259
  <select name="stage">
260
+ <option value="alpha" data-admin-version-text="version_alpha">Alpha(内测)</option>
261
+ <option value="beta" selected data-admin-version-text="version_beta">Beta(公测)</option>
262
+ <option value="rc" data-admin-version-text="version_rc">RC(候选发布)</option>
263
+ <option value="stable" data-admin-version-text="version_stable">Stable(稳定版)</option>
264
  </select>
265
  </label>
266
  <div class="field">
267
+ <span class="field-label" id="version-number-label" data-admin-version-text="version_number">版本号</span>
268
+ <div class="ver-inputs" role="group" aria-labelledby="version-number-label">
269
+ <input type="number" name="major" min="0" max="99" value="2" aria-label="主版本号" data-admin-version-label="version_major">
270
  <span class="ver-dot">.</span>
271
+ <input type="number" name="minor" min="0" max="99" value="0" aria-label="次版本号" data-admin-version-label="version_minor">
272
  <span class="ver-dot">.</span>
273
+ <input type="number" name="patch" min="0" max="99" value="0" aria-label="修订号" data-admin-version-label="version_patch">
274
  </div>
275
  </div>
276
  </div>
277
+ <div class="ver-preview" id="ver-preview" data-admin-version-text="version_initial" role="status" aria-atomic="true">当前版本:—</div>
278
+ <div class="form-error" id="form-version-error" role="alert" aria-atomic="true" hidden></div>
279
  <div>
280
  <button type="submit" id="btn-version-save" class="btn btn-primary">
281
+ <span class="btn-label" data-admin-version-text="version_apply">应用版本</span>
282
  <span class="btn-spinner" aria-hidden="true"></span>
283
  </button>
284
  </div>
 
290
  <div class="tab-panel" id="panel-broadcast" role="tabpanel" aria-labelledby="tab-broadcast" tabindex="0" hidden>
291
  <section class="page-head">
292
  <div class="page-head-text">
293
+ <h1 data-admin-broadcast-text="tab_broadcast">公告与广告</h1>
294
+ <p class="page-sub" data-admin-broadcast-text="broadcast_subtitle">发布全站系统公告,管理顶部广告横幅展示。</p>
295
  </div>
296
  </section>
297
 
298
  <!-- 📢 系统公告 -->
299
  <div class="admin-card">
300
  <div class="admin-card-head">
301
+ <h3 data-admin-broadcast-text="announcement_title">📢 全站系统公告</h3>
302
+ <span class="badge badge-warning" data-admin-broadcast-text="admin_privilege">管理员特权</span>
303
  </div>
304
  <div class="admin-card-body">
305
  <label class="field">
306
+ <span class="field-label" data-admin-broadcast-text="announcement_content">公告内容</span>
307
+ <textarea id="ann-content" rows="4" placeholder="输入要向全体用户推送的公告内容…" data-admin-broadcast-placeholder="announcement_placeholder"></textarea>
308
  </label>
309
  <div>
310
  <button type="button" id="btn-ann-publish" class="btn btn-primary">
311
+ <span class="btn-label" data-admin-broadcast-text="announcement_publish">🚀 确认发布</span>
312
  <span class="btn-spinner" aria-hidden="true"></span>
313
  </button>
314
  </div>
 
318
  <!-- 📰 广告横幅管理 -->
319
  <div class="admin-card">
320
  <div class="admin-card-head">
321
+ <h3 data-admin-broadcast-text="banner_manage_title">📰 广告横幅管理</h3>
322
+ <span class="badge" id="banner-status-badge" role="status" aria-atomic="true" data-admin-broadcast-text="banner_checking">检查中…</span>
323
  </div>
324
  <div class="admin-card-body">
325
  <label class="switch-row">
326
  <input type="checkbox" id="banner-enabled">
327
+ <span data-admin-broadcast-text="banner_enabled">启用广告展示</span>
328
  </label>
329
 
330
  <div class="grid-2">
331
  <div class="field">
332
+ <span class="field-label" data-admin-broadcast-text="banner_image_label">横幅图片(支持 GIF)</span>
333
+ <img id="banner-img-preview" class="upload-preview" hidden alt="横幅预览" data-admin-broadcast-alt="banner_image_preview">
334
  <button type="button" id="btn-banner-img" class="btn btn-secondary btn-upload">
335
+ <span class="btn-label" data-admin-broadcast-text="banner_select_image">🖼️ 选择横幅图片</span>
336
  </button>
337
  <input type="file" id="banner-img-file" accept="image/*" hidden>
338
  </div>
339
  <div class="field">
340
+ <span class="field-label" data-admin-broadcast-text="banner_detail_image_label">详情页大图</span>
341
+ <img id="banner-detail-img-preview" class="upload-preview" hidden alt="详情图预览" data-admin-broadcast-alt="banner_detail_preview">
342
  <button type="button" id="btn-detail-img" class="btn btn-secondary btn-upload">
343
+ <span class="btn-label" data-admin-broadcast-text="banner_select_detail">🖼️ 选择详情大图</span>
344
  </button>
345
  <input type="file" id="banner-detail-img-file" accept="image/*" hidden>
346
  </div>
 
348
 
349
  <div class="grid-2">
350
  <label class="field">
351
+ <span class="field-label" data-admin-broadcast-text="banner_title_label">广告标题</span>
352
+ <input type="text" id="banner-title" placeholder="广告主标题(可选)" data-admin-broadcast-placeholder="banner_title_placeholder">
353
  </label>
354
  <label class="field">
355
+ <span class="field-label" data-admin-broadcast-text="banner_description_label">广告描述</span>
356
+ <input type="text" id="banner-desc" placeholder="一句话描述(可选)" data-admin-broadcast-placeholder="banner_description_placeholder">
357
  </label>
358
  </div>
359
 
360
  <label class="field">
361
+ <span class="field-label" data-admin-broadcast-text="banner_detail_content_label">详情内容</span>
362
+ <textarea id="banner-detail-content" rows="6" placeholder="点击广告后展示的详细内容(可选)" data-admin-broadcast-placeholder="banner_detail_content_placeholder"></textarea>
363
  </label>
364
 
365
  <div>
366
  <button type="button" id="btn-banner-save" class="btn btn-primary">
367
+ <span class="btn-label" data-admin-broadcast-text="banner_save">💾 保存广告配置</span>
368
  <span class="btn-spinner" aria-hidden="true"></span>
369
  </button>
370
  </div>
 
376
  <div class="tab-panel" id="panel-promptcats" role="tabpanel" aria-labelledby="tab-promptcats" tabindex="0" hidden>
377
  <section class="page-head">
378
  <div class="page-head-text">
379
+ <h1 data-admin-pcat-text="tab_promptcats">提示词分类</h1>
380
+ <p class="page-sub" data-admin-pcat-text="pcat_subtitle">管理提示词市场图像/视频/音乐三个子界面的可选分类。</p>
381
  </div>
382
  <div class="page-head-actions">
383
  <button type="button" id="btn-pcat-refresh" class="btn btn-secondary" title="刷新">
384
+ <span class="btn-label" data-admin-pcat-text="pcat_refresh">刷新</span>
385
  </button>
386
  </div>
387
  </section>
388
 
389
+ <div class="filter-tabs" id="pcat-type-tabs" role="group">
390
+ <button type="button" class="filter-tab is-active" data-type="image" aria-pressed="true" data-admin-pcat-text="pcat_image_tab">🎨 图像提示词</button>
391
+ <button type="button" class="filter-tab" data-type="video" aria-pressed="false" data-admin-pcat-text="pcat_video_tab">🎬 视频提示词</button>
392
+ <button type="button" class="filter-tab" data-type="music" aria-pressed="false" data-admin-pcat-text="pcat_music_tab">🎵 音乐提示词</button>
393
  </div>
394
 
395
  <div class="admin-card">
396
  <div class="admin-card-head">
397
+ <h3 data-admin-pcat-text="pcat_add_title">➕ 添加分类</h3>
398
+ <span class="badge badge-warning" data-admin-pcat-text="admin_privilege">管理员特权</span>
399
  </div>
400
  <div class="admin-card-body">
401
  <div class="script-row">
402
  <label class="field script-field">
403
+ <span class="field-label" data-admin-pcat-text="pcat_name">分类名称</span>
404
  <input type="text" id="pcat-name" maxlength="8" placeholder="输入分类名称(8 字以内)…">
405
  </label>
406
  <button type="button" id="btn-pcat-add" class="btn btn-primary btn-script-run">
407
+ <span class="btn-label" data-admin-pcat-text="pcat_add">添加</span>
408
  <span class="btn-spinner" aria-hidden="true"></span>
409
  </button>
410
  </div>
 
413
 
414
  <div class="admin-card">
415
  <div class="admin-card-head">
416
+ <h3 data-admin-pcat-text="pcat_current_title">📋 当前分类</h3>
417
  <span class="badge" id="pcat-count-badge">—</span>
418
  </div>
419
  <div class="admin-card-body">
420
+ <div id="pcat-loading" class="state-block" role="status" aria-atomic="true" hidden>
421
  <div class="spinner-lg" aria-hidden="true"></div>
422
+ <p data-admin-pcat-text="pcat_loading">正在加载分类列表…</p>
423
  </div>
424
+ <div id="pcat-empty" class="state-block" role="status" aria-atomic="true" hidden>
425
  <div class="empty-mark">∅</div>
426
+ <h3 data-admin-pcat-text="pcat_empty_title">暂无分类</h3>
427
+ <p data-admin-pcat-text="pcat_empty_description">在上方输入框中添加第一个分类。</p>
428
  </div>
429
+ <div id="pcat-error" class="state-block state-error" role="status" aria-atomic="true" hidden>
430
  <div class="empty-mark">!</div>
431
+ <h3 data-admin-pcat-text="pcat_load_failed">加载失败</h3>
432
+ <p id="pcat-error-msg" data-admin-pcat-text="request_failed">请稍后重试。</p>
433
  </div>
434
+ <div id="pcat-list" aria-busy="false"></div>
435
  </div>
436
  </div>
437
  </div>
 
472
  <section id="view-forbidden" class="view view-forbidden" hidden>
473
  <div class="forbidden-card">
474
  <div class="forbidden-mark">⛔</div>
475
+ <h2 data-admin-session-text="forbidden_title">您不是管理员,无权访问</h2>
476
+ <p data-admin-session-text="forbidden_description">当前登录账号没有插件管理权限。请使用具备管理员权限的账号重新登录。</p>
477
+ <button type="button" id="btn-relogin" class="btn btn-primary" data-admin-session-text="forbidden_return">返回登录</button>
478
  </div>
479
  </section>
480
 
 
665
  <p id="modal-confirm-msg" class="confirm-msg">确认执行此操作?</p>
666
  </div>
667
  <footer class="modal-footer">
668
+ <button type="button" class="btn btn-ghost" data-close="modal-confirm" data-admin-session-text="confirm_cancel">取消</button>
669
  <button type="button" id="btn-confirm-ok" class="btn btn-danger">
670
  <span class="btn-label">确定</span>
671
  <span class="btn-spinner" aria-hidden="true"></span>