import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { OrgUser } from '../../../../core/api/models'; import { OrgUsersService } from '../../../../core/org-users.service'; import { MAppBarComponent } from '../../../components/m-app-bar/m-app-bar'; import { MEmptyComponent } from '../../../components/m-empty/m-empty'; /** * 移动·组织成员管理(2026-11-10) * * ## 为什么不是把 PC 表格搬过来 * * 手机上五列表格必然横向溢出、字小到不可读。移动端改为: * - **卡片行**:姓名 + 子信息纵向排布,整行可点 * - **搜索吸顶**:滚动时仍能改关键词 * - **操作走底部抽屉**:编辑/新增都是全屏 sheet,而不是居中小弹窗 * - **分页改为「加载更多」**:手机上点页码很别扭,上一页/下一页语义也更顺 * * 所有触控区 ≥44px,正文 ≥16px(移动端规范)。 */ @Component({ selector: 'app-mb-admin-users-page', changeDetection: ChangeDetectionStrategy.OnPush, imports: [FormsModule, MAppBarComponent, MEmptyComponent], template: `
@switch (svc.state()) { @case ('loading') { } @case ('error') { } @case ('empty') { @if (svc.hasKeyword()) { } @else { } } @default {
    @for (u of svc.items(); track u.id) {
  • }
@if (svc.page() < svc.pageCount()) { } @else if (svc.pageCount() > 1) {

已到底

} } }
@if (sheetOpen()) { } @if (removeTarget(); as target) {

移除成员

确定把「{{ target.nickname || target.username }}」移出本组织吗?对方将无法继续使用。

@if (formError()) { }
} `, styles: [ ` :host { display: flex; flex-direction: column; height: 100dvh; background: var(--canvas); } .m-fab { width: 44px; height: 44px; border: 0; background: transparent; color: var(--ink); } /* 搜索吸顶:滚动列表时仍可改关键词 */ .m-search { flex: none; display: flex; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); background: var(--surface); border-bottom: 1px solid var(--edge); } .m-search-input { flex: 1; min-height: 44px; padding: 0 var(--sp-3); border: 1px solid var(--edge); border-radius: var(--radius-m); background: var(--surface-2); color: var(--ink); font-size: var(--fs-16); /* iOS 输入框 <16px 会触发自动放大 */ } .m-search-btn { min-width: 64px; min-height: 44px; border: 1px solid var(--primary); border-radius: var(--radius-m); background: var(--primary); color: var(--inverse); font-size: var(--fs-14); } .m-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--sp-3) var(--sp-4) var(--sp-8); } .m-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-3); } .m-card { background: var(--surface); border: 1px solid var(--edge); border-radius: var(--radius-m); overflow: hidden; } .m-skel { height: 96px; background: var(--surface-2); } .m-row { display: flex; align-items: center; gap: var(--sp-3); width: 100%; min-height: 64px; padding: var(--sp-3) var(--sp-4); border: 0; background: transparent; text-align: left; color: var(--ink-2); cursor: pointer; } .m-avatar { flex: none; display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; background: var(--surface-2); color: var(--ink); font-size: var(--fs-16); } .m-main { flex: 1; min-width: 0; display: grid; gap: 2px; } .m-name { color: var(--ink); font-size: var(--fs-16); font-weight: 600; } .m-sub { color: var(--muted); font-size: var(--fs-13); } .m-id { color: var(--faint); font-size: var(--fs-12); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .m-actions { display: flex; border-top: 1px solid var(--edge); } .m-act { flex: 1; min-height: 44px; border: 0; background: transparent; color: var(--ink-2); font-size: var(--fs-14); } .m-act.danger { color: var(--danger); } .m-more { display: block; width: 100%; min-height: 44px; margin-top: var(--sp-4); border: 1px solid var(--edge); border-radius: var(--radius-m); background: var(--surface); color: var(--ink-2); font-size: var(--fs-14); } .m-end { text-align: center; color: var(--faint); font-size: var(--fs-12); margin: var(--sp-4) 0 0; } .m-retry { display: block; margin: var(--sp-4) auto 0; min-height: 44px; padding: 0 var(--sp-5); border: 1px solid var(--edge); border-radius: var(--radius-m); background: var(--surface); color: var(--ink-2); } /* 底部抽屉 */ .m-sheet-mask { position: fixed; inset: 0; background: var(--mask, rgba(0, 0, 0, 0.4)); z-index: var(--z-mask); } .m-sheet { position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-modal); max-height: 86dvh; overflow-y: auto; background: var(--surface); border-radius: var(--radius-l) var(--radius-l) 0 0; padding: var(--sp-4) var(--sp-4); padding-bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px)); } .m-sheet-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-4); } .m-sheet-head h2 { margin: 0; font-size: var(--fs-18); color: var(--ink); } .m-sheet-x { width: 44px; height: 44px; border: 0; background: transparent; color: var(--muted); font-size: var(--fs-18); } .m-field { display: grid; gap: var(--sp-1); margin-bottom: var(--sp-3); font-size: var(--fs-13); color: var(--muted); } .m-field input { min-height: 44px; padding: 0 var(--sp-3); border: 1px solid var(--edge); border-radius: var(--radius-m); background: var(--surface-2); color: var(--ink); font-size: var(--fs-16); } .m-confirm-text { margin: 0 0 var(--sp-4); color: var(--ink-2); font-size: var(--fs-16); line-height: 1.6; } .m-err { margin: 0 0 var(--sp-3); color: var(--danger); font-size: var(--fs-13); } .m-submit { width: 100%; min-height: 48px; border: 1px solid var(--primary); border-radius: var(--radius-m); background: var(--primary); color: var(--inverse); font-size: var(--fs-16); } .m-submit.danger { background: var(--danger); border-color: var(--danger); } .m-submit:disabled { opacity: 0.6; } `, ], }) export class MbAdminUsersPage implements OnInit { private readonly router = inject(Router); protected readonly svc = inject(OrgUsersService); protected keywordModel = ''; protected readonly mode = signal<'add' | 'edit'>('add'); protected readonly sheetOpen = signal(false); protected readonly removeTarget = signal(null); protected readonly submitting = signal(false); protected readonly formError = signal(null); private editingId = ''; form: { userId: string; nickname: string; mobile: string; password: string } = { userId: '', nickname: '', mobile: '', password: '', }; protected readonly sheetTitle = computed(() => (this.mode() === 'add' ? '添加成员' : '编辑成员')); async ngOnInit(): Promise { await this.svc.ensureLoaded(); } /** * 返回(2026-11-11 改)。 * * 企业管理落地页 `/mobile/admin` 已下线,原来固定跳回它会让用户落到 * 一个已不存在的中间页。改为**回到来处**: * · 从抽屉「成员管理」直接进来 → 回会话列表 * · 从别处(将来可能有)进来 → 正常后退 * 与移动端其他详情页(如 `pages/agent`)的返回写法保持同一口径。 */ protected back(): void { if (history.length > 1) history.back(); else void this.router.navigateByUrl('/mobile/messages'); } protected async reload(): Promise { await this.svc.refresh(); } protected async runSearch(): Promise { await this.svc.search(this.keywordModel); } protected async loadMore(): Promise { await this.svc.goTo(this.svc.page() + 1); } protected initialOf(u: OrgUser): string { return (u.nickname || u.username || '?').slice(0, 1).toUpperCase(); } protected openAdd(): void { this.mode.set('add'); this.editingId = ''; this.form = { userId: '', nickname: '', mobile: '', password: '' }; this.formError.set(null); this.sheetOpen.set(true); } protected openEdit(u: OrgUser): void { this.mode.set('edit'); this.editingId = u.id; this.form = { userId: u.id, nickname: u.nickname, mobile: u.mobile, password: '' }; this.formError.set(null); this.sheetOpen.set(true); } protected closeSheet(): void { if (this.submitting()) return; this.sheetOpen.set(false); } protected async submit(): Promise { if (this.submitting()) return; this.submitting.set(true); this.formError.set(null); try { if (this.mode() === 'add') { await this.svc.add(this.form.userId); } else { await this.svc.update(this.editingId, { nickname: this.form.nickname, mobile: this.form.mobile, password: this.form.password || undefined, }); } this.sheetOpen.set(false); } catch (e) { this.formError.set(e instanceof Error ? e.message : '提交失败'); } finally { this.submitting.set(false); } } protected askRemove(u: OrgUser): void { this.formError.set(null); this.removeTarget.set(u); } protected async confirmRemove(): Promise { const u = this.removeTarget(); if (!u || this.submitting()) return; this.submitting.set(true); this.formError.set(null); try { await this.svc.remove(u.id); this.removeTarget.set(null); } catch (e) { this.formError.set(e instanceof Error ? e.message : '移除失败'); } finally { this.submitting.set(false); } } }