diff --git a/src/locales/de-DE/logs.json b/src/locales/de-DE/logs.json
index fb5aee886..d2129a946 100644
--- a/src/locales/de-DE/logs.json
+++ b/src/locales/de-DE/logs.json
@@ -55,5 +55,14 @@
"total_logs": "Insgesamt {{length}} Protokolle gefunden",
"filtered_logs": "Nach Filterung {{length}} Protokolle",
"enter_filter_conditions": "Bitte geben Sie Filterbedingungen für die Abfrage ein",
- "custom_range": "Benutzerdefiniert"
+ "custom_range": "Benutzerdefiniert",
+ "copy_log_line": "Diesen Log-Eintrag kopieren",
+ "toggle_log_detail": "Details ein-/ausblenden",
+ "export_scope": "{{count}} gefilterte Logs exportieren",
+ "export_copy_text": "Als Text kopieren",
+ "export_download_log": ".log-Datei herunterladen",
+ "export_download_json": ".json-Datei herunterladen",
+ "copied_logs": "Kopierte Logs: {{count}}",
+ "copy_failed": "Kopieren fehlgeschlagen",
+ "export_failed": "Export fehlgeschlagen"
}
diff --git a/src/locales/en-US/logs.json b/src/locales/en-US/logs.json
index fb258f6a9..d58a94d58 100644
--- a/src/locales/en-US/logs.json
+++ b/src/locales/en-US/logs.json
@@ -55,5 +55,14 @@
"total_logs": "A total of {{length}} logs were queried",
"filtered_logs": "{{length}} logs after filtering",
"enter_filter_conditions": "Please Enter Filter Conditions for Query",
- "custom_range": "Custom Range"
+ "custom_range": "Custom Range",
+ "copy_log_line": "Copy this log",
+ "toggle_log_detail": "Toggle details",
+ "export_scope": "Export {{count}} filtered logs",
+ "export_copy_text": "Copy as text",
+ "export_download_log": "Download .log file",
+ "export_download_json": "Download .json file",
+ "copied_logs": "Logs copied: {{count}}",
+ "copy_failed": "Copy failed",
+ "export_failed": "Export failed"
}
diff --git a/src/locales/ja-JP/logs.json b/src/locales/ja-JP/logs.json
index 8bcee18ce..81549054e 100644
--- a/src/locales/ja-JP/logs.json
+++ b/src/locales/ja-JP/logs.json
@@ -55,5 +55,14 @@
"total_logs": "合計{{length}}件のログが見つかりました",
"filtered_logs": "フィルタリング後{{length}}件のログ",
"enter_filter_conditions": "フィルタリング条件を入力してクエリしてください",
- "custom_range": "カスタム範囲"
+ "custom_range": "カスタム範囲",
+ "copy_log_line": "このログをコピー",
+ "toggle_log_detail": "詳細の表示/非表示",
+ "export_scope": "現在の絞り込み結果をエクスポート · {{count}} 件",
+ "export_copy_text": "テキストとしてコピー",
+ "export_download_log": ".log ファイルをダウンロード",
+ "export_download_json": ".json ファイルをダウンロード",
+ "copied_logs": "{{count}} 件のログをコピーしました",
+ "copy_failed": "コピーに失敗しました",
+ "export_failed": "エクスポートに失敗しました"
}
diff --git a/src/locales/ko-KR/logs.json b/src/locales/ko-KR/logs.json
index dc771e31a..53095be6a 100644
--- a/src/locales/ko-KR/logs.json
+++ b/src/locales/ko-KR/logs.json
@@ -55,5 +55,14 @@
"total_logs": "총 {{length}}개의 로그가 조회되었습니다",
"filtered_logs": "필터링 후 {{length}}개의 로그",
"enter_filter_conditions": "조회할 필터 조건을 입력하세요",
- "custom_range": "사용자 지정 범위"
+ "custom_range": "사용자 지정 범위",
+ "copy_log_line": "이 로그 복사",
+ "toggle_log_detail": "상세 정보 펼치기/접기",
+ "export_scope": "현재 필터 결과 내보내기 · {{count}}개",
+ "export_copy_text": "텍스트로 복사",
+ "export_download_log": ".log 파일 다운로드",
+ "export_download_json": ".json 파일 다운로드",
+ "copied_logs": "로그 {{count}}개를 복사했습니다",
+ "copy_failed": "복사 실패",
+ "export_failed": "내보내기 실패"
}
diff --git a/src/locales/pt-BR/logs.json b/src/locales/pt-BR/logs.json
index 135264ae0..b4a0750bf 100644
--- a/src/locales/pt-BR/logs.json
+++ b/src/locales/pt-BR/logs.json
@@ -55,5 +55,14 @@
"total_logs": "Foram encontrados {{length}} logs no total",
"filtered_logs": "{{length}} logs após a filtragem",
"enter_filter_conditions": "Insira as condições de filtro para a consulta",
- "custom_range": "Intervalo personalizado"
+ "custom_range": "Intervalo personalizado",
+ "copy_log_line": "Copiar este log",
+ "toggle_log_detail": "Mostrar/ocultar detalhes",
+ "export_scope": "Exportar {{count}} logs filtrados",
+ "export_copy_text": "Copiar como texto",
+ "export_download_log": "Baixar arquivo .log",
+ "export_download_json": "Baixar arquivo .json",
+ "copied_logs": "Logs copiados: {{count}}",
+ "copy_failed": "Falha ao copiar",
+ "export_failed": "Falha ao exportar"
}
diff --git a/src/locales/ru-RU/logs.json b/src/locales/ru-RU/logs.json
index 88192af78..18ff480b8 100644
--- a/src/locales/ru-RU/logs.json
+++ b/src/locales/ru-RU/logs.json
@@ -55,5 +55,14 @@
"total_logs": "Найдено {{length}} записей журнала",
"filtered_logs": "После фильтрации {{length}} записей журнала",
"enter_filter_conditions": "Введите условия фильтрации для поиска",
- "custom_range": "Произвольный"
+ "custom_range": "Произвольный",
+ "copy_log_line": "Копировать эту запись",
+ "toggle_log_detail": "Показать/скрыть подробности",
+ "export_scope": "Экспорт отфильтрованных логов · {{count}}",
+ "export_copy_text": "Копировать как текст",
+ "export_download_log": "Скачать файл .log",
+ "export_download_json": "Скачать файл .json",
+ "copied_logs": "Скопировано логов: {{count}}",
+ "copy_failed": "Не удалось скопировать",
+ "export_failed": "Не удалось экспортировать"
}
diff --git a/src/locales/tr-TR/logs.json b/src/locales/tr-TR/logs.json
index 9fc5b627c..82f27dd3c 100644
--- a/src/locales/tr-TR/logs.json
+++ b/src/locales/tr-TR/logs.json
@@ -55,5 +55,14 @@
"total_logs": "Toplam {{length}} günlük sorgulandı",
"filtered_logs": "Filtrelemeden sonra {{length}} günlük",
"enter_filter_conditions": "Lütfen sorgu için filtre koşullarını girin",
- "custom_range": "Özel Aralık"
+ "custom_range": "Özel Aralık",
+ "copy_log_line": "Bu günlüğü kopyala",
+ "toggle_log_detail": "Ayrıntıları göster/gizle",
+ "export_scope": "Filtrelenen {{count}} günlüğü dışa aktar",
+ "export_copy_text": "Metin olarak kopyala",
+ "export_download_log": ".log dosyasını indir",
+ "export_download_json": ".json dosyasını indir",
+ "copied_logs": "Kopyalanan günlük: {{count}}",
+ "copy_failed": "Kopyalama başarısız",
+ "export_failed": "Dışa aktarma başarısız"
}
diff --git a/src/locales/vi-VN/logs.json b/src/locales/vi-VN/logs.json
index 9e9734a02..2a0276fc9 100644
--- a/src/locales/vi-VN/logs.json
+++ b/src/locales/vi-VN/logs.json
@@ -55,5 +55,14 @@
"total_logs": "Tổng cộng {{length}} nhật ký đã được truy vấn",
"filtered_logs": "{{length}} nhật ký sau khi lọc",
"enter_filter_conditions": "Vui lòng nhập điều kiện lọc để truy vấn",
- "custom_range": "Tùy chỉnh"
+ "custom_range": "Tùy chỉnh",
+ "copy_log_line": "Sao chép nhật ký này",
+ "toggle_log_detail": "Mở rộng/thu gọn chi tiết",
+ "export_scope": "Xuất {{count}} nhật ký đã lọc",
+ "export_copy_text": "Sao chép dạng văn bản",
+ "export_download_log": "Tải xuống tệp .log",
+ "export_download_json": "Tải xuống tệp .json",
+ "copied_logs": "Đã sao chép {{count}} nhật ký",
+ "copy_failed": "Sao chép thất bại",
+ "export_failed": "Xuất thất bại"
}
diff --git a/src/locales/zh-CN/logs.json b/src/locales/zh-CN/logs.json
index 0371611a5..e75b0bb52 100644
--- a/src/locales/zh-CN/logs.json
+++ b/src/locales/zh-CN/logs.json
@@ -55,5 +55,14 @@
"total_logs": "共查询到 {{length}} 条日志",
"filtered_logs": "筛选后 {{length}} 条日志",
"enter_filter_conditions": "请输入筛选条件进行查询",
- "custom_range": "自定义范围"
+ "custom_range": "自定义范围",
+ "copy_log_line": "复制此条日志",
+ "toggle_log_detail": "展开/收起详情",
+ "export_scope": "导出当前筛选结果 · {{count}} 条",
+ "export_copy_text": "复制为文本",
+ "export_download_log": "下载 .log 文件",
+ "export_download_json": "下载 .json 文件",
+ "copied_logs": "已复制 {{count}} 条日志",
+ "copy_failed": "复制失败",
+ "export_failed": "导出失败"
}
diff --git a/src/locales/zh-TW/logs.json b/src/locales/zh-TW/logs.json
index 51ad990d3..53f1ce856 100644
--- a/src/locales/zh-TW/logs.json
+++ b/src/locales/zh-TW/logs.json
@@ -55,5 +55,14 @@
"total_logs": "共查詢到 {{length}} 筆紀錄",
"filtered_logs": "篩選後 {{length}} 條紀錄",
"enter_filter_conditions": "請輸入篩選條件進行查詢",
- "custom_range": "自訂範圍"
+ "custom_range": "自訂範圍",
+ "copy_log_line": "複製此筆日誌",
+ "toggle_log_detail": "展開/收合詳情",
+ "export_scope": "匯出目前篩選結果 · {{count}} 筆",
+ "export_copy_text": "複製為文字",
+ "export_download_log": "下載 .log 檔案",
+ "export_download_json": "下載 .json 檔案",
+ "copied_logs": "已複製 {{count}} 筆日誌",
+ "copy_failed": "複製失敗",
+ "export_failed": "匯出失敗"
}
diff --git a/src/pages/options/routes/Logger/LoggerMobile.test.tsx b/src/pages/options/routes/Logger/LoggerMobile.test.tsx
deleted file mode 100644
index 6dc7661f0..000000000
--- a/src/pages/options/routes/Logger/LoggerMobile.test.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from "vitest";
-import { render, cleanup, screen, fireEvent } from "@testing-library/react";
-import { MemoryRouter } from "react-router-dom";
-import { t } from "@App/locales/locales";
-import { initTestLanguage } from "@Tests/initTestLanguage";
-import { useIsMobile } from "@App/pages/components/use-is-mobile";
-import type { Logger as LoggerEntry } from "@App/app/repo/logger";
-
-// 移动端重壳:整页在 useIsMobile=true 时切换为竖向堆叠布局(图标按钮 + 可横向滚动级别 chip + 消息换行)。
-vi.mock("@App/pages/components/use-is-mobile", () => ({
- useIsMobile: vi.fn(),
- MOBILE_BREAKPOINT: 768,
-}));
-
-const sampleLogs: LoggerEntry[] = [
- { id: 1, level: "error", message: "GM_xhr connect failed", label: { component: "GM_xhr" }, createtime: 3 },
- { id: 2, level: "warn", message: "retry timeout reached", label: { component: "GM_log" }, createtime: 2 },
- { id: 3, level: "info", message: "userscript started ok", label: { component: "GM_log" }, createtime: 1 },
-];
-
-const { mockLoggerData } = vi.hoisted(() => ({
- mockLoggerData: {
- logs: [] as LoggerEntry[],
- loading: false,
- reload: vi.fn(),
- clearLogs: vi.fn(() => Promise.resolve()),
- deleteLogs: vi.fn(() => Promise.resolve()),
- cleanCycle: 7,
- setCleanCycle: vi.fn(),
- refreshInterval: "off" as const,
- setRefreshInterval: vi.fn(),
- preset: "24h" as const,
- setPreset: vi.fn(),
- isNow: true,
- setIsNow: vi.fn(),
- range: { start: 0, end: 1 },
- setRange: vi.fn(),
- initialQueries: [],
- },
-}));
-vi.mock("./hooks", () => ({ useLogger: () => mockLoggerData }));
-vi.mock("@App/pages/components/ui/toast", () => ({
- notify: {
- success: vi.fn(),
- error: vi.fn(),
- info: vi.fn(),
- warning: vi.fn(),
- loading: vi.fn(),
- promise: vi.fn(),
- undo: vi.fn(),
- dismiss: vi.fn(),
- },
-}));
-
-import Logger from "./index";
-
-const mockedUseIsMobile = vi.mocked(useIsMobile);
-const renderPage = () => render(, { wrapper: MemoryRouter });
-
-beforeAll(() => initTestLanguage("zh-CN"));
-
-beforeEach(() => {
- mockLoggerData.logs = sampleLogs;
- mockedUseIsMobile.mockReturnValue(true);
- vi.clearAllMocks();
- mockedUseIsMobile.mockReturnValue(true);
-});
-
-afterEach(() => cleanup());
-
-describe("日志页面 - 移动端", () => {
- it("移动端顶栏的删除/清空按钮以图标按钮呈现(带无障碍名,不显示文字标签)", () => {
- renderPage();
- const del = screen.getByLabelText(t("logs:delete_current_logs"));
- const clear = screen.getByLabelText(t("logs:clear_logs"));
- // 图标按钮:可见文本里不再包含完整中文标签
- expect(del.textContent).not.toContain(t("logs:delete_current_logs"));
- expect(clear.textContent).not.toContain(t("logs:clear_logs"));
- });
-
- it("移动端日志行的消息允许换行而非截断", () => {
- renderPage();
- const msg = screen.getByText("GM_xhr connect failed");
- expect(msg.className).not.toContain("truncate");
- });
-
- it("移动端范围/统计条竖向堆叠,统计与自动清理各自独占整行", () => {
- renderPage();
- const bar = screen.getByTestId("logger-stats-bar");
- expect(bar.className).toContain("flex-col");
- // 两组必须撑满整行:否则长文本会溢出自身盒子压到相邻分组上(窄屏文字重叠)
- expect(screen.getByTestId("logger-stats-summary").className).toContain("w-full");
- expect(screen.getByTestId("logger-clean-schedule").className).toContain("w-full");
- });
-
- it("仍渲染全部日志且筛选交互可用", () => {
- renderPage();
- expect(screen.getByText("retry timeout reached")).toBeTruthy();
- fireEvent.click(screen.getByTestId("level-chip-warn"));
- expect(screen.queryByText("retry timeout reached")).toBeNull();
- });
-});
diff --git a/src/pages/options/routes/Logger/components.tsx b/src/pages/options/routes/Logger/components.tsx
index b1dac98a9..8dd14716d 100644
--- a/src/pages/options/routes/Logger/components.tsx
+++ b/src/pages/options/routes/Logger/components.tsx
@@ -7,6 +7,7 @@ import {
ChevronDown,
ChevronLeft,
ChevronRight,
+ Copy,
Plus,
RefreshCw,
X,
@@ -111,18 +112,22 @@ function detailJson(log: Logger): string {
}
/** 单条日志(控制台风格):时间 + 级别徽标 + 消息 + 标签 chips,左侧级别色条;可展开看完整标签。
- * 桌面端单行(消息截断);移动端竖向堆叠(时间/级别一行,消息整段换行)。 */
+ * 桌面端单行(消息截断);移动端竖向堆叠(时间/级别一行,消息整段换行)。
+ * 行本身不是按钮,文字可拖选复制;键盘展开走右侧箭头按钮。 */
export function LogRow({
log,
expanded,
onToggle,
+ onCopy,
mobile = false,
}: {
log: Logger;
expanded: boolean;
onToggle: () => void;
+ onCopy: () => void;
mobile?: boolean;
}) {
+ const { t } = useTranslation();
const bucket = levelBucket(log.level);
const m = LEVEL_META[bucket];
const labelChips = Object.entries(log.label).filter(([, v]) => typeof v === "string" || typeof v === "number") as [
@@ -130,14 +135,60 @@ export function LogRow({
string | number,
][];
+ // 拖选文字松开鼠标同样会触发 click,有选区时是在复制,不切换展开
+ const handleRowClick = () => {
+ if (window.getSelection()?.toString()) return;
+ onToggle();
+ };
+
+ const chips = labelChips.slice(0, 4).map(([k, v]) => (
+
+ {`${k}:${v}`}
+
+ ));
+
+ const actions = (
+
+
+
+
+ );
+
return (
-
-