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; } }); }