重构 monorepo 并完善网页端订阅与首页体验
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构 - 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐 - 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作 - 新增管理后台、支付与云端文件 API,更新 README 与项目规范 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var I = function () { return window.AdminI18n; };
|
||||
|
||||
function bar(innerHtml, actionsHtml) {
|
||||
return '<div class="soon-admin-filter-bar">' +
|
||||
'<div class="soon-admin-filter-bar__fields">' + innerHtml + '</div>' +
|
||||
(actionsHtml ? '<div class="soon-admin-filter-bar__actions">' + actionsHtml + '</div>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function field(label, inner) {
|
||||
return '<div class="soon-admin-filter-field"><span class="soon-admin-filter-field__label">' +
|
||||
I().esc(label) + '</span>' + inner + '</div>';
|
||||
}
|
||||
|
||||
function searchInput(id, value, placeholder) {
|
||||
return '<input type="search" id="' + id + '" class="soon-input soon-input--sm" value="' +
|
||||
I().esc(value || '') + '" placeholder="' + I().esc(placeholder || '搜索…') + '">';
|
||||
}
|
||||
|
||||
function select(id, optionsHtml) {
|
||||
return '<select id="' + id + '" class="soon-input soon-input--sm">' + optionsHtml + '</select>';
|
||||
}
|
||||
|
||||
window.AdminFilter = { bar: bar, field: field, searchInput: searchInput, select: select };
|
||||
})();
|
||||
@@ -0,0 +1,53 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var I = function () { return window.AdminI18n; };
|
||||
|
||||
function field(label, inner, hint) {
|
||||
var html = '<div class="soon-form-field"><label class="soon-form-label">' + I().esc(label) + '</label>' + inner;
|
||||
if (hint) {
|
||||
html += '<p class="soon-admin-form-hint">' + I().esc(hint) + '</p>';
|
||||
}
|
||||
return html + '</div>';
|
||||
}
|
||||
|
||||
function input(id, opts) {
|
||||
opts = opts || {};
|
||||
var type = opts.type || 'text';
|
||||
var cls = 'soon-input soon-input--sm' + (opts.className ? ' ' + opts.className : '');
|
||||
var attrs = 'id="' + id + '" class="' + cls + '"';
|
||||
if (opts.value != null) attrs += ' value="' + I().esc(opts.value) + '"';
|
||||
if (opts.placeholder) attrs += ' placeholder="' + I().esc(opts.placeholder) + '"';
|
||||
if (opts.disabled) attrs += ' disabled';
|
||||
if (opts.autocomplete) attrs += ' autocomplete="' + I().esc(opts.autocomplete) + '"';
|
||||
if (type === 'textarea') {
|
||||
return '<textarea ' + attrs.replace('value="' + I().esc(opts.value) + '"', '') + ' rows="' + (opts.rows || 4) + '">' +
|
||||
I().esc(opts.value || '') + '</textarea>';
|
||||
}
|
||||
return '<input type="' + type + '" ' + attrs + '>';
|
||||
}
|
||||
|
||||
function select(id, optionsHtml) {
|
||||
return '<select id="' + id + '" class="soon-input soon-input--sm">' + optionsHtml + '</select>';
|
||||
}
|
||||
|
||||
function actions(primaryLabel, primaryId, secondaryHtml) {
|
||||
return '<div class="soon-admin-form__actions">' +
|
||||
(secondaryHtml || '') +
|
||||
'<button type="button" class="soon-btn soon-btn--sm" id="' + (primaryId || 'adminFormSave') + '">' +
|
||||
I().esc(primaryLabel || '保存') + '</button></div>';
|
||||
}
|
||||
|
||||
function panel(title, bodyHtml) {
|
||||
return '<div class="soon-admin-form-panel"><h3 class="soon-admin-form-panel__title">' + I().esc(title) +
|
||||
'</h3><div class="soon-admin-form-panel__body">' + bodyHtml + '</div></div>';
|
||||
}
|
||||
|
||||
window.AdminForm = {
|
||||
field: field,
|
||||
input: input,
|
||||
select: select,
|
||||
actions: actions,
|
||||
panel: panel,
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,168 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var I = function () { return window.AdminI18n; };
|
||||
|
||||
function itemDataset(it) {
|
||||
if (!it.dataset) return '';
|
||||
return Object.keys(it.dataset).map(function (k) {
|
||||
return ' data-' + k + '="' + I().esc(it.dataset[k]) + '"';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function menuItemBtn(it) {
|
||||
if (it.disabled) {
|
||||
return '<button type="button" class="soon-admin-action-menu__item" disabled title="' +
|
||||
I().esc(it.label) + '">' + I().esc(it.label) + '</button>';
|
||||
}
|
||||
var cls = 'soon-admin-action-menu__item' + (it.className ? ' ' + it.className : '');
|
||||
return '<button type="button" class="' + cls + '" data-action="' + I().esc(it.action) + '"' +
|
||||
itemDataset(it) + '>' + I().esc(it.label) + '</button>';
|
||||
}
|
||||
|
||||
function inlineBtn(it) {
|
||||
if (it.disabled) {
|
||||
return '<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost soon-admin-row-actions__btn" disabled title="' +
|
||||
I().esc(it.label) + '">' + I().esc(it.label) + '</button>';
|
||||
}
|
||||
return '<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost soon-admin-row-actions__btn" data-action="' +
|
||||
I().esc(it.action) + '"' + itemDataset(it) + '>' + I().esc(it.label) + '</button>';
|
||||
}
|
||||
|
||||
function positionFloatingPanel(menu) {
|
||||
var toggle = menu.querySelector('.soon-admin-action-menu__toggle');
|
||||
var panel = menu.querySelector('.soon-admin-action-menu__panel');
|
||||
if (!toggle || !panel) return;
|
||||
panel.classList.add('soon-admin-action-menu__panel--floating');
|
||||
panel.style.display = 'block';
|
||||
panel.style.visibility = 'hidden';
|
||||
var r = toggle.getBoundingClientRect();
|
||||
var panelW = panel.offsetWidth;
|
||||
var panelH = panel.offsetHeight;
|
||||
var gap = 4;
|
||||
var left = r.right - panelW;
|
||||
if (left < 8) left = 8;
|
||||
if (left + panelW > window.innerWidth - 8) left = window.innerWidth - panelW - 8;
|
||||
var top = r.bottom + gap;
|
||||
if (top + panelH > window.innerHeight - 8) {
|
||||
top = Math.max(8, r.top - panelH - gap);
|
||||
}
|
||||
panel.style.left = left + 'px';
|
||||
panel.style.top = top + 'px';
|
||||
panel.style.visibility = '';
|
||||
}
|
||||
|
||||
function actionMenu(menuId, items) {
|
||||
var btns = items.map(menuItemBtn).join('');
|
||||
return '<div class="soon-admin-action-menu" data-menu="' + I().esc(menuId) + '">' +
|
||||
'<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost soon-admin-action-menu__toggle">操作 ▾</button>' +
|
||||
'<div class="soon-admin-action-menu__panel">' + btns + '</div></div>';
|
||||
}
|
||||
|
||||
function rowActions(menuId, inlineItems, moreItems) {
|
||||
var inline = (inlineItems || []).map(inlineBtn).join('');
|
||||
var more = (moreItems || []).map(menuItemBtn).join('');
|
||||
if (!more) {
|
||||
return '<div class="soon-admin-row-actions" data-menu="' + I().esc(menuId) + '">' + inline + '</div>';
|
||||
}
|
||||
return '<div class="soon-admin-row-actions" data-menu="' + I().esc(menuId) + '">' + inline +
|
||||
'<div class="soon-admin-action-menu soon-admin-action-menu--more">' +
|
||||
'<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost soon-admin-action-menu__toggle">更多 ▾</button>' +
|
||||
'<div class="soon-admin-action-menu__panel">' + more + '</div></div></div>';
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
document.querySelectorAll('.soon-admin-action-menu.is-open').forEach(function (m) {
|
||||
m.classList.remove('is-open');
|
||||
resetPanelStyle(m.querySelector('.soon-admin-action-menu__panel'));
|
||||
});
|
||||
}
|
||||
|
||||
function bindActionButtons(container, handler, selector) {
|
||||
container.querySelectorAll(selector).forEach(function (btn) {
|
||||
if (btn.disabled) return;
|
||||
btn.onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
closeAll();
|
||||
if (typeof handler === 'function') handler(btn.dataset.action, btn);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function resetPanelStyle(panel) {
|
||||
if (!panel) return;
|
||||
panel.style.display = '';
|
||||
panel.style.left = '';
|
||||
panel.style.top = '';
|
||||
panel.style.visibility = '';
|
||||
}
|
||||
|
||||
function bindHoverMore(container) {
|
||||
container.querySelectorAll('.soon-admin-action-menu--more').forEach(function (menu) {
|
||||
var hideTimer = null;
|
||||
var panel = menu.querySelector('.soon-admin-action-menu__panel');
|
||||
function openMenu() {
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = null;
|
||||
}
|
||||
closeAll();
|
||||
positionFloatingPanel(menu);
|
||||
menu.classList.add('is-open');
|
||||
}
|
||||
function scheduleClose() {
|
||||
hideTimer = setTimeout(function () {
|
||||
menu.classList.remove('is-open');
|
||||
resetPanelStyle(panel);
|
||||
}, 180);
|
||||
}
|
||||
function cancelClose() {
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = null;
|
||||
}
|
||||
}
|
||||
menu.addEventListener('mouseenter', openMenu);
|
||||
menu.addEventListener('mouseleave', scheduleClose);
|
||||
if (panel) {
|
||||
panel.addEventListener('mouseenter', cancelClose);
|
||||
panel.addEventListener('mouseleave', scheduleClose);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bind(container, handler) {
|
||||
if (!container) return;
|
||||
bindActionButtons(container, handler, '.soon-admin-row-actions__btn[data-action]');
|
||||
container.querySelectorAll('.soon-admin-action-menu__toggle').forEach(function (btn) {
|
||||
var menu = btn.closest('.soon-admin-action-menu');
|
||||
if (menu && menu.classList.contains('soon-admin-action-menu--more')) return;
|
||||
btn.onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
var wasOpen = menu.classList.contains('is-open');
|
||||
closeAll();
|
||||
if (!wasOpen) {
|
||||
positionFloatingPanel(menu);
|
||||
menu.classList.add('is-open');
|
||||
}
|
||||
};
|
||||
});
|
||||
bindActionButtons(container, handler, '.soon-admin-action-menu__item[data-action]');
|
||||
bindHoverMore(container);
|
||||
}
|
||||
|
||||
if (!window._soonAdminMenuDocBound) {
|
||||
window._soonAdminMenuDocBound = true;
|
||||
document.addEventListener('click', closeAll);
|
||||
window.addEventListener('scroll', closeAll, true);
|
||||
window.addEventListener('resize', closeAll);
|
||||
}
|
||||
|
||||
window.AdminMenu = {
|
||||
actionMenu: actionMenu,
|
||||
rowActions: rowActions,
|
||||
bind: bind,
|
||||
closeAll: closeAll,
|
||||
positionFloatingPanel: positionFloatingPanel,
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,83 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var SIZE_OPTIONS = [10, 20, 50, 100];
|
||||
|
||||
function pagesCount(total, size) {
|
||||
return Math.max(1, Math.ceil((total || 0) / (size || 20)));
|
||||
}
|
||||
|
||||
function html(opts) {
|
||||
opts = opts || {};
|
||||
var page = opts.page || 1;
|
||||
var size = opts.size || 20;
|
||||
var total = opts.total || 0;
|
||||
var pages = pagesCount(total, size);
|
||||
var sizes = opts.sizeOptions || SIZE_OPTIONS;
|
||||
if (total === 0) {
|
||||
return '<div class="soon-admin-pager soon-admin-pager--empty"><span class="soon-admin-pager__info">共 0 条</span></div>';
|
||||
}
|
||||
var sizeOpts = sizes.map(function (n) {
|
||||
return '<option value="' + n + '"' + (n === size ? ' selected' : '') + '>' + n + ' 条/页</option>';
|
||||
}).join('');
|
||||
return '<div class="soon-admin-pager" data-page="' + page + '" data-size="' + size + '" data-total="' + total + '">' +
|
||||
'<div class="soon-admin-pager__left">' +
|
||||
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost soon-pager-first"' +
|
||||
(page <= 1 ? ' disabled' : '') + ' title="首页">«</button>' +
|
||||
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost soon-pager-prev"' +
|
||||
(page <= 1 ? ' disabled' : '') + ' title="上一页">‹</button>' +
|
||||
'<span class="soon-admin-pager__info">第 <strong>' + page + '</strong> / ' + pages + ' 页,共 ' + total + ' 条</span>' +
|
||||
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost soon-pager-next"' +
|
||||
(page >= pages ? ' disabled' : '') + ' title="下一页">›</button>' +
|
||||
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost soon-pager-last"' +
|
||||
(page >= pages ? ' disabled' : '') + ' title="末页">»</button>' +
|
||||
'</div>' +
|
||||
'<div class="soon-admin-pager__right">' +
|
||||
'<label class="soon-admin-pager__size-label">每页<select class="soon-input soon-input--sm soon-pager-size">' +
|
||||
sizeOpts + '</select></label>' +
|
||||
'<label class="soon-admin-pager__jump-label">跳至<input type="number" class="soon-input soon-input--sm soon-pager-jump" min="1" max="' +
|
||||
pages + '" value="' + page + '">页<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost soon-pager-go">GO</button></label>' +
|
||||
'</div></div>';
|
||||
}
|
||||
|
||||
function bind(container, state, onChange) {
|
||||
if (!container) return;
|
||||
var page = state.page || 1;
|
||||
var size = state.size || 20;
|
||||
var total = state.total || 0;
|
||||
var pages = pagesCount(total, size);
|
||||
|
||||
function go(p) {
|
||||
p = Math.max(1, Math.min(pages, p));
|
||||
if (p !== page) onChange({ page: p });
|
||||
}
|
||||
|
||||
var first = container.querySelector('.soon-pager-first');
|
||||
var prev = container.querySelector('.soon-pager-prev');
|
||||
var next = container.querySelector('.soon-pager-next');
|
||||
var last = container.querySelector('.soon-pager-last');
|
||||
var sizeSel = container.querySelector('.soon-pager-size');
|
||||
var jump = container.querySelector('.soon-pager-jump');
|
||||
var goBtn = container.querySelector('.soon-pager-go');
|
||||
|
||||
if (first && !first.disabled) first.onclick = function () { go(1); };
|
||||
if (prev && !prev.disabled) prev.onclick = function () { go(page - 1); };
|
||||
if (next && !next.disabled) next.onclick = function () { go(page + 1); };
|
||||
if (last && !last.disabled) last.onclick = function () { go(pages); };
|
||||
if (sizeSel) {
|
||||
sizeSel.onchange = function () {
|
||||
onChange({ page: 1, size: parseInt(sizeSel.value, 10) || size });
|
||||
};
|
||||
}
|
||||
if (goBtn && jump) {
|
||||
goBtn.onclick = function () {
|
||||
go(parseInt(jump.value, 10) || page);
|
||||
};
|
||||
jump.onkeydown = function (e) {
|
||||
if (e.key === 'Enter') go(parseInt(jump.value, 10) || page);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
window.AdminPager = { html: html, bind: bind, pagesCount: pagesCount };
|
||||
})();
|
||||
@@ -0,0 +1,40 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var I = function () { return window.AdminI18n; };
|
||||
|
||||
function headerHtml(opts) {
|
||||
opts = opts || {};
|
||||
return '<header class="soon-admin-header" id="adminShellHeader">' +
|
||||
'<div class="soon-admin-header__brand">' +
|
||||
'<img class="soon-admin-logo" src="' + I().esc(opts.logoSrc || '../../assets/images/logo.png') + '" alt="">' +
|
||||
'<span>' + I().esc(opts.title || 'SoonDesign Admin') + '</span>' +
|
||||
(opts.env ? '<span class="soon-admin-header__env">' + I().esc(opts.env) + '</span>' : '') +
|
||||
'</div>' +
|
||||
'<div class="soon-admin-header__user">' +
|
||||
(opts.designEntry !== false
|
||||
? '<a href="' + I().esc(opts.designHref || '../index.web.html') +
|
||||
'" class="soon-btn soon-btn--sm" id="adminDesignEntry" target="_blank" rel="noopener">进入设计端</a>'
|
||||
: '') +
|
||||
'<span id="adminEmail" class="soon-admin-header__email"></span>' +
|
||||
'<button type="button" class="soon-btn soon-btn--ghost soon-btn--sm soon-admin-logout-btn" id="adminLogout">退出</button>' +
|
||||
'</div></header>';
|
||||
}
|
||||
|
||||
function mountHeader(container, opts) {
|
||||
if (!container) return;
|
||||
container.innerHTML = headerHtml(opts);
|
||||
}
|
||||
|
||||
function bindHeader(handlers) {
|
||||
handlers = handlers || {};
|
||||
var logout = document.getElementById('adminLogout');
|
||||
if (logout && handlers.onLogout) logout.onclick = handlers.onLogout;
|
||||
}
|
||||
|
||||
window.AdminShell = {
|
||||
headerHtml: headerHtml,
|
||||
mountHeader: mountHeader,
|
||||
bindHeader: bindHeader,
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,40 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var I = function () { return window.AdminI18n; };
|
||||
|
||||
function html(id, tabs, panels, activeId) {
|
||||
activeId = activeId || (tabs[0] && tabs[0].id);
|
||||
var nav = tabs.map(function (t) {
|
||||
var cls = t.id === activeId ? ' soon-admin-tabs__tab is-active' : ' soon-admin-tabs__tab';
|
||||
return '<button type="button" class="' + cls.trim() + '" data-tab="' + I().esc(t.id) + '">' +
|
||||
I().esc(t.label) + '</button>';
|
||||
}).join('');
|
||||
var body = tabs.map(function (t) {
|
||||
var vis = t.id === activeId ? ' is-active' : '';
|
||||
return '<div class="soon-admin-tabs__panel' + vis + '" data-panel="' + I().esc(t.id) + '">' +
|
||||
(panels[t.id] || '') + '</div>';
|
||||
}).join('');
|
||||
return '<div class="soon-admin-tabs" id="' + I().esc(id) + '">' +
|
||||
'<nav class="soon-admin-tabs__nav" role="tablist">' + nav + '</nav>' +
|
||||
'<div class="soon-admin-tabs__body">' + body + '</div></div>';
|
||||
}
|
||||
|
||||
function bind(root, onChange) {
|
||||
if (!root) return;
|
||||
root.querySelectorAll('.soon-admin-tabs__tab').forEach(function (btn) {
|
||||
btn.onclick = function () {
|
||||
var tabId = btn.dataset.tab;
|
||||
root.querySelectorAll('.soon-admin-tabs__tab').forEach(function (b) {
|
||||
b.classList.toggle('is-active', b.dataset.tab === tabId);
|
||||
});
|
||||
root.querySelectorAll('.soon-admin-tabs__panel').forEach(function (p) {
|
||||
p.classList.toggle('is-active', p.dataset.panel === tabId);
|
||||
});
|
||||
if (typeof onChange === 'function') onChange(tabId);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
window.AdminTabs = { html: html, bind: bind };
|
||||
})();
|
||||
Reference in New Issue
Block a user