Terminal.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import { useEffect, useRef, useState } from "react";
  2. import type { FC } from "react";
  3. import styles from "./Terminal.module.css";
  4. interface LogEntry {
  5. timestamp: string;
  6. level: string;
  7. name: string;
  8. message: string;
  9. }
  10. interface TerminalProps {
  11. onClose: () => void;
  12. }
  13. export const Terminal: FC<TerminalProps> = ({ onClose }) => {
  14. const [logs, setLogs] = useState<LogEntry[]>([]);
  15. const [isConnected, setIsConnected] = useState(false);
  16. const wsRef = useRef<WebSocket | null>(null);
  17. const logsEndRef = useRef<HTMLDivElement>(null);
  18. const [autoScroll, setAutoScroll] = useState(true);
  19. useEffect(() => {
  20. // 连接WebSocket
  21. const ws = new WebSocket("ws://43.106.118.91:8000/api/logs/watch");
  22. wsRef.current = ws;
  23. ws.onopen = () => {
  24. setIsConnected(true);
  25. console.log("Terminal WebSocket connected");
  26. };
  27. ws.onmessage = (event) => {
  28. try {
  29. const logEntry: LogEntry = JSON.parse(event.data);
  30. setLogs((prev) => [...prev, logEntry]);
  31. } catch (error) {
  32. console.error("Failed to parse log entry:", error);
  33. }
  34. };
  35. ws.onerror = (error) => {
  36. console.error("Terminal WebSocket error:", error);
  37. setIsConnected(false);
  38. };
  39. ws.onclose = () => {
  40. setIsConnected(false);
  41. console.log("Terminal WebSocket disconnected");
  42. };
  43. return () => {
  44. ws.close();
  45. };
  46. }, []);
  47. useEffect(() => {
  48. if (autoScroll && logsEndRef.current) {
  49. logsEndRef.current.scrollIntoView({ behavior: "smooth" });
  50. }
  51. }, [logs, autoScroll]);
  52. const handleClear = () => {
  53. setLogs([]);
  54. };
  55. const getLevelColor = (level: string) => {
  56. switch (level) {
  57. case "ERROR":
  58. return styles.error;
  59. case "WARNING":
  60. return styles.warning;
  61. case "INFO":
  62. return styles.info;
  63. case "DEBUG":
  64. return styles.debug;
  65. default:
  66. return "";
  67. }
  68. };
  69. return (
  70. <div className={styles.terminal}>
  71. <div className={styles.header}>
  72. <div className={styles.title}>
  73. <span className={styles.icon}>▶</span>
  74. <span>控制台输出</span>
  75. <span className={`${styles.status} ${isConnected ? styles.connected : styles.disconnected}`}>
  76. {isConnected ? "●" : "○"}
  77. </span>
  78. </div>
  79. <div className={styles.actions}>
  80. <button
  81. className={styles.button}
  82. onClick={handleClear}
  83. title="清空日志"
  84. >
  85. 清空
  86. </button>
  87. <button
  88. className={`${styles.button} ${autoScroll ? styles.active : ""}`}
  89. onClick={() => setAutoScroll(!autoScroll)}
  90. title="自动滚动"
  91. >
  92. {autoScroll ? "🔒" : "🔓"}
  93. </button>
  94. <button
  95. className={styles.closeButton}
  96. onClick={onClose}
  97. title="关闭"
  98. >
  99. ×
  100. </button>
  101. </div>
  102. </div>
  103. <div className={styles.content}>
  104. {logs.length === 0 ? (
  105. <div className={styles.empty}>等待日志输出...</div>
  106. ) : (
  107. logs.map((log, index) => (
  108. <div
  109. key={index}
  110. className={`${styles.logEntry} ${getLevelColor(log.level)}`}
  111. >
  112. <span className={styles.timestamp}>
  113. {new Date(log.timestamp).toLocaleTimeString()}
  114. </span>
  115. <span className={styles.level}>[{log.level}]</span>
  116. <span className={styles.name}>{log.name}:</span>
  117. <span className={styles.message}>{log.message}</span>
  118. </div>
  119. ))
  120. )}
  121. <div ref={logsEndRef} />
  122. </div>
  123. </div>
  124. );
  125. };