Add dark theme, richer Markdown and readable plugin results. Improve Android input evidence, response integrity and diagnostics. Unify lifecycle, guidance receipts and evidence sources across clients. Keep direct action dispatch without an added AI quota approval layer. Align version metadata at 2.2.0 and bundle Helper 1.2.2. Validation: 3063 backend tests before the version bump; 44 after. Frontend: 317 tests and production build passed.
67 lines
2.8 KiB
TypeScript
67 lines
2.8 KiB
TypeScript
import { Clipboard } from '@angular/cdk/clipboard';
|
||
import { Component, DestroyRef, Input, inject, signal } from '@angular/core';
|
||
|
||
@Component({
|
||
selector: 'app-task-id-copy',
|
||
standalone: true,
|
||
template: `
|
||
<span class="copy-target" role="button" tabindex="0"
|
||
[attr.aria-label]="'复制任务 ID:' + taskId" title="点击复制任务 ID,也可选中文字复制"
|
||
(click)="copy($event)" (keydown.enter)="copy($event)" (keydown.space)="copy($event)">
|
||
<span class="value">ID: {{ taskId }}</span>
|
||
<span class="material-symbols-outlined" aria-hidden="true">content_copy</span>
|
||
</span>
|
||
<span class="feedback" role="status" aria-live="polite">{{ feedback() }}</span>
|
||
`,
|
||
styles: [`
|
||
:host { display: block; min-width: 0; font-size: 11px; color: inherit; user-select: text; }
|
||
.copy-target { display: inline-flex; align-items: center; gap: 5px; max-width: 100%;
|
||
padding: 3px 0; cursor: pointer; user-select: text; border-radius: 3px; }
|
||
.value { min-width: 0; overflow-wrap: anywhere; user-select: text; }
|
||
.material-symbols-outlined { flex-shrink: 0; font-size: 14px; user-select: none; }
|
||
.copy-target:hover { color: var(--app-primary-text, #2563eb); }
|
||
.copy-target:focus-visible { outline: 2px solid #60a5fa; outline-offset: 2px; }
|
||
.feedback { display: block; line-height: 1.6; user-select: text; }
|
||
.feedback:empty { display: none; }
|
||
`]
|
||
})
|
||
export class TaskIdCopyComponent {
|
||
@Input({ required: true }) taskId = '';
|
||
readonly feedback = signal('');
|
||
private readonly clipboard = inject(Clipboard);
|
||
private readonly destroyRef = inject(DestroyRef);
|
||
private timer: ReturnType<typeof setTimeout> | undefined;
|
||
|
||
constructor() {
|
||
this.destroyRef.onDestroy(() => clearTimeout(this.timer));
|
||
}
|
||
|
||
async copy(event: Event): Promise<void> {
|
||
event.stopPropagation();
|
||
// A drag selection must remain available for the browser's normal copy action.
|
||
const selection = window.getSelection();
|
||
if (event.type === 'click' && selection && !selection.isCollapsed
|
||
&& (event.currentTarget as HTMLElement).contains(selection.anchorNode)) return;
|
||
event.preventDefault();
|
||
let copied = false;
|
||
try {
|
||
if (navigator.clipboard?.writeText) {
|
||
await navigator.clipboard.writeText(this.taskId);
|
||
copied = true;
|
||
} else {
|
||
copied = this.clipboard.copy(this.taskId);
|
||
}
|
||
} catch {
|
||
try {
|
||
copied = this.clipboard.copy(this.taskId);
|
||
} catch {
|
||
// Keep manual selection available when the browser blocks both clipboard APIs.
|
||
}
|
||
}
|
||
if (this.destroyRef.destroyed) return;
|
||
this.feedback.set(copied ? '任务 ID 已复制' : '复制失败,请选中任务 ID 手动复制');
|
||
clearTimeout(this.timer);
|
||
this.timer = setTimeout(() => this.feedback.set(''), 3000);
|
||
}
|
||
}
|