Spaces:
Running
Running
Upload 3 files
Browse files- 管理界面/app.js +329 -63
- 管理界面/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('
|
| 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 || '
|
| 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 || '
|
| 1431 |
-
<span class="badge ${isPending ? 'is-off' : 'is-on'}">${isPending ? '
|
| 1432 |
</div>
|
| 1433 |
-
<div class="row-card-amount">${formatNumber(amount)}
|
| 1434 |
</div>
|
| 1435 |
<dl class="row-card-meta">
|
| 1436 |
-
<div><dt>
|
| 1437 |
-
<div><dt>
|
| 1438 |
-
<div><dt>
|
| 1439 |
-
<div><dt>
|
| 1440 |
</dl>
|
| 1441 |
${isPending ? `
|
| 1442 |
<div class="row-card-action">
|
| 1443 |
-
<input type="text" class="row-input js-order-input"
|
| 1444 |
<button type="button" class="btn btn-primary btn-sm js-confirm-pay">
|
| 1445 |
-
<span class="btn-label">
|
| 1446 |
<span class="btn-spinner" aria-hidden="true"></span>
|
| 1447 |
</button>
|
| 1448 |
</div>` : `
|
| 1449 |
-
<div class="row-card-done">
|
| 1450 |
`;
|
| 1451 |
if (isPending) {
|
| 1452 |
const btn = $('.js-confirm-pay', card);
|
| 1453 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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('
|
|
|
|
|
|
|
| 1462 |
confirmAction({
|
| 1463 |
-
title: '
|
| 1464 |
-
message
|
| 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('
|
| 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) =>
|
|
|
|
|
|
|
|
|
|
| 1489 |
refreshWithdrawals();
|
| 1490 |
});
|
| 1491 |
});
|
|
@@ -1758,13 +1956,16 @@
|
|
| 1758 |
}
|
| 1759 |
|
| 1760 |
// -------------------- 🗂️ 提示词分类 --------------------
|
| 1761 |
-
const PCAT_TYPE_LABELS = { image: '
|
|
|
|
| 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 =
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 1816 |
confirmAction({
|
| 1817 |
-
title: '
|
| 1818 |
-
message:
|
| 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('
|
| 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) =>
|
|
|
|
|
|
|
|
|
|
| 1839 |
refreshPromptCats();
|
| 1840 |
});
|
| 1841 |
});
|
| 1842 |
const addBtn = $('#btn-pcat-add');
|
| 1843 |
const nameInput = $('#pcat-name');
|
| 1844 |
-
|
| 1845 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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('
|
| 1852 |
-
if (name.length > 8) { toast('
|
| 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)
|
| 1860 |
-
|
|
|
|
|
|
|
|
|
|
| 1861 |
await refreshPromptCats();
|
| 1862 |
} catch (err) {
|
| 1863 |
if (isStaleSessionError(err) || !finishBusy.isCurrent()) return;
|
| 1864 |
-
toast(err.message || '
|
| 1865 |
} finally {
|
| 1866 |
finishBusy();
|
| 1867 |
}
|
|
@@ -1901,7 +2130,16 @@
|
|
| 1901 |
}
|
| 1902 |
|
| 1903 |
// -------------------- 🏷️ 版本管理 --------------------
|
| 1904 |
-
const STAGE_LABELS = { alpha: '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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('
|
| 2109 |
confirmAction({
|
| 2110 |
-
title: '
|
| 2111 |
-
message:
|
| 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('
|
| 2120 |
}
|
| 2121 |
});
|
| 2122 |
});
|
|
@@ -2134,7 +2385,7 @@
|
|
| 2134 |
const draft = readBannerDraft();
|
| 2135 |
const revision = bannerRevision;
|
| 2136 |
if (draft.enabled && !draft.bannerImage) {
|
| 2137 |
-
toast('
|
| 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('
|
| 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 || '
|
| 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('
|
| 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>
|