重构 monorepo 并完善网页端订阅与首页体验

- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构
- 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐
- 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作
- 新增管理后台、支付与云端文件 API,更新 README 与项目规范

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-08 18:17:39 +08:00
parent 5814b7bc0e
commit 88c6ce8ccc
511 changed files with 189528 additions and 22804 deletions
@@ -0,0 +1,265 @@
(function () {
'use strict';
var TOKEN_KEY = 'soon_access';
var REFRESH_KEY = 'soon_refresh';
var refreshPromise = null;
var scopeControllers = {};
function getAdminBase() {
var cfg = window.SOON_DEPLOY_CONFIG || {};
return (cfg.api_admin_base || '').replace(/\/+$/, '');
}
function getV1Base() {
var cfg = window.SOON_DEPLOY_CONFIG || {};
return (cfg.api_v1_base || '').replace(/\/+$/, '');
}
function getToken() {
try {
return localStorage.getItem(TOKEN_KEY) || '';
} catch (e) {
return '';
}
}
function getRefreshToken() {
try {
return localStorage.getItem(REFRESH_KEY) || '';
} catch (e) {
return '';
}
}
function clearToken() {
try {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_KEY);
} catch (e) { /* ignore */ }
}
function loginRedirectUrl() {
var hash = location.hash || '';
var redirect = 'index.html' + hash;
return 'login.html?redirect=' + encodeURIComponent(redirect);
}
function goLogin() {
window.location.href = loginRedirectUrl();
}
function apiUrl(path) {
var p = String(path || '').replace(/^\//, '');
return getAdminBase() + '/' + p;
}
function defaultScope() {
if (window.AdminRouter && typeof AdminRouter.currentTab === 'function') {
return AdminRouter.currentTab() || 'app';
}
return 'app';
}
function abortScope(scope) {
if (!scope) return;
var ctrl = scopeControllers[scope];
if (!ctrl) return;
ctrl.abort();
delete scopeControllers[scope];
}
function attachScope(scope) {
abortScope(scope);
var ctrl = new AbortController();
scopeControllers[scope] = ctrl;
return ctrl.signal;
}
function isAbortError(err) {
return err && (err.name === 'AbortError' || err.code === 20);
}
function refreshAccessToken() {
if (refreshPromise) return refreshPromise;
var refresh = getRefreshToken();
if (!refresh) {
return Promise.reject(new Error('no_refresh_token'));
}
refreshPromise = window.fetch(getV1Base() + '/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refresh }),
}).then(function (r) {
return r.json().then(function (j) {
if (!r.ok || !j.ok || !j.data || !j.data.access_token) {
throw new Error(j.message || 'refresh_failed');
}
localStorage.setItem(TOKEN_KEY, j.data.access_token);
if (j.data.refresh_token) {
localStorage.setItem(REFRESH_KEY, j.data.refresh_token);
}
return j.data.access_token;
});
}).finally(function () {
refreshPromise = null;
});
return refreshPromise;
}
function sessionExpired() {
clearToken();
if (typeof soonToast === 'function') soonToast('登录已过期,请重新登录', 'warn');
setTimeout(goLogin, 600);
}
async function readJsonResponse(r, signal) {
if (signal && signal.aborted) return null;
var j;
try {
j = await r.json();
} catch (e) {
if (isAbortError(e)) return null;
if (typeof soonToast === 'function') soonToast('响应解析失败', 'error');
return null;
}
if (signal && signal.aborted) return null;
return j;
}
async function httpRequest(url, opts) {
opts = opts || {};
var signal = opts.signal;
var headers = Object.assign({
Authorization: 'Bearer ' + getToken(),
'Content-Type': 'application/json',
}, opts.headers || {});
var reqOpts = Object.assign({}, opts, { headers: headers, signal: signal });
delete reqOpts.scope;
delete reqOpts.retry401;
try {
return await window.fetch(url, reqOpts);
} catch (e) {
if (isAbortError(e)) return null;
throw e;
}
}
async function requestWithAuth(url, opts) {
opts = opts || {};
var signal = opts.signal;
var r = await httpRequest(url, opts);
if (!r) return null;
if (r.status === 401 && !opts.retry401) {
try {
await refreshAccessToken();
} catch (e) {
sessionExpired();
return null;
}
var retryOpts = Object.assign({}, opts, { retry401: true });
retryOpts.headers = Object.assign({}, opts.headers || {}, {
Authorization: 'Bearer ' + getToken(),
});
r = await httpRequest(url, retryOpts);
if (!r) return null;
if (r.status === 401) {
sessionExpired();
return null;
}
}
if (r.status === 403) {
var forbidden = await readJsonResponse(r, signal);
if (forbidden && typeof soonToast === 'function') {
soonToast(forbidden.message || '无权限执行此操作', 'warn');
}
return null;
}
return r;
}
async function fetch(path, opts) {
opts = opts || {};
var scope = opts.scope || defaultScope();
var signal = opts.signal || attachScope(scope);
var r;
try {
r = await requestWithAuth(apiUrl(path), Object.assign({}, opts, { signal: signal }));
} catch (e) {
if (isAbortError(e)) return null;
if (typeof soonToast === 'function') soonToast('网络错误,请稍后重试', 'error');
return null;
}
if (!r || (signal && signal.aborted)) return null;
var j = await readJsonResponse(r, signal);
if (!j || (signal && signal.aborted)) return null;
if (!j.ok) {
if (typeof soonToast === 'function') soonToast(j.message || '操作失败', 'error');
return null;
}
return j;
}
async function get(path, opts) {
return fetch(path, Object.assign({}, opts || {}, { method: 'GET' }));
}
async function postJson(path, body, opts) {
return fetch(path, Object.assign({}, opts || {}, {
method: 'POST',
body: JSON.stringify(body || {}),
}));
}
async function putJson(path, body, opts) {
return fetch(path, Object.assign({}, opts || {}, {
method: 'PUT',
body: JSON.stringify(body || {}),
}));
}
async function del(path, opts) {
return fetch(path, Object.assign({}, opts || {}, { method: 'DELETE' }));
}
async function fetchMe() {
if (!getToken()) return null;
var signal = attachScope('auth');
var r;
try {
r = await requestWithAuth(getV1Base() + '/auth/me', {
method: 'GET',
signal: signal,
headers: { 'Content-Type': 'application/json' },
});
} catch (e) {
if (isAbortError(e)) return null;
return null;
}
if (!r || signal.aborted) return null;
var j = await readJsonResponse(r, signal);
if (!j || signal.aborted) return null;
if (!j.ok || !j.data) return null;
return j.data;
}
window.AdminApi = {
getToken: getToken,
clearToken: clearToken,
goLogin: goLogin,
getV1Base: getV1Base,
fetch: fetch,
get: get,
postJson: postJson,
putJson: putJson,
del: del,
fetchMe: fetchMe,
abortScope: abortScope,
refreshAccessToken: refreshAccessToken,
};
})();
@@ -0,0 +1,124 @@
(function () {
'use strict';
var STATUS = {
active: '正常',
disabled: '已停用',
pending: '待支付',
paid: '已支付',
cancelled: '已取消',
refunded: '已退款',
expired: '已过期',
};
var REFUND_STATUS = {
none: '无申请',
pending: '待审核',
approved: '已通过',
rejected: '已拒绝',
};
var ROLE = {
user: '普通用户',
admin: '管理员',
};
var CHANNEL = {
alipay: '支付宝',
wechat: '微信支付',
};
var ACTION = {
'stats.summary': '查看概览',
'users.list': '浏览用户列表',
'users.create': '创建用户',
'users.update': '编辑用户',
'users.delete': '删除用户',
'users.detail': '查看用户详情',
'users.setPassword': '重置用户密码',
'users.setSubscription': '设置用户会员',
'users.setStatus': '变更用户状态',
'orders.list': '浏览订单列表',
'orders.detail': '查看订单详情',
'orders.cancel': '取消订单',
'orders.refundApprove': '通过退款',
'orders.refundReject': '拒绝退款',
'plans.list': '浏览套餐列表',
'plans.upsert': '编辑套餐',
'settings.list': '浏览系统设置',
'settings.set': '修改系统设置',
'settings.set_batch': '批量修改系统设置',
'payment.key.upload': '上传支付密钥',
'payment.key.status': '查看支付密钥',
};
var STATUS_BADGE = {
active: 'soon-badge--active',
disabled: 'soon-badge--disabled',
pending: 'soon-badge--pending',
paid: 'soon-badge--paid',
cancelled: 'soon-badge--disabled',
refunded: 'soon-badge--pending',
expired: 'soon-badge--disabled',
};
var REFUND_BADGE = {
none: 'soon-badge--disabled',
pending: 'soon-badge--pending',
approved: 'soon-badge--paid',
rejected: 'soon-badge--disabled',
};
function t(map, v) {
if (v == null || v === '') return '-';
return map[v] || String(v);
}
function esc(s) {
if (typeof soonEscapeHtml === 'function') return soonEscapeHtml(s);
if (s == null) return '';
return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function badgeStatus(v) {
var cls = STATUS_BADGE[v] || 'soon-badge--pending';
return '<span class="soon-badge ' + cls + '">' + esc(t(STATUS, v)) + '</span>';
}
function badgeRefundStatus(v) {
var key = v || 'none';
var cls = REFUND_BADGE[key] || 'soon-badge--disabled';
return '<span class="soon-badge ' + cls + '">' + esc(t(REFUND_STATUS, key)) + '</span>';
}
function formatCents(n) {
var c = parseInt(n, 10) || 0;
return '¥' + (c / 100).toFixed(2);
}
function formatBytes(n) {
var b = parseInt(n, 10) || 0;
if (b < 1024) return b + ' B';
if (b < 1024 * 1024) return (b / 1024).toFixed(1) + ' KB';
return (b / (1024 * 1024)).toFixed(2) + ' MB';
}
function formatDateTime(s) {
if (!s) return '-';
return String(s).replace('T', ' ').slice(0, 19);
}
window.AdminI18n = {
tStatus: function (v) { return t(STATUS, v); },
tRole: function (v) { return t(ROLE, v); },
tChannel: function (v) { return t(CHANNEL, v); },
tAction: function (v) { return t(ACTION, v); },
badgeStatus: badgeStatus,
badgeRefundStatus: badgeRefundStatus,
tRefundStatus: function (v) { return t(REFUND_STATUS, v || 'none'); },
formatCents: formatCents,
formatBytes: formatBytes,
formatDateTime: formatDateTime,
esc: esc,
};
})();
@@ -0,0 +1,44 @@
(function () {
'use strict';
var FULL_ONLY = { settings: true, audits: true, payment: true };
function normalizeLevel(level) {
return level === 'ops' ? 'ops' : 'full';
}
function canAccessTab(tab, level) {
level = normalizeLevel(level);
if (level === 'full') return true;
return !FULL_ONLY[tab];
}
function defaultTab(level) {
return 'dashboard';
}
function applyNav(level) {
level = normalizeLevel(level);
document.querySelectorAll('.soon-admin-nav-item').forEach(function (el) {
var req = el.getAttribute('data-require') || 'ops';
var show = req !== 'full' || level === 'full';
el.style.display = show ? '' : 'none';
});
}
function filterQuickLinks(links, level) {
level = normalizeLevel(level);
return links.filter(function (l) {
var tab = (l.href || '').replace(/^#\//, '').split(/[/?]/)[0];
return canAccessTab(tab, level);
});
}
window.AdminPermissions = {
canAccessTab: canAccessTab,
defaultTab: defaultTab,
applyNav: applyNav,
filterQuickLinks: filterQuickLinks,
normalizeLevel: normalizeLevel,
};
})();
@@ -0,0 +1,297 @@
(function () {
'use strict';
var State = function () { return window.AdminState; };
var Perms = function () { return window.AdminPermissions; };
var ROUTES = {
'': 'dashboard',
'/': 'dashboard',
'/dashboard': 'dashboard',
'/users': 'users',
'/orders': 'orders',
'/plans': 'plans',
'/settings': 'settings',
'/audits': 'audits',
'/payment': 'payment',
};
var TAB_TO_PATH = {
dashboard: '/dashboard',
users: '/users',
orders: '/orders',
plans: '/plans',
settings: '/settings',
audits: '/audits',
payment: '/payment',
};
var DETAIL_TABS = { users: true, orders: true };
var FILTER_DEFS = {
users: {
stateKey: 'users',
defaults: { page: 1, size: 20, q: '' },
fields: {
q: 'string',
page: 'int',
size: 'int',
},
},
orders: {
stateKey: 'orders',
defaults: { page: 1, size: 20, q: '', status: '', refund_status: '', channel: '', from: '', to: '' },
fields: {
q: 'string',
page: 'int',
size: 'int',
status: 'string',
refund_status: 'string',
channel: 'string',
from: 'string',
to: 'string',
},
},
audits: {
stateKey: 'audits',
defaults: { page: 1, size: 50, q: '', from: '', to: '', action: '' },
fields: {
q: 'string',
page: 'int',
size: 'int',
from: 'string',
to: 'string',
action: 'string',
},
},
settings: {
stateKey: 'settings',
defaults: { tab: 'site', q: '' },
fields: {
tab: 'string',
q: 'string',
},
},
};
var handlers = {};
var currentTab = null;
var pendingDetailId = null;
var suppressHash = false;
function parseQuery(qs) {
var out = {};
if (!qs) return out;
qs.split('&').forEach(function (pair) {
if (!pair) return;
var idx = pair.indexOf('=');
var k = idx >= 0 ? pair.slice(0, idx) : pair;
var v = idx >= 0 ? pair.slice(idx + 1) : '';
try {
k = decodeURIComponent(k.replace(/\+/g, ' '));
v = decodeURIComponent(v.replace(/\+/g, ' '));
} catch (e) { /* keep raw */ }
if (k) out[k] = v;
});
return out;
}
function parseHash() {
var raw = (location.hash || '#/dashboard').replace(/^#/, '');
if (!raw.startsWith('/')) raw = '/' + raw;
var qIdx = raw.indexOf('?');
var pathStr = qIdx >= 0 ? raw.substring(0, qIdx) : raw;
var query = parseQuery(qIdx >= 0 ? raw.substring(qIdx + 1) : '');
var segments = pathStr.split('/').filter(function (s) { return s; });
var head = segments[0] || 'dashboard';
var tab = ROUTES['/' + head] || 'dashboard';
var id = null;
if (segments[1] && DETAIL_TABS[tab]) {
var n = parseInt(segments[1], 10);
if (!isNaN(n) && n > 0) id = n;
}
return { tab: tab, id: id, query: query };
}
function coerceField(type, val) {
if (type === 'int') {
var n = parseInt(val, 10);
return isNaN(n) ? null : n;
}
return val == null ? '' : String(val);
}
function applyUrlToState(loc) {
loc = loc || parseHash();
Object.keys(FILTER_DEFS).forEach(function (tab) {
if (loc.tab !== tab) return;
var def = FILTER_DEFS[tab];
var st = State()[def.stateKey];
if (!st) return;
Object.keys(def.defaults).forEach(function (key) {
st[key] = def.defaults[key];
});
Object.keys(def.fields).forEach(function (key) {
if (!(key in loc.query)) return;
var val = coerceField(def.fields[key], loc.query[key]);
if (val == null) return;
if (def.fields[key] === 'int' && val <= 0) return;
st[key] = val;
});
if (tab === 'settings' && st.tab !== 'site' && st.tab !== 'all') {
st.tab = 'site';
}
});
}
function shouldIncludeParam(key, val, defVal) {
if (val == null || val === '') return false;
if (typeof defVal === 'number') return val !== defVal;
return String(val) !== String(defVal);
}
function buildHash(tab, opts) {
opts = opts || {};
var path = TAB_TO_PATH[tab] || '/dashboard';
if (opts.id && DETAIL_TABS[tab]) {
path += '/' + opts.id;
}
var def = FILTER_DEFS[tab];
var qs = '';
if (def) {
var st = State()[def.stateKey] || {};
var parts = [];
Object.keys(def.fields).forEach(function (key) {
var val = st[key];
if (shouldIncludeParam(key, val, def.defaults[key])) {
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(val)));
}
});
if (parts.length) qs = '?' + parts.join('&');
}
return '#' + path + qs;
}
function setHash(hash) {
if (location.hash === hash) return;
suppressHash = true;
location.hash = hash;
suppressHash = false;
}
function showTab(name, loc) {
var level = State().adminLevel || 'full';
if (Perms() && !Perms().canAccessTab(name, level)) {
name = Perms().defaultTab(level);
var safeHash = buildHash(name);
if (location.hash !== safeHash) {
setHash(safeHash);
return;
}
}
if (!handlers[name]) name = 'dashboard';
var prevTab = currentTab;
if (prevTab && prevTab !== name && window.AdminApi && typeof AdminApi.abortScope === 'function') {
AdminApi.abortScope(prevTab);
}
currentTab = name;
pendingDetailId = loc && loc.id ? loc.id : null;
document.querySelectorAll('.panel').forEach(function (p) {
p.classList.remove('is-visible');
});
var panel = document.getElementById('panel-' + name);
if (panel) panel.classList.add('is-visible');
document.querySelectorAll('.soon-admin-nav-item').forEach(function (li) {
li.classList.toggle('is-active', li.dataset.tab === name);
});
if (handlers[name] && typeof handlers[name].render === 'function') {
handlers[name].render();
}
}
function onHashChange() {
if (suppressHash) return;
var loc = parseHash();
applyUrlToState(loc);
showTab(loc.tab, loc);
}
function navigate(tab) {
pendingDetailId = null;
var hash = buildHash(tab);
if (location.hash === hash) {
showTab(tab, parseHash());
} else {
setHash(hash);
}
}
function syncQuery(opts) {
if (!currentTab) return;
var hash = buildHash(currentTab, opts || {});
setHash(hash);
}
function openDetail(id) {
if (!currentTab || !DETAIL_TABS[currentTab]) return;
var n = parseInt(id, 10);
if (isNaN(n) || n <= 0) return;
setHash(buildHash(currentTab, { id: n }));
}
function closeDetail() {
if (!currentTab || !DETAIL_TABS[currentTab]) return;
var loc = parseHash();
if (!loc.id) return;
pendingDetailId = null;
setHash(buildHash(currentTab));
}
function hasPendingDetail() {
return pendingDetailId != null;
}
function consumeDetailId() {
var id = pendingDetailId;
pendingDetailId = null;
return id;
}
function onDrawerClosed() {
closeDetail();
}
function init(viewHandlers) {
handlers = viewHandlers || {};
window.addEventListener('hashchange', onHashChange);
document.querySelectorAll('.soon-admin-nav-item').forEach(function (li) {
li.onclick = function () {
navigate(li.dataset.tab);
};
});
if (!location.hash) {
setHash('#/dashboard');
}
var loc = parseHash();
applyUrlToState(loc);
showTab(loc.tab, loc);
}
window.AdminRouter = {
init: init,
navigate: navigate,
syncQuery: syncQuery,
openDetail: openDetail,
closeDetail: closeDetail,
hasPendingDetail: hasPendingDetail,
consumeDetailId: consumeDetailId,
onDrawerClosed: onDrawerClosed,
currentTab: function () { return currentTab; },
};
})();
@@ -0,0 +1,14 @@
(function () {
'use strict';
// 分页与 URL 状态:docs/API-PAGINATION.md、AdminRouter
window.AdminState = {
currentAdminId: null,
adminLevel: 'full',
users: { page: 1, size: 20, q: '' },
orders: { page: 1, size: 20, q: '', status: '', refund_status: '', channel: '', from: '', to: '' },
audits: { page: 1, size: 50, q: '', from: '', to: '', action: '' },
settings: { tab: 'site', q: '' },
};
})();
@@ -0,0 +1,246 @@
(function () {
'use strict';
var I = function () { return window.AdminI18n; };
function pageHeader(title, desc, actionsHtml) {
return '<div class="soon-admin-page-header">' +
'<div class="soon-admin-page-header__main">' +
'<h1>' + I().esc(title) + '</h1>' +
(desc ? '<p class="soon-admin-page-header__desc">' + I().esc(desc) + '</p>' : '') +
'</div>' +
(actionsHtml ? '<div class="soon-admin-page-header__actions">' + actionsHtml + '</div>' : '') +
'</div>';
}
function dataTable(headers, rowsHtml, colSpan) {
var th = headers.map(function (h) { return '<th>' + h + '</th>'; }).join('');
var empty = '<tr><td colspan="' + (colSpan || headers.length) + '" class="soon-admin-empty-row">' +
emptyBlock('暂无数据', '', '') + '</td></tr>';
return '<div class="soon-admin-table-wrap"><table class="soon-admin-table"><thead><tr>' + th +
'</tr></thead><tbody>' + (rowsHtml || empty) + '</tbody></table></div>';
}
function emptyBlock(title, hint, actionHtml) {
var html = '<div class="soon-empty soon-empty--inline">' +
'<img src="../../assets/images/web/admin/empty.svg" alt="" class="soon-admin-empty-icon">' +
'<p class="soon-empty__title">' + I().esc(title) + '</p>';
if (hint) html += '<p class="soon-empty__hint">' + I().esc(hint) + '</p>';
if (actionHtml) html += actionHtml;
html += '</div>';
return html;
}
function skeletonStats(n) {
var c = n || 4;
var html = '<div class="soon-admin-stat-grid">';
for (var i = 0; i < c; i++) {
html += '<div class="soon-skeleton soon-skeleton--h80"></div>';
}
return html + '</div>';
}
function skeletonTable() {
return '<div class="soon-skeleton soon-skeleton--h120"></div>';
}
function pagerHtml(page, size, total) {
if (window.AdminPager) {
return AdminPager.html({ page: page, size: size, total: total });
}
page = page || 1;
size = size || 20;
total = total || 0;
var pages = Math.max(1, Math.ceil(total / size));
return '<div class="soon-admin-pager">' +
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost soon-pager-prev"' +
(page <= 1 ? ' disabled' : '') + '>上一页</button>' +
'<span class="soon-admin-pager__info">第 ' + page + ' / ' + pages + ' 页(共 ' + total + ' 条)</span>' +
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost soon-pager-next"' +
(page >= pages ? ' disabled' : '') + '>下一页</button></div>';
}
function bindPager(container, page, size, total, onPageChange) {
if (!container) return;
if (window.AdminPager) {
AdminPager.bind(container, { page: page, size: size, total: total }, function (chg) {
if (chg.size && chg.size !== size) {
onPageChange(1, chg.size);
} else if (chg.page) {
onPageChange(chg.page, size);
}
});
return;
}
var prev = container.querySelector('.soon-pager-prev');
var next = container.querySelector('.soon-pager-next');
if (prev && !prev.disabled) {
prev.onclick = function () { onPageChange(Math.max(1, page - 1)); };
}
if (next && !next.disabled) {
next.onclick = function () {
var pages = Math.max(1, Math.ceil(total / size));
onPageChange(Math.min(pages, page + 1));
};
}
}
function confirm(msg, onOk) {
if (typeof layer === 'undefined') {
if (window.confirm(msg)) onOk();
return;
}
layer.confirm(msg, { skin: 'soon-layer', title: '确认' }, function (idx) {
layer.close(idx);
onOk();
});
}
function openDrawer(title, bodyHtml) {
closeDrawer();
var overlay = document.createElement('div');
overlay.className = 'soon-admin-drawer-overlay';
overlay.id = 'adminDrawerOverlay';
overlay.onclick = closeDrawer;
var drawer = document.createElement('div');
drawer.className = 'soon-admin-drawer';
drawer.id = 'adminDrawer';
drawer.innerHTML =
'<div class="soon-admin-drawer__header">' +
'<h3>' + I().esc(title) + '</h3>' +
'<button type="button" class="soon-admin-drawer__close" aria-label="关闭">&times;</button>' +
'</div><div class="soon-admin-drawer__body">' + bodyHtml + '</div>';
drawer.querySelector('.soon-admin-drawer__close').onclick = closeDrawer;
document.body.appendChild(overlay);
document.body.appendChild(drawer);
}
function closeDrawer() {
var o = document.getElementById('adminDrawerOverlay');
var d = document.getElementById('adminDrawer');
if (o) o.remove();
if (d) d.remove();
if (window.AdminRouter && typeof AdminRouter.onDrawerClosed === 'function') {
AdminRouter.onDrawerClosed();
}
}
function runFormSubmit(btn, handler, opts) {
opts = opts || {};
if (!btn || btn.disabled) return;
var ret;
try {
ret = handler();
} catch (e) {
return;
}
if (ret === false) return;
if (ret && typeof ret.then === 'function') {
setBtnLoading(btn, true, opts.loadingLabel);
ret.finally(function () {
setBtnLoading(btn, false);
});
}
}
function openFormModal(title, formHtml, onSave, opts) {
if (typeof layer === 'undefined') return;
opts = opts || {};
layer.open({
type: 1,
skin: 'soon-layer',
title: title,
area: opts.area || ['420px', 'auto'],
maxWidth: opts.maxWidth || '96vw',
content: formHtml,
success: function (layero) {
var area = opts.area || ['420px', 'auto'];
if (area[1] && area[1] !== 'auto') {
var contentEl = null;
if (layero && typeof layero.find === 'function') {
contentEl = layero.find('.layui-layer-content')[0];
} else if (layero && layero.querySelector) {
contentEl = layero.querySelector('.layui-layer-content');
}
if (contentEl) contentEl.classList.add('soon-layer-content--scroll');
}
var btn = document.getElementById('adminModalSave');
if (btn) {
btn.onclick = function () {
runFormSubmit(btn, onSave, opts);
};
}
if (typeof opts.success === 'function') opts.success();
},
});
}
function bindFormSubmit(btnOrId, handler, loadingLabel) {
var btn = typeof btnOrId === 'string' ? document.getElementById(btnOrId) : btnOrId;
if (!btn) return;
btn.onclick = function () {
runFormSubmit(btn, handler, { loadingLabel: loadingLabel });
};
}
function detailSection(title, rowsHtml) {
return '<div class="soon-admin-detail-section"><h4>' + I().esc(title) + '</h4>' + rowsHtml + '</div>';
}
function detailRow(label, value) {
return '<div class="soon-admin-detail-row"><span class="soon-admin-detail-row__label">' +
I().esc(label) + '</span><span class="soon-admin-detail-row__value">' + value + '</span></div>';
}
function filterDateField(id, label, value) {
return '<div class="soon-admin-filter-field">' +
'<span class="soon-admin-filter-field__label">' + I().esc(label) + '</span>' +
'<span class="soon-admin-date-wrap">' +
'<input type="date" id="' + id + '" class="soon-input soon-input--sm soon-input--date" value="' + I().esc(value || '') + '">' +
'</span></div>';
}
function listError(message, retryBtnId) {
return '<div class="soon-admin-list-error">' +
'<p>' + I().esc(message || '加载失败') + '</p>' +
'<button type="button" class="soon-btn soon-btn--sm" id="' + (retryBtnId || 'listRetry') + '">重试</button></div>';
}
function setBtnLoading(btn, loading, label) {
if (!btn) return;
if (loading) {
if (!btn.dataset.soonLabel) btn.dataset.soonLabel = btn.textContent;
btn.disabled = true;
btn.textContent = label || '处理中…';
} else {
btn.disabled = false;
if (btn.dataset.soonLabel) btn.textContent = btn.dataset.soonLabel;
}
}
function el(id) { return document.getElementById(id); }
window.AdminUi = {
pageHeader: pageHeader,
dataTable: dataTable,
emptyBlock: emptyBlock,
skeletonStats: skeletonStats,
skeletonTable: skeletonTable,
pagerHtml: pagerHtml,
bindPager: bindPager,
confirm: confirm,
openDrawer: openDrawer,
closeDrawer: closeDrawer,
openFormModal: openFormModal,
runFormSubmit: runFormSubmit,
bindFormSubmit: bindFormSubmit,
detailSection: detailSection,
detailRow: detailRow,
filterDateField: filterDateField,
listError: listError,
setBtnLoading: setBtnLoading,
el: el,
};
})();