feat: update UI screenshots, add fonts, and improve terminal progress dialog

This commit is contained in:
lintsinghua 2025-11-28 22:55:32 +08:00
parent 36622e1da8
commit 3d908ad93e
14 changed files with 390 additions and 157 deletions

View File

@ -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.

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

View File

@ -2,6 +2,14 @@
@tailwind components; @tailwind components;
@tailwind utilities; @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 XCodeReviewer Design System
Aesthetic: Terminal Retro + Pixel Art + Mechanical + Cassette Futurism Aesthetic: Terminal Retro + Pixel Art + Mechanical + Cassette Futurism
@ -357,8 +365,30 @@
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
/* Smooth transitions */
/* Smooth transitions */ /* Smooth transitions */
.terminal-transition { .terminal-transition {
transition: all 0.15s ease; 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;
}
} }

View File

@ -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 { Dialog, DialogOverlay, DialogPortal } from "@/components/ui/dialog";
import * as DialogPrimitive from "@radix-ui/react-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 { cn, calculateTaskProgress } from "@/shared/utils/utils";
import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import { taskControl } from "@/shared/services/taskControl"; import { taskControl } from "@/shared/services/taskControl";
@ -16,6 +16,7 @@ interface TerminalProgressDialogProps {
} }
interface LogEntry { interface LogEntry {
id: string;
timestamp: string; timestamp: string;
message: string; message: string;
type: "info" | "success" | "error" | "warning"; type: "info" | "success" | "error" | "warning";
@ -36,15 +37,39 @@ export default function TerminalProgressDialog({
const pollIntervalRef = useRef<number | null>(null); const pollIntervalRef = useRef<number | null>(null);
const hasInitializedLogsRef = useRef(false); 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", { const timestamp = new Date().toLocaleTimeString("zh-CN", {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
second: "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 () => { const handleCancel = async () => {
@ -57,13 +82,14 @@ export default function TerminalProgressDialog({
// 1. 标记任务为取消状态 // 1. 标记任务为取消状态
taskControl.cancelTask(taskId); taskControl.cancelTask(taskId);
setIsCancelled(true); setIsCancelled(true);
addLog("🛑 用户取消任务,正在停止...", "error"); addLog("[ERR] 用户取消任务,正在停止...", "error");
// 2. 立即更新数据库状态 // 2. 立即更新数据库状态
try { try {
const { api } = await import("@/shared/config/database"); const { api } = await import("@/shared/config/database");
// biome-ignore lint/suspicious/noExplicitAny: API type mismatch workaround
await api.updateAuditTask(taskId, { status: 'cancelled' } as any); await api.updateAuditTask(taskId, { status: 'cancelled' } as any);
addLog(" 任务状态已更新为已取消", "warning"); addLog("[WARN] 任务状态已更新为已取消", "warning");
toast.success("任务已取消"); toast.success("任务已取消");
} catch (error) { } catch (error) {
console.error('更新取消状态失败:', 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(() => { useEffect(() => {
logsEndRef.current?.scrollIntoView({ behavior: "smooth" }); logsEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [logs]); }, [logs]);
@ -89,15 +116,17 @@ export default function TerminalProgressDialog({
return () => { return () => {
clearInterval(timeInterval); clearInterval(timeInterval);
}; };
}, [open, isCompleted, isFailed]); }, [open, isCompleted, isFailed, isCancelled]);
// 轮询任务状态 // 轮询任务状态
useEffect(() => { useEffect(() => {
if (!open || !taskId) { if (!open || !taskId) {
// 清理状态 // 清理状态
setLogs([]); setLogs([]);
logsRef.current = [];
setIsCompleted(false); setIsCompleted(false);
setIsFailed(false); setIsFailed(false);
setIsCancelled(false);
hasInitializedLogsRef.current = false; hasInitializedLogsRef.current = false;
if (pollIntervalRef.current) { if (pollIntervalRef.current) {
clearInterval(pollIntervalRef.current); clearInterval(pollIntervalRef.current);
@ -111,24 +140,24 @@ export default function TerminalProgressDialog({
hasInitializedLogsRef.current = true; hasInitializedLogsRef.current = true;
// 初始化日志 // 初始化日志
addLog("🚀 审计任务已启动", "info"); addLog("[INFO] 审计任务已启动", "info");
addLog(`任务ID: ${taskId}`, "info"); addLog(`TASK_ID: ${taskId}`, "info");
addLog(`任务类型: ${taskType === "repository" ? "仓库审计" : "ZIP文件审计"}`, "info"); addLog(`TYPE: ${taskType === "repository" ? "REPO_AUDIT" : "ZIP_AUDIT"}`, "info");
addLog(" 正在初始化审计环境...", "info"); addLog("[WAIT] 正在初始化审计环境...", "info");
} }
let lastScannedFiles = 0; let lastScannedFiles = 0;
let lastIssuesCount = 0; let lastIssuesCount = 0;
let lastTotalLines = 0; let lastTotalLines = 0;
let lastStatus = ""; let lastStatus = "";
let pollCount = 0; let _pollCount = 0;
let hasDataChange = false; let hasDataChange = false;
let isFirstPoll = true; let isFirstPoll = true;
// 开始轮询 // 开始轮询
const pollTask = async () => { const pollTask = async () => {
// 如果任务已完成或失败,停止轮询 // 如果任务已完成或失败,停止轮询
if (isCompleted || isFailed) { if (isCompletedRef.current || isFailedRef.current) {
if (pollIntervalRef.current) { if (pollIntervalRef.current) {
clearInterval(pollIntervalRef.current); clearInterval(pollIntervalRef.current);
pollIntervalRef.current = null; pollIntervalRef.current = null;
@ -137,7 +166,7 @@ export default function TerminalProgressDialog({
} }
try { try {
pollCount++; _pollCount++;
hasDataChange = false; hasDataChange = false;
const requestStartTime = Date.now(); const requestStartTime = Date.now();
@ -149,7 +178,7 @@ export default function TerminalProgressDialog({
const requestDuration = Date.now() - requestStartTime; const requestDuration = Date.now() - requestStartTime;
if (!task) { if (!task) {
addLog(` 任务不存在 (${requestDuration}ms)`, "error"); addLog(`[ERR] 任务不存在 (${requestDuration}ms)`, "error");
throw new Error("任务不存在"); throw new Error("任务不存在");
} }
@ -168,9 +197,9 @@ export default function TerminalProgressDialog({
// 只在有变化时显示请求/响应信息(跳过 pending 状态) // 只在有变化时显示请求/响应信息(跳过 pending 状态)
if (hasDataChange && task.status !== "pending") { if (hasDataChange && task.status !== "pending") {
addLog(`🔄 正在获取任务状态...`, "info"); addLog(`[NET] 正在获取任务状态...`, "info");
addLog( 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" "success"
); );
} }
@ -185,12 +214,12 @@ export default function TerminalProgressDialog({
// 静默跳过 pending 状态,不显示任何日志 // 静默跳过 pending 状态,不显示任何日志
} else if (task.status === "running") { } else if (task.status === "running") {
// 首次进入运行状态 // 首次进入运行状态
if (statusChanged && logs.filter(l => l.message.includes("开始扫描")).length === 0) { if (statusChanged && logsRef.current.filter(l => l.message.includes("开始扫描")).length === 0) {
addLog("🔍 开始扫描代码文件...", "info"); addLog("[SCAN] 开始扫描代码文件...", "info");
if (task.project) { if (task.project) {
addLog(`📁 项目: ${task.project.name}`, "info"); addLog(`[PROJ] 项目: ${task.project.name}`, "info");
if (task.branch_name) { 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 progress = calculateTaskProgress(task.scanned_files, task.total_files);
const filesProcessed = task.scanned_files - lastScannedFiles; const filesProcessed = task.scanned_files - lastScannedFiles;
addLog( addLog(
`📊 扫描进度: ${task.scanned_files || 0}/${task.total_files || 0} 文件 (${progress}%) [+${filesProcessed}]`, `[PROG] 扫描进度: ${task.scanned_files || 0}/${task.total_files || 0} 文件 (${progress}%) [+${filesProcessed}]`,
"info" "info"
); );
lastScannedFiles = task.scanned_files; lastScannedFiles = task.scanned_files;
@ -209,25 +238,25 @@ export default function TerminalProgressDialog({
// 显示问题发现(仅在有变化时) // 显示问题发现(仅在有变化时)
if (issuesChanged && task.issues_count > lastIssuesCount) { if (issuesChanged && task.issues_count > lastIssuesCount) {
const newIssues = 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; lastIssuesCount = task.issues_count;
} }
// 显示代码行数(仅在有变化时) // 显示代码行数(仅在有变化时)
if (linesChanged && task.total_lines > lastTotalLines) { if (linesChanged && task.total_lines > lastTotalLines) {
const newLines = 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; lastTotalLines = task.total_lines;
} }
} else if (task.status === "completed") { } else if (task.status === "completed") {
// 任务完成 // 任务完成
if (!isCompleted) { if (!isCompletedRef.current) {
addLog("", "info"); // 空行分隔 addLog("", "info"); // 空行分隔
addLog(" 代码扫描完成", "success"); addLog("[DONE] 代码扫描完成", "success");
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "info"); addLog("----------------------------------", "info");
addLog(`📊 总计扫描: ${task.total_files} 个文件`, "success"); addLog(`[STAT] 总计扫描: ${task.total_files} 个文件`, "success");
addLog(`📝 总计分析: ${task.total_lines.toLocaleString()} 行代码`, "success"); addLog(`[STAT] 总计分析: ${task.total_lines.toLocaleString()} 行代码`, "success");
addLog(`⚠️ 发现问题: ${task.issues_count}`, task.issues_count > 0 ? "warning" : "success"); addLog(`[RSLT] 发现问题: ${task.issues_count}`, task.issues_count > 0 ? "warning" : "success");
// 解析问题类型分布 // 解析问题类型分布
if (task.issues_count > 0) { if (task.issues_count > 0) {
@ -243,31 +272,31 @@ export default function TerminalProgressDialog({
}; };
if (severityCounts.critical > 0) { if (severityCounts.critical > 0) {
addLog(` 🔴 严重: ${severityCounts.critical}`, "error"); addLog(` [CRIT] 严重: ${severityCounts.critical}`, "error");
} }
if (severityCounts.high > 0) { if (severityCounts.high > 0) {
addLog(` 🟠 高: ${severityCounts.high}`, "warning"); addLog(` [HIGH] 高: ${severityCounts.high}`, "warning");
} }
if (severityCounts.medium > 0) { if (severityCounts.medium > 0) {
addLog(` 🟡 中等: ${severityCounts.medium}`, "warning"); addLog(` [MED] 中等: ${severityCounts.medium}`, "warning");
} }
if (severityCounts.low > 0) { 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(`[SCOR] 质量评分: ${task.quality_score.toFixed(1)}/100`, "success");
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "info"); addLog("----------------------------------", "info");
addLog("🎉 审计任务已完成!", "success"); addLog("[FIN] 审计任务已完成!", "success");
if (task.completed_at) { if (task.completed_at) {
const startTime = new Date(task.created_at).getTime(); const startTime = new Date(task.created_at).getTime();
const endTime = new Date(task.completed_at).getTime(); const endTime = new Date(task.completed_at).getTime();
const duration = Math.round((endTime - startTime) / 1000); const duration = Math.round((endTime - startTime) / 1000);
addLog(`⏱️ 总耗时: ${duration}`, "info"); addLog(`[TIME] 总耗时: ${duration}`, "info");
} }
setIsCompleted(true); setIsCompleted(true);
@ -278,16 +307,16 @@ export default function TerminalProgressDialog({
} }
} else if (task.status === "cancelled") { } else if (task.status === "cancelled") {
// 任务被取消 // 任务被取消
if (!isCancelled) { if (!isCancelledRef.current) {
addLog("", "info"); // 空行分隔 addLog("", "info"); // 空行分隔
addLog("🛑 任务已被用户取消", "warning"); addLog("[STOP] 任务已被用户取消", "warning");
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "warning"); addLog("----------------------------------", "warning");
addLog(`📊 完成统计:`, "info"); addLog(`[STAT] 完成统计:`, "info");
addLog(` • 已分析文件: ${task.scanned_files}/${task.total_files}`, "info"); addLog(` • 已分析文件: ${task.scanned_files}/${task.total_files}`, "info");
addLog(` • 发现问题: ${task.issues_count}`, "info"); addLog(` • 发现问题: ${task.issues_count}`, "info");
addLog(` • 代码行数: ${task.total_lines.toLocaleString()}`, "info"); addLog(` • 代码行数: ${task.total_lines.toLocaleString()}`, "info");
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "warning"); addLog("----------------------------------", "warning");
addLog(" 已分析的结果已保存到数据库", "success"); addLog("[SAVE] 已分析的结果已保存到数据库", "success");
setIsCancelled(true); setIsCancelled(true);
if (pollIntervalRef.current) { if (pollIntervalRef.current) {
@ -297,10 +326,10 @@ export default function TerminalProgressDialog({
} }
} else if (task.status === "failed") { } else if (task.status === "failed") {
// 任务失败 // 任务失败
if (!isFailed) { if (!isFailedRef.current) {
addLog("", "info"); // 空行分隔 addLog("", "info"); // 空行分隔
addLog(" 审计任务执行失败", "error"); addLog("[FAIL] 审计任务执行失败", "error");
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error"); addLog("----------------------------------", "error");
// 尝试从日志系统获取具体错误信息 // 尝试从日志系统获取具体错误信息
try { try {
@ -338,7 +367,7 @@ export default function TerminalProgressDialog({
addLog(" • GitHub/GitLab API 限流", "error"); addLog(" • GitHub/GitLab API 限流", "error");
addLog(" • LLM API 配置错误或额度不足", "error"); addLog(" • LLM API 配置错误或额度不足", "error");
} }
} catch (e) { } catch (_e) {
// 如果获取日志失败,显示常见原因 // 如果获取日志失败,显示常见原因
addLog("可能的原因:", "error"); addLog("可能的原因:", "error");
addLog(" • 网络连接问题", "error"); addLog(" • 网络连接问题", "error");
@ -347,9 +376,9 @@ export default function TerminalProgressDialog({
addLog(" • LLM API 配置错误或额度不足", "error"); addLog(" • LLM API 配置错误或额度不足", "error");
} }
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error"); addLog("----------------------------------", "error");
addLog("💡 建议: 检查系统配置和网络连接后重试", "warning"); addLog("[HINT] 建议: 检查系统配置和网络连接后重试", "warning");
addLog("📋 查看完整日志: 导航栏 -> 系统日志", "warning"); addLog("[LOGS] 查看完整日志: 导航栏 -> 系统日志", "warning");
setIsFailed(true); setIsFailed(true);
if (pollIntervalRef.current) { if (pollIntervalRef.current) {
@ -358,8 +387,9 @@ export default function TerminalProgressDialog({
} }
} }
} }
} catch (error: any) { } catch (error: unknown) {
addLog(`${error.message || "未知错误"}`, "error"); const errorMessage = error instanceof Error ? error.message : "未知错误";
addLog(`[ERR] ${errorMessage}`, "error");
// 不中断轮询,继续尝试 // 不中断轮询,继续尝试
} }
}; };
@ -377,42 +407,41 @@ export default function TerminalProgressDialog({
pollIntervalRef.current = null; pollIntervalRef.current = null;
} }
}; };
}, [open, taskId, taskType]); }, [open, taskId, taskType, addLog]);
// 获取日志颜色 - 使用优雅的深红色主题 // 获取日志颜色 - 简化配色,减少颜色数量
const getLogColor = (type: LogEntry["type"]) => { const getLogColor = (type: LogEntry["type"]) => {
switch (type) { switch (type) {
case "success": case "success":
return "text-green-500"; return "text-[#00ff41]"; // 纯绿色
case "error": case "error":
return "text-red-500"; return "text-[#ff3333]"; // 纯红色
case "warning": case "warning":
return "text-yellow-500"; return "text-[#ffb900]"; // 琥珀色
default: default:
return "text-gray-300"; return "text-[#cccccc]"; // 浅灰色 (原为青色)
} }
}; };
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogPortal> <DialogPortal>
<DialogOverlay className="bg-black/50 backdrop-blur-sm" /> <DialogOverlay className="bg-black/80 backdrop-blur-sm" />
<DialogPrimitive.Content <DialogPrimitive.Content
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]", "fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
"w-[90vw] aspect-[16/9]", "w-[95vw] max-w-[1000px] h-[85vh] max-h-[700px]",
"max-w-[1200px] max-h-[800px]", "p-0 gap-0 rounded-sm overflow-hidden",
"p-0 gap-0 rounded-none overflow-hidden", "bg-[#e0e0e0] border-4 border-[#4a4a4a]", // 机械外壳颜色
"bg-black border-4 border-gray-500 shadow-[10px_10px_0px_0px_rgba(0,0,0,0.5)]", "shadow-[15px_15px_0px_0px_rgba(0,0,0,0.5)]", // 硬阴影
"data-[state=open]:animate-in data-[state=closed]:animate-out", "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]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", "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()} onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()} onInteractOutside={(e) => e.preventDefault()}
> >
{/* 无障碍访问标题 */}
<VisuallyHidden.Root> <VisuallyHidden.Root>
<DialogPrimitive.Title></DialogPrimitive.Title> <DialogPrimitive.Title></DialogPrimitive.Title>
<DialogPrimitive.Description> <DialogPrimitive.Description>
@ -420,111 +449,191 @@ export default function TerminalProgressDialog({
</DialogPrimitive.Description> </DialogPrimitive.Description>
</VisuallyHidden.Root> </VisuallyHidden.Root>
{/* 终端头部 */} {/* 机械外壳装饰 - 螺丝 */}
<div className="flex items-center justify-between px-4 py-2 bg-gray-300 border-b-4 border-gray-500"> <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="flex items-center space-x-3"> <div className="w-2 h-0.5 bg-[#606060] rotate-45"></div>
<Terminal className="w-5 h-5 text-black" /> </div>
<span className="text-sm font-bold text-black uppercase font-display tracking-wider">TERMINAL // 审计进度监控</span> <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>
<div className="flex items-center space-x-2">
{/* 模拟窗口控制按钮 */} <div className="flex items-center space-x-4">
<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 className="flex space-x-1 bg-[#222] p-1 rounded-sm border-b border-white/20">
</div> <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-4 h-4 border-2 border-black bg-white flex items-center justify-center"> <div className={`w-3 h-3 rounded-full ${isFailed ? 'bg-[#ff0033] shadow-[0_0_5px_#ff0033]' : 'bg-[#4d000f]'}`} title="Error"></div>
<div className="w-2 h-2 border border-black"></div> <div className={`w-3 h-3 rounded-full ${isCompleted ? 'bg-[#00ccff] shadow-[0_0_5px_#00ccff]' : 'bg-[#00334d]'}`} title="Ready"></div>
</div> </div>
<button <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)} onClick={() => onOpenChange(false)}
title="关闭" title="关闭电源"
aria-label="关闭"
> >
<XIcon className="w-3 h-3 text-white" /> <XIcon className="w-5 h-5 text-white stroke-[3]" />
</button> </button>
</div> </div>
</div> </div>
{/* 终端内容 */} {/* 主体内容区 - 包含侧边栏和屏幕 */}
<div className="p-6 bg-black overflow-y-auto h-[calc(100%-100px)] font-mono text-sm relative"> <div className="flex h-[calc(100%-56px)] bg-[#c0c0c0]">
{/* 扫描线效果 */} {/* 左侧数据面板 */}
<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="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"> <div className="space-y-1 z-10">
{logs.map((log, index) => ( <div className="text-[10px] font-bold text-[#666] uppercase">Task ID</div>
<div key={index} className="flex items-start space-x-3 hover:bg-white/5 px-2 py-0.5 transition-colors"> <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>
<span className="text-gray-500 text-xs flex-shrink-0 w-24 font-bold"> </div>
[{log.timestamp}]
</span> <div className="space-y-1 z-10">
<span className={`${getLogColor(log.type)} flex-1 font-bold tracking-wide`}> <div className="text-[10px] font-bold text-[#666] uppercase">Type</div>
{log.type === 'info' && '> '} <div className="flex items-center space-x-2 bg-white/50 p-1 border border-[#999]">
{log.type === 'success' && '✓ '} {taskType === 'repository' ? <Cpu className="w-3 h-3" /> : <HardDrive className="w-3 h-3" />}
{log.type === 'error' && '✗ '} <span className="text-xs font-bold text-[#333] uppercase">{taskType}</span>
{log.type === 'warning' && '! '}
{log.message}
</span>
</div> </div>
))} </div>
{/* 光标旋转闪烁效果 */} <div className="flex-1"></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> <div className="h-24 w-full bg-[#333] p-2 flex flex-col justify-between opacity-80">
<span className="inline-block text-green-500 animate-pulse font-bold text-base">_</span> <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>
)} <div className="text-[8px] text-[#00ff41] font-mono leading-none">
MEM: 64K OK<br />
<div ref={logsEndRef} /> CPU: ACTIVE<br />
</div> NET: LINKED
</div> </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> </div>
<div className="flex items-center space-x-3"> {/* 中央屏幕区域 */}
{/* 运行中显示取消按钮 */} <div className="flex-1 p-6 flex flex-col relative">
{!isCompleted && !isFailed && !isCancelled && ( {/* 屏幕边框 */}
<Button <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">
size="sm" {/* 屏幕内边框 */}
variant="outline" <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>
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>
)}
{/* 失败时显示查看日志按钮 */} {/* 屏幕内容 */}
{isFailed && ( <div className="w-full h-full bg-black p-6 overflow-y-auto font-mono text-sm relative z-10 custom-scrollbar">
<button {/* CRT 效果层 */}
onClick={() => { <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>
window.open('/logs', '_blank'); <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>
}}
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>
)}
{/* 已完成/失败/取消显示关闭按钮 */} {/* 像素网格 */}
{(isCompleted || isFailed || isCancelled) && ( <div className="absolute inset-0 pointer-events-none z-0 opacity-10" style={{
<button backgroundImage: 'linear-gradient(#333 1px, transparent 1px), linear-gradient(90deg, #333 1px, transparent 1px)',
onClick={() => onOpenChange(false)} backgroundSize: '20px 20px'
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" }}></div>
>
<div className="relative z-10 space-y-1 pb-10">
</button> {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>
</div> </div>
</DialogPrimitive.Content> </DialogPrimitive.Content>

View File

@ -20,7 +20,7 @@ export default {
extend: { extend: {
// Typography - Pixel-perfect monospace for terminal aesthetic // Typography - Pixel-perfect monospace for terminal aesthetic
fontFamily: { fontFamily: {
mono: ['"JetBrains Mono"', '"Roboto Mono"', '"Courier New"', 'monospace'], mono: ['"ArkPixel"', '"JetBrains Mono"', '"Roboto Mono"', '"Courier New"', 'monospace'],
sans: ['"Inter"', 'system-ui', 'sans-serif'], sans: ['"Inter"', 'system-ui', 'sans-serif'],
display: ['"Orbitron"', '"Rajdhani"', 'sans-serif'], display: ['"Orbitron"', '"Rajdhani"', 'sans-serif'],
}, },