|
|
@@ -1,69 +1,63 @@
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
-import { ChangeDetectionStrategy, Component, OnInit, inject, signal } from '@angular/core';
|
|
|
+import { ChangeDetectionStrategy, Component, OnInit, computed, inject, signal } from '@angular/core';
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
|
|
-import { ArrowLeft, Eye, EyeOff, LoaderCircle, LucideAngularModule, RefreshCw, ShieldAlert, UserRound } from 'lucide-angular';
|
|
|
-import { OperationsUserDetail } from '../admin.models';
|
|
|
-import { OperationsService } from '../operations.service';
|
|
|
+import { ArrowLeft, BookOpen, CalendarDays, ClipboardList, Clock3, Eye, FileClock, GraduationCap, LoaderCircle, LucideAngularModule, MessageSquarePlus, RefreshCw, Save, ShieldAlert, UserRound, WalletCards } from 'lucide-angular';
|
|
|
+import { MemberAgentOption, OperationsUserDetail, ScheduleCalendarData } from '../admin.models';
|
|
|
+import { OperationsListKind, OperationsService } from '../operations.service';
|
|
|
+
|
|
|
+type MemberTab = 'overview' | 'courses' | 'appointments' | 'lessons' | 'balances' | 'audit';
|
|
|
|
|
|
@Component({
|
|
|
- selector: 'app-operations-user-detail',
|
|
|
- standalone: true,
|
|
|
+ selector: 'app-operations-user-detail', standalone: true,
|
|
|
imports: [CommonModule, FormsModule, RouterLink, LucideAngularModule],
|
|
|
- templateUrl: './operations-user-detail.component.html',
|
|
|
- styleUrl: './operations-user-detail.component.scss',
|
|
|
+ templateUrl: './operations-user-detail.component.html', styleUrl: './operations-user-detail.component.scss',
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
})
|
|
|
export class OperationsUserDetailComponent implements OnInit {
|
|
|
- readonly user = signal<OperationsUserDetail | null>(null);
|
|
|
- readonly loading = signal(true);
|
|
|
- readonly saving = signal(false);
|
|
|
- readonly error = signal('');
|
|
|
- readonly message = signal('');
|
|
|
- readonly reason = signal('');
|
|
|
- readonly groupId = signal(0);
|
|
|
- readonly parentUserId = signal(0);
|
|
|
- readonly balanceType = signal(1);
|
|
|
- readonly balanceDirection = signal<'add' | 'deduct'>('add');
|
|
|
- readonly balanceAmount = signal(0);
|
|
|
- readonly revealMobile = signal(false);
|
|
|
- readonly icons = { ArrowLeft, Eye, EyeOff, LoaderCircle, RefreshCw, ShieldAlert, UserRound };
|
|
|
- private readonly route = inject(ActivatedRoute);
|
|
|
- private readonly operations = inject(OperationsService);
|
|
|
- private readonly objectId = this.route.snapshot.paramMap.get('objectId') || '';
|
|
|
+ readonly user = signal<OperationsUserDetail | null>(null); readonly tab = signal<MemberTab>('overview'); readonly related = signal<Record<MemberTab, Record<string, unknown>[]>>({ overview: [], courses: [], appointments: [], lessons: [], balances: [], audit: [] });
|
|
|
+ readonly notes = signal<Record<string, unknown>[]>([]); readonly loading = signal(true); readonly sectionLoading = signal(false); readonly saving = signal(false); readonly error = signal(''); readonly message = signal(''); readonly reason = signal('');
|
|
|
+ readonly editName = signal(''); readonly editNickname = signal(''); readonly editMobile = signal(''); readonly editEmail = signal(''); readonly editGender = signal(''); readonly editGrade = signal(''); readonly parentUserId = signal(0); readonly agentSearch = signal('');
|
|
|
+ readonly balanceType = signal(1); readonly balanceDirection = signal<'add' | 'deduct'>('add'); readonly balanceAmount = signal(0); readonly payMethod = signal(''); readonly resetPassword = signal(''); readonly noteContent = signal(''); readonly revealMobile = signal(false);
|
|
|
+ readonly courseId = signal(0); readonly courseOpenedCount = signal(1); readonly courseSearch = signal(''); readonly courseOptions = signal<ScheduleCalendarData['courses']>([]);
|
|
|
+ readonly icons = { ArrowLeft, BookOpen, CalendarDays, ClipboardList, Clock3, Eye, FileClock, GraduationCap, LoaderCircle, MessageSquarePlus, RefreshCw, Save, ShieldAlert, UserRound, WalletCards };
|
|
|
+ private readonly route = inject(ActivatedRoute); private readonly operations = inject(OperationsService); private readonly objectId = this.route.snapshot.paramMap.get('objectId') || '';
|
|
|
+ readonly filteredAgents = computed(() => { const keyword = this.agentSearch().trim().toLowerCase(); return (this.user()?.agents || []).filter(agent => !keyword || `${agent.displayName} ${agent.userId}`.toLowerCase().includes(keyword)).slice(0, 30); });
|
|
|
+ readonly filteredCourses = computed(() => { const keyword = this.courseSearch().trim().toLowerCase(); return this.courseOptions().filter(course => !keyword || `${course.courseName} ${course.courseId}`.toLowerCase().includes(keyword)).slice(0, 30); });
|
|
|
|
|
|
ngOnInit(): void { void this.load(); }
|
|
|
-
|
|
|
async load(): Promise<void> {
|
|
|
this.loading.set(true); this.error.set('');
|
|
|
- try {
|
|
|
- const user = await this.operations.userDetail(this.objectId);
|
|
|
- this.user.set(user); this.groupId.set(user.groupId); this.parentUserId.set(user.parentUserId); this.revealMobile.set(Boolean(user.mobile));
|
|
|
- } catch (error) { this.error.set(error instanceof Error ? error.message : '用户详情加载失败'); }
|
|
|
+ try { const user = await this.operations.userDetail(this.objectId); this.user.set(user); this.editName.set(user.displayName); this.editNickname.set(user.nickname || ''); this.editMobile.set(user.mobile || user.mobileMasked); this.editEmail.set(user.email || ''); this.editGender.set(user.gender || ''); this.editGrade.set(user.grade || ''); this.parentUserId.set(user.parentUserId); this.agentSearch.set(user.parentDisplayName || ''); this.revealMobile.set(Boolean(user.mobile)); }
|
|
|
+ catch (error) { this.error.set(error instanceof Error ? error.message : '会员详情加载失败'); }
|
|
|
finally { this.loading.set(false); }
|
|
|
}
|
|
|
-
|
|
|
- async run(operation: string, payload: Record<string, unknown>, success: string): Promise<void> {
|
|
|
- const user = this.user();
|
|
|
- if (!user || !this.reason().trim()) { this.error.set('请输入操作原因'); return; }
|
|
|
- this.saving.set(true); this.error.set(''); this.message.set('');
|
|
|
+ async selectTab(tab: MemberTab): Promise<void> { this.tab.set(tab); if (tab !== 'overview') await this.loadSection(tab); }
|
|
|
+ private kindFor(tab: MemberTab): OperationsListKind | null { return ({ courses: 'course-bindings', appointments: 'appointments', lessons: 'lessons', balances: 'money-logs' } as Partial<Record<MemberTab, OperationsListKind>>)[tab] || null; }
|
|
|
+ async loadSection(tab = this.tab()): Promise<void> {
|
|
|
+ const user = this.user(); if (!user || tab === 'overview') return; this.sectionLoading.set(true); this.error.set('');
|
|
|
try {
|
|
|
- await this.operations.mutate(operation, user.objectId, this.reason(), payload, user.updatedAt);
|
|
|
- this.message.set(success); this.reason.set(''); await this.load();
|
|
|
- } catch (error) { this.error.set(error instanceof Error ? error.message : '操作失败'); }
|
|
|
- finally { this.saving.set(false); }
|
|
|
+ if (tab === 'audit') { const [notes, audit] = await Promise.all([this.operations.memberRows('ops/users/notes', user.objectId), this.operations.memberRows('ops/users/audit', user.objectId)]); this.notes.set(notes.items); this.related.update(value => ({ ...value, audit: audit.items })); }
|
|
|
+ else { const kind = this.kindFor(tab); if (kind) { const page = await this.operations.list(kind, 1, 100, String(user.userId)); this.related.update(value => ({ ...value, [tab]: page.items })); } }
|
|
|
+ if (tab === 'courses' && !this.courseOptions().length) { const today = new Date().toISOString().slice(0, 10); const schedule = await this.operations.schedule(today, today); this.courseOptions.set(schedule.courses); }
|
|
|
+ } catch (error) { this.error.set(error instanceof Error ? error.message : '业务记录加载失败'); }
|
|
|
+ finally { this.sectionLoading.set(false); }
|
|
|
}
|
|
|
-
|
|
|
- changeStatus(): void { const user = this.user(); if (user) void this.run('ops/users/change-status', { disabled: user.status !== 'disabled' }, user.status === 'disabled' ? '账号已恢复' : '账号已停用'); }
|
|
|
- changeGroup(): void { void this.run('ops/users/change-group', { groupId: this.groupId() }, '用户组已更新'); }
|
|
|
- saveRelation(): void { void this.run('ops/relations/save', { parentUserId: this.parentUserId() }, '所属代理已更新'); }
|
|
|
- adjustBalance(): void { void this.run('ops/users/adjust-balance', { type: this.balanceType(), direction: this.balanceDirection(), amount: this.balanceAmount() }, '课时或余额已调整'); }
|
|
|
- async reveal(): Promise<void> {
|
|
|
- const user = this.user(); if (!user) return;
|
|
|
- this.error.set('');
|
|
|
- try {
|
|
|
- const result = await this.operations.mutate<{ mobile: string }>('ops/users/reveal-mobile', user.objectId, '运营核对用户联系方式');
|
|
|
- this.user.set({ ...user, mobile: result.mobile }); this.revealMobile.set(true);
|
|
|
- } catch (error) { this.error.set(error instanceof Error ? error.message : '手机号查看失败'); }
|
|
|
+ async run(operation: string, payload: Record<string, unknown>, success: string, targetId = this.user()?.objectId || '', expectedUpdatedAt = this.user()?.updatedAt || ''): Promise<boolean> {
|
|
|
+ if (!this.reason().trim()) { this.error.set('请输入操作原因'); return false; } this.saving.set(true); this.error.set(''); this.message.set('');
|
|
|
+ try { await this.operations.mutate(operation, targetId, this.reason(), payload, expectedUpdatedAt); this.message.set(success); this.reason.set(''); return true; }
|
|
|
+ catch (error) { this.error.set(error instanceof Error ? error.message : '操作失败'); return false; }
|
|
|
+ finally { this.saving.set(false); }
|
|
|
}
|
|
|
+ selectAgent(agent: MemberAgentOption): void { this.parentUserId.set(agent.userId); this.agentSearch.set(agent.displayName); }
|
|
|
+ clearAgent(): void { this.parentUserId.set(0); this.agentSearch.set(''); }
|
|
|
+ selectCourse(course: ScheduleCalendarData['courses'][number]): void { this.courseId.set(course.courseId); this.courseSearch.set(course.courseName); }
|
|
|
+ async saveProfile(): Promise<void> { if (await this.run('ops/users/update-profile', { displayName: this.editName(), nickname: this.editNickname(), mobile: this.revealMobile() ? this.editMobile() : '', email: this.editEmail(), gender: this.editGender(), grade: this.editGrade(), parentUserId: this.parentUserId() }, '会员资料已更新')) await this.load(); }
|
|
|
+ async changeStatus(): Promise<void> { const user = this.user(); if (user && await this.run('ops/users/change-status', { disabled: user.status !== 'disabled' }, user.status === 'disabled' ? '账号已恢复' : '账号已停用')) await this.load(); }
|
|
|
+ async adjustBalance(): Promise<void> { if (!this.payMethod()) { this.error.set('请选择付款或调整方式'); return; } if (await this.run('ops/users/adjust-balance', { type: this.balanceType(), direction: this.balanceDirection(), amount: this.balanceAmount(), payMethod: this.payMethod() }, '课时已调整并写入流水')) { this.balanceAmount.set(0); await this.load(); await this.loadSection('balances'); } }
|
|
|
+ async resetMemberPassword(): Promise<void> { if (this.resetPassword().length < 8) { this.error.set('临时密码至少 8 位'); return; } if (await this.run('ops/users/reset-password', { password: this.resetPassword() }, '临时密码已更新,旧会话已退出')) { this.resetPassword.set(''); await this.load(); } }
|
|
|
+ async saveNote(): Promise<void> { if (!this.noteContent().trim()) { this.error.set('请输入跟进备注'); return; } if (await this.run('ops/users/save-note', { content: this.noteContent() }, '跟进备注已保存')) { this.noteContent.set(''); await this.loadSection('audit'); } }
|
|
|
+ async bindCourse(): Promise<void> { const user = this.user(); if (!user || !this.courseId()) { this.error.set('请选择课程'); return; } if (await this.run('ops/course-bindings/save', { studentId: user.userId, courseId: this.courseId(), openedCount: this.courseOpenedCount() }, '课程已绑定')) { this.courseId.set(0); this.courseSearch.set(''); await this.load(); await this.loadSection('courses'); } }
|
|
|
+ async toggleCourse(row: Record<string, unknown>): Promise<void> { const recycled = Number(row['status']) === -2; if (await this.run(recycled ? 'ops/course-bindings/restore' : 'ops/course-bindings/recycle', {}, recycled ? '课程绑定已恢复' : '课程绑定已移入回收站', String(row['targetId'] || ''), String(row['updatedAt'] || ''))) await this.loadSection('courses'); }
|
|
|
+ async reveal(): Promise<void> { const user = this.user(); if (!user) return; this.error.set(''); try { const result = await this.operations.mutate<{ mobile: string }>('ops/users/reveal-mobile', user.objectId, '运营核对会员联系方式'); this.user.set({ ...user, mobile: result.mobile }); this.editMobile.set(result.mobile); this.revealMobile.set(true); } catch (error) { this.error.set(error instanceof Error ? error.message : '手机号查看失败'); } }
|
|
|
}
|