Просмотр исходного кода

fix: align admin resource table behavior

彭峰 1 месяц назад
Родитель
Сommit
621961b7ef

+ 1 - 0
docs/migration/admin-migration-status.md

@@ -14,6 +14,7 @@
 - 后台迁移状态页直接读取线上 `xiaoshu.app.gateway/migration_status`,动态显示已实现 action 数量及完整阻塞原因清单,不再使用前端硬编码阻塞计数。
 - 后台“全部数据类”目录由管理员云函数实时返回白名单,所有已开放规范化类均可从界面进入动态列表/编辑页;`_Session`、`Function` 等敏感系统类不进入目录。
 - 动态编辑器对 Object/Array 执行 JSON 类型校验,错误格式会阻止保存;Relation、File、GeoPoint 等无法安全通用写入的 Parse 类型由前后端共同设为只读,避免结构化字段被字符串覆盖。
+- 动态资源表按 `objectId/name/title/username` 等业务字段稳定优先展示,总页数采用向上取整;只读类使用查看抽屉且不显示保存/删除操作。
 - Parse 登录只在浏览器保存当前 `sessionToken`;`masterKey` 只在部署进程中使用。
 - 云函数网关强制校验管理员身份、`company` 帐套、类白名单、字段白名单和敏感字段过滤。
 - `_Session` 不开放,`Function` 源码不开放通用查询或编辑;用户密码只允许专用重置操作。

+ 4 - 4
src/app/admin/pages/admin-resource.component.html

@@ -11,17 +11,17 @@
   } @else {
     <div class="table-scroll"><table><thead><tr>@for (field of columns(); track field.name) { <th>{{ field.name }}</th> }<th class="actions">操作</th></tr></thead><tbody>
       @for (row of page()?.results || []; track row['objectId']) {
-        <tr>@for (field of columns(); track field.name) { <td [title]="display(row[field.name])">{{ display(row[field.name]) }}</td> }<td class="actions"><button type="button" aria-label="编辑" (click)="openEdit(row)"><lucide-icon [img]="icons.Pencil" [size]="16" /></button>@if (schema()?.writable) { <button class="danger" type="button" aria-label="删除" (click)="remove(row)"><lucide-icon [img]="icons.Trash2" [size]="16" /></button> }</td></tr>
+        <tr>@for (field of columns(); track field.name) { <td [title]="display(row[field.name])">{{ display(row[field.name]) }}</td> }<td class="actions"><button type="button" [attr.aria-label]="schema()?.writable ? '编辑' : '查看'" (click)="openEdit(row)"><lucide-icon [img]="schema()?.writable ? icons.Pencil : icons.Eye" [size]="16" /></button>@if (schema()?.writable) { <button class="danger" type="button" aria-label="删除" (click)="remove(row)"><lucide-icon [img]="icons.Trash2" [size]="16" /></button> }</td></tr>
       } @empty { <tr><td class="empty-cell" [attr.colspan]="columns().length + 1">没有符合条件的数据</td></tr> }
     </tbody></table></div>
-    <footer class="pagination"><span>第 {{ page()?.page || 1 }} 页,共 {{ ((page()?.total || 0) / (page()?.pageSize || 20)) | number:'1.0-0' }} 页</span><div><button type="button" [disabled]="(page()?.page || 1) <= 1 || loading()" (click)="load((page()?.page || 1) - 1)"><lucide-icon [img]="icons.ChevronLeft" [size]="17" />上一页</button><button type="button" [disabled]="(page()?.page || 1) * (page()?.pageSize || 20) >= (page()?.total || 0) || loading()" (click)="load((page()?.page || 1) + 1)">下一页<lucide-icon [img]="icons.ChevronRight" [size]="17" /></button></div></footer>
+    <footer class="pagination"><span>第 {{ page()?.page || 1 }} 页,共 {{ pageCount() }} 页</span><div><button type="button" [disabled]="(page()?.page || 1) <= 1 || loading()" (click)="load((page()?.page || 1) - 1)"><lucide-icon [img]="icons.ChevronLeft" [size]="17" />上一页</button><button type="button" [disabled]="(page()?.page || 1) * (page()?.pageSize || 20) >= (page()?.total || 0) || loading()" (click)="load((page()?.page || 1) + 1)">下一页<lucide-icon [img]="icons.ChevronRight" [size]="17" /></button></div></footer>
   }
 </section>
 
 @if (editorOpen()) {
   <button class="drawer-mask" type="button" aria-label="关闭编辑器" (click)="closeEditor()"></button>
   <aside class="editor-drawer">
-    <header><div><span>{{ draftId() ? '编辑记录' : '新建记录' }}</span><strong>{{ schema()?.label }}</strong></div><button type="button" (click)="closeEditor()"><lucide-icon [img]="icons.X" [size]="20" /></button></header>
+    <header><div><span>{{ !schema()?.writable ? '查看记录' : draftId() ? '编辑记录' : '新建记录' }}</span><strong>{{ schema()?.label }}</strong></div><button type="button" (click)="closeEditor()"><lucide-icon [img]="icons.X" [size]="20" /></button></header>
     <div class="editor-fields">
       @for (field of schema()?.fields || []; track field.name) { @if (field.writable) {
         <label><span>{{ field.name }} <small>{{ field.type }}{{ field.required ? ' · 必填' : '' }}</small></span>
@@ -31,6 +31,6 @@
         </label>
       } }
     </div>
-    <footer><button type="button" (click)="closeEditor()">取消</button><button class="primary" type="button" [disabled]="saving()" (click)="save()">{{ saving() ? '保存中…' : '保存记录' }}</button></footer>
+    <footer><button type="button" (click)="closeEditor()">{{ schema()?.writable ? '取消' : '关闭' }}</button>@if (schema()?.writable) { <button class="primary" type="button" [disabled]="saving()" (click)="save()">{{ saving() ? '保存中…' : '保存记录' }}</button> }</footer>
   </aside>
 }

