Compare commits

...
3 changed files with 763 additions and 0 deletions
+1
View File
@@ -64,6 +64,7 @@ class AccountHttpMixin:
"username": str(self.auth_user["username"]),
"role": str(self.auth_user.get("role") or "user"),
"membership": service.membership(),
"created_at": str(self.auth_user.get("created_at") or ""),
},
"csrf_token": str(self.auth_user["csrf_token"]),
}
+193
View File
@@ -3174,3 +3174,196 @@
border-top: 1px solid var(--border);
transform: translateY(calc(-1 * var(--m-keyboard-inset, 0px)));
}
/* ===== 系统管理(P4):账号资料 / 修改密码 / 会员状态 / 系统设置 / 会员管理 ===== */
/* 只读键值行 */
.m-card--kv { padding: 0; overflow: hidden; }
.m-kv-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
height: 44px;
padding: 0 14px;
}
.m-kv-row + .m-kv-row { border-top: 1px solid var(--border-subtle); }
.m-kv-label { font-size: var(--font-size-label); color: var(--text-secondary); flex: 0 0 auto; }
.m-kv-value {
font-size: var(--font-size-body);
font-weight: 500;
color: var(--text-primary);
text-align: right;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-variant-numeric: tabular-nums;
}
.m-kv-value--danger { color: var(--market-up); }
.m-kv-value--muted { color: var(--text-tertiary); font-weight: 400; }
.m-kv-note {
padding: 6px 14px 14px;
font-size: 12px;
color: var(--text-tertiary);
line-height: 1.5;
}
/* 身份徽标 */
.m-id-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
white-space: nowrap;
}
.m-id-badge--admin { background: var(--warning-soft); color: var(--warning); }
.m-id-badge--member { background: var(--action-soft); color: var(--action); }
.m-id-badge--user { background: var(--surface-muted); color: var(--text-secondary); }
.m-id-badge--suspended { background: var(--market-up-soft); color: var(--market-up); }
/* 卡外底部说明 */
.m-footnote {
margin: 10px 14px 16px;
font-size: var(--font-size-aux);
color: var(--text-tertiary);
line-height: 1.5;
}
/* 表单 label 右侧规则小字 */
.m-field-hint {
font-size: var(--font-size-aux);
color: var(--text-tertiary);
font-weight: 400;
margin-left: 6px;
}
/* 会员状态 hero 卡 */
.m-membership-hero {
display: flex;
flex-direction: column;
gap: 6px;
padding: 16px;
}
.m-membership-hero-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.m-membership-hero-status { font-size: 12px; color: var(--text-secondary); }
.m-membership-hero-status--down { color: var(--market-down); }
.m-membership-hero-status--up { color: var(--market-up); }
.m-membership-hero-status--muted { color: var(--text-tertiary); }
.m-membership-hero-title {
font-size: var(--font-size-metric);
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
}
.m-membership-hero-sub {
font-size: var(--font-size-label);
color: var(--text-secondary);
line-height: 1.5;
}
/* 系统设置:刷新卡 */
.m-admin-view {
margin: 10px 14px 0;
font-size: 12px;
color: var(--text-tertiary);
}
.m-refresh-desc {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 14px;
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
}
.m-refresh-desc svg { flex: 0 0 auto; color: var(--text-tertiary); margin-top: 1px; }
.m-refresh-result {
display: flex;
align-items: center;
gap: 6px;
margin-top: 10px;
font-size: 12px;
line-height: 1.5;
}
.m-refresh-result svg { flex: 0 0 auto; }
.m-refresh-result--ok { color: var(--market-down); }
.m-refresh-result--busy { color: var(--warning); }
/* 会员管理 */
.m-members-summary {
margin: 10px 14px 2px;
font-size: 12px;
color: var(--text-secondary);
}
.m-member-list { padding: 0 12px; }
.m-member-row {
appearance: none;
border: none;
background: transparent;
border-bottom: 1px solid var(--border);
padding: 10px 0;
min-height: 64px;
display: flex;
align-items: center;
gap: 10px;
width: 100%;
text-align: left;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.m-member-row:active { background: var(--surface-hover); }
.m-member-row-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.m-member-row-title {
font-size: var(--font-size-body);
font-weight: 500;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.m-member-me { font-size: var(--font-size-aux); color: var(--text-tertiary); font-weight: 400; }
.m-member-row-summary {
font-size: 12px;
color: var(--text-tertiary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.m-member-side {
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.m-member-used { font-size: var(--font-size-aux); color: var(--text-tertiary); }
.m-member-row-chevron { flex: 0 0 auto; color: var(--text-tertiary); display: inline-flex; }
/* 会员操作抽屉 */
.m-member-drawer-summary {
margin: 0 0 14px;
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
}
.m-member-drawer-field { margin-bottom: 14px; }
.m-member-drawer-label {
display: block;
margin-bottom: 8px;
font-size: var(--font-size-label);
color: var(--text-secondary);
}
.m-member-drawer-hint {
margin: -6px 0 12px;
font-size: var(--font-size-aux);
color: var(--text-tertiary);
}
+569
View File
@@ -90,6 +90,9 @@
"calendar-check": '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="m9 16 2 2 4-4"/>',
"sticky-note": '<path d="M16 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z"/><path d="M15 3v4a2 2 0 0 0 2 2h4"/>',
bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>',
lock: '<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
"check-circle": '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
"alert-circle": '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>',
};
function icon(name, size) {
@@ -186,6 +189,19 @@
form: null,
dirty: false,
},
system: {
subKey: "",
profile: null,
status: null,
members: null,
memberDraft: null,
memberInitialStatus: "inactive",
memberStatus: "inactive",
memberDuration: "1_month",
refreshSubmitting: false,
refreshResult: "",
refreshError: "",
},
};
let sheetToken = 0;
@@ -3067,6 +3083,11 @@
"review/daily": setupReviewPage,
"review/notes": setupReviewPage,
"review/alerts": setupReviewPage,
"system/profile": setupSystemPage,
"system/password": setupSystemPage,
"system/membership": setupSystemPage,
"system/admin": setupSystemPage,
"system/members": setupSystemPage,
};
const COMPLEX_LOADERS = {
@@ -3085,6 +3106,11 @@
"review/daily": loadReview,
"review/notes": loadReview,
"review/alerts": loadReview,
"system/profile": loadSystem,
"system/password": loadSystem,
"system/membership": loadSystem,
"system/admin": loadSystem,
"system/members": loadSystem,
};
function isComplexPage(key) {
@@ -4323,6 +4349,537 @@
});
}
/* ---------------------------------------------------------------- 系统管理(P4 */
function systemSubKey(key) {
const parts = String(key || "").split("/");
return parts[1] || "";
}
function systemTitle(key) {
const map = {
profile: "账号资料",
password: "修改密码",
membership: "会员状态",
admin: "系统设置",
members: "会员管理"
};
return map[systemSubKey(key)] || "系统管理";
}
function systemIdentity(role, membership) {
if (role === "admin" || (membership && membership.is_admin)) return "admin";
if (membership && membership.subscribed) return "member";
if (membership && membership.status === "suspended") return "suspended";
return "user";
}
function idBadgeHtml(kind) {
const labels = { admin: "管理员", member: "有效会员", user: "普通用户", suspended: "已停用" };
return '<span class="m-id-badge m-id-badge--' + kind + '">' + escapeHtml(labels[kind] || "普通用户") + "</span>";
}
function kvRowHtml(label, valueHtml, tone) {
const cls = tone ? " m-kv-value--" + tone : "";
return '<div class="m-kv-row">' +
'<span class="m-kv-label">' + escapeHtml(label) + "</span>" +
'<span class="m-kv-value' + cls + '">' + valueHtml + "</span>" +
"</div>";
}
function systemAccountStatus(membership) {
const m = membership || {};
if (m.subscribed) return { text: "生效中", tone: "" };
if (m.status === "suspended") return { text: "已停用", tone: "danger" };
return { text: "未开通会员", tone: "muted" };
}
function memberStatusSmall(membership) {
const m = membership || {};
if (m.status === "active") return { text: "生效中", cls: "down" };
if (m.status === "suspended") return { text: "已停用", cls: "up" };
return { text: "未开通", cls: "muted" };
}
function memberSummaryText(u) {
if (u.membership_subscribed) {
if (u.membership_plan === "永久") return "长期有效";
const expires = String(u.membership_expires_at || "").slice(0, 10);
return expires ? "有效至 " + expires : "有效会员";
}
if (u.membership_status === "suspended") return "会员已停用";
const expires = String(u.membership_expires_at || "").slice(0, 10);
if (expires) return "已于 " + expires + " 到期";
return "尚未开通";
}
function systemErrorState(kind) {
return '<div class="m-state m-state--error m-motion-fade-in"><p>' + escapeHtml(kind) + "加载失败</p>" +
'<button class="m-btn-primary m-btn-retry" type="button" data-action="retry">重试</button></div>';
}
function adminDeniedState() {
return '<div class="m-state m-motion-rise-in">' +
'<span class="m-state-icon">' + icon("lock", 26) + "</span>" +
"<p>需要管理员权限</p>" +
'<button class="m-btn-primary m-btn-retry" type="button" data-route="#/hub/system">返回系统管理</button>' +
"</div>";
}
function setupSystemPage(key) {
const subKey = systemSubKey(key);
state.key = key;
state.system.subKey = subKey;
state.system.profile = null;
state.system.status = null;
state.system.members = null;
state.system.memberDraft = null;
state.system.memberInitialStatus = "inactive";
state.system.memberStatus = "inactive";
state.system.memberDuration = "1_month";
state.system.refreshSubmitting = false;
state.system.refreshResult = "";
state.system.refreshError = "";
document.getElementById("m-view").classList.add("m-view-feature");
global.MobileRouter.updateHeader({ title: systemTitle(key), back: true, actions: "" });
const adminOnly = subKey === "admin" || subKey === "members";
if (adminOnly && !global.MobileSession.isAdmin()) {
document.getElementById("m-view").innerHTML = complexFrame(key, complexScroll(adminDeniedState()));
return;
}
if (subKey === "password") {
document.getElementById("m-view").innerHTML = renderPasswordForm();
bindPasswordForm();
return;
}
if (subKey === "admin") {
document.getElementById("m-view").innerHTML = complexFrame(key, complexScroll(renderSystemAdmin()));
return;
}
document.getElementById("m-view").innerHTML = complexFrame(key, complexScroll(skeletonHtml(subKey === "members" ? 5 : 4)));
}
function loadSystem() {
const subKey = state.system.subKey;
if ((subKey === "admin" || subKey === "members") && !global.MobileSession.isAdmin()) return;
if (subKey === "profile") loadSystemProfile();
else if (subKey === "membership") loadSystemMembership();
else if (subKey === "members") loadSystemMembers();
}
/* ----- 账号资料 ----- */
function loadSystemProfile() {
const seq = nextSeq();
global.MobileAPI.request("/api/auth/me").then(function (payload) {
if (seq !== state.seq) return;
state.system.profile = payload || {};
renderSystemProfile();
}).catch(function () {
if (seq !== state.seq) return;
const scroll = document.getElementById("m-scroll");
if (scroll) scroll.innerHTML = systemErrorState("账号资料");
});
}
function renderSystemProfile() {
const scroll = document.getElementById("m-scroll");
if (!scroll) return;
const user = (state.system.profile && state.system.profile.user) || {};
const membership = user.membership || {};
const identity = systemIdentity(user.role, membership);
const accountStatus = systemAccountStatus(membership);
const created = String(user.created_at || "").slice(0, 10);
scroll.innerHTML =
'<div class="m-section-title">账号</div>' +
'<div class="m-card m-card--kv">' +
kvRowHtml("用户名", escapeHtml(user.username || "--")) +
kvRowHtml("身份", idBadgeHtml(identity)) +
kvRowHtml("注册时间", escapeHtml(created || "--")) +
kvRowHtml("账号状态", escapeHtml(accountStatus.text), accountStatus.tone) +
"</div>" +
'<p class="m-footnote">账号信息为只读展示;修改密码与会员详情请前往对应页面。</p>';
}
/* ----- 修改密码 ----- */
function renderPasswordForm() {
return '<div class="m-page m-form-page" data-page="' + escapeHtml(state.key) + '">' +
'<div class="m-scroll" id="m-scroll">' +
'<div class="m-form-body">' +
'<p class="m-auth-error" id="m-password-error" hidden></p>' +
'<label class="m-form-field"><span>当前密码<em class="m-field-req">*</em></span><input id="m-password-current" type="password" autocomplete="current-password"></label>' +
'<label class="m-form-field"><span>新密码<em class="m-field-req">*</em><small class="m-field-hint">8128 位,字母/数字/符号至少两类</small></span><input id="m-password-new" type="password" autocomplete="new-password"></label>' +
'<label class="m-form-field"><span>确认新密码<em class="m-field-req">*</em></span><input id="m-password-confirm" type="password" autocomplete="new-password"></label>' +
"</div>" +
"</div>" +
'<div class="m-form-bar"><button class="m-btn-primary" type="button" data-system-password-submit>更新密码</button></div>' +
"</div>";
}
function bindPasswordForm() {
const scroll = document.getElementById("m-scroll");
if (!scroll) return;
scroll.addEventListener("input", function (event) {
if (event.target && event.target.matches("input")) {
clearFieldError(event.target.id);
const errEl = document.getElementById("m-password-error");
if (errEl) errEl.hidden = true;
}
});
}
function submitPassword() {
const current = document.getElementById("m-password-current").value;
const next = document.getElementById("m-password-new").value;
const confirm = document.getElementById("m-password-confirm").value;
const errEl = document.getElementById("m-password-error");
if (errEl) errEl.hidden = true;
clearFieldError("m-password-current");
clearFieldError("m-password-new");
clearFieldError("m-password-confirm");
let invalid = false;
if (!current) { fieldError("m-password-current", "请输入当前密码"); invalid = true; }
if (!next) { fieldError("m-password-new", "请输入新密码"); invalid = true; }
if (!confirm) { fieldError("m-password-confirm", "请再次输入新密码"); invalid = true; }
if (invalid) return;
if (next !== confirm) {
fieldError("m-password-confirm", "两次输入的新密码不一致。");
return;
}
const btn = document.querySelector("[data-system-password-submit]");
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="m-btn-spinner" aria-hidden="true"></span><span>更新中…</span>';
}
global.MobileAPI.request("/api/account/password", "POST", {
current_password: current,
new_password: next,
confirm_password: confirm
}).then(function () {
document.getElementById("m-password-current").value = "";
document.getElementById("m-password-new").value = "";
document.getElementById("m-password-confirm").value = "";
if (btn) { btn.disabled = false; btn.innerHTML = "更新密码"; }
showToast("密码已更新");
}).catch(function (error) {
if (btn) { btn.disabled = false; btn.innerHTML = "更新密码"; }
const message = (error && error.message) || "密码更新失败";
const bar = document.getElementById("m-password-error");
if (bar) { bar.textContent = message; bar.hidden = false; }
});
}
/* ----- 会员状态 ----- */
function loadSystemMembership() {
const seq = nextSeq();
Promise.all([
global.MobileAPI.request("/api/auth/me"),
global.MobileAPI.request("/api/account/status")
]).then(function (results) {
if (seq !== state.seq) return;
state.system.profile = results[0] || {};
state.system.status = results[1] || {};
renderSystemMembership();
}).catch(function () {
if (seq !== state.seq) return;
const scroll = document.getElementById("m-scroll");
if (scroll) scroll.innerHTML = systemErrorState("会员状态");
});
}
function renderSystemMembership() {
const scroll = document.getElementById("m-scroll");
if (!scroll) return;
const user = (state.system.profile && state.system.profile.user) || {};
const membership = user.membership || {};
const llm = (state.system.status && state.system.status.llm_access) || {};
const isAdmin = Boolean(membership.is_admin);
const subscribed = Boolean(membership.subscribed);
const identity = systemIdentity(user.role, membership);
const statusSmall = memberStatusSmall(membership);
let heroTitle;
let heroSub;
if (isAdmin) {
heroTitle = "管理员";
heroSub = "管理员拥有智能功能管理权限";
} else if (subscribed) {
const plan = membership.plan || "";
heroTitle = plan ? plan + "会员" : "会员";
if (plan === "永久") {
heroSub = "永久 · 长期有效";
} else {
const expires = String(membership.expires_at || "").slice(0, 10);
const remaining = membership.remaining_days;
let sub = plan + " · 有效至 " + (expires || "--");
if (remaining != null && remaining >= 0) sub += "(剩余 " + remaining + " 天)";
heroSub = sub;
}
} else {
heroTitle = "普通用户";
heroSub = "开通会员后可使用智能选股、问师、复盘助手等智能功能。";
}
let used;
let limit;
let remaining;
if (isAdmin) {
used = llm.used_today != null ? String(llm.used_today) : "--";
limit = "不限";
remaining = "不限";
} else if (subscribed) {
used = llm.used_today != null ? String(llm.used_today) : "--";
limit = llm.daily_limit != null ? String(llm.daily_limit) : "--";
remaining = llm.remaining_calls != null ? String(llm.remaining_calls) : "--";
} else {
used = "--";
limit = "--";
remaining = "--";
}
let smartFeature;
let quota;
if (isAdmin) {
smartFeature = "全部可用";
quota = "不限量";
} else if (subscribed) {
smartFeature = "智能选股 · 问师 · 复盘助手 已开通";
quota = (llm.daily_limit != null ? llm.daily_limit : "--") + " 次/日";
} else {
smartFeature = "未开通";
quota = (llm.daily_limit != null ? llm.daily_limit : "--") + " 次/日";
}
scroll.innerHTML =
'<div class="m-card m-membership-hero">' +
'<div class="m-membership-hero-top">' + idBadgeHtml(identity) +
'<span class="m-membership-hero-status m-membership-hero-status--' + statusSmall.cls + '">' + escapeHtml(statusSmall.text) + "</span></div>" +
'<div class="m-membership-hero-title">' + escapeHtml(heroTitle) + "</div>" +
'<div class="m-membership-hero-sub">' + escapeHtml(heroSub) + "</div>" +
"</div>" +
'<div class="m-strip">' +
stripCell("今日已用", escapeHtml(used)) +
stripCell("每日额度", escapeHtml(limit)) +
stripCell("剩余次数", escapeHtml(remaining)) +
"</div>" +
'<div class="m-section-title">会员权益</div>' +
'<div class="m-card m-card--kv">' +
kvRowHtml("智能功能", escapeHtml(smartFeature), (subscribed || isAdmin) ? "" : "muted") +
kvRowHtml("每日智能分析额度", escapeHtml(quota)) +
'<div class="m-kv-note">会员额度由管理员统一设置</div>' +
"</div>" +
'<p class="m-footnote">开通、续期与停用由管理员在『会员管理』操作,站内无在线支付。</p>';
}
/* ----- 系统设置(仅管理员) ----- */
function renderSystemAdmin() {
const err = state.system.refreshError;
return '<p class="m-admin-view">管理员视图</p>' +
'<div class="m-section-title">数据刷新</div>' +
'<div class="m-card">' +
'<div class="m-refresh-desc">' + icon("refresh-cw", 16) + "<span>立即拉取今日最新行情数据。刷新在后台进行,期间页面数据保持不变,完成后自动生效。</span></div>" +
(err ? '<p class="m-auth-error">' + escapeHtml(err) + "</p>" : "") +
'<button class="m-btn-primary" type="button" data-system-refresh>' + icon("refresh-cw", 16) + "<span>立即刷新</span></button>" +
(state.system.refreshResult || "") +
"</div>" +
'<p class="m-footnote">行情源、模型与会员额度等配置请在电脑端『系统设置』修改。</p>';
}
function currentTimeHM() {
const d = new Date();
return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
}
function refreshResultHtml(started, message) {
if (started) {
return '<p class="m-refresh-result m-refresh-result--ok">' + icon("check-circle", 14) +
"<span>" + escapeHtml(message) + " · 今天 " + escapeHtml(currentTimeHM()) + "</span></p>";
}
return '<p class="m-refresh-result m-refresh-result--busy">' + icon("alert-circle", 14) +
"<span>" + escapeHtml(message) + "</span></p>";
}
function startSystemRefresh() {
if (state.system.refreshSubmitting) return;
state.system.refreshSubmitting = true;
state.system.refreshResult = "";
state.system.refreshError = "";
const btn = document.querySelector("[data-system-refresh]");
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="m-btn-spinner" aria-hidden="true"></span><span>刷新中…</span>';
}
global.MobileAPI.request("/api/admin/refresh", "POST", { trade_date: todayString() }).then(function (payload) {
state.system.refreshSubmitting = false;
const started = Boolean(payload && payload.started);
const message = (payload && payload.message) || (started ? "后台刷新已开始" : "已有后台刷新任务正在运行");
state.system.refreshResult = refreshResultHtml(started, message);
const scroll = document.getElementById("m-scroll");
if (scroll) scroll.innerHTML = renderSystemAdmin();
}).catch(function (error) {
state.system.refreshSubmitting = false;
state.system.refreshError = (error && error.message) || "刷新失败";
const scroll = document.getElementById("m-scroll");
if (scroll) scroll.innerHTML = renderSystemAdmin();
});
}
/* ----- 会员管理(仅管理员) ----- */
function loadSystemMembers() {
const seq = nextSeq();
global.MobileAPI.request("/api/admin/settings").then(function (payload) {
if (seq !== state.seq) return;
state.system.members = (payload && payload.users) || [];
renderSystemMembers();
}).catch(function () {
if (seq !== state.seq) return;
const scroll = document.getElementById("m-scroll");
if (scroll) scroll.innerHTML = systemErrorState("会员管理");
});
}
function renderSystemMembers() {
const scroll = document.getElementById("m-scroll");
if (!scroll) return;
const users = state.system.members || [];
const me = global.MobileSession.state.user || {};
const meId = me.id != null ? String(me.id) : "";
const meUsername = me.username || "";
const total = users.length;
const activeCount = users.filter(function (u) { return u.membership_subscribed; }).length;
const adminCount = users.filter(function (u) { return String(u.role) === "admin"; }).length;
const summary = "共 " + total + " 个账号 · 有效会员 " + activeCount + " · 管理员 " + adminCount;
const rows = users.map(function (u) {
const isMe = (meId && String(u.id) === meId) || (meUsername && u.username === meUsername);
const identity = systemIdentity(String(u.role), { is_admin: String(u.role) === "admin", subscribed: Boolean(u.membership_subscribed), status: String(u.membership_status || "inactive") });
const usedText = "今日 " + (u.used_today != null ? u.used_today : 0) + " 次";
return '<button class="m-member-row" type="button" data-member-id="' + escapeHtml(String(u.id)) + '">' +
'<span class="m-member-row-main">' +
'<span class="m-member-row-title">' + escapeHtml(u.username || "--") + (isMe ? '<span class="m-member-me">(我)</span>' : "") + "</span>" +
'<span class="m-member-row-summary">' + escapeHtml(memberSummaryText(u)) + "</span>" +
"</span>" +
'<span class="m-member-side">' + idBadgeHtml(identity) + '<span class="m-member-used">' + escapeHtml(usedText) + "</span></span>" +
'<span class="m-member-row-chevron">' + icon("chevron-right", 16) + "</span>" +
"</button>";
}).join("");
scroll.innerHTML =
'<p class="m-members-summary">' + escapeHtml(summary) + "</p>" +
'<div class="m-member-list">' + rows + "</div>" +
'<p class="m-footnote">删除账号等高危操作请在电脑端进行。</p>';
}
function openMemberDrawer(userId) {
const user = (state.system.members || []).find(function (u) { return String(u.id) === String(userId); });
if (!user) return;
state.system.memberDraft = user;
let initialStatus = "inactive";
if (String(user.membership_status) === "suspended") initialStatus = "suspended";
else if (user.membership_subscribed) initialStatus = "active";
state.system.memberInitialStatus = initialStatus;
state.system.memberStatus = initialStatus;
state.system.memberDuration = "1_month";
openSheet(memberDrawerBody(user), { detail: false });
syncMemberApplyButton();
}
function memberSegHtml(group, options, selected) {
return '<div class="m-seg" role="radiogroup" data-member-seg="' + group + '">' +
options.map(function (opt) {
const active = opt.key === selected;
return '<button class="m-seg-opt' + (active ? " active" : "") + '" type="button" role="radio" aria-checked="' + (active ? "true" : "false") + '" data-member-seg-key="' + group + '" data-member-seg-val="' + escapeHtml(opt.key) + '">' + escapeHtml(opt.label) + "</button>";
}).join("") +
"</div>";
}
function memberDrawerBody(user) {
const statusOpts = [
{ key: "inactive", label: "未开通" },
{ key: "active", label: "有效" },
{ key: "suspended", label: "停用" }
];
const durationOpts = [
{ key: "1_month", label: "1个月" },
{ key: "3_months", label: "3个月" },
{ key: "12_months", label: "12个月" },
{ key: "3_years", label: "3年" },
{ key: "permanent", label: "永久" }
];
return '<div class="m-sheet-head"><h2>会员设置 · ' + escapeHtml(user.username || "") + '</h2>' +
'<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
'<div class="m-sheet-body">' +
'<p class="m-member-drawer-summary">当前:' + escapeHtml(memberSummaryText(user)) + "</p>" +
'<div class="m-member-drawer-field"><span class="m-member-drawer-label">会员状态</span>' + memberSegHtml("status", statusOpts, state.system.memberStatus) + "</div>" +
'<div class="m-member-drawer-field"><span class="m-member-drawer-label">开通/续期时长</span>' + memberSegHtml("duration", durationOpts, state.system.memberDuration) + "</div>" +
'<p class="m-member-drawer-hint">续期从当前到期日顺延</p>' +
'<p class="m-auth-error" id="m-member-drawer-error" hidden></p>' +
'<button class="m-btn-primary" type="button" data-member-apply>应用</button>' +
"</div>";
}
function syncMemberApplyButton() {
const btn = document.querySelector("[data-member-apply]");
if (!btn) return;
const dirty = state.system.memberStatus !== state.system.memberInitialStatus || state.system.memberDuration !== "1_month";
btn.disabled = !dirty;
}
function selectMemberSeg(seg) {
const group = seg.dataset.memberSegKey;
const val = seg.dataset.memberSegVal;
if (group === "status") state.system.memberStatus = val;
else if (group === "duration") state.system.memberDuration = val;
seg.parentElement.querySelectorAll("[data-member-seg-val]").forEach(function (b) {
b.classList.toggle("active", b === seg);
b.setAttribute("aria-checked", String(b === seg));
});
const errEl = document.getElementById("m-member-drawer-error");
if (errEl) errEl.hidden = true;
syncMemberApplyButton();
}
function applyMember() {
const user = state.system.memberDraft;
if (!user) return;
const status = state.system.memberStatus;
const duration = state.system.memberDuration;
const btn = document.querySelector("[data-member-apply]");
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="m-btn-spinner" aria-hidden="true"></span><span>保存中…</span>';
}
const body = { user_id: Number(user.id), status: status };
if (status === "active") body.duration = duration;
global.MobileAPI.request("/api/admin/membership", "POST", body).then(function (payload) {
state.system.members = (payload && payload.users) || state.system.members;
closeSheet();
renderSystemMembers();
showToast("会员设置已更新");
}).catch(function (error) {
const errEl = document.getElementById("m-member-drawer-error");
if (errEl) {
errEl.textContent = (error && error.message) || "保存失败";
errEl.hidden = false;
}
if (btn) {
btn.disabled = false;
btn.innerHTML = "应用";
syncMemberApplyButton();
}
});
}
/* ----- 全屏表单页 ----- */
function openForm(kind, opts) {
@@ -5123,6 +5680,18 @@
renderChatMentorBar();
return;
}
// 系统管理:账号资料 / 修改密码 / 会员状态 / 系统设置 / 会员管理
const memberRow = event.target.closest("[data-member-id]");
if (memberRow) { openMemberDrawer(memberRow.dataset.memberId); return; }
const memberSeg = event.target.closest("[data-member-seg-val]");
if (memberSeg) { selectMemberSeg(memberSeg); return; }
const memberApply = event.target.closest("[data-member-apply]");
if (memberApply) { applyMember(); return; }
const systemRefresh = event.target.closest("[data-system-refresh]");
if (systemRefresh) { startSystemRefresh(); return; }
const systemPassword = event.target.closest("[data-system-password-submit]");
if (systemPassword) { submitPassword(); return; }
});
}