|
@@ -1,45 +1,35 @@
|
|
|
import { CommonModule } from '@angular/common';
|
|
import { CommonModule } from '@angular/common';
|
|
|
-import { HttpErrorResponse } from '@angular/common/http';
|
|
|
|
|
-import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
|
|
|
|
|
|
+import { Component, OnDestroy, OnInit } from '@angular/core';
|
|
|
import { FormsModule } from '@angular/forms';
|
|
import { FormsModule } from '@angular/forms';
|
|
|
-import { ActivatedRoute } from '@angular/router';
|
|
|
|
|
|
|
+import { ActivatedRoute, Router } from '@angular/router';
|
|
|
import {
|
|
import {
|
|
|
AlertTriangle,
|
|
AlertTriangle,
|
|
|
CalendarDays,
|
|
CalendarDays,
|
|
|
- Check,
|
|
|
|
|
ChevronRight,
|
|
ChevronRight,
|
|
|
- CircleDot,
|
|
|
|
|
- ClipboardList,
|
|
|
|
|
Columns3,
|
|
Columns3,
|
|
|
- Database,
|
|
|
|
|
FilterX,
|
|
FilterX,
|
|
|
LayoutList,
|
|
LayoutList,
|
|
|
- Lightbulb,
|
|
|
|
|
- Link2,
|
|
|
|
|
- LoaderCircle,
|
|
|
|
|
LucideAngularModule,
|
|
LucideAngularModule,
|
|
|
- Package,
|
|
|
|
|
Plus,
|
|
Plus,
|
|
|
RefreshCw,
|
|
RefreshCw,
|
|
|
- Save,
|
|
|
|
|
Search,
|
|
Search,
|
|
|
- Target,
|
|
|
|
|
UserRound,
|
|
UserRound,
|
|
|
- X,
|
|
|
|
|
} from 'lucide-angular';
|
|
} from 'lucide-angular';
|
|
|
import { EMPTY, Observable, Subject, catchError, expand, forkJoin, map, of, reduce, takeUntil } from 'rxjs';
|
|
import { EMPTY, Observable, Subject, catchError, expand, forkJoin, map, of, reduce, takeUntil } from 'rxjs';
|
|
|
import { RUNTIME_CONFIG } from '../../../app/core/config/runtime-config';
|
|
import { RUNTIME_CONFIG } from '../../../app/core/config/runtime-config';
|
|
|
import {
|
|
import {
|
|
|
SaasActionItem,
|
|
SaasActionItem,
|
|
|
- SaasAnalysisRun,
|
|
|
|
|
SaasContext,
|
|
SaasContext,
|
|
|
CursorPage,
|
|
CursorPage,
|
|
|
SaasPlatformService,
|
|
SaasPlatformService,
|
|
|
} from '../../../app/core/services/saas-platform.service';
|
|
} from '../../../app/core/services/saas-platform.service';
|
|
|
import { ActionCreatePanelComponent } from '../../shared/components/action-create-panel/action-create-panel.component';
|
|
import { ActionCreatePanelComponent } from '../../shared/components/action-create-panel/action-create-panel.component';
|
|
|
import { BoardTabItem, BoardTabsComponent } from '../../shared/components/board-tabs/board-tabs.component';
|
|
import { BoardTabItem, BoardTabsComponent } from '../../shared/components/board-tabs/board-tabs.component';
|
|
|
|
|
+import { EmptyStateComponent } from '../../shared/components/empty-state/empty-state.component';
|
|
|
|
|
+import { ErrorStateComponent } from '../../shared/components/error-state/error-state.component';
|
|
|
import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component';
|
|
import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component';
|
|
|
import { PageHeaderComponent } from '../../shared/components/page-header/page-header.component';
|
|
import { PageHeaderComponent } from '../../shared/components/page-header/page-header.component';
|
|
|
|
|
+import { PageToolbarComponent } from '../../shared/components/page-toolbar/page-toolbar.component';
|
|
|
|
|
|
|
|
type ActionSection = 'kanban' | 'experience' | 'iteration' | 'strategy';
|
|
type ActionSection = 'kanban' | 'experience' | 'iteration' | 'strategy';
|
|
|
type ActionViewMode = 'table' | 'board';
|
|
type ActionViewMode = 'table' | 'board';
|
|
@@ -64,13 +54,6 @@ interface ActionLane {
|
|
|
statuses: ActionStatus[];
|
|
statuses: ActionStatus[];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-interface InsightSummary {
|
|
|
|
|
- title: string;
|
|
|
|
|
- finding: string;
|
|
|
|
|
- recommendation: string;
|
|
|
|
|
- confidence: string;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
@Component({
|
|
@Component({
|
|
|
selector: 'app-domestic-action-workbench',
|
|
selector: 'app-domestic-action-workbench',
|
|
|
standalone: true,
|
|
standalone: true,
|
|
@@ -80,17 +63,17 @@ interface InsightSummary {
|
|
|
LucideAngularModule,
|
|
LucideAngularModule,
|
|
|
ActionCreatePanelComponent,
|
|
ActionCreatePanelComponent,
|
|
|
BoardTabsComponent,
|
|
BoardTabsComponent,
|
|
|
|
|
+ EmptyStateComponent,
|
|
|
|
|
+ ErrorStateComponent,
|
|
|
LoadingSpinnerComponent,
|
|
LoadingSpinnerComponent,
|
|
|
PageHeaderComponent,
|
|
PageHeaderComponent,
|
|
|
|
|
+ PageToolbarComponent,
|
|
|
],
|
|
],
|
|
|
templateUrl: './domestic-action-workbench.component.html',
|
|
templateUrl: './domestic-action-workbench.component.html',
|
|
|
styleUrls: ['./domestic-framework.shared.scss'],
|
|
styleUrls: ['./domestic-framework.shared.scss'],
|
|
|
})
|
|
})
|
|
|
export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
private readonly destroy$ = new Subject<void>();
|
|
private readonly destroy$ = new Subject<void>();
|
|
|
- private detailReturnFocus: HTMLElement | null = null;
|
|
|
|
|
-
|
|
|
|
|
- @ViewChild('actionDetailDrawer') private actionDetailDrawer?: ElementRef<HTMLElement>;
|
|
|
|
|
|
|
|
|
|
readonly workspaceId = RUNTIME_CONFIG.domesticWorkspaceId;
|
|
readonly workspaceId = RUNTIME_CONFIG.domesticWorkspaceId;
|
|
|
readonly tabs: BoardTabItem[] = [
|
|
readonly tabs: BoardTabItem[] = [
|
|
@@ -123,25 +106,14 @@ export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
readonly icons = {
|
|
readonly icons = {
|
|
|
alert: AlertTriangle,
|
|
alert: AlertTriangle,
|
|
|
calendar: CalendarDays,
|
|
calendar: CalendarDays,
|
|
|
- check: Check,
|
|
|
|
|
chevron: ChevronRight,
|
|
chevron: ChevronRight,
|
|
|
- circle: CircleDot,
|
|
|
|
|
- clipboard: ClipboardList,
|
|
|
|
|
board: Columns3,
|
|
board: Columns3,
|
|
|
- database: Database,
|
|
|
|
|
clear: FilterX,
|
|
clear: FilterX,
|
|
|
list: LayoutList,
|
|
list: LayoutList,
|
|
|
- lightbulb: Lightbulb,
|
|
|
|
|
- link: Link2,
|
|
|
|
|
- loader: LoaderCircle,
|
|
|
|
|
- package: Package,
|
|
|
|
|
plus: Plus,
|
|
plus: Plus,
|
|
|
refresh: RefreshCw,
|
|
refresh: RefreshCw,
|
|
|
- save: Save,
|
|
|
|
|
search: Search,
|
|
search: Search,
|
|
|
- target: Target,
|
|
|
|
|
user: UserRound,
|
|
user: UserRound,
|
|
|
- x: X,
|
|
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
section: ActionSection = 'kanban';
|
|
section: ActionSection = 'kanban';
|
|
@@ -156,19 +128,13 @@ export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
refreshing = false;
|
|
refreshing = false;
|
|
|
error = '';
|
|
error = '';
|
|
|
loadWarning = '';
|
|
loadWarning = '';
|
|
|
- updateError = '';
|
|
|
|
|
- updating = false;
|
|
|
|
|
createPanelOpen = false;
|
|
createPanelOpen = false;
|
|
|
actions: SaasActionItem[] = [];
|
|
actions: SaasActionItem[] = [];
|
|
|
- analyses: SaasAnalysisRun[] = [];
|
|
|
|
|
context: SaasContext | null = null;
|
|
context: SaasContext | null = null;
|
|
|
- selectedAction: SaasActionItem | null = null;
|
|
|
|
|
- editStatus: ActionStatus = 'open';
|
|
|
|
|
- editAssignee = '';
|
|
|
|
|
- editDueDate = '';
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
constructor(
|
|
|
private readonly route: ActivatedRoute,
|
|
private readonly route: ActivatedRoute,
|
|
|
|
|
+ private readonly router: Router,
|
|
|
private readonly platform: SaasPlatformService,
|
|
private readonly platform: SaasPlatformService,
|
|
|
) {}
|
|
) {}
|
|
|
|
|
|
|
@@ -185,17 +151,6 @@ export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
this.destroy$.complete();
|
|
this.destroy$.complete();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- @HostListener('document:keydown', ['$event'])
|
|
|
|
|
- handleDocumentKeydown(event: KeyboardEvent): void {
|
|
|
|
|
- if (this.createPanelOpen || !this.selectedAction) return;
|
|
|
|
|
- if (event.key === 'Escape') {
|
|
|
|
|
- event.preventDefault();
|
|
|
|
|
- this.closeDetail();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- if (event.key === 'Tab') this.keepFocusInDetail(event);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
get filteredActions(): SaasActionItem[] {
|
|
get filteredActions(): SaasActionItem[] {
|
|
|
const term = this.searchTerm.trim().toLocaleLowerCase('zh-CN');
|
|
const term = this.searchTerm.trim().toLocaleLowerCase('zh-CN');
|
|
|
return this.actions
|
|
return this.actions
|
|
@@ -244,27 +199,6 @@ export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
return this.actions.filter((action) => this.isOverdue(action)).length;
|
|
return this.actions.filter((action) => this.isOverdue(action)).length;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- get selectedAnalysis(): SaasAnalysisRun | null {
|
|
|
|
|
- const analysisId = this.selectedAction?.sourceAnalysisId;
|
|
|
|
|
- return analysisId ? this.analyses.find((run) => run.id === analysisId) ?? null : null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- get selectedInsight(): InsightSummary | null {
|
|
|
|
|
- const action = this.selectedAction;
|
|
|
|
|
- const analysis = this.selectedAnalysis;
|
|
|
|
|
- if (!action?.sourceInsightId || !analysis?.result) return null;
|
|
|
|
|
- const insights = analysis.result['insights'];
|
|
|
|
|
- if (!Array.isArray(insights)) return null;
|
|
|
|
|
- const source = insights.find((item) => this.isRecord(item) && item['id'] === action.sourceInsightId);
|
|
|
|
|
- if (!this.isRecord(source)) return null;
|
|
|
|
|
- return {
|
|
|
|
|
- title: this.recordString(source, 'title'),
|
|
|
|
|
- finding: this.recordString(source, 'finding'),
|
|
|
|
|
- recommendation: this.recordString(source, 'recommendation'),
|
|
|
|
|
- confidence: this.recordString(source, 'confidence'),
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
get currentTypeLabel(): string {
|
|
get currentTypeLabel(): string {
|
|
|
return this.typeFilter === 'all' ? '全部类型' : this.actionTypeLabel(this.typeFilter);
|
|
return this.typeFilter === 'all' ? '全部类型' : this.actionTypeLabel(this.typeFilter);
|
|
|
}
|
|
}
|
|
@@ -273,6 +207,16 @@ export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
return this.typeFilter === 'all' ? 'general' : this.typeFilter;
|
|
return this.typeFilter === 'all' ? 'general' : this.typeFilter;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ get toolbarFields(): Array<{ label: string; value: string }> {
|
|
|
|
|
+ return [
|
|
|
|
|
+ { label: '当前视图', value: this.currentTypeLabel },
|
|
|
|
|
+ { label: '行动项', value: String(this.actions.length) },
|
|
|
|
|
+ { label: '店铺', value: '—' },
|
|
|
|
|
+ { label: '类目', value: '—' },
|
|
|
|
|
+ { label: '负责人', value: '—' },
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
get hasActiveFilters(): boolean {
|
|
get hasActiveFilters(): boolean {
|
|
|
return Boolean(
|
|
return Boolean(
|
|
|
this.searchTerm.trim()
|
|
this.searchTerm.trim()
|
|
@@ -289,23 +233,15 @@ export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
this.loadWarning = '';
|
|
this.loadWarning = '';
|
|
|
forkJoin({
|
|
forkJoin({
|
|
|
actions: this.collectPages((cursor) => this.platform.actions(this.workspaceId, '', cursor, 100)),
|
|
actions: this.collectPages((cursor) => this.platform.actions(this.workspaceId, '', cursor, 100)),
|
|
|
- analyses: this.collectPages((cursor) => this.platform.analyses(this.workspaceId, '', cursor, 100)).pipe(
|
|
|
|
|
- map((data) => ({ data, warning: '' })),
|
|
|
|
|
- catchError(() => of({ data: [] as SaasAnalysisRun[], warning: '来源分析暂未加载' })),
|
|
|
|
|
- ),
|
|
|
|
|
context: this.platform.context().pipe(
|
|
context: this.platform.context().pipe(
|
|
|
map((data) => ({ data, warning: '' })),
|
|
map((data) => ({ data, warning: '' })),
|
|
|
catchError(() => of({ data: null, warning: '工作空间成员信息暂未加载' })),
|
|
catchError(() => of({ data: null, warning: '工作空间成员信息暂未加载' })),
|
|
|
),
|
|
),
|
|
|
}).pipe(takeUntil(this.destroy$)).subscribe({
|
|
}).pipe(takeUntil(this.destroy$)).subscribe({
|
|
|
- next: ({ actions, analyses, context }) => {
|
|
|
|
|
|
|
+ next: ({ actions, context }) => {
|
|
|
this.actions = actions;
|
|
this.actions = actions;
|
|
|
- this.analyses = analyses.data;
|
|
|
|
|
this.context = context.data;
|
|
this.context = context.data;
|
|
|
- this.loadWarning = [analyses.warning, context.warning].filter(Boolean).join(';');
|
|
|
|
|
- if (this.selectedAction) {
|
|
|
|
|
- this.selectedAction = this.actions.find((item) => item.id === this.selectedAction?.id) ?? null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ this.loadWarning = context.warning;
|
|
|
this.loading = false;
|
|
this.loading = false;
|
|
|
this.refreshing = false;
|
|
this.refreshing = false;
|
|
|
},
|
|
},
|
|
@@ -333,22 +269,14 @@ export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
openDetail(action: SaasActionItem): void {
|
|
openDetail(action: SaasActionItem): void {
|
|
|
- this.detailReturnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
|
|
|
- this.selectedAction = action;
|
|
|
|
|
- this.editStatus = action.status;
|
|
|
|
|
- this.editAssignee = action.assigneeUserId ?? '';
|
|
|
|
|
- this.editDueDate = action.dueAt?.slice(0, 10) ?? '';
|
|
|
|
|
- this.updateError = '';
|
|
|
|
|
- window.setTimeout(() => {
|
|
|
|
|
- this.actionDetailDrawer?.nativeElement.querySelector<HTMLElement>('[aria-label="关闭行动详情"]')?.focus();
|
|
|
|
|
|
|
+ void this.router.navigate(['/action-suggestion/kanban', action.id], {
|
|
|
|
|
+ queryParams: this.section === 'kanban' ? {} : { from: this.section },
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- closeDetail(): void {
|
|
|
|
|
- if (this.updating) return;
|
|
|
|
|
- this.selectedAction = null;
|
|
|
|
|
- this.updateError = '';
|
|
|
|
|
- window.setTimeout(() => this.detailReturnFocus?.focus());
|
|
|
|
|
|
|
+ onEmptyAction(): void {
|
|
|
|
|
+ if (this.hasActiveFilters) this.resetFilters();
|
|
|
|
|
+ else this.openCreatePanel();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
openCreatePanel(): void {
|
|
openCreatePanel(): void {
|
|
@@ -365,31 +293,6 @@ export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
window.setTimeout(() => this.closeCreatePanel(), 450);
|
|
window.setTimeout(() => this.closeCreatePanel(), 450);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- saveAction(): void {
|
|
|
|
|
- const action = this.selectedAction;
|
|
|
|
|
- if (!action || this.updating) return;
|
|
|
|
|
- this.updating = true;
|
|
|
|
|
- this.updateError = '';
|
|
|
|
|
- this.platform.updateAction(this.workspaceId, action.id, {
|
|
|
|
|
- status: this.editStatus,
|
|
|
|
|
- assigneeUserId: this.editAssignee || null,
|
|
|
|
|
- dueAt: this.editDueDate ? new Date(`${this.editDueDate}T23:59:59.999`).toISOString() : null,
|
|
|
|
|
- }).pipe(takeUntil(this.destroy$)).subscribe({
|
|
|
|
|
- next: ({ action: updated }) => {
|
|
|
|
|
- this.actions = this.actions.map((item) => item.id === updated.id ? updated : item);
|
|
|
|
|
- this.selectedAction = updated;
|
|
|
|
|
- this.editStatus = updated.status;
|
|
|
|
|
- this.editAssignee = updated.assigneeUserId ?? '';
|
|
|
|
|
- this.editDueDate = updated.dueAt?.slice(0, 10) ?? '';
|
|
|
|
|
- this.updating = false;
|
|
|
|
|
- },
|
|
|
|
|
- error: (error: unknown) => {
|
|
|
|
|
- this.updateError = this.updateErrorMessage(error);
|
|
|
|
|
- this.updating = false;
|
|
|
|
|
- },
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
sourceValue(action: SaasActionItem): ActionSource {
|
|
sourceValue(action: SaasActionItem): ActionSource {
|
|
|
return action.sourceKind ?? (action.sourceAnalysisId ? 'insight' : 'rule_action');
|
|
return action.sourceKind ?? (action.sourceAnalysisId ? 'insight' : 'rule_action');
|
|
|
}
|
|
}
|
|
@@ -426,7 +329,7 @@ export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
ownerLabel(userId: string | null): string {
|
|
ownerLabel(userId: string | null): string {
|
|
|
- if (!userId) return '待分配';
|
|
|
|
|
|
|
+ if (!userId) return '—';
|
|
|
if (userId === this.context?.principal.userId) return `${this.context.principal.displayName}(我)`;
|
|
if (userId === this.context?.principal.userId) return `${this.context.principal.displayName}(我)`;
|
|
|
return userId;
|
|
return userId;
|
|
|
}
|
|
}
|
|
@@ -437,17 +340,10 @@ export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
date(value: string | null): string {
|
|
date(value: string | null): string {
|
|
|
- if (!value) return '未设置';
|
|
|
|
|
|
|
+ if (!value) return '—';
|
|
|
return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(value));
|
|
return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(value));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- dateTime(value: string | null): string {
|
|
|
|
|
- if (!value) return '-';
|
|
|
|
|
- return new Intl.DateTimeFormat('zh-CN', {
|
|
|
|
|
- year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false,
|
|
|
|
|
- }).format(new Date(value));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private compareActions(left: SaasActionItem, right: SaasActionItem): number {
|
|
private compareActions(left: SaasActionItem, right: SaasActionItem): number {
|
|
|
const priority = { critical: 0, high: 1, medium: 2, low: 3 } as const;
|
|
const priority = { critical: 0, high: 1, medium: 2, low: 3 } as const;
|
|
|
const byPriority = priority[left.priority] - priority[right.priority];
|
|
const byPriority = priority[left.priority] - priority[right.priority];
|
|
@@ -466,15 +362,6 @@ export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
return ({ kanban: 'all', experience: 'experience', iteration: 'product', strategy: 'strategy' } as const)[section];
|
|
return ({ kanban: 'all', experience: 'experience', iteration: 'product', strategy: 'strategy' } as const)[section];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
|
|
- return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private recordString(record: Record<string, unknown>, key: string): string {
|
|
|
|
|
- const value = record[key];
|
|
|
|
|
- return typeof value === 'string' ? value : '';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private collectPages<T>(request: (cursor: string) => Observable<CursorPage<T>>): Observable<T[]> {
|
|
private collectPages<T>(request: (cursor: string) => Observable<CursorPage<T>>): Observable<T[]> {
|
|
|
return request('').pipe(
|
|
return request('').pipe(
|
|
|
expand((page) => page.nextCursor ? request(page.nextCursor) : EMPTY),
|
|
expand((page) => page.nextCursor ? request(page.nextCursor) : EMPTY),
|
|
@@ -482,33 +369,4 @@ export class DomesticActionWorkbenchComponent implements OnInit, OnDestroy {
|
|
|
reduce((items, pageItems) => [...items, ...pageItems], [] as T[]),
|
|
reduce((items, pageItems) => [...items, ...pageItems], [] as T[]),
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- private keepFocusInDetail(event: KeyboardEvent): void {
|
|
|
|
|
- const root = this.actionDetailDrawer?.nativeElement;
|
|
|
|
|
- if (!root) return;
|
|
|
|
|
- const focusable = Array.from(root.querySelectorAll<HTMLElement>(
|
|
|
|
|
- 'button:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
|
|
|
- ));
|
|
|
|
|
- if (!focusable.length) return;
|
|
|
|
|
- const first = focusable[0];
|
|
|
|
|
- const last = focusable[focusable.length - 1];
|
|
|
|
|
- if (event.shiftKey && document.activeElement === first) {
|
|
|
|
|
- event.preventDefault();
|
|
|
|
|
- last.focus();
|
|
|
|
|
- } else if (!event.shiftKey && document.activeElement === last) {
|
|
|
|
|
- event.preventDefault();
|
|
|
|
|
- first.focus();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private updateErrorMessage(error: unknown): string {
|
|
|
|
|
- if (error instanceof HttpErrorResponse) {
|
|
|
|
|
- const message = (error.error as { message?: unknown } | null)?.message;
|
|
|
|
|
- if (typeof message === 'string' && message.trim()) return `行动项更新失败:${message}`;
|
|
|
|
|
- if (error.status === 0) return '行动项更新失败:服务不可达,请检查网络后重试。';
|
|
|
|
|
- if (error.status === 403) return '行动项更新失败:当前账号没有更新权限。';
|
|
|
|
|
- if (error.status === 404) return '行动项更新失败:该记录已不存在,请刷新列表。';
|
|
|
|
|
- }
|
|
|
|
|
- return '行动项更新失败,请稍后重试。';
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|