refactor: 统一前端剪贴板复制逻辑

This commit is contained in:
2026-06-01 18:37:34 +08:00
parent 118c5b1ab7
commit ac0df2d399
5 changed files with 35 additions and 65 deletions
+6
View File
@@ -638,3 +638,9 @@
- `app/utils/files.py` now provides `remove_file_safely()` for best-effort temporary file cleanup. - `app/utils/files.py` now provides `remove_file_safely()` for best-effort temporary file cleanup.
- Database table export and history archive download routes now reuse this helper instead of carrying duplicate private `_remove_file()` functions. - Database table export and history archive download routes now reuse this helper instead of carrying duplicate private `_remove_file()` functions.
- Verification performed: `.venv\Scripts\python.exe -m compileall app`, `npm run build`, and `cargo check --manifest-path src-tauri\Cargo.toml` with a temporary sidecar placeholder all passed. Generated build output, Python caches, and temporary Tauri sidecar files were removed after verification. - Verification performed: `.venv\Scripts\python.exe -m compileall app`, `npm run build`, and `cargo check --manifest-path src-tauri\Cargo.toml` with a temporary sidecar placeholder all passed. Generated build output, Python caches, and temporary Tauri sidecar files were removed after verification.
## 2026-06-01: Clipboard helper cleanup
- `frontend/src/composables/clipboard.ts` centralizes browser clipboard writes with the existing hidden-textarea fallback.
- API documentation, API Token management, and history detail log copying now reuse `writeClipboardText()` instead of each component carrying its own clipboard fallback.
- Verification performed: `npm run build`, `.venv\Scripts\python.exe -m compileall app`, and `cargo check --manifest-path src-tauri\Cargo.toml` with a temporary sidecar placeholder all passed. Generated build output, Python caches, and temporary Tauri sidecar files were removed after verification.
+2 -20
View File
@@ -26,6 +26,7 @@ import SwaggerUIBundle from 'swagger-ui-dist/swagger-ui-bundle.js';
import 'swagger-ui-dist/swagger-ui.css'; import 'swagger-ui-dist/swagger-ui.css';
import { apiUrl, getApiBaseUrl, getToken } from '../api/client'; import { apiUrl, getApiBaseUrl, getToken } from '../api/client';
import { writeClipboardText } from '../composables/clipboard';
type SwaggerSystem = { type SwaggerSystem = {
getSystem?: () => { getSystem?: () => {
@@ -92,28 +93,9 @@ function initSwagger() {
} }
async function copyHeaderSample() { async function copyHeaderSample() {
await writeClipboard('Authorization: Bearer <token>\nX-API-Token: <token>'); await writeClipboardText('Authorization: Bearer <token>\nX-API-Token: <token>');
message.success('传参示例已复制'); message.success('传参示例已复制');
} }
async function writeClipboard(text: string) {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.top = '-1000px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} finally {
document.body.removeChild(textarea);
}
}
</script> </script>
<style scoped> <style scoped>
+3 -21
View File
@@ -171,6 +171,7 @@ import { useDialog, useMessage, type DropdownOption } from 'naive-ui';
import { AddOutline, ChevronDownOutline, CopyOutline, EyeOffOutline, EyeOutline, RefreshOutline } from '@vicons/ionicons5'; import { AddOutline, ChevronDownOutline, CopyOutline, EyeOffOutline, EyeOutline, RefreshOutline } from '@vicons/ionicons5';
import { apiGet, apiPost } from '../api/client'; import { apiGet, apiPost } from '../api/client';
import { writeClipboardText } from '../composables/clipboard';
import type { ApiMessage, ApiTokenListResponse, ApiTokenMutationResponse, ApiTokenRecord } from '../types'; import type { ApiMessage, ApiTokenListResponse, ApiTokenMutationResponse, ApiTokenRecord } from '../types';
defineProps<{ defineProps<{
@@ -365,12 +366,12 @@ async function copyToken(token: ApiTokenRecord) {
message.warning('该 Token 是旧版本生成的,未保存完整值,请重生成后再复制'); message.warning('该 Token 是旧版本生成的,未保存完整值,请重生成后再复制');
return; return;
} }
await writeClipboard(token.token); await writeClipboardText(token.token);
message.success('Token 已复制'); message.success('Token 已复制');
} }
async function copyRawToken() { async function copyRawToken() {
await writeClipboard(rawToken.value); await writeClipboardText(rawToken.value);
message.success('Token 已复制'); message.success('Token 已复制');
} }
@@ -433,25 +434,6 @@ function tokenDisplayValue(token: ApiTokenRecord): string {
return `${token.prefix}...${token.suffix}`; return `${token.prefix}...${token.suffix}`;
} }
async function writeClipboard(text: string) {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.top = '-1000px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} finally {
document.body.removeChild(textarea);
}
}
function tokenStatusText(token: ApiTokenRecord): string { function tokenStatusText(token: ApiTokenRecord): string {
if (!token.enabled) return '已停用'; if (!token.enabled) return '已停用';
if (token.expired) return '已过期'; if (token.expired) return '已过期';
+1 -24
View File
@@ -186,6 +186,7 @@ import {
import { apiGet, apiPost, download as downloadFile } from '../api/client'; import { apiGet, apiPost, download as downloadFile } from '../api/client';
import type { CacheSize, HistoryDetail, HistoryFileEntry, HistoryFilesResponse, HistoryRecord } from '../types'; import type { CacheSize, HistoryDetail, HistoryFileEntry, HistoryFilesResponse, HistoryRecord } from '../types';
import { writeClipboardText } from '../composables/clipboard';
import { showDownloadCompleteDialog } from '../composables/downloadFeedback'; import { showDownloadCompleteDialog } from '../composables/downloadFeedback';
import { toColoredLogLines } from '../composables/logLines'; import { toColoredLogLines } from '../composables/logLines';
import { resetPageHeader, setPageHeader } from '../composables/pageHeader'; import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
@@ -401,30 +402,6 @@ async function copyDetailLogs() {
} }
} }
async function writeClipboardText(text: string) {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', 'true');
textarea.style.position = 'fixed';
textarea.style.top = '-1000px';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
if (!document.execCommand('copy')) {
throw new Error('copy failed');
}
} finally {
document.body.removeChild(textarea);
}
}
function confirmDelete(recordId: string) { function confirmDelete(recordId: string) {
dialog.warning({ dialog.warning({
title: '删除历史记录', title: '删除历史记录',
+23
View File
@@ -0,0 +1,23 @@
export async function writeClipboardText(text: string): Promise<void> {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', 'true');
textarea.style.position = 'fixed';
textarea.style.top = '-1000px';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
if (!document.execCommand('copy')) {
throw new Error('copy failed');
}
} finally {
document.body.removeChild(textarea);
}
}