| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- 'use client';
- import { useState, useEffect } from 'react';
- import api from '@/lib/api';
- import { FileText, Search, RefreshCw, AlertCircle, Download } from 'lucide-react';
- interface ServerConfig {
- host: string;
- port: number;
- username: string;
- password?: string;
- key_file?: string;
- project_path: string;
- }
- export default function LogViewer({
- serverConfig,
- serverId,
- projectPath
- }: {
- serverConfig: ServerConfig;
- serverId?: string;
- projectPath?: string;
- }) {
- const [logFiles, setLogFiles] = useState<string[]>([]);
- const [selectedLog, setSelectedLog] = useState<string>('');
- const [logContent, setLogContent] = useState<string>('');
- const [lines, setLines] = useState(100);
- const [fromHead, setFromHead] = useState(false);
- const [full, setFull] = useState(false);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | null>(null);
- const fetchLogList = async () => {
- setLoading(true);
- setError(null);
- try {
- const url = serverId ? '/api/remote/server/log/list' : '/api/remote/log/list';
- const payload = serverId ? { server_id: serverId, project_path: projectPath } : { ...serverConfig, project_path: projectPath };
- const response = await api.post(url, payload);
- if (response.data.code === 0) {
- setLogFiles(response.data.data.log_files || []);
- } else {
- setError(response.data.message || '获取日志列表失败');
- }
- } catch (err: any) {
- setError(err.response?.data?.message || err.message || '请求失败');
- } finally {
- setLoading(false);
- }
- };
- const fetchLogContent = async () => {
- if (!selectedLog) return;
-
- setLoading(true);
- setError(null);
- try {
- const url = serverId ? '/api/remote/server/log/read' : '/api/remote/log/read';
- const payload = serverId
- ? {
- server_id: serverId,
- log_file: selectedLog,
- lines: full ? 100 : lines,
- from_head: fromHead,
- full: full,
- project_path: projectPath
- }
- : {
- ...serverConfig,
- log_file: selectedLog,
- lines: full ? 100 : lines,
- from_head: fromHead,
- full: full,
- project_path: projectPath,
- };
- const response = await api.post(url, payload);
- if (response.data.code === 0) {
- setLogContent(response.data.data.content || '');
- } else {
- setError(response.data.message || '读取日志失败');
- }
- } catch (err: any) {
- setError(err.response?.data?.message || err.message || '读取日志失败');
- } finally {
- setLoading(false);
- }
- };
- useEffect(() => {
- fetchLogList();
- }, [serverId, projectPath]);
- useEffect(() => {
- if (selectedLog) {
- fetchLogContent();
- }
- }, [selectedLog, lines, fromHead, full]);
- const downloadLog = () => {
- if (!logContent) return;
- const blob = new Blob([logContent], { type: 'text/plain' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = selectedLog || 'log.txt';
- a.click();
- URL.revokeObjectURL(url);
- };
- return (
- <div className="space-y-4 sm:space-y-6">
- <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
- <h3 className="text-lg font-semibold text-slate-800">日志文件查看</h3>
- <button
- onClick={fetchLogList}
- disabled={loading}
- className="w-full sm:w-auto px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
- >
- <RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
- 刷新列表
- </button>
- </div>
- {error && (
- <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md flex items-center gap-2 text-sm">
- <AlertCircle size={18} />
- {error}
- </div>
- )}
- <div className="flex flex-col lg:grid lg:grid-cols-3 gap-6">
- {/* 日志文件列表 */}
- <div className="bg-slate-50 rounded-lg border border-slate-200 p-4">
- <h4 className="text-sm font-semibold text-slate-700 mb-3">日志文件列表</h4>
- <div className="space-y-1 max-h-48 sm:max-h-96 overflow-y-auto">
- {logFiles.length === 0 ? (
- <p className="text-sm text-slate-500 text-center py-4">暂无日志文件</p>
- ) : (
- logFiles.map((file) => (
- <button
- key={file}
- onClick={() => setSelectedLog(file)}
- className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors truncate ${
- selectedLog === file
- ? 'bg-blue-600 text-white'
- : 'bg-white text-slate-700 hover:bg-slate-100'
- }`}
- >
- <FileText size={16} className="inline mr-2 shrink-0" />
- {file}
- </button>
- ))
- )}
- </div>
- </div>
- {/* 日志内容 */}
- <div className="lg:col-span-2 space-y-4">
- {selectedLog ? (
- <>
- {/* 控制栏 */}
- <div className="bg-white rounded-lg border border-slate-200 p-3 sm:p-4">
- <div className="flex flex-wrap items-center gap-3 sm:gap-4">
- <div className="flex items-center gap-2">
- <label className="text-xs sm:text-sm text-slate-700 whitespace-nowrap">行数:</label>
- <input
- type="number"
- value={lines}
- onChange={(e) => {
- setLines(parseInt(e.target.value) || 100);
- setFull(false);
- }}
- disabled={full || loading}
- className="w-16 sm:w-20 px-2 py-1 border border-slate-300 rounded text-xs sm:text-sm disabled:opacity-50"
- />
- </div>
- <div className="flex items-center gap-4">
- <label className="flex items-center gap-1.5 text-xs sm:text-sm text-slate-700 cursor-pointer">
- <input
- type="checkbox"
- checked={fromHead}
- onChange={(e) => {
- setFromHead(e.target.checked);
- setFull(false);
- }}
- disabled={full || loading}
- className="rounded text-blue-600 focus:ring-blue-500"
- />
- 从开头
- </label>
- <label className="flex items-center gap-1.5 text-xs sm:text-sm text-slate-700 cursor-pointer">
- <input
- type="checkbox"
- checked={full}
- onChange={(e) => {
- setFull(e.target.checked);
- if (e.target.checked) {
- setFromHead(false);
- }
- }}
- disabled={loading}
- className="rounded text-blue-600 focus:ring-blue-500"
- />
- 全部
- </label>
- </div>
- <div className="flex items-center gap-2 w-full sm:w-auto">
- <button
- onClick={fetchLogContent}
- disabled={loading}
- className="flex-1 sm:flex-none px-3 py-1.5 bg-blue-600 text-white text-xs sm:text-sm rounded hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-1.5"
- >
- <RefreshCw size={14} className={loading ? 'animate-spin' : ''} />
- 刷新
- </button>
- <button
- onClick={downloadLog}
- disabled={!logContent}
- className="flex-1 sm:flex-none px-3 py-1.5 bg-green-600 text-white text-xs sm:text-sm rounded hover:bg-green-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-1.5"
- >
- <Download size={14} />
- 下载
- </button>
- </div>
- </div>
- </div>
- {/* 日志内容显示 */}
- <div className="bg-slate-900 rounded-lg border border-slate-200 p-3 sm:p-4">
- <div className="flex items-center justify-between mb-2">
- <h4 className="text-xs sm:text-sm font-semibold text-white truncate mr-2">{selectedLog}</h4>
- <span className="text-[10px] sm:text-xs text-slate-400 whitespace-nowrap">
- {logContent.split('\n').length} 行
- </span>
- </div>
- <pre className="text-[10px] sm:text-xs text-green-400 font-mono overflow-x-auto max-h-[400px] sm:max-h-[600px] overflow-y-auto whitespace-pre-wrap">
- {logContent || (loading ? '加载中...' : '暂无内容')}
- </pre>
- </div>
- </>
- ) : (
- <div className="bg-slate-50 rounded-lg border border-slate-200 p-6 sm:p-8 text-center">
- <FileText className="mx-auto text-slate-400 mb-3 w-8 h-8 sm:w-12 sm:h-12" />
- <p className="text-sm sm:text-base text-slate-500">请选择一个日志文件查看</p>
- </div>
- )}
- </div>
- </div>
- </div>
- );
- }
|