diff --git a/app/backend/features/accounts/http.py b/app/backend/features/accounts/http.py
index 4bc282e..bbcc3fa 100644
--- a/app/backend/features/accounts/http.py
+++ b/app/backend/features/accounts/http.py
@@ -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"]),
}
diff --git a/app/frontend/m/css/features.css b/app/frontend/m/css/features.css
index 79addc8..62a6fa0 100644
--- a/app/frontend/m/css/features.css
+++ b/app/frontend/m/css/features.css
@@ -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);
+}
diff --git a/app/frontend/m/js/pages.js b/app/frontend/m/js/pages.js
index 6ca154f..35e1792 100644
--- a/app/frontend/m/js/pages.js
+++ b/app/frontend/m/js/pages.js
@@ -90,6 +90,9 @@
"calendar-check": '',
"sticky-note": '',
bell: '',
+ lock: '',
+ "check-circle": '',
+ "alert-circle": '',
};
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,536 @@
});
}
+ /* ---------------------------------------------------------------- 系统管理(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 '' + escapeHtml(labels[kind] || "普通用户") + "";
+ }
+
+ function kvRowHtml(label, valueHtml, tone) {
+ const cls = tone ? " m-kv-value--" + tone : "";
+ return '
' +
+ '' + escapeHtml(label) + "" +
+ '' + valueHtml + "" +
+ "
";
+ }
+
+ 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 '' + escapeHtml(kind) + "加载失败
" +
+ '
';
+ }
+
+ function adminDeniedState() {
+ return '' +
+ '
' + icon("lock", 26) + "" +
+ "
需要管理员权限
" +
+ '
' +
+ "
";
+ }
+
+ 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 === "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 =
+ '账号
' +
+ '' +
+ kvRowHtml("用户名", escapeHtml(user.username || "--")) +
+ kvRowHtml("身份", idBadgeHtml(identity)) +
+ kvRowHtml("注册时间", escapeHtml(created || "--")) +
+ kvRowHtml("账号状态", escapeHtml(accountStatus.text), accountStatus.tone) +
+ "
" +
+ '';
+ }
+
+ /* ----- 修改密码 ----- */
+
+ function renderPasswordForm() {
+ return '' +
+ '
" +
+ '
' +
+ "
";
+ }
+
+ 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 = '更新中…';
+ }
+ 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 =
+ '' +
+ '
' + idBadgeHtml(identity) +
+ '' + escapeHtml(statusSmall.text) + "
" +
+ '
' + escapeHtml(heroTitle) + "
" +
+ '
' + escapeHtml(heroSub) + "
" +
+ "
" +
+ '' +
+ stripCell("今日已用", escapeHtml(used)) +
+ stripCell("每日额度", escapeHtml(limit)) +
+ stripCell("剩余次数", escapeHtml(remaining)) +
+ "
" +
+ '会员权益
' +
+ '' +
+ kvRowHtml("智能功能", escapeHtml(smartFeature), (subscribed || isAdmin) ? "" : "muted") +
+ kvRowHtml("每日智能分析额度", escapeHtml(quota)) +
+ '
会员额度由管理员统一设置
' +
+ "
" +
+ '';
+ }
+
+ /* ----- 系统设置(仅管理员) ----- */
+
+ function renderSystemAdmin() {
+ const err = state.system.refreshError;
+ return '管理员视图
' +
+ '数据刷新
' +
+ '' +
+ '
' + icon("refresh-cw", 16) + "立即拉取今日最新行情数据。刷新在后台进行,期间页面数据保持不变,完成后自动生效。
" +
+ (err ? '
' + escapeHtml(err) + "
" : "") +
+ '
" +
+ (state.system.refreshResult || "") +
+ "
" +
+ '';
+ }
+
+ 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 '' + icon("check-circle", 14) +
+ "" + escapeHtml(message) + " · 今天 " + escapeHtml(currentTimeHM()) + "
";
+ }
+ return '' + icon("alert-circle", 14) +
+ "" + escapeHtml(message) + "
";
+ }
+
+ 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 = '刷新中…';
+ }
+ 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 '";
+ }).join("");
+
+ scroll.innerHTML =
+ '' + escapeHtml(summary) + "
" +
+ '' + rows + "
" +
+ '';
+ }
+
+ 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 '' +
+ options.map(function (opt) {
+ const active = opt.key === selected;
+ return '";
+ }).join("") +
+ "
";
+ }
+
+ 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 '会员设置 · ' + escapeHtml(user.username || "") + '
' +
+ '" +
+ '' +
+ '
当前:' + escapeHtml(memberSummaryText(user)) + "
" +
+ '
会员状态' + memberSegHtml("status", statusOpts, state.system.memberStatus) + "
" +
+ '
开通/续期时长' + memberSegHtml("duration", durationOpts, state.system.memberDuration) + "
" +
+ '
续期从当前到期日顺延
' +
+ '
' +
+ '
' +
+ "
";
+ }
+
+ 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 = '保存中…';
+ }
+ 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 +5679,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; }
});
}