@@ -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">8– 128 位,字母/数字/符号至少两类</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 ; }
} ) ;
}