重构 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,417 @@
(function () {
'use strict';
var Api = function () { return window.AdminApi; };
var Ui = function () { return window.AdminUi; };
var I = function () { return window.AdminI18n; };
var State = function () { return window.AdminState; };
var Filter = function () { return window.AdminFilter; };
var Menu = function () { return window.AdminMenu; };
var Router = function () { return window.AdminRouter; };
function buildQuery() {
var st = State().users;
var q = 'users?page=' + st.page + '&size=' + st.size;
if (st.q) q += '&q=' + encodeURIComponent(st.q);
return q;
}
function formField(label, inner) {
return '<div class="soon-form-field"><label class="soon-form-label">' + label + '</label>' + inner + '</div>';
}
function roleOptions(selected) {
var roles = ['user'];
if (State().adminLevel === 'full') roles.push('admin');
return roles.map(function (r) {
return '<option value="' + r + '"' + (selected === r ? ' selected' : '') + '>' + I().tRole(r) + '</option>';
}).join('');
}
function adminLevelOptions(selected) {
return [
{ v: 'full', l: '超级管理员' },
{ v: 'ops', l: '运营管理员' },
].map(function (o) {
return '<option value="' + o.v + '"' + (selected === o.v ? ' selected' : '') + '>' + o.l + '</option>';
}).join('');
}
function bindAdminLevelField() {
var roleEl = Ui().el('ueRole') || Ui().el('ucRole');
var wrap = Ui().el('adminLevelField');
if (!roleEl || !wrap) return;
function sync() {
wrap.style.display = roleEl.value === 'admin' ? '' : 'none';
}
roleEl.onchange = sync;
sync();
}
function showCreateModal() {
var form = '<div class="soon-admin-form">' +
formField('邮箱', '<input id="ucEmail" type="email" class="soon-input soon-input--sm" autocomplete="off">') +
formField('密码', '<input id="ucPwd" type="password" class="soon-input soon-input--sm" autocomplete="new-password" placeholder="至少 8 位">') +
formField('角色', '<select id="ucRole" class="soon-input soon-input--sm">' + roleOptions('user') + '</select>') +
(State().adminLevel === 'full'
? formField('管理级别', '<div id="adminLevelField" style="display:none"><select id="ucAdminLevel" class="soon-input soon-input--sm">' +
adminLevelOptions('ops') + '</select></div>', '运营管理员不可访问设置、审计与支付密钥')
: '') +
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">创建</button></div>';
Ui().openFormModal('新增用户', form, function () {
var email = Ui().el('ucEmail').value.trim();
var pwd = Ui().el('ucPwd').value;
if (!email || pwd.length < 8) {
soonToast('请填写有效邮箱与至少 8 位密码', 'warn');
return false;
}
var body = {
email: email,
password: pwd,
role: Ui().el('ucRole').value,
};
if (body.role === 'admin') {
var lv = Ui().el('ucAdminLevel');
body.admin_level = lv ? lv.value : 'ops';
}
return Api().postJson('users', body).then(function (r) {
if (r) {
layer.closeAll();
soonToast('用户已创建', 'success');
render();
}
});
}, {
success: function () { bindAdminLevelField(); },
});
}
function showEditModal(user) {
var level = user.admin_level || 'full';
var form = '<div class="soon-admin-form">' +
formField('邮箱', '<input id="ueEmail" type="email" class="soon-input soon-input--sm" value="' + I().esc(user.email) + '">') +
formField('角色', '<select id="ueRole" class="soon-input soon-input--sm">' + roleOptions(user.role) + '</select>') +
(State().adminLevel === 'full'
? formField('管理级别', '<div id="adminLevelField"><select id="ueAdminLevel" class="soon-input soon-input--sm">' +
adminLevelOptions(level) + '</select></div>', '仅对管理员账号生效')
: '') +
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">保存</button></div>';
Ui().openFormModal('编辑用户 #' + user.id, form, function () {
var body = {
email: Ui().el('ueEmail').value.trim(),
role: Ui().el('ueRole').value,
};
if (body.role === 'admin') {
var lv = Ui().el('ueAdminLevel');
if (lv) body.admin_level = lv.value;
}
return Api().putJson('users/' + user.id, body).then(function (r) {
if (r) {
layer.closeAll();
soonToast('用户信息已更新', 'success');
render();
}
});
}, {
success: function () { bindAdminLevelField(); },
});
}
function showPasswordModal(userId) {
var form = '<div class="soon-admin-form">' +
formField('新密码', '<input id="upPwd" type="password" class="soon-input soon-input--sm" autocomplete="new-password" placeholder="至少 8 位">') +
formField('确认密码', '<input id="upPwd2" type="password" class="soon-input soon-input--sm" autocomplete="new-password">') +
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">重置密码</button></div>';
Ui().openFormModal('重置密码', form, function () {
var p1 = Ui().el('upPwd').value;
var p2 = Ui().el('upPwd2').value;
if (p1.length < 8) {
soonToast('密码至少 8 位', 'warn');
return false;
}
if (p1 !== p2) {
soonToast('两次密码不一致', 'warn');
return false;
}
return Api().postJson('users/' + userId + '/password', { password: p1 }).then(function (r) {
if (r) {
layer.closeAll();
soonToast('密码已重置', 'success');
}
});
});
}
function showSubscriptionModal(userId) {
Api().get('plans').then(function (j) {
if (!j) return;
var opts = (j.data.items || []).map(function (p) {
return '<option value="' + p.id + '" data-days="' + p.duration_days + '">' +
I().esc(p.name) + ' (' + p.duration_days + ' 天)</option>';
}).join('');
var form = '<div class="soon-admin-form">' +
formField('套餐', '<select id="usPlan" class="soon-input soon-input--sm">' + opts + '</select>') +
formField('有效天数', '<input id="usDays" type="number" min="0" class="soon-input soon-input--sm" placeholder="留空则用套餐默认天数">') +
'<p class="soon-admin-form-hint">选择免费版将取消当前有效会员。</p>' +
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">保存</button></div>';
Ui().openFormModal('设置会员', form, function () {
var planSel = Ui().el('usPlan');
var daysVal = Ui().el('usDays').value.trim();
var body = { plan_id: parseInt(planSel.value, 10) };
if (daysVal) body.days = parseInt(daysVal, 10);
return Api().postJson('users/' + userId + '/subscription', body).then(function (r) {
if (r) {
layer.closeAll();
soonToast('会员已更新', 'success');
render();
}
});
});
var planEl = Ui().el('usPlan');
if (planEl) {
planEl.onchange = function () {
var opt = planEl.options[planEl.selectedIndex];
Ui().el('usDays').placeholder = '默认 ' + (opt.getAttribute('data-days') || '0') + ' 天';
};
planEl.onchange();
}
});
}
function showUserDetail(id, opts) {
opts = opts || {};
if (!opts.fromRoute) Router().openDetail(id);
Api().get('users/' + id).then(function (j) {
if (!j) return;
var d = j.data;
var u = d.user || {};
var sub = d.subscription;
var usage = d.usage || {};
var adminId = State().currentAdminId;
var levelLabel = u.role === 'admin'
? (u.admin_level === 'ops' ? '运营管理员' : '超级管理员')
: '—';
var account = Ui().detailRow('邮箱', I().esc(u.email)) +
Ui().detailRow('角色', I().esc(I().tRole(u.role))) +
(u.role === 'admin' ? Ui().detailRow('管理级别', I().esc(levelLabel)) : '') +
Ui().detailRow('状态', I().badgeStatus(u.status)) +
Ui().detailRow('注册时间', I().esc(I().formatDateTime(u.created_at)));
var subHtml;
if (sub && sub.plan_name) {
subHtml = Ui().detailRow('套餐', I().esc(sub.plan_name)) +
Ui().detailRow('状态', I().badgeStatus(sub.status)) +
Ui().detailRow('开始时间', I().esc(I().formatDateTime(sub.started_at))) +
Ui().detailRow('到期时间', I().esc(I().formatDateTime(sub.expires_at)));
} else {
subHtml = '<p class="soon-admin-detail-empty">免费版 / 无有效订阅</p>';
}
var usageHtml = Ui().detailRow('文件数', I().esc(usage.files_count || 0)) +
Ui().detailRow('占用空间', I().esc(I().formatBytes(usage.storage_bytes)));
var orderRows = (d.recent_orders || []).map(function (o) {
return '<tr><td>' + I().esc(o.order_no || o.id) + '</td><td>' + I().formatCents(o.amount_cents) +
'</td><td>' + I().badgeStatus(o.status) + '</td><td>' + I().formatDateTime(o.created_at) + '</td></tr>';
}).join('');
var ordersHtml = Ui().dataTable(['订单号', '金额', '状态', '时间'], orderRows, 4);
var actions = '<div class="soon-admin-drawer-actions">' +
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost" id="drawerEdit">编辑</button>' +
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost" id="drawerPwd">改密</button>' +
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost" id="drawerSub">设会员</button>' +
(u.id !== adminId ? '<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost soon-btn--danger" id="drawerDel">删除</button>' : '') +
'</div>';
Ui().openDrawer('用户 #' + u.id, actions +
Ui().detailSection('账号信息', account) +
Ui().detailSection('当前会员', subHtml) +
Ui().detailSection('云端用量', usageHtml) +
Ui().detailSection('最近订单', ordersHtml));
Ui().el('drawerEdit').onclick = function () { Ui().closeDrawer(); showEditModal(u); };
Ui().el('drawerPwd').onclick = function () { showPasswordModal(u.id); };
Ui().el('drawerSub').onclick = function () { showSubscriptionModal(u.id); };
var delBtn = Ui().el('drawerDel');
if (delBtn) {
delBtn.onclick = function () {
Ui().confirm('确认删除该用户?其云端文件将标记删除且不可恢复。', function () {
Api().del('users/' + u.id).then(function (r) {
if (r) {
Ui().closeDrawer();
soonToast('用户已删除', 'success');
render();
}
});
});
};
}
});
}
function userRowActions(u, adminId) {
var isSelf = adminId && u.id === adminId;
var editDisabled = State().adminLevel === 'ops' && u.role === 'admin';
var ds = { id: String(u.id) };
var editDs = {
id: String(u.id),
email: u.email,
role: u.role,
admin_level: u.admin_level || 'full',
};
var inline = [
{ label: '详情', action: 'detail', dataset: ds },
{ label: '编辑', action: 'edit', dataset: editDs, disabled: editDisabled },
{ label: '订阅', action: 'sub', dataset: ds },
];
var more = [
{ label: '改密', action: 'pwd', dataset: ds },
];
if (u.status === 'disabled') {
more.push({ label: '启用', action: 'status', dataset: { id: String(u.id), st: 'active' } });
} else if (!isSelf) {
more.push({ label: '停用', action: 'status', dataset: { id: String(u.id), st: 'disabled' } });
} else {
more.push({ label: '停用', action: 'status', disabled: true });
}
if (!isSelf) {
more.push({
label: '删除',
action: 'delete',
className: 'soon-admin-action-menu__item--danger',
dataset: ds,
});
}
return Menu().rowActions('user-' + u.id, inline, more);
}
function bindUserMenus(panel, adminId) {
Menu().bind(panel, function (action, btn) {
var id = parseInt(btn.dataset.id, 10);
if (action === 'detail') showUserDetail(id);
if (action === 'edit') {
showEditModal({
id: id,
email: btn.dataset.email,
role: btn.dataset.role,
admin_level: btn.dataset.adminLevel || 'full',
});
}
if (action === 'pwd') showPasswordModal(id);
if (action === 'sub') showSubscriptionModal(id);
if (action === 'status' && btn.dataset.st) {
Ui().confirm('确认变更用户状态?', function () {
Api().postJson('users/' + id + '/status', { status: btn.dataset.st }).then(function (r) {
if (r) {
soonToast('状态已更新', 'success');
render();
}
});
});
}
if (action === 'delete') {
Ui().confirm('确认删除该用户?其云端文件将标记删除。', function () {
Api().del('users/' + id).then(function (r) {
if (r) {
soonToast('用户已删除', 'success');
render();
}
});
});
}
});
}
function renderFilterBar(st) {
return Filter().bar(
Filter().field('邮箱', Filter().searchInput('userSearchQ', st.q, '搜索邮箱')),
'<button type="button" class="soon-btn soon-btn--sm" id="userSearchBtn">搜索</button>' +
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost" id="userSearchReset">重置</button>'
);
}
function bindFilters(st) {
Ui().el('userSearchBtn').onclick = function () {
st.q = Ui().el('userSearchQ').value.trim();
st.page = 1;
Router().syncQuery();
render();
};
var reset = Ui().el('userSearchReset');
if (reset) {
reset.onclick = function () {
st.q = '';
st.page = 1;
Router().syncQuery();
render();
};
}
Ui().el('userSearchQ').onkeydown = function (e) {
if (e.key === 'Enter') {
st.q = Ui().el('userSearchQ').value.trim();
st.page = 1;
Router().syncQuery();
render();
}
};
}
function render() {
var panel = Ui().el('panel-users');
if (!panel) return;
if (!Router().hasPendingDetail()) Ui().closeDrawer();
var st = State().users;
var pageShell = '<div class="soon-admin-page">' +
Ui().pageHeader('用户管理', '增删改用户、重置密码与设置会员',
'<button type="button" class="soon-btn soon-btn--sm" id="userCreateBtn">新增用户</button>') +
renderFilterBar(st) + Ui().skeletonTable() + '</div>';
panel.innerHTML = pageShell;
Api().get(buildQuery()).then(function (j) {
if (!j) {
panel.innerHTML = '<div class="soon-admin-page">' +
Ui().pageHeader('用户管理', '增删改用户、重置密码与设置会员',
'<button type="button" class="soon-btn soon-btn--sm" id="userCreateBtn">新增用户</button>') +
renderFilterBar(st) + Ui().listError('用户列表加载失败', 'usersListRetry') + '</div>';
Ui().el('userCreateBtn').onclick = showCreateModal;
Ui().el('usersListRetry').onclick = render;
bindFilters(st);
var errDetailId = Router().consumeDetailId();
if (errDetailId) showUserDetail(errDetailId, { fromRoute: true });
return;
}
var items = j.data.items || [];
var adminId = State().currentAdminId;
var rows = items.map(function (u) {
var planLabel = u.plan_name ? I().esc(u.plan_name) : '免费版';
return '<tr><td>' + u.id + '</td><td>' + I().esc(u.email) + '</td><td>' + I().esc(I().tRole(u.role)) +
'</td><td>' + planLabel + '</td><td>' + I().badgeStatus(u.status) + '</td><td>' +
I().formatDateTime(u.created_at) + '</td><td class="soon-admin-table__actions">' +
userRowActions(u, adminId) + '</td></tr>';
}).join('');
panel.innerHTML = '<div class="soon-admin-page">' +
Ui().pageHeader('用户管理', '增删改用户、重置密码与设置会员',
'<button type="button" class="soon-btn soon-btn--sm" id="userCreateBtn">新增用户</button>') +
renderFilterBar(st) +
Ui().dataTable(['ID', '邮箱', '角色', '会员', '状态', '注册时间', '操作'], rows) +
Ui().pagerHtml(j.data.page, j.data.size, j.data.total) + '</div>';
Ui().el('userCreateBtn').onclick = showCreateModal;
bindFilters(st);
Ui().bindPager(panel, j.data.page, j.data.size, j.data.total, function (p, newSize) {
st.page = p;
if (newSize) st.size = newSize;
Router().syncQuery();
render();
});
bindUserMenus(panel, adminId);
var detailId = Router().consumeDetailId();
if (detailId) showUserDetail(detailId, { fromRoute: true });
});
}
window.AdminViews = window.AdminViews || {};
window.AdminViews.users = { render: render };
})();