| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748 |
- @if (loading() && !user()) { <div class="state"><lucide-icon class="spin" [img]="icons.LoaderCircle" [size]="28" />正在加载会员资料…</div> }
- @else if (error() && !user()) { <div class="state error" role="alert"><span>{{ error() }}</span><button type="button" (click)="refreshBusiness()" [disabled]="loading()"><lucide-icon [img]="icons.RefreshCw" [size]="17" />重新加载</button></div> }
- @else { @if (user(); as detail) {
- <section class="profile-summary" aria-label="会员资料与操作">
- <div class="avatar"><lucide-icon [img]="icons.UserRound" [size]="28" /></div>
- <div class="profile-identity">
- <div class="profile-title"><h1>{{ detail.displayName }}</h1><span class="status" [class.disabled]="detail.status === 'disabled'">{{ detail.statusLabel }}</span></div>
- <p>会员 ID {{ detail.userId || '—' }}@if (detail.username && detail.username !== detail.displayName) { · 账号 {{ detail.username }} }</p>
- <div class="profile-facts"><span>{{ detail.learningStatusLabel }}</span><span>{{ detail.parentDisplayName || '未绑定门店' }}</span><span>{{ detail.registeredAt | date:'yyyy-MM-dd' }} 注册</span></div>
- </div>
- <nav class="quick-actions" aria-label="会员常用操作"><button class="profile-refresh" type="button" (click)="refreshBusiness()" [disabled]="loading()"><lucide-icon [img]="icons.RefreshCw" [class.spin]="loading()" [size]="17" />刷新</button><button type="button" (click)="selectTab('balances')"><lucide-icon [img]="icons.WalletCards" [size]="17" />{{ detail.permissions.canManage ? '充值课时' : '查看课时' }}</button><button type="button" (click)="selectTab('courses')"><lucide-icon [img]="icons.BookOpen" [size]="17" />{{ detail.permissions.canEdit ? '按分类开课' : '查看课程' }}</button><a class="action-link primary" routerLink="/admin/schedule" [queryParams]="{studentId:detail.userId}"><lucide-icon [img]="icons.CalendarDays" [size]="17" />为该学员排课</a></nav>
- </section>
- @if (detail.writeReady === false) { <p class="notice" role="status">{{ detail.readOnlyReason }}</p> }
- @if (message()) { <p class="notice" role="status">{{ message() }}</p> } @if (error()) { <p class="notice error" role="alert">{{ error() }}</p> }
- <nav class="tabs" aria-label="会员详情分类">
- <button type="button" [class.active]="tab()==='overview'" [attr.aria-pressed]="tab()==='overview'" (click)="selectTab('overview')"><lucide-icon [img]="icons.UserRound" [size]="17" />概览</button>
- <button type="button" [class.active]="tab()==='courses'" [attr.aria-pressed]="tab()==='courses'" (click)="selectTab('courses')"><lucide-icon [img]="icons.BookOpen" [size]="17" />课程</button>
- <button type="button" [class.active]="tab()==='appointments'" [attr.aria-pressed]="tab()==='appointments'" (click)="selectTab('appointments')"><lucide-icon [img]="icons.CalendarDays" [size]="17" />预约</button>
- <button type="button" [class.active]="tab()==='lessons'" [attr.aria-pressed]="tab()==='lessons'" (click)="selectTab('lessons')"><lucide-icon [img]="icons.GraduationCap" [size]="17" />上课记录</button>
- <button type="button" [class.active]="tab()==='reports'" [attr.aria-pressed]="tab()==='reports'" (click)="selectTab('reports')"><lucide-icon [img]="icons.FileChartColumn" [size]="17" />学习报表</button>
- <button type="button" [class.active]="tab()==='balances'" [attr.aria-pressed]="tab()==='balances'" (click)="selectTab('balances')"><lucide-icon [img]="icons.WalletCards" [size]="17" />课时流水</button>
- <button type="button" [class.active]="tab()==='audit'" [attr.aria-pressed]="tab()==='audit'" (click)="selectTab('audit')"><lucide-icon [img]="icons.FileClock" [size]="17" />操作审计</button>
- </nav>
- @if (sectionLoading()) { <div class="section-loading"><lucide-icon class="spin" [img]="icons.LoaderCircle" [size]="22" />正在读取业务记录…</div> }
- @if ((tab()==='overview'||tab()==='audit') && (detail.permissions.canEdit||detail.permissions.canManage)) { <section class="reason-bar"><label><span>本次操作原因 <b aria-hidden="true">*</b></span><input [ngModel]="reason()" (ngModelChange)="reason.set($event)" minlength="2" maxlength="500" aria-describedby="operation-reason-hint" placeholder="填写修改原因,至少 2 个字" /></label><small id="operation-reason-hint">保存资料、变更账号或添加备注时必填,记入审计日志</small></section> }
- @switch (tab()) {
- @case ('overview') {
- <div class="detail-grid">
- <section class="panel profile-editor"><header><div><h2>基本资料</h2><span>只展示运营需要的业务字段</span></div>@if (detail.permissions.canEdit) { <button type="button" class="primary" (click)="saveProfile()" [disabled]="saving()"><lucide-icon [img]="icons.Save" [size]="16" />保存资料</button> }</header><div class="form-grid"><label><span>会员姓名</span><input [disabled]="!detail.permissions.canEdit" [ngModel]="editName()" (ngModelChange)="editName.set($event)" /></label><label><span>昵称</span><input [disabled]="!detail.permissions.canEdit" [ngModel]="editNickname()" (ngModelChange)="editNickname.set($event)" /></label><label><span>手机号</span><div class="mobile-control"><input [disabled]="!detail.permissions.canEdit || !revealMobile()" [ngModel]="revealMobile() ? editMobile() : detail.mobileMasked" (ngModelChange)="editMobile.set($event)" />@if (!revealMobile() && detail.permissions.canRevealMobile) { <button type="button" (click)="reveal()"><lucide-icon [img]="icons.Eye" [size]="15" />查看</button> }</div></label><label><span>邮箱</span><input [disabled]="!detail.permissions.canEdit" [ngModel]="editEmail()" (ngModelChange)="editEmail.set($event)" /></label><label><span>性别</span><select [disabled]="!detail.permissions.canEdit" [ngModel]="editGender()" (ngModelChange)="editGender.set($event)"><option value="">未填写</option><option value="男">男</option><option value="女">女</option></select></label><label><span>年级</span><input [disabled]="!detail.permissions.canEdit" [ngModel]="editGrade()" (ngModelChange)="editGrade.set($event)" /></label></div><dl class="account-facts"><div><dt>注册时间</dt><dd>{{ detail.registeredAt | date:'yyyy-MM-dd HH:mm' }}</dd></div><div><dt>最后登录</dt><dd>{{ detail.lastLoginAt ? (detail.lastLoginAt | date:'yyyy-MM-dd HH:mm') : '—' }}</dd></div><div><dt>登录次数</dt><dd>{{ detail.loginCount }}</dd></div></dl></section>
- <section class="panel"><header><div><h2>学习与课时</h2><span>{{ detail.learningStatusReason }}</span></div><span class="learning-status" [attr.data-status]="detail.learningStatus">{{ detail.learningStatusLabel }}</span></header><div class="metric-grid"><article><strong>{{ detail.periods30 }}</strong><span>30 分钟课时</span></article><article><strong>{{ detail.periods60 }}</strong><span>60 分钟课时</span></article><article><strong>{{ detail.trialPeriods }}</strong><span>体验课时</span></article><article><strong>{{ detail.balances.userPoint }}</strong><span>陪练时长</span></article><article><strong>{{ detail.activeCourseCount }}</strong><span>有效课程</span></article><article><strong>{{ detail.studyRecordCount }}</strong><span>学习记录</span></article></div><div class="risk-list">@for (risk of detail.riskFlags || []; track risk) { <span>{{ risk }}</span> } @empty { <small>当前没有运营风险提示</small> }</div></section>
- <section class="panel"><header><div><h2>所属门店/代理</h2><span>选择后点击“保存资料”生效</span></div></header><label><span>搜索门店或代理</span><div class="search-control"><input [disabled]="!detail.permissions.canEdit" [ngModel]="agentSearch()" (ngModelChange)="agentSearch.set($event)" placeholder="输入门店或代理名称" /></div></label>@if (detail.permissions.canEdit) { <div class="option-list"><button type="button" [class.selected]="!parentUserId()" (click)="clearAgent()">暂不绑定</button>@for (agent of filteredAgents(); track agent.userId) { <button type="button" [class.selected]="parentUserId()===agent.userId" (click)="selectAgent(agent)"><strong>{{ agent.displayName }}</strong><span>{{ parentUserId()===agent.userId ? '已选择' : '选择门店' }}</span></button> }</div> }</section>
- <section class="panel risk-panel"><header><div><h2>账号安全</h2><span>高风险操作仅运营经理可执行</span></div></header>@if (detail.permissions.canManage) { <div class="security-actions"><button type="button" class="danger" (click)="changeStatus()" [disabled]="saving()"><lucide-icon [img]="icons.ShieldAlert" [size]="17" />{{ detail.status === 'disabled' ? '恢复账号' : '停用账号' }}</button>@if (detail.permissions.canResetPassword) { <label><span>新临时密码</span><input type="password" autocomplete="new-password" [ngModel]="resetPassword()" (ngModelChange)="resetPassword.set($event)" placeholder="至少 8 位" /></label><button type="button" (click)="resetMemberPassword()" [disabled]="saving()">重置密码并退出旧会话</button> }</div> } @else { <p class="muted">当前账号没有高风险操作权限。</p> }</section>
- </div>
- }
- @case ('courses') {
- <section class="panel"><header><div><h2>已开课程</h2><span>查看学习进度,取消绑定进入回收站并可恢复</span></div></header>@if (detail.permissions.canEdit) { <app-member-enrollment [studentId]="detail.userId" [refreshVersion]="refreshVersion()" (changed)="refreshBusiness()" /> }@if(detail.permissions.canManage){<label class="course-reason"><span>课程回收 / 恢复原因</span><input [ngModel]="reason()" (ngModelChange)="reason.set($event)" placeholder="操作下方已开课程前填写,至少 2 个字" maxlength="500" /></label>}<div class="record-table"><table><thead><tr><th>分类与课程</th><th>已学/内容数量</th><th>进度</th><th>状态</th><th>操作</th></tr></thead><tbody>@for (row of related().courses; track row['targetId']) { <tr><td>{{ row['categoryName'] || '待确认分类' }} · {{ row['courseName'] || '未命名课程' }}</td><td>{{ row['learnedCount'] || 0 }} / {{ row['openedCount'] || 0 }}</td><td>{{ row['progressStage'] || '未记录' }}</td><td>{{ row['enrollmentStatus'] === 'pending' ? '分类待确认' : row['statusLabel'] }}</td><td>@if (detail.permissions.canManage) { <button type="button" (click)="toggleCourse(row)" [disabled]="saving() || sectionLoading()">{{ $any(row['status']) === -2 ? '恢复' : '移入回收站' }}</button> }</td></tr> } @empty { <tr><td colspan="5">暂无课程绑定</td></tr> }</tbody></table></div></section>
- }
- @case ('appointments') { <section class="panel"><header><div><h2>预约记录</h2><span>查看课程、陪练、预约时间和业务状态</span></div></header><div class="record-table"><table><thead><tr><th>课程</th><th>陪练老师</th><th>预约时间</th><th>课型</th><th>状态</th></tr></thead><tbody>@for (row of related().appointments; track row['targetId']) { <tr><td>{{ row['courseName'] }}</td><td>{{ row['coachName'] || '待分配' }}</td><td>{{ $any(row['appointmentAt'] || row['startsAt']) | date:'yyyy-MM-dd HH:mm' }}</td><td>{{ row['classTypeLabel'] }}</td><td>{{ row['statusLabel'] }}</td></tr> } @empty { <tr><td colspan="5">暂无预约记录</td></tr> }</tbody></table></div></section> }
- @case ('lessons') { <app-lesson-records [studentId]="detail.userId" [embedded]="true" [refreshVersion]="refreshVersion()" /> }
- @case ('reports') { <section class="panel"><header><div><h2>单词学习报表</h2><span>查看该会员每次学习的掌握情况和复习计划</span></div><a class="primary action-link" routerLink="/admin/learning-reports" [queryParams]="{ studentObjectId: detail.objectId }">进入报表中心</a></header><div class="date-filters"><app-operations-date-filter label="开始日期" [value]="reportFrom()" (valueChange)="reportFrom.set($event)" /><app-operations-date-filter label="结束日期" [value]="reportTo()" (valueChange)="reportTo.set($event)" /><button type="button" class="primary" (click)="filterReports()" [disabled]="sectionLoading()">查询</button><button type="button" (click)="filterReports(true)" [disabled]="sectionLoading()">重置</button></div><div class="record-table"><table><thead><tr><th>学习日期</th><th>课程</th><th>陪练老师</th><th>学习结果</th><th>掌握率</th><th>状态</th><th>操作</th></tr></thead><tbody>@for (row of related().reports; track row['targetId']) { <tr><td>{{ row['studyDate'] || '日期缺失' }}</td><td>{{ row['courseName'] }}</td><td>{{ row['coachName'] }}</td><td>{{ row['learnedCount'] }} 个 · 掌握 {{ row['masteredCount'] }} · 待复习 {{ row['reviewDueCount'] }}</td><td>{{ row['masteryRate'] }}%</td><td>{{ row['statusLabel'] }}</td><td><a class="action-link compact" [routerLink]="['/admin/learning-reports']" [queryParams]="{ studentObjectId: detail.objectId, report: row['targetId'] }">查看报表</a></td></tr> } @empty { <tr><td colspan="7">暂无学习报表</td></tr> }</tbody></table></div></section> }
- @case ('balances') { <section class="panel"><header><div><h2>课时与流水</h2><span>门店划拨与退回原门店均记录操作原因</span></div></header><app-member-credit-account [objectId]="detail.objectId" [refreshVersion]="refreshVersion()" (changed)="refreshBusiness()" /><h3 class="record-heading">历史课时明细</h3><div class="record-table"><table><thead><tr><th>项目</th><th>变化</th><th>调整前</th><th>余额</th><th>时间</th><th>说明</th></tr></thead><tbody>@for (row of related().balances; track row['targetId']) { <tr><td>{{ row['moneyTypeLabel'] }}</td><td>{{ row['score'] }}</td><td>{{ row['scoreBefore'] }}</td><td>{{ row['balanceAfter'] }}</td><td>{{ $any(row['happenedAt']) | date:'yyyy-MM-dd HH:mm' }}</td><td>{{ row['detail'] }}</td></tr> } @empty { <tr><td colspan="6">暂无课时流水</td></tr> }</tbody></table></div></section> }
- @case ('audit') { <div class="audit-grid"><section class="panel"><header><div><h2>运营跟进备注</h2><span>记录后续联系和服务事项</span></div></header>@if (detail.permissions.canEdit) { <label><span>新增备注</span><textarea [ngModel]="noteContent()" (ngModelChange)="noteContent.set($event)" placeholder="填写需要跟进的事项或沟通结果"></textarea></label><button class="primary" type="button" (click)="saveNote()" [disabled]="saving()"><lucide-icon [img]="icons.MessageSquarePlus" [size]="16" />保存备注</button> }<div class="timeline">@for (note of notes(); track note['objectId']) { <article><strong>{{ note['operatorName'] || '运营人员' }}</strong><time>{{ $any(note['createdAt']) | date:'yyyy-MM-dd HH:mm' }}</time><p>{{ note['content'] }}</p></article> } @empty { <p class="muted">暂无跟进备注</p> }</div></section><section class="panel"><header><div><h2>操作审计</h2><span>账号、资料、关系、课程和课时修改记录</span></div></header><div class="timeline audit">@for (item of related().audit; track item['objectId']) { <article><strong>{{ item['operatorName'] || '系统' }}</strong><time>{{ $any(item['createdAt']) | date:'yyyy-MM-dd HH:mm' }}</time><p>{{ item['summary'] || '会员业务操作' }}</p></article> } @empty { <p class="muted">暂无操作审计记录</p> }</div></section></div> }
- }
- @if(tab()!=='overview'&&tab()!=='audit'&&tab()!=='lessons') {<nav class="pagination" aria-label="会员业务记录分页"><span>共 {{ recordTotal() }} 条 · 每页 {{ pageSize }} 条</span><button type="button" (click)="changePage(recordPage()-1)" [disabled]="sectionLoading()||recordPage()<=1">上一页</button><strong>{{ recordPage() }} / {{ pageCount() }}</strong><button type="button" (click)="changePage(recordPage()+1)" [disabled]="sectionLoading()||recordPage()>=pageCount()">下一页</button></nav>}
- } }
|