| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485 |
- import { CommonModule } from '@angular/common';
- import { ChangeDetectionStrategy, Component, OnInit, inject, signal } from '@angular/core';
- import { FormsModule } from '@angular/forms';
- import { ActivatedRoute, RouterLink } from '@angular/router';
- import { ChevronLeft, ChevronRight, Eye, Link2, LoaderCircle, LucideAngularModule, Plus, RefreshCw, RotateCcw, Search, Trash2 } from 'lucide-angular';
- import { AdminSessionService } from '../admin-session.service';
- import { OperationsPage } from '../admin.models';
- import { OperationsListKind, OperationsService } from '../operations.service';
- interface ListColumn { key: string; label: string; format?: 'date' | 'status' | 'number'; }
- interface ListConfig { title: string; eyebrow: string; description: string; operationLabel: string; rowKey: string; columns: ListColumn[]; }
- const CONFIGS: Record<OperationsListKind, ListConfig> = {
- relations: { title: '陪练与代理关系', eyebrow: 'USER RELATIONS', description: '核对用户、所属代理及直属关系', operationLabel: '关系记录', rowKey: 'userObjectId', columns: [{ key: 'displayName', label: '用户' }, { key: 'userId', label: '用户 ID' }, { key: 'groupName', label: '用户组' }, { key: 'parentDisplayName', label: '所属代理' }, { key: 'parentUserId', label: '代理 ID' }, { key: 'registeredAt', label: '注册时间', format: 'date' }] },
- 'course-bindings': { title: '用户课程绑定', eyebrow: 'COURSE BINDINGS', description: '查看学员课程、学习进度和绑定状态', operationLabel: '绑定记录', rowKey: 'targetId', columns: [{ key: 'studentName', label: '学员' }, { key: 'studentId', label: '学员 ID' }, { key: 'courseName', label: '课程' }, { key: 'learnedCount', label: '已学', format: 'number' }, { key: 'openedCount', label: '课程内容量', format: 'number' }, { key: 'progressStage', label: '进度' }, { key: 'statusLabel', label: '状态', format: 'status' }, { key: 'createdAt', label: '绑定时间', format: 'date' }] },
- appointments: { title: '预约排课', eyebrow: 'APPOINTMENTS', description: '按学员、陪练、课程和状态核对预约', operationLabel: '预约记录', rowKey: 'targetId', columns: [{ key: 'studentName', label: '学员' }, { key: 'coachName', label: '陪练' }, { key: 'courseName', label: '课程' }, { key: 'appointmentAt', label: '预约时间', format: 'date' }, { key: 'classTypeLabel', label: '课程类型' }, { key: 'billingTypeLabel', label: '计费方式' }, { key: 'statusLabel', label: '状态', format: 'status' }] },
- lessons: { title: '上课记录', eyebrow: 'LESSON RECORDS', description: '查看学员、陪练、课程、评价和上课时间', operationLabel: '上课记录', rowKey: 'targetId', columns: [{ key: 'studentName', label: '学员' }, { key: 'coachName', label: '陪练' }, { key: 'courseName', label: '课程' }, { key: 'classTypeLabel', label: '课程类型' }, { key: 'score', label: '评分', format: 'number' }, { key: 'comment', label: '评价' }, { key: 'lessonAt', label: '上课时间', format: 'date' }] },
- 'learning-records': { title: '每日学习记录', eyebrow: 'LEARNING RECORDS', description: '跟踪学员学习、掌握和待复习情况', operationLabel: '学习记录', rowKey: 'targetId', columns: [{ key: 'studentName', label: '学员' }, { key: 'courseName', label: '课程' }, { key: 'studyDate', label: '学习日期', format: 'date' }, { key: 'learned', label: '学习数量', format: 'number' }, { key: 'mastered', label: '已掌握', format: 'number' }, { key: 'reviewDue', label: '待复习', format: 'number' }, { key: 'statusLabel', label: '状态', format: 'status' }] },
- finance: { title: '用户课时与余额', eyebrow: 'PERIODS & BALANCES', description: '核对用户 30/60 分钟课时、经验和积分', operationLabel: '用户余额', rowKey: 'userObjectId', columns: [{ key: 'displayName', label: '用户' }, { key: 'userId', label: '用户 ID' }, { key: 'periods30', label: '30 分钟', format: 'number' }, { key: 'periods60', label: '60 分钟', format: 'number' }, { key: 'trialPeriods', label: '体验/经验', format: 'number' }, { key: 'userPoint', label: '积分', format: 'number' }, { key: 'statusLabel', label: '账号状态', format: 'status' }] },
- 'money-logs': { title: '资金积分流水', eyebrow: 'FINANCE LEDGER', description: '查看课时、余额、经验和积分变动记录', operationLabel: '流水记录', rowKey: 'targetId', columns: [{ key: 'userName', label: '用户' }, { key: 'userId', label: '用户 ID' }, { key: 'moneyTypeLabel', label: '项目' }, { key: 'score', label: '变动', format: 'number' }, { key: 'scoreBefore', label: '变动前', format: 'number' }, { key: 'detail', label: '说明' }, { key: 'occurredAt', label: '时间', format: 'date' }] },
- };
- @Component({
- selector: 'app-operations-list', standalone: true,
- imports: [CommonModule, FormsModule, RouterLink, LucideAngularModule],
- templateUrl: './operations-list.component.html', styleUrl: './operations-list.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush,
- })
- export class OperationsListComponent implements OnInit {
- readonly kind: OperationsListKind;
- readonly config: ListConfig;
- readonly page = signal<OperationsPage | null>(null);
- readonly query = signal(''); readonly loading = signal(true); readonly saving = signal(false); readonly error = signal(''); readonly message = signal('');
- readonly createOpen = signal(false); readonly bindingStudentId = signal(0); readonly bindingCourseId = signal(0); readonly bindingOpenedCount = signal(0); readonly bindingProgress = signal(''); readonly actionReason = signal('');
- readonly icons = { ChevronLeft, ChevronRight, Eye, Link2, LoaderCircle, Plus, RefreshCw, RotateCcw, Search, Trash2 };
- private readonly operations = inject(OperationsService); private readonly sessions = inject(AdminSessionService);
- constructor(route: ActivatedRoute) { this.kind = route.snapshot.data['kind'] as OperationsListKind; this.config = CONFIGS[this.kind]; }
- ngOnInit(): void { void this.load(1); }
- canMutate(): boolean { return this.sessions.session()?.operationsRole !== 'ops-auditor'; }
- isManager(): boolean { return this.sessions.session()?.operationsRole === 'ops-manager' || this.sessions.session()?.isSuperAdmin === true; }
- pageCount(): number { const value = this.page(); return Math.max(1, Math.ceil((value?.total ?? 0) / (value?.pageSize ?? 20))); }
- rowId(row: Record<string, unknown>): string { return String(row[this.config.rowKey] ?? ''); }
- value(row: Record<string, unknown>, column: ListColumn): unknown { return row[column.key] ?? '—'; }
- numericValue(row: Record<string, unknown>, column: ListColumn): number { return Number(row[column.key] ?? 0); }
- isRecycled(row: Record<string, unknown>): boolean { return Number(row['status']) === -2; }
- appointmentStatus(row: Record<string, unknown>): number { return Number(row['statusCode'] ?? 0); }
- date(value: unknown): string { if (!value) return '—'; const date = new Date(String(value)); return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat('zh-CN', { dateStyle: 'short', timeStyle: 'short' }).format(date); }
- async load(page = this.page()?.page ?? 1): Promise<void> { this.loading.set(true); this.error.set(''); try { this.page.set(await this.operations.list(this.kind, page, 20, this.query())); } catch (error) { this.error.set(error instanceof Error ? error.message : '运营数据加载失败'); } finally { this.loading.set(false); } }
- reset(): void { this.query.set(''); void this.load(1); }
- async createBinding(): Promise<void> {
- if (!this.bindingStudentId() || !this.bindingCourseId() || !this.actionReason().trim()) { this.error.set('请输入学员 ID、课程 ID 和操作原因'); return; }
- await this.mutate('ops/course-bindings/save', '', { studentId: this.bindingStudentId(), courseId: this.bindingCourseId(), openedCount: this.bindingOpenedCount(), progressStage: this.bindingProgress() }, '课程绑定已创建');
- this.createOpen.set(false);
- }
- async rowAction(row: Record<string, unknown>): Promise<void> {
- if (this.kind === 'relations') {
- const parent = globalThis.prompt('请输入新的上级代理用户 ID;输入 0 表示解除关系', String(row['parentUserId'] ?? 0)); if (parent === null) return;
- const reason = globalThis.prompt('请输入修改原因'); if (!reason?.trim()) return;
- this.actionReason.set(reason); await this.mutate('ops/relations/save', this.rowId(row), { parentUserId: Number(parent) }, '代理关系已更新'); return;
- }
- if (this.kind === 'course-bindings') {
- const recycled = Number(row['status']) === -2; const reason = globalThis.prompt(recycled ? '请输入恢复绑定原因' : '请输入取消绑定原因'); if (!reason?.trim()) return;
- this.actionReason.set(reason); await this.mutate(recycled ? 'ops/course-bindings/restore' : 'ops/course-bindings/recycle', this.rowId(row), {}, recycled ? '课程绑定已恢复' : '课程绑定已移入回收站', String(row['updatedAt'] ?? '')); return;
- }
- if (this.kind === 'appointments') {
- const next = this.nextAppointmentStatus(Number(row['statusCode'])); if (next === null) return;
- const reason = globalThis.prompt('请输入预约状态变更原因'); if (!reason?.trim()) return;
- this.actionReason.set(reason); await this.mutate('ops/appointments/transition', this.rowId(row), { status: next }, '预约状态已更新', String(row['updatedAt'] ?? ''));
- }
- }
- nextAppointmentStatus(status: number): number | null { return status === 0 ? 10 : status === 10 ? 11 : status === 11 ? 20 : status === 20 ? 30 : null; }
- nextAppointmentLabel(status: number): string { return status === 0 ? '开始上课' : status === 10 ? '完成课程' : status === 11 ? '确认完成' : status === 20 ? '归档' : ''; }
- private async mutate(operation: string, targetId: string, payload: Record<string, unknown>, success: string, expectedUpdatedAt = ''): Promise<void> {
- this.saving.set(true); this.error.set(''); this.message.set('');
- try { await this.operations.mutate(operation, targetId, this.actionReason(), payload, expectedUpdatedAt); this.message.set(success); this.actionReason.set(''); await this.load(1); }
- catch (error) { this.error.set(error instanceof Error ? error.message : '操作失败'); }
- finally { this.saving.set(false); }
- }
- }
|