|
|
@@ -51,6 +51,9 @@ export class AdminResourceComponent implements OnInit {
|
|
|
readonly updatingContent = signal(false);
|
|
|
readonly contentActionError = signal('');
|
|
|
readonly contentActionMessage = signal('');
|
|
|
+ readonly selectedIds = signal<string[]>([]);
|
|
|
+ readonly bulkError = signal('');
|
|
|
+ readonly bulkMessage = signal('');
|
|
|
readonly icons = { ChevronLeft, ChevronRight, Eye, KeyRound, LoaderCircle, Pencil, Plus, RefreshCw, Search, Trash2, X };
|
|
|
readonly columns = computed(() => {
|
|
|
const fields = this.schema()?.fields ?? [];
|
|
|
@@ -59,6 +62,11 @@ export class AdminResourceComponent implements OnInit {
|
|
|
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))));
|
|
|
+ readonly bulkEnabled = computed(() => ['_User', 'CommonModel'].includes(this.className()));
|
|
|
+ readonly allPageSelected = computed(() => {
|
|
|
+ const ids = (this.page()?.results || []).map((row) => String(row['objectId'] || '')).filter(Boolean);
|
|
|
+ return Boolean(ids.length) && ids.every((id) => this.selectedIds().includes(id));
|
|
|
+ });
|
|
|
private readonly functions = inject(CloudFunctionsService);
|
|
|
private readonly route = inject(ActivatedRoute);
|
|
|
private readonly destroyRef = inject(DestroyRef);
|
|
|
@@ -72,10 +80,11 @@ export class AdminResourceComponent implements OnInit {
|
|
|
}
|
|
|
|
|
|
async load(pageNumber = this.page()?.page ?? 1, reloadSchema = false): Promise<void> {
|
|
|
- this.loading.set(true); this.error.set('');
|
|
|
+ this.loading.set(true); this.error.set(''); this.selectedIds.set([]); this.bulkError.set(''); this.bulkMessage.set('');
|
|
|
try {
|
|
|
if (reloadSchema || !this.schema()) this.schema.set(await this.functions.admin<AdminClassSchema>('schema', { className: this.className() }));
|
|
|
this.page.set(await this.functions.admin<ResourcePage>('list', { className: this.className(), page: pageNumber, pageSize: 20, search: this.search().trim() }));
|
|
|
+ if (this.className() === '_User' && reloadSchema) await this.loadUserGroups();
|
|
|
} catch (error) { this.error.set(error instanceof Error ? error.message : '数据加载失败'); }
|
|
|
finally { this.loading.set(false); }
|
|
|
}
|
|
|
@@ -222,6 +231,54 @@ export class AdminResourceComponent implements OnInit {
|
|
|
} catch (error) { this.contentActionError.set(error instanceof Error ? error.message : '内容操作失败'); }
|
|
|
finally { this.updatingContent.set(false); }
|
|
|
}
|
|
|
+ toggleSelection(objectId: string, selected: boolean): void {
|
|
|
+ this.selectedIds.update((current) => selected ? [...new Set([...current, objectId])] : current.filter((id) => id !== objectId));
|
|
|
+ }
|
|
|
+ togglePageSelection(selected: boolean): void {
|
|
|
+ const pageIds = (this.page()?.results || []).map((row) => String(row['objectId'] || '')).filter(Boolean);
|
|
|
+ this.selectedIds.set(selected ? pageIds : []);
|
|
|
+ }
|
|
|
+ isSelected(objectId: unknown): boolean { return this.selectedIds().includes(String(objectId || '')); }
|
|
|
+ async applyBulkUserAction(action: 'lock' | 'unlock' | 'move'): Promise<void> {
|
|
|
+ const objectIds = this.selectedIds(); this.bulkError.set(''); this.bulkMessage.set('');
|
|
|
+ if (this.className() !== '_User' || !objectIds.length) { this.bulkError.set('请先选择用户'); return; }
|
|
|
+ if (action === 'lock' && !confirm(`确认停用选中的 ${objectIds.length} 个账号并撤销全部会话?`)) return;
|
|
|
+ const payload: Record<string, unknown> = { className: '_User', action, objectIds };
|
|
|
+ if (action === 'move') {
|
|
|
+ const groupId = this.selectedUserGroupId();
|
|
|
+ if (!Number.isInteger(groupId) || groupId < 1) { this.bulkError.set('请选择有效用户组'); return; }
|
|
|
+ payload['groupId'] = groupId;
|
|
|
+ } else payload['reason'] = this.userActionReason().trim();
|
|
|
+ this.updatingUser.set(true);
|
|
|
+ try {
|
|
|
+ const result = await this.functions.admin<{ updated: number; revokedSessions: number }>('userBatch', payload);
|
|
|
+ const message = action === 'lock' ? `已停用 ${result.updated} 个账号并撤销 ${result.revokedSessions || 0} 个会话` : action === 'unlock' ? `已解锁 ${result.updated} 个账号` : `已移动 ${result.updated} 个用户`;
|
|
|
+ await this.load(); this.bulkMessage.set(message);
|
|
|
+ } catch (error) { this.bulkError.set(error instanceof Error ? error.message : '批量用户操作失败'); }
|
|
|
+ finally { this.updatingUser.set(false); }
|
|
|
+ }
|
|
|
+ async applyBulkContentAction(action: 'status' | 'recycle' | 'recover' | 'move'): Promise<void> {
|
|
|
+ const objectIds = this.selectedIds(); this.bulkError.set(''); this.bulkMessage.set('');
|
|
|
+ if (this.className() !== 'CommonModel' || !objectIds.length) { this.bulkError.set('请先选择内容'); return; }
|
|
|
+ if (action === 'recycle' && !confirm(`确认将选中的 ${objectIds.length} 条内容移入回收站?`)) return;
|
|
|
+ const payload: Record<string, unknown> = { className: 'CommonModel', action, objectIds };
|
|
|
+ if (action === 'status') {
|
|
|
+ const status = this.selectedContentStatus();
|
|
|
+ if (![-3, -1, 0, 99].includes(status)) { this.bulkError.set('请选择有效内容状态'); return; }
|
|
|
+ payload['status'] = status;
|
|
|
+ }
|
|
|
+ if (action === 'move') {
|
|
|
+ const nodeId = this.targetContentNodeId();
|
|
|
+ if (!Number.isInteger(nodeId) || nodeId < 1) { this.bulkError.set('请输入有效目标节点 ID'); return; }
|
|
|
+ payload['nodeId'] = nodeId;
|
|
|
+ }
|
|
|
+ this.updatingContent.set(true);
|
|
|
+ try {
|
|
|
+ const result = await this.functions.admin<{ updated: number }>('contentBatch', payload);
|
|
|
+ const message = `已处理 ${result.updated} 条内容`; await this.load(); this.bulkMessage.set(message);
|
|
|
+ } catch (error) { this.bulkError.set(error instanceof Error ? error.message : '批量内容操作失败'); }
|
|
|
+ finally { this.updatingContent.set(false); }
|
|
|
+ }
|
|
|
async remove(row: Record<string, unknown>): Promise<void> {
|
|
|
const objectId = String(row['objectId'] ?? '');
|
|
|
const verb = this.className() === 'CommonModel' ? '移入回收站' : '删除';
|