feat: update UI screenshots, add fonts, and improve terminal progress dialog
This commit is contained in:
parent
36622e1da8
commit
3d908ad93e
|
|
@ -0,0 +1,94 @@
|
|||
Copyright (c) 2021, TakWolf (https://takwolf.com),
|
||||
with Reserved Font Name "Ark Pixel".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 230 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 273 KiB After Width: | Height: | Size: 145 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 150 KiB |
|
|
@ -2,6 +2,14 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: 'ArkPixel';
|
||||
src: url('/fonts/ark-pixel-12px-monospaced-zh_cn.ttf.woff2') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/*
|
||||
XCodeReviewer Design System
|
||||
Aesthetic: Terminal Retro + Pixel Art + Mechanical + Cassette Futurism
|
||||
|
|
@ -357,8 +365,30 @@
|
|||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
/* Smooth transitions */
|
||||
.terminal-transition {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
/* Text Glow Effect */
|
||||
.text-shadow-glow {
|
||||
text-shadow: 0 0 2px currentColor;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for Terminal */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #4a4a4a;
|
||||
border: 2px solid #1a1a1a;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #666;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { Dialog, DialogOverlay, DialogPortal } from "@/components/ui/dialog";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { Terminal, X as XIcon } from "lucide-react";
|
||||
import { Terminal, X as XIcon, Activity, Cpu, HardDrive, AlertTriangle, CheckCircle2 } from "lucide-react";
|
||||
import { cn, calculateTaskProgress } from "@/shared/utils/utils";
|
||||
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
|
||||
import { taskControl } from "@/shared/services/taskControl";
|
||||
|
|
@ -16,6 +16,7 @@ interface TerminalProgressDialogProps {
|
|||
}
|
||||
|
||||
interface LogEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
message: string;
|
||||
type: "info" | "success" | "error" | "warning";
|
||||
|
|
@ -36,15 +37,39 @@ export default function TerminalProgressDialog({
|
|||
const pollIntervalRef = useRef<number | null>(null);
|
||||
const hasInitializedLogsRef = useRef(false);
|
||||
|
||||
// Refs for state accessed in intervals/effects to avoid dependency cycles
|
||||
const logsRef = useRef<LogEntry[]>([]);
|
||||
const isCompletedRef = useRef(false);
|
||||
const isFailedRef = useRef(false);
|
||||
const isCancelledRef = useRef(false);
|
||||
|
||||
// Sync refs with state
|
||||
useEffect(() => {
|
||||
logsRef.current = logs;
|
||||
}, [logs]);
|
||||
|
||||
useEffect(() => {
|
||||
isCompletedRef.current = isCompleted;
|
||||
}, [isCompleted]);
|
||||
|
||||
useEffect(() => {
|
||||
isFailedRef.current = isFailed;
|
||||
}, [isFailed]);
|
||||
|
||||
useEffect(() => {
|
||||
isCancelledRef.current = isCancelled;
|
||||
}, [isCancelled]);
|
||||
|
||||
// 添加日志条目
|
||||
const addLog = (message: string, type: LogEntry["type"] = "info") => {
|
||||
const addLog = useCallback((message: string, type: LogEntry["type"] = "info") => {
|
||||
const timestamp = new Date().toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
});
|
||||
setLogs(prev => [...prev, { timestamp, message, type }]);
|
||||
};
|
||||
const newLog = { id: Math.random().toString(36).substr(2, 9), timestamp, message, type };
|
||||
setLogs(prev => [...prev, newLog]);
|
||||
}, []);
|
||||
|
||||
// 取消任务处理
|
||||
const handleCancel = async () => {
|
||||
|
|
@ -57,13 +82,14 @@ export default function TerminalProgressDialog({
|
|||
// 1. 标记任务为取消状态
|
||||
taskControl.cancelTask(taskId);
|
||||
setIsCancelled(true);
|
||||
addLog("🛑 用户取消任务,正在停止...", "error");
|
||||
addLog("[ERR] 用户取消任务,正在停止...", "error");
|
||||
|
||||
// 2. 立即更新数据库状态
|
||||
try {
|
||||
const { api } = await import("@/shared/config/database");
|
||||
// biome-ignore lint/suspicious/noExplicitAny: API type mismatch workaround
|
||||
await api.updateAuditTask(taskId, { status: 'cancelled' } as any);
|
||||
addLog("✓ 任务状态已更新为已取消", "warning");
|
||||
addLog("[WARN] 任务状态已更新为已取消", "warning");
|
||||
toast.success("任务已取消");
|
||||
} catch (error) {
|
||||
console.error('更新取消状态失败:', error);
|
||||
|
|
@ -72,6 +98,7 @@ export default function TerminalProgressDialog({
|
|||
};
|
||||
|
||||
// 自动滚动到底部
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We want to scroll when logs change
|
||||
useEffect(() => {
|
||||
logsEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [logs]);
|
||||
|
|
@ -89,15 +116,17 @@ export default function TerminalProgressDialog({
|
|||
return () => {
|
||||
clearInterval(timeInterval);
|
||||
};
|
||||
}, [open, isCompleted, isFailed]);
|
||||
}, [open, isCompleted, isFailed, isCancelled]);
|
||||
|
||||
// 轮询任务状态
|
||||
useEffect(() => {
|
||||
if (!open || !taskId) {
|
||||
// 清理状态
|
||||
setLogs([]);
|
||||
logsRef.current = [];
|
||||
setIsCompleted(false);
|
||||
setIsFailed(false);
|
||||
setIsCancelled(false);
|
||||
hasInitializedLogsRef.current = false;
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
|
|
@ -111,24 +140,24 @@ export default function TerminalProgressDialog({
|
|||
hasInitializedLogsRef.current = true;
|
||||
|
||||
// 初始化日志
|
||||
addLog("🚀 审计任务已启动", "info");
|
||||
addLog(`任务ID: ${taskId}`, "info");
|
||||
addLog(`任务类型: ${taskType === "repository" ? "仓库审计" : "ZIP文件审计"}`, "info");
|
||||
addLog("⏳ 正在初始化审计环境...", "info");
|
||||
addLog("[INFO] 审计任务已启动", "info");
|
||||
addLog(`TASK_ID: ${taskId}`, "info");
|
||||
addLog(`TYPE: ${taskType === "repository" ? "REPO_AUDIT" : "ZIP_AUDIT"}`, "info");
|
||||
addLog("[WAIT] 正在初始化审计环境...", "info");
|
||||
}
|
||||
|
||||
let lastScannedFiles = 0;
|
||||
let lastIssuesCount = 0;
|
||||
let lastTotalLines = 0;
|
||||
let lastStatus = "";
|
||||
let pollCount = 0;
|
||||
let _pollCount = 0;
|
||||
let hasDataChange = false;
|
||||
let isFirstPoll = true;
|
||||
|
||||
// 开始轮询
|
||||
const pollTask = async () => {
|
||||
// 如果任务已完成或失败,停止轮询
|
||||
if (isCompleted || isFailed) {
|
||||
if (isCompletedRef.current || isFailedRef.current) {
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
pollIntervalRef.current = null;
|
||||
|
|
@ -137,7 +166,7 @@ export default function TerminalProgressDialog({
|
|||
}
|
||||
|
||||
try {
|
||||
pollCount++;
|
||||
_pollCount++;
|
||||
hasDataChange = false;
|
||||
|
||||
const requestStartTime = Date.now();
|
||||
|
|
@ -149,7 +178,7 @@ export default function TerminalProgressDialog({
|
|||
const requestDuration = Date.now() - requestStartTime;
|
||||
|
||||
if (!task) {
|
||||
addLog(`❌ 任务不存在 (${requestDuration}ms)`, "error");
|
||||
addLog(`[ERR] 任务不存在 (${requestDuration}ms)`, "error");
|
||||
throw new Error("任务不存在");
|
||||
}
|
||||
|
||||
|
|
@ -168,9 +197,9 @@ export default function TerminalProgressDialog({
|
|||
|
||||
// 只在有变化时显示请求/响应信息(跳过 pending 状态)
|
||||
if (hasDataChange && task.status !== "pending") {
|
||||
addLog(`🔄 正在获取任务状态...`, "info");
|
||||
addLog(`[NET] 正在获取任务状态...`, "info");
|
||||
addLog(
|
||||
`✓ 状态: ${task.status} | 文件: ${task.scanned_files}/${task.total_files} | 问题: ${task.issues_count} (${requestDuration}ms)`,
|
||||
`[OK] 状态: ${task.status} | 文件: ${task.scanned_files}/${task.total_files} | 问题: ${task.issues_count} (${requestDuration}ms)`,
|
||||
"success"
|
||||
);
|
||||
}
|
||||
|
|
@ -185,12 +214,12 @@ export default function TerminalProgressDialog({
|
|||
// 静默跳过 pending 状态,不显示任何日志
|
||||
} else if (task.status === "running") {
|
||||
// 首次进入运行状态
|
||||
if (statusChanged && logs.filter(l => l.message.includes("开始扫描")).length === 0) {
|
||||
addLog("🔍 开始扫描代码文件...", "info");
|
||||
if (statusChanged && logsRef.current.filter(l => l.message.includes("开始扫描")).length === 0) {
|
||||
addLog("[SCAN] 开始扫描代码文件...", "info");
|
||||
if (task.project) {
|
||||
addLog(`📁 项目: ${task.project.name}`, "info");
|
||||
addLog(`[PROJ] 项目: ${task.project.name}`, "info");
|
||||
if (task.branch_name) {
|
||||
addLog(`🌿 分支: ${task.branch_name}`, "info");
|
||||
addLog(`[BRCH] 分支: ${task.branch_name}`, "info");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -200,7 +229,7 @@ export default function TerminalProgressDialog({
|
|||
const progress = calculateTaskProgress(task.scanned_files, task.total_files);
|
||||
const filesProcessed = task.scanned_files - lastScannedFiles;
|
||||
addLog(
|
||||
`📊 扫描进度: ${task.scanned_files || 0}/${task.total_files || 0} 文件 (${progress}%) [+${filesProcessed}]`,
|
||||
`[PROG] 扫描进度: ${task.scanned_files || 0}/${task.total_files || 0} 文件 (${progress}%) [+${filesProcessed}]`,
|
||||
"info"
|
||||
);
|
||||
lastScannedFiles = task.scanned_files;
|
||||
|
|
@ -209,25 +238,25 @@ export default function TerminalProgressDialog({
|
|||
// 显示问题发现(仅在有变化时)
|
||||
if (issuesChanged && task.issues_count > lastIssuesCount) {
|
||||
const newIssues = task.issues_count - lastIssuesCount;
|
||||
addLog(`⚠️ 发现 ${newIssues} 个新问题 (总计: ${task.issues_count})`, "warning");
|
||||
addLog(`[WARN] 发现 ${newIssues} 个新问题 (总计: ${task.issues_count})`, "warning");
|
||||
lastIssuesCount = task.issues_count;
|
||||
}
|
||||
|
||||
// 显示代码行数(仅在有变化时)
|
||||
if (linesChanged && task.total_lines > lastTotalLines) {
|
||||
const newLines = task.total_lines - lastTotalLines;
|
||||
addLog(`📝 已分析 ${task.total_lines.toLocaleString()} 行代码 [+${newLines.toLocaleString()}]`, "info");
|
||||
addLog(`[STAT] 已分析 ${task.total_lines.toLocaleString()} 行代码 [+${newLines.toLocaleString()}]`, "info");
|
||||
lastTotalLines = task.total_lines;
|
||||
}
|
||||
} else if (task.status === "completed") {
|
||||
// 任务完成
|
||||
if (!isCompleted) {
|
||||
if (!isCompletedRef.current) {
|
||||
addLog("", "info"); // 空行分隔
|
||||
addLog("✅ 代码扫描完成", "success");
|
||||
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "info");
|
||||
addLog(`📊 总计扫描: ${task.total_files} 个文件`, "success");
|
||||
addLog(`📝 总计分析: ${task.total_lines.toLocaleString()} 行代码`, "success");
|
||||
addLog(`⚠️ 发现问题: ${task.issues_count} 个`, task.issues_count > 0 ? "warning" : "success");
|
||||
addLog("[DONE] 代码扫描完成", "success");
|
||||
addLog("----------------------------------", "info");
|
||||
addLog(`[STAT] 总计扫描: ${task.total_files} 个文件`, "success");
|
||||
addLog(`[STAT] 总计分析: ${task.total_lines.toLocaleString()} 行代码`, "success");
|
||||
addLog(`[RSLT] 发现问题: ${task.issues_count} 个`, task.issues_count > 0 ? "warning" : "success");
|
||||
|
||||
// 解析问题类型分布
|
||||
if (task.issues_count > 0) {
|
||||
|
|
@ -243,31 +272,31 @@ export default function TerminalProgressDialog({
|
|||
};
|
||||
|
||||
if (severityCounts.critical > 0) {
|
||||
addLog(` 🔴 严重: ${severityCounts.critical} 个`, "error");
|
||||
addLog(` [CRIT] 严重: ${severityCounts.critical} 个`, "error");
|
||||
}
|
||||
if (severityCounts.high > 0) {
|
||||
addLog(` 🟠 高: ${severityCounts.high} 个`, "warning");
|
||||
addLog(` [HIGH] 高: ${severityCounts.high} 个`, "warning");
|
||||
}
|
||||
if (severityCounts.medium > 0) {
|
||||
addLog(` 🟡 中等: ${severityCounts.medium} 个`, "warning");
|
||||
addLog(` [MED] 中等: ${severityCounts.medium} 个`, "warning");
|
||||
}
|
||||
if (severityCounts.low > 0) {
|
||||
addLog(` 🟢 低: ${severityCounts.low} 个`, "info");
|
||||
addLog(` [LOW] 低: ${severityCounts.low} 个`, "info");
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// 静默处理错误
|
||||
}
|
||||
}
|
||||
|
||||
addLog(`⭐ 质量评分: ${task.quality_score.toFixed(1)}/100`, "success");
|
||||
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "info");
|
||||
addLog("🎉 审计任务已完成!", "success");
|
||||
addLog(`[SCOR] 质量评分: ${task.quality_score.toFixed(1)}/100`, "success");
|
||||
addLog("----------------------------------", "info");
|
||||
addLog("[FIN] 审计任务已完成!", "success");
|
||||
|
||||
if (task.completed_at) {
|
||||
const startTime = new Date(task.created_at).getTime();
|
||||
const endTime = new Date(task.completed_at).getTime();
|
||||
const duration = Math.round((endTime - startTime) / 1000);
|
||||
addLog(`⏱️ 总耗时: ${duration} 秒`, "info");
|
||||
addLog(`[TIME] 总耗时: ${duration} 秒`, "info");
|
||||
}
|
||||
|
||||
setIsCompleted(true);
|
||||
|
|
@ -278,16 +307,16 @@ export default function TerminalProgressDialog({
|
|||
}
|
||||
} else if (task.status === "cancelled") {
|
||||
// 任务被取消
|
||||
if (!isCancelled) {
|
||||
if (!isCancelledRef.current) {
|
||||
addLog("", "info"); // 空行分隔
|
||||
addLog("🛑 任务已被用户取消", "warning");
|
||||
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "warning");
|
||||
addLog(`📊 完成统计:`, "info");
|
||||
addLog("[STOP] 任务已被用户取消", "warning");
|
||||
addLog("----------------------------------", "warning");
|
||||
addLog(`[STAT] 完成统计:`, "info");
|
||||
addLog(` • 已分析文件: ${task.scanned_files}/${task.total_files}`, "info");
|
||||
addLog(` • 发现问题: ${task.issues_count} 个`, "info");
|
||||
addLog(` • 代码行数: ${task.total_lines.toLocaleString()} 行`, "info");
|
||||
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "warning");
|
||||
addLog("✓ 已分析的结果已保存到数据库", "success");
|
||||
addLog("----------------------------------", "warning");
|
||||
addLog("[SAVE] 已分析的结果已保存到数据库", "success");
|
||||
|
||||
setIsCancelled(true);
|
||||
if (pollIntervalRef.current) {
|
||||
|
|
@ -297,10 +326,10 @@ export default function TerminalProgressDialog({
|
|||
}
|
||||
} else if (task.status === "failed") {
|
||||
// 任务失败
|
||||
if (!isFailed) {
|
||||
if (!isFailedRef.current) {
|
||||
addLog("", "info"); // 空行分隔
|
||||
addLog("❌ 审计任务执行失败", "error");
|
||||
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error");
|
||||
addLog("[FAIL] 审计任务执行失败", "error");
|
||||
addLog("----------------------------------", "error");
|
||||
|
||||
// 尝试从日志系统获取具体错误信息
|
||||
try {
|
||||
|
|
@ -338,7 +367,7 @@ export default function TerminalProgressDialog({
|
|||
addLog(" • GitHub/GitLab API 限流", "error");
|
||||
addLog(" • LLM API 配置错误或额度不足", "error");
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// 如果获取日志失败,显示常见原因
|
||||
addLog("可能的原因:", "error");
|
||||
addLog(" • 网络连接问题", "error");
|
||||
|
|
@ -347,9 +376,9 @@ export default function TerminalProgressDialog({
|
|||
addLog(" • LLM API 配置错误或额度不足", "error");
|
||||
}
|
||||
|
||||
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error");
|
||||
addLog("💡 建议: 检查系统配置和网络连接后重试", "warning");
|
||||
addLog("📋 查看完整日志: 导航栏 -> 系统日志", "warning");
|
||||
addLog("----------------------------------", "error");
|
||||
addLog("[HINT] 建议: 检查系统配置和网络连接后重试", "warning");
|
||||
addLog("[LOGS] 查看完整日志: 导航栏 -> 系统日志", "warning");
|
||||
|
||||
setIsFailed(true);
|
||||
if (pollIntervalRef.current) {
|
||||
|
|
@ -358,8 +387,9 @@ export default function TerminalProgressDialog({
|
|||
}
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
addLog(`❌ ${error.message || "未知错误"}`, "error");
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
||||
addLog(`[ERR] ${errorMessage}`, "error");
|
||||
// 不中断轮询,继续尝试
|
||||
}
|
||||
};
|
||||
|
|
@ -377,42 +407,41 @@ export default function TerminalProgressDialog({
|
|||
pollIntervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [open, taskId, taskType]);
|
||||
}, [open, taskId, taskType, addLog]);
|
||||
|
||||
// 获取日志颜色 - 使用优雅的深红色主题
|
||||
// 获取日志颜色 - 简化配色,减少颜色数量
|
||||
const getLogColor = (type: LogEntry["type"]) => {
|
||||
switch (type) {
|
||||
case "success":
|
||||
return "text-green-500";
|
||||
return "text-[#00ff41]"; // 纯绿色
|
||||
case "error":
|
||||
return "text-red-500";
|
||||
return "text-[#ff3333]"; // 纯红色
|
||||
case "warning":
|
||||
return "text-yellow-500";
|
||||
return "text-[#ffb900]"; // 琥珀色
|
||||
default:
|
||||
return "text-gray-300";
|
||||
return "text-[#cccccc]"; // 浅灰色 (原为青色)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPortal>
|
||||
<DialogOverlay className="bg-black/50 backdrop-blur-sm" />
|
||||
<DialogOverlay className="bg-black/80 backdrop-blur-sm" />
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
|
||||
"w-[90vw] aspect-[16/9]",
|
||||
"max-w-[1200px] max-h-[800px]",
|
||||
"p-0 gap-0 rounded-none overflow-hidden",
|
||||
"bg-black border-4 border-gray-500 shadow-[10px_10px_0px_0px_rgba(0,0,0,0.5)]",
|
||||
"w-[95vw] max-w-[1000px] h-[85vh] max-h-[700px]",
|
||||
"p-0 gap-0 rounded-sm overflow-hidden",
|
||||
"bg-[#e0e0e0] border-4 border-[#4a4a4a]", // 机械外壳颜色
|
||||
"shadow-[15px_15px_0px_0px_rgba(0,0,0,0.5)]", // 硬阴影
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
"duration-200"
|
||||
"duration-300 font-mono tracking-tight" // 增加 tracking-tight 模拟像素感
|
||||
)}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
{/* 无障碍访问标题 */}
|
||||
<VisuallyHidden.Root>
|
||||
<DialogPrimitive.Title>审计进度监控</DialogPrimitive.Title>
|
||||
<DialogPrimitive.Description>
|
||||
|
|
@ -420,111 +449,191 @@ export default function TerminalProgressDialog({
|
|||
</DialogPrimitive.Description>
|
||||
</VisuallyHidden.Root>
|
||||
|
||||
{/* 终端头部 */}
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-gray-300 border-b-4 border-gray-500">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Terminal className="w-5 h-5 text-black" />
|
||||
<span className="text-sm font-bold text-black uppercase font-display tracking-wider">TERMINAL // 审计进度监控</span>
|
||||
{/* 机械外壳装饰 - 螺丝 */}
|
||||
<div className="absolute top-2 left-2 w-3 h-3 rounded-full bg-[#b0b0b0] border border-[#808080] shadow-inner flex items-center justify-center z-50">
|
||||
<div className="w-2 h-0.5 bg-[#606060] rotate-45"></div>
|
||||
</div>
|
||||
<div className="absolute top-2 right-2 w-3 h-3 rounded-full bg-[#b0b0b0] border border-[#808080] shadow-inner flex items-center justify-center z-50">
|
||||
<div className="w-2 h-0.5 bg-[#606060] rotate-45"></div>
|
||||
</div>
|
||||
<div className="absolute bottom-2 left-2 w-3 h-3 rounded-full bg-[#b0b0b0] border border-[#808080] shadow-inner flex items-center justify-center z-50">
|
||||
<div className="w-2 h-0.5 bg-[#606060] rotate-45"></div>
|
||||
</div>
|
||||
<div className="absolute bottom-2 right-2 w-3 h-3 rounded-full bg-[#b0b0b0] border border-[#808080] shadow-inner flex items-center justify-center z-50">
|
||||
<div className="w-2 h-0.5 bg-[#606060] rotate-45"></div>
|
||||
</div>
|
||||
|
||||
{/* 顶部控制面板 */}
|
||||
<div className="h-14 bg-[#d0d0d0] border-b-4 border-[#4a4a4a] flex items-center justify-between px-8 relative">
|
||||
{/* 装饰条纹 */}
|
||||
<div className="absolute top-0 left-16 right-16 h-1 bg-[repeating-linear-gradient(90deg,transparent,transparent_2px,#000_2px,#000_4px)] opacity-20"></div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="bg-[#333] p-1.5 rounded-sm border border-white/20 shadow-md">
|
||||
<Terminal className="w-5 h-5 text-[#00ff41]" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold text-[#666] uppercase tracking-widest leading-none mb-0.5">System Monitor</span>
|
||||
<span className="text-lg font-black text-[#333] uppercase tracking-tighter leading-none font-display">AUDIT_TERMINAL_V2.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* 模拟窗口控制按钮 */}
|
||||
<div className="w-4 h-4 border-2 border-black bg-white flex items-center justify-center">
|
||||
<div className="w-2 h-0.5 bg-black"></div>
|
||||
</div>
|
||||
<div className="w-4 h-4 border-2 border-black bg-white flex items-center justify-center">
|
||||
<div className="w-2 h-2 border border-black"></div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* 状态指示灯组 */}
|
||||
<div className="flex space-x-1 bg-[#222] p-1 rounded-sm border-b border-white/20">
|
||||
<div className={`w-3 h-3 rounded-full ${!isCompleted && !isFailed ? 'bg-[#00ff41] shadow-[0_0_5px_#00ff41] animate-pulse' : 'bg-[#1a4d26]'}`} title="Processing"></div>
|
||||
<div className={`w-3 h-3 rounded-full ${isFailed ? 'bg-[#ff0033] shadow-[0_0_5px_#ff0033]' : 'bg-[#4d000f]'}`} title="Error"></div>
|
||||
<div className={`w-3 h-3 rounded-full ${isCompleted ? 'bg-[#00ccff] shadow-[0_0_5px_#00ccff]' : 'bg-[#00334d]'}`} title="Ready"></div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="w-4 h-4 border-2 border-black bg-primary hover:bg-red-600 cursor-pointer transition-colors focus:outline-none flex items-center justify-center"
|
||||
type="button"
|
||||
className="w-8 h-8 bg-[#ff4444] border-b-4 border-r-4 border-[#990000] active:border-0 active:translate-y-1 active:translate-x-1 transition-all flex items-center justify-center hover:bg-[#ff6666]"
|
||||
onClick={() => onOpenChange(false)}
|
||||
title="关闭"
|
||||
aria-label="关闭"
|
||||
title="关闭电源"
|
||||
>
|
||||
<XIcon className="w-3 h-3 text-white" />
|
||||
<XIcon className="w-5 h-5 text-white stroke-[3]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 终端内容 */}
|
||||
<div className="p-6 bg-black overflow-y-auto h-[calc(100%-100px)] font-mono text-sm relative">
|
||||
{/* 扫描线效果 */}
|
||||
<div className="absolute inset-0 pointer-events-none bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,255,0.06))] z-10 bg-[length:100%_2px,3px_100%]"></div>
|
||||
{/* 主体内容区 - 包含侧边栏和屏幕 */}
|
||||
<div className="flex h-[calc(100%-56px)] bg-[#c0c0c0]">
|
||||
{/* 左侧数据面板 */}
|
||||
<div className="w-48 bg-[#d4d4d4] border-r-4 border-[#4a4a4a] p-4 flex flex-col gap-4 relative overflow-hidden">
|
||||
{/* 装饰背景 */}
|
||||
<div className="absolute inset-0 opacity-5 pointer-events-none bg-[radial-gradient(circle_at_center,#000_1px,transparent_1px)] bg-[length:4px_4px]"></div>
|
||||
|
||||
<div className="space-y-1 relative z-20">
|
||||
{logs.map((log, index) => (
|
||||
<div key={index} className="flex items-start space-x-3 hover:bg-white/5 px-2 py-0.5 transition-colors">
|
||||
<span className="text-gray-500 text-xs flex-shrink-0 w-24 font-bold">
|
||||
[{log.timestamp}]
|
||||
</span>
|
||||
<span className={`${getLogColor(log.type)} flex-1 font-bold tracking-wide`}>
|
||||
{log.type === 'info' && '> '}
|
||||
{log.type === 'success' && '✓ '}
|
||||
{log.type === 'error' && '✗ '}
|
||||
{log.type === 'warning' && '! '}
|
||||
{log.message}
|
||||
</span>
|
||||
<div className="space-y-1 z-10">
|
||||
<div className="text-[10px] font-bold text-[#666] uppercase">Task ID</div>
|
||||
<div className="text-xs font-mono font-bold text-[#333] break-all bg-white/50 p-1 border border-[#999]">{taskId?.slice(0, 8)}...</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 z-10">
|
||||
<div className="text-[10px] font-bold text-[#666] uppercase">Type</div>
|
||||
<div className="flex items-center space-x-2 bg-white/50 p-1 border border-[#999]">
|
||||
{taskType === 'repository' ? <Cpu className="w-3 h-3" /> : <HardDrive className="w-3 h-3" />}
|
||||
<span className="text-xs font-bold text-[#333] uppercase">{taskType}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 光标旋转闪烁效果 */}
|
||||
{!isCompleted && !isFailed && (
|
||||
<div className="flex items-center space-x-2 mt-4 px-2">
|
||||
<span className="text-gray-500 text-xs w-24 font-bold">[{currentTime}]</span>
|
||||
<span className="inline-block text-green-500 animate-pulse font-bold text-base">_</span>
|
||||
<div className="flex-1"></div>
|
||||
|
||||
{/* 装饰性条形码/数据块 */}
|
||||
<div className="h-24 w-full bg-[#333] p-2 flex flex-col justify-between opacity-80">
|
||||
<div className="flex justify-between">
|
||||
<div className="w-1 h-8 bg-[#ffb900]"></div>
|
||||
<div className="w-1 h-6 bg-[#ffb900]"></div>
|
||||
<div className="w-1 h-10 bg-[#ffb900]"></div>
|
||||
<div className="w-1 h-4 bg-[#ffb900]"></div>
|
||||
<div className="w-1 h-7 bg-[#ffb900]"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部控制和提示 */}
|
||||
<div className="px-4 py-3 bg-gray-200 border-t-4 border-gray-500 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-3 h-3 border-2 border-black ${isFailed ? 'bg-red-500' : isCompleted ? 'bg-green-500' : 'bg-yellow-400 animate-pulse'}`}></div>
|
||||
<span className="text-xs font-bold text-black uppercase font-mono tracking-tight">
|
||||
{isCancelled ? "STATUS: CANCELLED // 任务已取消" :
|
||||
isCompleted ? "STATUS: COMPLETED // 任务已完成" :
|
||||
isFailed ? "STATUS: FAILED // 任务失败" :
|
||||
"STATUS: RUNNING // 审计进行中..."}
|
||||
</span>
|
||||
<div className="text-[8px] text-[#00ff41] font-mono leading-none">
|
||||
MEM: 64K OK<br />
|
||||
CPU: ACTIVE<br />
|
||||
NET: LINKED
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* 运行中显示取消按钮 */}
|
||||
{!isCompleted && !isFailed && !isCancelled && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
className="h-8 text-xs bg-white border-2 border-black text-black hover:bg-red-100 hover:text-red-900 font-bold uppercase rounded-none shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] active:shadow-none"
|
||||
>
|
||||
<XIcon className="w-3 h-3 mr-1" />
|
||||
取消任务
|
||||
</Button>
|
||||
)}
|
||||
{/* 中央屏幕区域 */}
|
||||
<div className="flex-1 p-6 flex flex-col relative">
|
||||
{/* 屏幕边框 */}
|
||||
<div className="flex-1 bg-[#1a1a1a] rounded-lg p-1 shadow-[inset_0_0_20px_rgba(0,0,0,0.8)] border-b-2 border-white/10 relative overflow-hidden">
|
||||
{/* 屏幕内边框 */}
|
||||
<div className="absolute inset-0 border-[16px] border-[#2a2a2a] rounded-lg pointer-events-none z-20 shadow-[inset_0_0_10px_rgba(0,0,0,1)]"></div>
|
||||
|
||||
{/* 失败时显示查看日志按钮 */}
|
||||
{isFailed && (
|
||||
<button
|
||||
onClick={() => {
|
||||
window.open('/logs', '_blank');
|
||||
}}
|
||||
className="px-4 py-1.5 bg-yellow-400 border-2 border-black text-black hover:bg-yellow-500 text-xs font-bold uppercase rounded-none shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] active:shadow-none transition-all"
|
||||
>
|
||||
📋 查看日志
|
||||
</button>
|
||||
)}
|
||||
{/* 屏幕内容 */}
|
||||
<div className="w-full h-full bg-black p-6 overflow-y-auto font-mono text-sm relative z-10 custom-scrollbar">
|
||||
{/* CRT 效果层 */}
|
||||
<div className="absolute inset-0 pointer-events-none z-30 bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,255,0.06))] bg-[length:100%_2px,3px_100%] opacity-20"></div>
|
||||
<div className="absolute inset-0 pointer-events-none z-30 bg-[radial-gradient(circle_at_center,transparent_50%,rgba(0,0,0,0.4)_100%)]"></div>
|
||||
|
||||
{/* 已完成/失败/取消显示关闭按钮 */}
|
||||
{(isCompleted || isFailed || isCancelled) && (
|
||||
<button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="px-4 py-1.5 bg-primary border-2 border-black text-white hover:bg-primary/90 text-xs font-bold uppercase rounded-none shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] active:shadow-none transition-all"
|
||||
>
|
||||
关闭窗口
|
||||
</button>
|
||||
)}
|
||||
{/* 像素网格 */}
|
||||
<div className="absolute inset-0 pointer-events-none z-0 opacity-10" style={{
|
||||
backgroundImage: 'linear-gradient(#333 1px, transparent 1px), linear-gradient(90deg, #333 1px, transparent 1px)',
|
||||
backgroundSize: '20px 20px'
|
||||
}}></div>
|
||||
|
||||
<div className="relative z-10 space-y-1 pb-10">
|
||||
{logs.map((log) => (
|
||||
<div key={log.id} className="flex items-start space-x-3 hover:bg-white/5 px-2 py-0.5 transition-colors group">
|
||||
<span className="text-[#666] text-xs flex-shrink-0 w-24 font-bold group-hover:text-[#888]">
|
||||
{log.timestamp}
|
||||
</span>
|
||||
<span className={`${getLogColor(log.type)} flex-1 font-bold tracking-wide font-mono`}>
|
||||
{log.message}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!isCompleted && !isFailed && (
|
||||
<div className="flex items-center space-x-2 mt-4 px-2">
|
||||
<span className="text-[#666] text-xs w-24 font-bold">{currentTime}</span>
|
||||
<span className="inline-block text-[#00ff41] animate-pulse font-bold text-base">_</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 屏幕下方控制区 */}
|
||||
<div className="mt-4 h-12 bg-[#d0d0d0] border-t-2 border-white/50 flex items-center justify-between px-2">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-bold text-[#666] uppercase">Status</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
{isCancelled ? (
|
||||
<span className="text-xs font-black text-[#ffb900] bg-[#333] px-2 py-0.5 rounded-sm">CANCELLED</span>
|
||||
) : isCompleted ? (
|
||||
<span className="text-xs font-black text-[#00ccff] bg-[#333] px-2 py-0.5 rounded-sm">COMPLETED</span>
|
||||
) : isFailed ? (
|
||||
<span className="text-xs font-black text-[#ff0033] bg-[#333] px-2 py-0.5 rounded-sm">FAILED</span>
|
||||
) : (
|
||||
<span className="text-xs font-black text-[#00ff41] bg-[#333] px-2 py-0.5 rounded-sm animate-pulse">RUNNING...</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
{!isCompleted && !isFailed && !isCancelled && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
className="h-8 bg-[#e0e0e0] border-2 border-[#4a4a4a] text-[#333] hover:bg-[#ffcccc] hover:border-[#990000] hover:text-[#990000] font-bold uppercase rounded-none shadow-[2px_2px_0px_0px_rgba(0,0,0,0.5)] active:translate-x-[1px] active:translate-y-[1px] active:shadow-none transition-all"
|
||||
>
|
||||
<AlertTriangle className="w-3 h-3 mr-1" />
|
||||
取消任务
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isFailed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open('/logs', '_blank')}
|
||||
className="px-4 py-1.5 bg-[#ffb900] border-2 border-black text-black hover:bg-[#ffcc33] text-xs font-bold uppercase rounded-none shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] active:shadow-none transition-all flex items-center"
|
||||
>
|
||||
<Activity className="w-3 h-3 mr-1" />
|
||||
查看日志
|
||||
</button>
|
||||
)}
|
||||
|
||||
{(isCompleted || isFailed || isCancelled) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="px-4 py-1.5 bg-[#333] border-2 border-black text-white hover:bg-[#000] text-xs font-bold uppercase rounded-none shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] active:shadow-none transition-all flex items-center"
|
||||
>
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
确认
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogPrimitive.Content>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default {
|
|||
extend: {
|
||||
// Typography - Pixel-perfect monospace for terminal aesthetic
|
||||
fontFamily: {
|
||||
mono: ['"JetBrains Mono"', '"Roboto Mono"', '"Courier New"', 'monospace'],
|
||||
mono: ['"ArkPixel"', '"JetBrains Mono"', '"Roboto Mono"', '"Courier New"', 'monospace'],
|
||||
sans: ['"Inter"', 'system-ui', 'sans-serif'],
|
||||
display: ['"Orbitron"', '"Rajdhani"', 'sans-serif'],
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue