admin-resource.component.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import { CommonModule } from '@angular/common';
  2. import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, computed, inject, signal } from '@angular/core';
  3. import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
  4. import { FormsModule } from '@angular/forms';
  5. import { ActivatedRoute } from '@angular/router';
  6. import { ChevronLeft, ChevronRight, LoaderCircle, LucideAngularModule, Pencil, Plus, RefreshCw, Search, Trash2, X } from 'lucide-angular';
  7. import { AdminClassSchema, AdminFieldSchema, ResourcePage } from '../admin.models';
  8. import { CloudFunctionsService } from '../cloud-functions.service';
  9. @Component({
  10. selector: 'app-admin-resource',
  11. standalone: true,
  12. imports: [CommonModule, FormsModule, LucideAngularModule],
  13. templateUrl: './admin-resource.component.html',
  14. styleUrl: './admin-resource.component.scss',
  15. changeDetection: ChangeDetectionStrategy.OnPush,
  16. })
  17. export class AdminResourceComponent implements OnInit {
  18. readonly className = signal('');
  19. readonly schema = signal<AdminClassSchema | null>(null);
  20. readonly page = signal<ResourcePage | null>(null);
  21. readonly loading = signal(true);
  22. readonly saving = signal(false);
  23. readonly error = signal('');
  24. readonly search = signal('');
  25. readonly editorOpen = signal(false);
  26. readonly draftId = signal('');
  27. readonly draft = signal<Record<string, unknown>>({});
  28. readonly fieldErrors = signal<Record<string, string>>({});
  29. readonly fieldInputs = signal<Record<string, string>>({});
  30. readonly icons = { ChevronLeft, ChevronRight, LoaderCircle, Pencil, Plus, RefreshCw, Search, Trash2, X };
  31. readonly columns = computed(() => {
  32. const fields = this.schema()?.fields ?? [];
  33. const preferred = ['objectId', 'name', 'title', 'username', 'realName', 'mobile', 'sourceKey', 'status', 'createdAt', 'updatedAt'];
  34. return [...fields].sort((a, b) => preferred.indexOf(a.name) - preferred.indexOf(b.name)).slice(0, 7);
  35. });
  36. private readonly functions = inject(CloudFunctionsService);
  37. private readonly route = inject(ActivatedRoute);
  38. private readonly destroyRef = inject(DestroyRef);
  39. ngOnInit(): void {
  40. this.route.paramMap.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
  41. this.className.set(params.get('className') ?? '');
  42. this.search.set('');
  43. void this.load(1, true);
  44. });
  45. }
  46. async load(pageNumber = this.page()?.page ?? 1, reloadSchema = false): Promise<void> {
  47. this.loading.set(true); this.error.set('');
  48. try {
  49. if (reloadSchema || !this.schema()) this.schema.set(await this.functions.admin<AdminClassSchema>('schema', { className: this.className() }));
  50. this.page.set(await this.functions.admin<ResourcePage>('list', { className: this.className(), page: pageNumber, pageSize: 20, search: this.search().trim() }));
  51. } catch (error) { this.error.set(error instanceof Error ? error.message : '数据加载失败'); }
  52. finally { this.loading.set(false); }
  53. }
  54. openCreate(): void { this.draftId.set(''); this.draft.set({}); this.fieldErrors.set({}); this.fieldInputs.set({}); this.editorOpen.set(true); }
  55. async openEdit(row: Record<string, unknown>): Promise<void> {
  56. const objectId = String(row['objectId'] ?? '');
  57. this.error.set('');
  58. try {
  59. const detail = await this.functions.admin<Record<string, unknown>>('get', { className: this.className(), objectId });
  60. this.draftId.set(objectId); this.draft.set(detail); this.fieldErrors.set({}); this.fieldInputs.set({}); this.editorOpen.set(true);
  61. } catch (error) { this.error.set(error instanceof Error ? error.message : '详情加载失败'); }
  62. }
  63. closeEditor(): void { this.editorOpen.set(false); this.draft.set({}); this.draftId.set(''); this.fieldErrors.set({}); this.fieldInputs.set({}); }
  64. setField(field: AdminFieldSchema, raw: unknown): void {
  65. let value = raw;
  66. if (field.type === 'Number') value = raw === '' ? null : Number(raw);
  67. if (field.type === 'Pointer') value = raw ? { __type: 'Pointer', className: field.targetClass, objectId: String(raw) } : null;
  68. if ((field.type === 'Object' || field.type === 'Array') && typeof raw === 'string') {
  69. this.fieldInputs.update((current) => ({ ...current, [field.name]: raw }));
  70. try {
  71. value = raw.trim() ? JSON.parse(raw) : field.type === 'Array' ? [] : {};
  72. const valid = field.type === 'Array' ? Array.isArray(value) : Boolean(value) && typeof value === 'object' && !Array.isArray(value);
  73. if (!valid) throw new Error();
  74. this.fieldErrors.update((current) => { const next = { ...current }; delete next[field.name]; return next; });
  75. } catch {
  76. this.fieldErrors.update((current) => ({ ...current, [field.name]: field.type === 'Array' ? '请输入有效的 JSON 数组' : '请输入有效的 JSON 对象' }));
  77. return;
  78. }
  79. }
  80. this.draft.update((current) => ({ ...current, [field.name]: value }));
  81. }
  82. inputValue(field: AdminFieldSchema): string | number {
  83. const value = this.draft()[field.name];
  84. if (field.type === 'Pointer') return String((value as { objectId?: string } | null)?.objectId ?? '');
  85. if (field.type === 'Date') return String((value as { iso?: string } | string | null)?.hasOwnProperty?.('iso') ? (value as { iso: string }).iso.slice(0, 16) : String(value ?? '').slice(0, 16));
  86. if (field.type === 'Object' || field.type === 'Array') return this.fieldInputs()[field.name] ?? (value == null ? '' : JSON.stringify(value, null, 2));
  87. return typeof value === 'number' ? value : String(value ?? '');
  88. }
  89. async save(): Promise<void> {
  90. const invalidFields = Object.keys(this.fieldErrors());
  91. if (invalidFields.length) { this.error.set(`请先修正字段格式:${invalidFields.join('、')}`); return; }
  92. this.saving.set(true); this.error.set('');
  93. try {
  94. await this.functions.admin('save', { className: this.className(), objectId: this.draftId() || undefined, fields: this.draft() });
  95. this.closeEditor(); await this.load();
  96. } catch (error) { this.error.set(error instanceof Error ? error.message : '保存失败'); }
  97. finally { this.saving.set(false); }
  98. }
  99. async remove(row: Record<string, unknown>): Promise<void> {
  100. const objectId = String(row['objectId'] ?? '');
  101. if (!objectId || !confirm(`确认删除 ${this.className()} / ${objectId}?`)) return;
  102. try { await this.functions.admin('delete', { className: this.className(), objectId }); await this.load(); }
  103. catch (error) { this.error.set(error instanceof Error ? error.message : '删除失败'); }
  104. }
  105. display(value: unknown): string {
  106. if (value == null || value === '') return '—';
  107. if (typeof value === 'object') {
  108. const data = value as Record<string, unknown>;
  109. return String(data['name'] ?? data['title'] ?? data['username'] ?? data['objectId'] ?? JSON.stringify(value));
  110. }
  111. return String(value);
  112. }
  113. }