+ 14 - 0
src/app/admin/pages/admin-resource.component.spec.ts

@@ -39,4 +39,18 @@ describe('AdminResourceComponent', () => {
     expect(component.fieldErrors()['payload']).toBeUndefined();
     expect(component.draft()['payload']).toEqual({ enabled: true });
   });
+
+  it('keeps preferred columns first and rounds page counts upward', () => {
+    component.schema.set({
+      className: 'CommonModel', label: '内容', writable: true, supportsSoftDelete: true,
+      fields: [
+        { name: 'misc', type: 'String', writable: true },
+        { name: 'title', type: 'String', writable: true },
+        { name: 'objectId', type: 'String', writable: false },
+      ],
+    });
+    component.page.set({ className: 'CommonModel', page: 1, pageSize: 20, total: 21, results: [] });
+    expect(component.columns().map((field) => field.name)).toEqual(['objectId', 'title', 'misc']);
+    expect(component.pageCount()).toBe(2);
+  });
 });

+ 5 - 3
src/app/admin/pages/admin-resource.component.ts

@@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, computed, injec
 import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
-import { ChevronLeft, ChevronRight, LoaderCircle, LucideAngularModule, Pencil, Plus, RefreshCw, Search, Trash2, X } from 'lucide-angular';
+import { ChevronLeft, ChevronRight, Eye, LoaderCircle, LucideAngularModule, Pencil, Plus, RefreshCw, Search, Trash2, X } from 'lucide-angular';
 import { AdminClassSchema, AdminFieldSchema, ResourcePage } from '../admin.models';
 import { CloudFunctionsService } from '../cloud-functions.service';
 
@@ -28,12 +28,14 @@ export class AdminResourceComponent implements OnInit {
   readonly draft = signal<Record<string, unknown>>({});
   readonly fieldErrors = signal<Record<string, string>>({});
   readonly fieldInputs = signal<Record<string, string>>({});
-  readonly icons = { ChevronLeft, ChevronRight, LoaderCircle, Pencil, Plus, RefreshCw, Search, Trash2, X };
+  readonly icons = { ChevronLeft, ChevronRight, Eye, LoaderCircle, Pencil, Plus, RefreshCw, Search, Trash2, X };
   readonly columns = computed(() => {
     const fields = this.schema()?.fields ?? [];
     const preferred = ['objectId', 'name', 'title', 'username', 'realName', 'mobile', 'sourceKey', 'status', 'createdAt', 'updatedAt'];
-    return [...fields].sort((a, b) => preferred.indexOf(a.name) - preferred.indexOf(b.name)).slice(0, 7);
+    const rank = (name: string): number => { const index = preferred.indexOf(name); return index < 0 ? preferred.length : index; };
+    return fields.map((field, index) => ({ field, index })).sort((a, b) => rank(a.field.name) - rank(b.field.name) || a.index - b.index).map(({ field }) => field).slice(0, 7);
   });
+  readonly pageCount = computed(() => Math.max(1, Math.ceil((this.page()?.total ?? 0) / (this.page()?.pageSize || 20))));
   private readonly functions = inject(CloudFunctionsService);
   private readonly route = inject(ActivatedRoute);
   private readonly destroyRef = inject(DestroyRef);