refactor: 统一前端剪贴板复制逻辑
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 '已过期';
|
||||||
|
|||||||
@@ -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: '删除历史记录',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user