From 0da01dd8edc61de6b694f24edf2279de1949275c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E4=BF=9D=E7=A5=A5?= Date: Sun, 22 Dec 2024 04:30:30 +0800 Subject: [PATCH] 1.0.27 log viewer upgrade --- README.md | 2 +- package.json | 3 +- src/common/frame/WsData.ts | 1 + src/common/frame/ws.server.ts | 4 + src/main/domain/file/file.controller.ts | 6 + src/main/domain/file/file.service.ts | 203 ++++++++++++++---- src/main/domain/sys/sys.process.service.ts | 7 +- src/main/domain/sys/sys.systemd.service.ts | 5 + .../project/component/file/component/Ace.tsx | 12 +- .../component/file/component/LogViewer.tsx | 154 ++++++++----- src/web/project/util/noty.ts | 14 ++ 11 files changed, 315 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 4bb4ca6..549533d 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ url: http://116.198.245.137:5569/ 5. [excalidraw](https://github.com/excalidraw/excalidraw)绘图编辑器,这是一个很好用白板工具。 5. 切换根目录,在设置中添加多个文件夹路径后,可以在右上角选择切换根目录,只对一个session生效。 6. 终端,默认是bash, windwos下是 powershell。 - 7. 超大文本日志查看器,对任意文本右键使用作为日志类型查看,点击窗口后可以使用上下键来快速滚动翻页。 + 7. 超大文本日志查看器,对任意大小的文本右键使用作为日志类型查看,点击窗口后可以使用上下键来快速滚动翻页,还可以实时输出内容。 - ssh代理,ftp代理: 可以管理多个linux服务器,作用和winscp类似,让终端和文件管理更方便。 - 网站,是网址收藏夹,可用于保存服务器上其它的网站 - ddns diff --git a/package.json b/package.json index faf94ec..fa01915 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "filecat", - "version": "1.0.26", + "version": "1.0.27", "description": "filecat 文件管理器", "author": "xiaobaidadada", "scripts": { @@ -94,6 +94,7 @@ "axios": "^1.7.7", "babel-loader": "^9.1.3", "chart.js": "^4.4.2", + "chokidar": "^4.0.3", "copy-webpack-plugin": "^12.0.2", "css-loader": "^6.10.0", "fluent-ffmpeg": "^2.1.3", diff --git a/src/common/frame/WsData.ts b/src/common/frame/WsData.ts index 7c9240e..91000ba 100644 --- a/src/common/frame/WsData.ts +++ b/src/common/frame/WsData.ts @@ -90,6 +90,7 @@ export enum CmdType { file_compress, file_compress_progress, log_viewer, + log_viewer_watch, // rtsp rtsp_get, diff --git a/src/common/frame/ws.server.ts b/src/common/frame/ws.server.ts index 96798f5..5530789 100644 --- a/src/common/frame/ws.server.ts +++ b/src/common/frame/ws.server.ts @@ -39,6 +39,10 @@ export class Wss { } } + /** + * 暂时有不少 通过 on('close', 设置的函数没有使用这里 + * @param close + */ public setClose(close: Function) { this._close.push(close); } diff --git a/src/main/domain/file/file.controller.ts b/src/main/domain/file/file.controller.ts index 650074c..6423bb7 100644 --- a/src/main/domain/file/file.controller.ts +++ b/src/main/domain/file/file.controller.ts @@ -173,6 +173,12 @@ export class FileController { return FileServiceImpl.log_viewer(data); } + @msg(CmdType.log_viewer_watch) + async log_viewer_watch(data: WsData) { + // 如果一行太长 现在会进行截断成多个分裂的行 + return FileServiceImpl.log_viewer_watch(data); + } + // 获取studio路径 @Post("/studio/get/item") async studio_get_item(@Body() data: { path: string }, @Req() ctx) { diff --git a/src/main/domain/file/file.service.ts b/src/main/domain/file/file.service.ts index 9636f4a..39316ee 100644 --- a/src/main/domain/file/file.service.ts +++ b/src/main/domain/file/file.service.ts @@ -31,6 +31,7 @@ const archiver = require('archiver'); const mime = require('mime-types'); import multer from 'multer'; import {Request, Response} from "express"; +const chokidar = require('chokidar'); class FileService extends FileCompress { @@ -141,11 +142,11 @@ class FileService extends FileCompress { const sysPath = path.join(settingService.getFileRootPath(token), filePath ? decodeURIComponent(filePath) : ""); // if (!file) { // // 目录 - if ((req.query.dir === "1") && !fs.existsSync(sysPath)) { - // 目录不存在,创建目录 - fs.mkdirSync(sysPath, {recursive: true}); - return; - } + if ((req.query.dir === "1") && !fs.existsSync(sysPath)) { + // 目录不存在,创建目录 + fs.mkdirSync(sysPath, {recursive: true}); + return; + } // return; // } req['fileDir'] = path.dirname(sysPath); @@ -524,49 +525,76 @@ class FileService extends FileCompress { return pojo; } + isFirstByte(byte) { + // 确保 byte 是一个有效的字节 (0 - 255) + if (byte === undefined || byte < 0 || byte > 255) { + throw 'Invalid byte'; + } + // 1 字节: 0xxxxxxx (0x00 ~ 0x7F) 不需要校验 + // 2 字节: 110xxxxx (0x80 ~ 0x7FF) + // 3 字节: 1110xxxx (0x800 ~ 0xFFFF) + // 4 字节: 11110xxx (0x10000 ~ 0x10FFFF) + // 使用掩码和位运算判断 + return (byte & 0xE0) === 0xC0 || (byte & 0xF0) === 0xE0 || (byte & 0xF8) === 0xF0; + } + + go_forward_log( pojo : LogViewerPojo ,file_path) { // 开始查找 let linesRead = 0; // 行数 let haveReadSize = 0; // 已经读取的字节数 const fd = fs.openSync(file_path, "r"); + let max_count = 100; while (haveReadSize < pojo.once_max_size) { - // 创建一个 100 字节的缓冲区 - const buffer = Buffer.alloc(1024); + if (max_count<=0) { + break; + } + max_count--; + // 创建一个 10 kb字节的缓冲区 + const buffer = Buffer.alloc(10240); // 返回实际读取的字节数 - const bytesRead = fs.readSync(fd, buffer, - 0, // 相对于当前的偏移位置 - buffer.length, // 读取的长度 - pojo.position // 当前位置 - ); + let bytesRead = fs.readSync(fd, buffer, + 0, // 相对于当前的偏移位置 + buffer.length, // 读取的长度 + pojo.position // 当前位置 + ); // 遍历 buffer 中的每一个字节 let done = false; let last_h = -1; // 上一个/n 未开始的也算 /n 都是不包括 - for (let i = 0; i <= bytesRead; i++) { + for (let i = 0,ch_byte_i = bytesRead-1; i < bytesRead; i++) { // 如果字节是换行符 '\n'(ASCII值为 10) - if (buffer[i] === 10 || i === bytesRead ) { // 换行或者 最后一个字符 + if (buffer[i] === 10 || i === ch_byte_i ) { // 换行或者 最后一个字符 + let index = i; + if(i === ch_byte_i && (buffer[i] & 0x80) !== 0) { + // 最后一位 不是单字节字符 需要找到首字节 + for (let j = i; j > last_h; j--) { + if (this.isFirstByte(buffer[j])) { + index = j-1; + break; + } + } + } linesRead++; - // 总的来说 字符串要不包括\n 但是结束位置包括\n - const end_offset = i === bytesRead?i+1:i; - pojo.context_list.push(buffer.toString('utf8', last_h+1, end_offset)); // i 不包括 /n - pojo.context_start_position_list.push(pojo.position + last_h + 1); // 开始位置 - pojo.context_position_list.push(pojo.position + end_offset); // 结束位置 是/n的位置 + // 以/n做字符串结尾,扫描到的/n 或者文件的最后一个字符 + const now_str_start = last_h+1; + const next_str_start = index + 1; + pojo.context_list.push(buffer.toString('utf8', now_str_start, next_str_start)); // i 不包括 /n + pojo.context_start_position_list.push(pojo.position + now_str_start); // 开始位置 + pojo.context_position_list.push(pojo.position + next_str_start); // 结束位置 是/n的位置 if (linesRead >= pojo.line) { done = true; break; } - last_h = i; + last_h = index; } } - if (done) { + if (done || bytesRead === 0) { break; } else { - haveReadSize += bytesRead; + haveReadSize += last_h; // 更新文件位置 - pojo.position += bytesRead; - } - if (bytesRead === 0) { - break; + pojo.position += last_h +1; // 往前进一个字符 } } // 关闭文件 @@ -579,13 +607,16 @@ class FileService extends FileCompress { let linesRead = 0; // 行数 let haveReadSize = 0; // 已经读取的字节数 const fd = fs.openSync(file_path, "r"); - let buffer_len = 1024; + let buffer_len = 10240; + let max_count = 100; while (haveReadSize < pojo.once_max_size) { + if (max_count<=0) { + break; + } + max_count--; if (pojo.position < buffer_len) { // buffer_len = Math.floor(pojo.position / 2); buffer_len = pojo.position; // 全部读完 - } else { - buffer_len = pojo.position - buffer_len; } let buffer = Buffer.alloc(buffer_len); // 缓冲区满足当前位置往前移动的距离 pojo.position = pojo.position - buffer.length; // 位置前移 @@ -600,30 +631,38 @@ class FileService extends FileCompress { let done = false; let last_h = bytesRead; // 上一个 \n for (let i = bytesRead; i >= 0; i--) { + let index = i; // 如果字节是换行符 '\n'(ASCII值为 10) if (buffer[i] === 10 || i===0) { + if(i === 0 && (buffer[i] & 0x80) !== 0) { + // 找到首字节 + for (let j = 0; j < last_h; j++) { + if (this.isFirstByte(buffer[j])) { + index = j +1; + break; + } + } + } linesRead++; - const start_offset = i===0?i:i+1; - pojo.context_list.push(buffer.toString('utf8', i===0?i:i+1, last_h)); - pojo.context_start_position_list.push(pojo.position + start_offset); - pojo.context_position_list.push(pojo.position + last_h); + const now_str_start = index ===0 && pojo.position ===0 ?0:index+1; + const next_str_start = last_h + 1; + pojo.context_list.push(buffer.toString('utf8', now_str_start, next_str_start)); + pojo.context_start_position_list.push(pojo.position + now_str_start); + pojo.context_position_list.push(pojo.position + next_str_start); if (linesRead >= pojo.line) { done = true; break; } - last_h = i; + last_h = index; } } - if (done) { + if (done || bytesRead === 0 || (last_h<=0 && pojo.position ===0)) { break; } else { - haveReadSize += bytesRead; + haveReadSize += (bytesRead-last_h); // 更新文件位置 - pojo.position -= bytesRead; - } - if (bytesRead === 0 || pojo.position <= 0) { - break; + pojo.position -= last_h -1; } } // 关闭文件 @@ -651,6 +690,90 @@ class FileService extends FileCompress { if (pojo.back) return this.go_back_log(pojo,file_path); return this.go_forward_log(pojo, file_path); } + + file_change_watcher_map = new Map(); + + log_viewer_watch(data: WsData) { + const pojo = data.context as LogViewerPojo; + if(this.file_change_watcher_map.has(pojo.token)) { + return; + } + + const wss = data.wss as Wss; + pojo.context = ""; + pojo.context_list = []; + pojo.context_position_list = []; + pojo.context_start_position_list = []; + const root_path = settingService.getFileRootPath(pojo.token); + const file_path = path.join(root_path, decodeURIComponent(pojo.path)); + // 使用 chokidar 监控文件变化 + let watcher = chokidar.watch(file_path, { + persistent: true, // 持续监听 + usePolling: true, // 使用事件驱动模式(默认是) + // interval: 100, // 轮询间隔(如果启用了轮询模式) + }); + this.file_change_watcher_map.set(pojo.token,watcher); + wss.setClose(()=>{ + watcher.close(); + this.file_change_watcher_map.delete(pojo.token); + }) + // 已读取的字节数 + let bytesRead = pojo.max_size; + // 监听文件变化事件 + watcher.on('change', (changedFilePath) => { + if (changedFilePath === file_path) { + // 获取当前文件的状态 + fs.stat(file_path, (err, stats) => { + if (err) { + console.error('Failed to get file stats:', err); + watcher.close(); + this.file_change_watcher_map.delete(pojo.token); + return; + } + if (stats.size > bytesRead) { // 文件变大 + // 文件变大,创建新的读取流 + const newStream = fs.createReadStream(file_path, { encoding: 'utf8', start: bytesRead }); + newStream.on('data', (chunk) => { + const str = chunk.toString(); + let now_str_start = bytesRead; + let next_str_start = bytesRead + chunk.length+1; // todo +1? + let index = 0; + for (let i =0 ;i i) { + index = i+1; + now_str_start = bytesRead + index; + next_str_start + index; + break; + } + } + // send + pojo.context_list.push(str.slice(index,chunk.length)); + pojo.context_start_position_list.push(now_str_start ); + pojo.context_position_list.push(next_str_start); + pojo.max_size = bytesRead + chunk.length; + const result = new WsData(CmdType.log_viewer_watch); + result.context = pojo; + wss.sendData(result.encode()); + bytesRead += Buffer.byteLength(chunk, 'utf8'); // chunk 是字符串而不是字节流 所以要求实际长度一下 + + // init + pojo.context_list = []; + pojo.context_position_list = []; + pojo.context_start_position_list = []; + }); + } + }); + } + }); + // 监听错误 + watcher.on('error', (error) => { + watcher.close(); + this.file_change_watcher_map.delete(pojo.token); + }); + } } export const FileServiceImpl = new FileService(); diff --git a/src/main/domain/sys/sys.process.service.ts b/src/main/domain/sys/sys.process.service.ts index aa4f0b4..3e15751 100644 --- a/src/main/domain/sys/sys.process.service.ts +++ b/src/main/domain/sys/sys.process.service.ts @@ -174,13 +174,18 @@ export class SysProcessService { if (processWssMap.get(id)) { return; } + (data.wss as Wss).setClose(()=>{ + this.processClose(data); + }) processWssMap.set((data.wss as Wss).id, (data.wss as Wss)) await this.openProcessPush((data.wss as Wss)); } async processClose(data: WsData) { - SystemUtil.killProcess(data.context.pid) + clearInterval(processJobInterval); + processJobInterval = null; + this.killSpwn(); } } diff --git a/src/main/domain/sys/sys.systemd.service.ts b/src/main/domain/sys/sys.systemd.service.ts index eed5305..2d6cc77 100644 --- a/src/main/domain/sys/sys.systemd.service.ts +++ b/src/main/domain/sys/sys.systemd.service.ts @@ -186,6 +186,11 @@ export class SysSystemdService { return; } if (!jobInterval) { + (data.wss as Wss).setClose(()=>{ + clearInterval(jobInterval); + jobInterval = null; + this.clear(); + }) jobInterval = setInterval(async () => { if (processWssSet.size === 0) { clearInterval(jobInterval); diff --git a/src/web/project/component/file/component/Ace.tsx b/src/web/project/component/file/component/Ace.tsx index b9f4ee1..52f4f20 100644 --- a/src/web/project/component/file/component/Ace.tsx +++ b/src/web/project/component/file/component/Ace.tsx @@ -7,6 +7,7 @@ import "ace-builds/src-noconflict/mode-markdown"; import "ace-builds/src-noconflict/mode-tsx"; import "ace-builds/src-noconflict/mode-python"; import "ace-builds/src-noconflict/mode-sh"; +import "ace-builds/src-noconflict/mode-yaml"; import "ace-builds/src-noconflict/theme-cloud9_day"; ace.config.set("basePath", `https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/`); ace.config.set('modePath', `https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/`); @@ -16,7 +17,7 @@ import modelist from "ace-builds/src-noconflict/ext-modelist"; import {useRecoilState} from "recoil"; import {$stroe} from "../../../util/store"; import {editor_data} from "../../../util/store.util"; - +import "ace-builds/src-noconflict/ext-language_tools"; @@ -31,14 +32,17 @@ export default function Ace(props:{name: string,on_change?:()=>void,options?: Pa theme: "ace/theme/cloud9_day", mode: modelist.getModeForPath(props.name ?? '').mode, wrap: false, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true, - enableSnippets: true, highlightActiveLine:false, // 鼠标放在一行上的高亮 fontSize:14, // fontFamily:"JetBrains Mono" ...props.options, }); + // 语言智能提醒需要 import "ace-builds/src-noconflict/ext-language_tools"; + editor.setOptions({ + enableBasicAutocompletion: true, // 语言的基本自动补全 需要按 table + enableSnippets: true, // 快速插入模板,会有提示 安装enter键入 fori这样的 + enableLiveAutocompletion: true // 实时提醒 + }); // 监听滚动事件 editor.container.addEventListener("wheel", function (e) { e.preventDefault() diff --git a/src/web/project/component/file/component/LogViewer.tsx b/src/web/project/component/file/component/LogViewer.tsx index 1d7e7c9..604dd8d 100644 --- a/src/web/project/component/file/component/LogViewer.tsx +++ b/src/web/project/component/file/component/LogViewer.tsx @@ -1,18 +1,14 @@ import React, {useEffect, useRef, useState} from 'react' -import {Terminal} from '@xterm/xterm'; import {CmdType, WsData} from "../../../../../common/frame/WsData"; import {ws} from "../../../util/ws"; -import {SysPojo} from "../../../../../common/req/sys.pojo"; import {useRecoilState} from "recoil"; import {$stroe} from "../../../util/store"; -import {Shell} from "../../shell/Shell"; -import {ShellInitPojo} from "../../../../../common/req/ssh.pojo"; import Header from "../../../../meta/component/Header"; import {ActionButton} from "../../../../meta/component/Button"; -import {FileCompressPojo, LogViewerPojo} from "../../../../../common/file.pojo"; +import {LogViewerPojo} from "../../../../../common/file.pojo"; import {getRouterAfter} from "../../../util/WebPath"; -import {NotyWaring} from "../../../util/noty"; +import {NotyFail, NotyInfo, NotyWaring} from "../../../util/noty"; import {InputTextIcon} from "../../../../meta/component/Input"; import {useTranslation} from "react-i18next"; import {deleteList} from "../../../../../common/ListUtil"; @@ -60,30 +56,35 @@ import {deleteList} from "../../../../../common/ListUtil"; const history_max_line = 300; // 最多创建多少个dom对象 -// todo dom元素铺满的时候 走dom的值覆盖 而不是插入新的dom对象 - -// let insert_num = 0; // 插入次数 +let open_watch = false; let last_position = 0; -let alert = false; +let top_alert = true; var req :LogViewerPojo; - +/** + * + * 1. 如果整个视图只用一个div 每次更新都会完全刷新dom的全部内容 效果会不好 + * 2. 如果按固定大小输出 而不管行数 每次更新内容 最后一行 大概率会断行 + * 3. 对于fs.watch 而言 我们可以相信是按行数输出的 所以可以按大小输出 + */ var dom_children_list = []; export default function LogViewer(props) { const [shellShow, setShellShow] = useRecoilState($stroe.log_viewer); const shellRef = useRef(null); const [progress,set_progress ] = useState(0); - const [go_progress,set_go_progress ] = useState(0); + const [go_progress,set_go_progress ] = useState(100); + const [tip,set_tip ] = useState(false); const { t } = useTranslation(); const insert_dom = (data,position:number,start_postion:number,back = false,firstChild?:any)=> { // insert_num++; - const update = shellRef.current.scrollTop === shellRef.current.scrollHeight + // const update = shellRef.current.scrollTop === shellRef.current.scrollHeight const newDiv = document.createElement('div'); newDiv.textContent = data; - newDiv.style.whiteSpace = 'break-word'; + newDiv.style.whiteSpace = 'pre-wrap'; // 或者 'pre' (不会自动换行) \r\n 在最后面不会有额外换行,在前面有换行 + // newDiv.style.whiteSpace = 'break-word'; newDiv.style.overflowWrap = 'break-word'; newDiv.setAttribute('position',`${position}`); newDiv.setAttribute('start_position',`${start_postion}`); @@ -94,8 +95,8 @@ export default function LogViewer(props) { shellRef.current.appendChild(newDiv); dom_children_list.push(newDiv); } - if (update) - shellRef.current.scrollTop = shellRef.current.scrollHeight; + // if (update) + // shellRef.current.scrollTop = shellRef.current.scrollHeight; return newDiv; } @@ -108,14 +109,17 @@ export default function LogViewer(props) { const insert_v2 = (data:string|string[],position_list:number[],start_position_list:number[],back:boolean = false) => { if(!data)return; const list = Array.isArray(data)?data:data.split("\n"); + debugger if (!back) { // 正向插入 for (let i=0; i < list.length; i++) { insert_dom(list[i],position_list[i],start_position_list[i]); } if (dom_children_list.length > history_max_line) { - for (let i = 0; i < dom_children_list.length-history_max_line ; i++) { - delete_dom(dom_children_list[i]); + let max = dom_children_list.length - history_max_line; + while (max >0) { + delete_dom(dom_children_list[0]); + max --; } } } else { @@ -125,8 +129,10 @@ export default function LogViewer(props) { last_div = insert_dom(list[i],position_list[i] ,start_position_list[i],true,i === 0?shellRef.current.firstChild :last_div ); } if (dom_children_list.length > history_max_line) { - for (let i = dom_children_list.length-1; i >= history_max_line ; i--) { - delete_dom(dom_children_list[i]); + let max = dom_children_list.length-1; + while (max >=history_max_line) { + delete_dom(dom_children_list[dom_children_list.length-1]); + max--; } } } @@ -139,43 +145,83 @@ export default function LogViewer(props) { const pojo = data.context as LogViewerPojo; req = pojo; // console.log(req) + // return insert_v2(pojo.context_list,pojo.context_position_list,pojo.context_start_position_list,req.back); - if(req.back && dom_children_list.length > 0) { - // debugger - if (req.context_list.length > dom_children_list.length) { - set_progress(Math.floor(100 * parseInt(dom_children_list[req.context_list.length - dom_children_list.length ].getAttribute('position')) / req.max_size)) + if (req.context_list.length >0) { + if(req.back && dom_children_list.length > 0) { + if (req.context_list.length > dom_children_list.length) { + set_progress(Math.floor(100 * parseInt(dom_children_list[req.context_list.length - dom_children_list.length ].getAttribute('position')) / req.max_size)) + } else { + set_progress(Math.floor(100 * parseInt(dom_children_list[dom_children_list.length - req.context_list.length ].getAttribute('position')) / req.max_size)) + } + top_alert = false; } else { - set_progress(Math.floor(100 * parseInt(dom_children_list[dom_children_list.length - req.context_list.length ].getAttribute('position')) / req.max_size)) + set_progress(Math.floor(100 * (req.context_position_list[req.context_position_list.length-1] ??0) / req.max_size)) } - } else { - set_progress(Math.floor(100 * (req.context_position_list[req.context_position_list.length-1] ??0) / req.max_size)) + + } + if (shellRef.current.clientHeight === shellRef.current.scrollHeight) { + watch(0); } + } } + const watch = (position)=>{ + if(open_watch)return; + open_watch = true; + set_progress(100); + ws.addMsg(CmdType.log_viewer_watch,(wsData: WsData)=>{ + const pojo = wsData.context as LogViewerPojo; + if(!pojo) { + return; + } + req = pojo; + insert_v2(pojo.context_list,pojo.context_position_list,pojo.context_start_position_list,false); + shellRef.current.scrollTop = shellRef.current.scrollHeight; + } ); + req.context = ''; + req.context_list = []; + req.context_position_list = []; + req.context_start_position_list = []; + req.position = position; + ws.sendData(CmdType.log_viewer_watch, req) ; + set_tip(true); + } + const cancel_watch = ()=>{ + if (!open_watch)return; + open_watch = false; + ws.unConnect(); + set_tip(false) + // console.log('取消实时监听') + } const initTerminal = async () => { // 监听滚动事件 const handleScroll = async () => { - // if (insert_num > 0) { - // console.log(insert_num) - // insert_num --; - // // return; - // } const element = shellRef.current; if (element) { + + if (last_position > element.scrollTop) { + cancel_watch(); // 往上滑就取消实时监听 + } // 检测是否滚动到底部 - if (last_position < element.scrollTop && dom_children_list.length > 0&& element.scrollTop + element.clientHeight + 500 >= element.scrollHeight) { + if (open_watch) { + last_position = element.scrollTop; + return; + } if (last_position < element.scrollTop && dom_children_list.length > 0&& element.scrollTop + element.clientHeight + 500 >= element.scrollHeight) { // console.log("滚动到达底部"); const position = parseInt(dom_children_list[dom_children_list.length -1].getAttribute('position')) if (position >= req.max_size) { - if (!alert) { - alert = true; - NotyWaring('到达底部'); - } + // if (!top_alert) { + // top_alert = true; + watch(position); + // NotyInfo('到达底部开始实时监听'); + // } // console.log(position,req.max_size) - last_position = element.scrollTop; + // last_position = element.scrollTop; return; } + // console.log(11) req.line = 20; req.context = ""; req.context_list = []; @@ -191,13 +237,14 @@ export default function LogViewer(props) { // console.log("滚动到达顶部"); const position = parseInt(dom_children_list[0].getAttribute('start_position')); if (position <= 0) { - if (!alert) { - alert = true; + if (!top_alert) { + top_alert = true; NotyWaring('到达顶部'); } last_position = element.scrollTop; return; } + req.line = 10; req.context = ""; req.context_list = []; @@ -209,7 +256,6 @@ export default function LogViewer(props) { // insert_v2("好",[parseInt(shellRef.current.children[0].getAttribute('position'))],true); } last_position = element.scrollTop; - alert = false; } }; shellRef.current.addEventListener("scroll", handleScroll); @@ -231,7 +277,9 @@ export default function LogViewer(props) { }; shellRef.current.addEventListener("wheel", handleWheel, { passive: false }); - set_go_progress(0) + // set_go_progress(0) + set_tip(false); + open_watch = false; last_position = 0; dom_children_list = []; req = new LogViewerPojo(); @@ -240,16 +288,19 @@ export default function LogViewer(props) { req.line = history_max_line; req.path = `${getRouterAfter('file', location.pathname)}${shellShow.fileName}`; req.token = localStorage.getItem("token"); - + // insert_v2(`12 + // 2321888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 + // 312`,[0],[0],true); await send(); } - const close = () => { + const close = async () => { if (shellRef.current) { // shellRef.current.removeEventListener("scroll", handleScroll); shellRef.current.remove(); } + await ws.unConnect(); dom_children_list = []; } useEffect(() => { @@ -278,7 +329,8 @@ export default function LogViewer(props) { let v = 0; try { v= parseInt(go_progress); - if (v >100) { + if (v >100 || v <0) { + NotyFail('范围在0-100') throw "超过最大范围"; } for (const item of dom_children_list) { @@ -289,16 +341,20 @@ export default function LogViewer(props) { NotyWaring(e); return; } - req.back = v === 100 ; + req.back = v >= 100 ; req.position = Math.floor(req.max_size * (v / 100)); - // console.log(req) + // console.log(v,req.position) await send(); } return
{ setShellShow({show: false}) - }}/>]}> - 加载进度{progress} { + }}/>, + {shellShow.fileName}<span>加载完成</span>, + ]}> + {tip && 正在实时监听 } + 当前加载进度{progress} + { set_go_progress(v); }}/> diff --git a/src/web/project/util/noty.ts b/src/web/project/util/noty.ts index 4aee334..008deb8 100644 --- a/src/web/project/util/noty.ts +++ b/src/web/project/util/noty.ts @@ -41,3 +41,17 @@ export function NotyWaring(text) { layout: "topRight" }).show(); } + +export function NotyInfo(text) { + if (Date.now() - now < 500) { + return ; + } + now = Date.now(); + new Noty({ + type: 'info', + text: text, + // timeout:false, // 需要点击才消失 + timeout: 300, // 设置通知消失的时间(单位:毫秒) + layout: "topRight" + }).show(); +}