88c6ce8ccc
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构 - 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐 - 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作 - 新增管理后台、支付与云端文件 API,更新 README 与项目规范 Co-authored-by: Cursor <cursoragent@cursor.com>
418 lines
17 KiB
JavaScript
418 lines
17 KiB
JavaScript
(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 };
|
|
})();
|