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') {
@for (i of [1, 2, 3, 4]; track i) {
}
}
@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()) {
{{ 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);
}
}
}