LogViewer.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. 'use client';
  2. import { useState, useEffect } from 'react';
  3. import api from '@/lib/api';
  4. import { FileText, Search, RefreshCw, AlertCircle, Download } from 'lucide-react';
  5. interface ServerConfig {
  6. host: string;
  7. port: number;
  8. username: string;
  9. password?: string;
  10. key_file?: string;
  11. project_path: string;
  12. }
  13. export default function LogViewer({
  14. serverConfig,
  15. serverId,
  16. projectPath
  17. }: {
  18. serverConfig: ServerConfig;
  19. serverId?: string;
  20. projectPath?: string;
  21. }) {
  22. const [logFiles, setLogFiles] = useState<string[]>([]);
  23. const [selectedLog, setSelectedLog] = useState<string>('');
  24. const [logContent, setLogContent] = useState<string>('');
  25. const [lines, setLines] = useState(100);
  26. const [fromHead, setFromHead] = useState(false);
  27. const [full, setFull] = useState(false);
  28. const [loading, setLoading] = useState(false);
  29. const [error, setError] = useState<string | null>(null);
  30. const fetchLogList = async () => {
  31. setLoading(true);
  32. setError(null);
  33. try {
  34. const url = serverId ? '/api/remote/server/log/list' : '/api/remote/log/list';
  35. const payload = serverId ? { server_id: serverId, project_path: projectPath } : { ...serverConfig, project_path: projectPath };
  36. const response = await api.post(url, payload);
  37. if (response.data.code === 0) {
  38. setLogFiles(response.data.data.log_files || []);
  39. } else {
  40. setError(response.data.message || '获取日志列表失败');
  41. }
  42. } catch (err: any) {
  43. setError(err.response?.data?.message || err.message || '请求失败');
  44. } finally {
  45. setLoading(false);
  46. }
  47. };
  48. const fetchLogContent = async () => {
  49. if (!selectedLog) return;
  50. setLoading(true);
  51. setError(null);
  52. try {
  53. const url = serverId ? '/api/remote/server/log/read' : '/api/remote/log/read';
  54. const payload = serverId
  55. ? {
  56. server_id: serverId,
  57. log_file: selectedLog,
  58. lines: full ? 100 : lines,
  59. from_head: fromHead,
  60. full: full,
  61. project_path: projectPath
  62. }
  63. : {
  64. ...serverConfig,
  65. log_file: selectedLog,
  66. lines: full ? 100 : lines,
  67. from_head: fromHead,
  68. full: full,
  69. project_path: projectPath,
  70. };
  71. const response = await api.post(url, payload);
  72. if (response.data.code === 0) {
  73. setLogContent(response.data.data.content || '');
  74. } else {
  75. setError(response.data.message || '读取日志失败');
  76. }
  77. } catch (err: any) {
  78. setError(err.response?.data?.message || err.message || '读取日志失败');
  79. } finally {
  80. setLoading(false);
  81. }
  82. };
  83. useEffect(() => {
  84. fetchLogList();
  85. }, [serverId, projectPath]);
  86. useEffect(() => {
  87. if (selectedLog) {
  88. fetchLogContent();
  89. }
  90. }, [selectedLog, lines, fromHead, full]);
  91. const downloadLog = () => {
  92. if (!logContent) return;
  93. const blob = new Blob([logContent], { type: 'text/plain' });
  94. const url = URL.createObjectURL(blob);
  95. const a = document.createElement('a');
  96. a.href = url;
  97. a.download = selectedLog || 'log.txt';
  98. a.click();
  99. URL.revokeObjectURL(url);
  100. };
  101. return (
  102. <div className="space-y-4 sm:space-y-6">
  103. <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
  104. <h3 className="text-lg font-semibold text-slate-800">日志文件查看</h3>
  105. <button
  106. onClick={fetchLogList}
  107. disabled={loading}
  108. 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"
  109. >
  110. <RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
  111. 刷新列表
  112. </button>
  113. </div>
  114. {error && (
  115. <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">
  116. <AlertCircle size={18} />
  117. {error}
  118. </div>
  119. )}
  120. <div className="flex flex-col lg:grid lg:grid-cols-3 gap-6">
  121. {/* 日志文件列表 */}
  122. <div className="bg-slate-50 rounded-lg border border-slate-200 p-4">
  123. <h4 className="text-sm font-semibold text-slate-700 mb-3">日志文件列表</h4>
  124. <div className="space-y-1 max-h-48 sm:max-h-96 overflow-y-auto">
  125. {logFiles.length === 0 ? (
  126. <p className="text-sm text-slate-500 text-center py-4">暂无日志文件</p>
  127. ) : (
  128. logFiles.map((file) => (
  129. <button
  130. key={file}
  131. onClick={() => setSelectedLog(file)}
  132. className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors truncate ${
  133. selectedLog === file
  134. ? 'bg-blue-600 text-white'
  135. : 'bg-white text-slate-700 hover:bg-slate-100'
  136. }`}
  137. >
  138. <FileText size={16} className="inline mr-2 shrink-0" />
  139. {file}
  140. </button>
  141. ))
  142. )}
  143. </div>
  144. </div>
  145. {/* 日志内容 */}
  146. <div className="lg:col-span-2 space-y-4">
  147. {selectedLog ? (
  148. <>
  149. {/* 控制栏 */}
  150. <div className="bg-white rounded-lg border border-slate-200 p-3 sm:p-4">
  151. <div className="flex flex-wrap items-center gap-3 sm:gap-4">
  152. <div className="flex items-center gap-2">
  153. <label className="text-xs sm:text-sm text-slate-700 whitespace-nowrap">行数:</label>
  154. <input
  155. type="number"
  156. value={lines}
  157. onChange={(e) => {
  158. setLines(parseInt(e.target.value) || 100);
  159. setFull(false);
  160. }}
  161. disabled={full || loading}
  162. className="w-16 sm:w-20 px-2 py-1 border border-slate-300 rounded text-xs sm:text-sm disabled:opacity-50"
  163. />
  164. </div>
  165. <div className="flex items-center gap-4">
  166. <label className="flex items-center gap-1.5 text-xs sm:text-sm text-slate-700 cursor-pointer">
  167. <input
  168. type="checkbox"
  169. checked={fromHead}
  170. onChange={(e) => {
  171. setFromHead(e.target.checked);
  172. setFull(false);
  173. }}
  174. disabled={full || loading}
  175. className="rounded text-blue-600 focus:ring-blue-500"
  176. />
  177. 从开头
  178. </label>
  179. <label className="flex items-center gap-1.5 text-xs sm:text-sm text-slate-700 cursor-pointer">
  180. <input
  181. type="checkbox"
  182. checked={full}
  183. onChange={(e) => {
  184. setFull(e.target.checked);
  185. if (e.target.checked) {
  186. setFromHead(false);
  187. }
  188. }}
  189. disabled={loading}
  190. className="rounded text-blue-600 focus:ring-blue-500"
  191. />
  192. 全部
  193. </label>
  194. </div>
  195. <div className="flex items-center gap-2 w-full sm:w-auto">
  196. <button
  197. onClick={fetchLogContent}
  198. disabled={loading}
  199. 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"
  200. >
  201. <RefreshCw size={14} className={loading ? 'animate-spin' : ''} />
  202. 刷新
  203. </button>
  204. <button
  205. onClick={downloadLog}
  206. disabled={!logContent}
  207. 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"
  208. >
  209. <Download size={14} />
  210. 下载
  211. </button>
  212. </div>
  213. </div>
  214. </div>
  215. {/* 日志内容显示 */}
  216. <div className="bg-slate-900 rounded-lg border border-slate-200 p-3 sm:p-4">
  217. <div className="flex items-center justify-between mb-2">
  218. <h4 className="text-xs sm:text-sm font-semibold text-white truncate mr-2">{selectedLog}</h4>
  219. <span className="text-[10px] sm:text-xs text-slate-400 whitespace-nowrap">
  220. {logContent.split('\n').length} 行
  221. </span>
  222. </div>
  223. <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">
  224. {logContent || (loading ? '加载中...' : '暂无内容')}
  225. </pre>
  226. </div>
  227. </>
  228. ) : (
  229. <div className="bg-slate-50 rounded-lg border border-slate-200 p-6 sm:p-8 text-center">
  230. <FileText className="mx-auto text-slate-400 mb-3 w-8 h-8 sm:w-12 sm:h-12" />
  231. <p className="text-sm sm:text-base text-slate-500">请选择一个日志文件查看</p>
  232. </div>
  233. )}
  234. </div>
  235. </div>
  236. </div>
  237. );
  238. }