Просмотр исходного кода

style: balance member overview panels and compact profile layout

彭峰 7 часов назад
Родитель
Сommit
40777c7008

+ 4 - 4
projects/xiaoshu-admin/src/app/pages/operations-user-detail.component.html

@@ -28,10 +28,10 @@
   @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>
+        <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 learning-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 store-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"><div class="account-status-action"><div><strong>账号状态</strong><span>{{ detail.statusLabel }}</span></div><button type="button" class="danger" (click)="changeStatus()" [disabled]="saving()"><lucide-icon [img]="icons.ShieldAlert" [size]="17" />{{ detail.status === 'disabled' ? '恢复账号' : '停用账号' }}</button></div>@if (detail.permissions.canResetPassword) { <div class="password-reset-group"><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> }</div> } @else { <p class="muted">当前账号没有高风险操作权限。</p> }</section>
       </div>
     }
     @case ('courses') {

+ 22 - 8
projects/xiaoshu-admin/src/app/pages/operations-user-detail.component.scss

@@ -1,13 +1,13 @@
 @use '../components/member-controls' as member;
 :host { display:block; color:#263733; font-size:14px; line-height:1.5; }
 @include member.controls;
-.profile-summary { display:flex; align-items:center; flex-wrap:wrap; gap:18px; padding:24px; border-radius:14px; color:#fff; background:linear-gradient(120deg,#163b58,#176953); }
+.profile-summary { display:flex; align-items:center; flex-wrap:wrap; gap:16px; padding:20px; border-radius:14px; color:#fff; background:linear-gradient(120deg,#163b58,#176953); }
 .avatar { display:grid; flex:none; place-items:center; width:50px; height:50px; border-radius:12px; background:#ffffff24; }
 .profile-identity { flex:1 1 320px; min-width:0; }
 .profile-title { display:flex; align-items:center; flex-wrap:wrap; gap:12px; margin-bottom:5px; }
-.profile-title h1 { margin:0; font-size:26px; line-height:1.4; overflow-wrap:anywhere; }
+.profile-title h1 { margin:0; font-size:24px; line-height:1.4; overflow-wrap:anywhere; }
 .profile-summary p { margin:0; color:#d3e5de; font-size:13px; overflow-wrap:anywhere; }
-.profile-facts { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; }
+.profile-facts { display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }
 .profile-facts span { padding:4px 9px; border:1px solid #ffffff40; border-radius:999px; color:#e0eee8; font-size:12px; }
 .quick-actions { display:flex; flex-wrap:wrap; gap:10px; margin-left:auto; }
 .quick-actions .profile-refresh { color:#e0eee8; border-color:#ffffff50; background:transparent; }
@@ -17,7 +17,7 @@
 .status.disabled { color:#923b44; background:#ffe7e9; }
 .notice { margin:14px 0; padding:12px 16px; border-radius:9px; color:#216b51; background:#e9f7f1; }
 .notice.error,.state.error { color:#a53e47; background:#fff0f1; }
-.tabs { display:flex; gap:4px; overflow:auto; margin:18px 0; padding:5px; border:1px solid #dfe7e4; border-radius:12px; background:#fff; }
+.tabs { display:flex; gap:4px; overflow:auto; margin:16px 0; padding:5px; border:1px solid #dfe7e4; border-radius:12px; background:#fff; }
 .tabs button { flex:1; min-width:max-content; border:0; color:#5c7067; }
 .tabs button.active { color:#fff; background:#19765c; }
 .section-loading,.state { display:grid; place-items:center; gap:8px; min-height:80px; color:#637b6f; }
@@ -28,7 +28,15 @@
 .panel h2 { margin:0; font-size:18px; line-height:1.4; }
 .panel header span { display:block; margin-top:5px; color:#697b74; font-size:13px; }
 .panel header .learning-status { margin:0; color:#17654e; font-size:12px; }
-.profile-editor { grid-row:span 2; }
+.detail-grid { grid-template-columns:minmax(0,1.15fr) minmax(0,1fr); align-items:stretch; gap:16px; }
+.detail-grid .panel { display:flex; flex-direction:column; }
+.detail-grid .panel>header { margin-bottom:16px; }
+.profile-editor .form-grid { gap:12px 16px; }
+.profile-editor .account-facts { margin-top:auto; padding-top:16px; }
+.learning-panel .metric-grid { flex:1; }
+.learning-panel .metric-grid article { align-content:center; }
+.store-panel .option-list { max-height:132px; }
+@media(min-width:1400px) { .profile-editor .form-grid { grid-template-columns:repeat(3,minmax(0,1fr)); } }
 .form-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px; }
 .mobile-control { display:flex; gap:8px; min-width:0; }
 .mobile-control button { white-space:nowrap; }
@@ -47,9 +55,15 @@
 .option-list button { display:grid; justify-items:start; gap:3px; padding:9px 12px; }
 .option-list button span { color:#687b72; font-size:12px; font-weight:400; }
 .option-list button.selected { border-color:#1f8063; color:#176c50; background:#e8f6f1; }
-.risk-panel { grid-column:2; }
-.security-actions { display:flex; flex-wrap:wrap; align-items:end; gap:12px; }
-.security-actions label { flex:1 1 180px; }
+.security-actions { display:grid; gap:16px; }
+.account-status-action { display:flex; align-items:center; justify-content:space-between; gap:12px; padding-bottom:14px; border-bottom:1px solid #edf1ef; }
+.account-status-action>div { display:flex; align-items:center; gap:12px; }
+.account-status-action strong { font-size:13px; font-weight:600; }
+.account-status-action span { color:#687b72; font-size:13px; }
+.password-reset-group { display:flex; flex-wrap:wrap; align-items:end; gap:10px; }
+.password-reset-group label { flex:1 1 180px; }
+.password-reset-group button { flex:0 0 auto; }
+@media(max-width:650px) { .password-reset-group button { width:100%; } }
 .record-table { overflow:auto; margin-top:16px; border:1px solid #e4ebe8; border-radius:10px; }
 .record-table table { width:100%; min-width:720px; border-collapse:collapse; }
 .record-table th,.record-table td { padding:13px 14px; border-bottom:1px solid #e8efeb; text-align:left; font-size:14px; line-height:1.6; }