2025-05-28 16:20:01 +08:00

569 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class RunLog extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.logFiles = [];
this.expandedLogs = new Set();
this.logDir = '/log'; // 直接设置日志目录路径
// 添加分页相关的状态变量
this.currentPage = 1;
this.pageSize = 10; // 每页显示10个日志文件
this.totalPages = 1;
this.currentLogContent = []; // 存储当前日志文件的所有内容
}
connectedCallback() {
this.render();
this.setupEventListeners();
this.loadLogFileList();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
overflow: auto;
padding: 16px;
box-sizing: border-box;
}
.run-log-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 16px;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.log-list {
flex: 1;
overflow: auto;
}
.log-item {
margin-bottom: 8px;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.log-item-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f5f5f5;
}
.log-item-info {
flex: 1;
}
.log-item-title {
font-weight: 500;
margin-bottom: 4px;
}
.log-item-time {
color: #666;
font-size: 0.9em;
}
.log-item-source {
color: #888;
font-size: 0.8em;
margin-top: 2px;
}
.log-item-toggle {
background-color: transparent;
color: #333;
border: none;
border-radius: 4px;
padding: 4px;
cursor: pointer;
transition: background-color 0.3s;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
}
.log-item-toggle img {
width: 16px;
height: 16px;
transition: transform 0.3s;
}
.log-item-toggle:hover {
background-color: rgba(0,0,0,0.05);
}
.log-item.expanded .log-item-toggle {
background-color: transparent;
}
.log-item-content {
display: none;
background-color: #f5f5f5;
padding: 0;
margin: 0;
font-family: monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
color: #333;
max-height: 300px;
overflow: auto;
}
/* 清除所有在contentElement中可能出现的空白 */
.log-item-content > * {
margin: 0;
padding: 0;
}
.log-item.expanded .log-item-content {
display: block;
}
.log-filters {
display: flex;
margin: 0;
padding: 4px 8px;
gap: 8px;
background-color: #eaeaea;
border-radius: 0;
border-bottom: 1px solid #ddd;
position: sticky;
top: 0;
z-index: 1;
line-height: normal;
}
.log-filter {
padding: 4px 12px;
margin: 0;
background-color: #f0f0f0;
border-radius: 16px;
cursor: pointer;
font-size: 14px;
user-select: none;
}
.log-filter.active {
background-color: #8B6DB3;
color: white;
}
.log-content-messages {
padding: 8px;
margin: 0;
display: block;
}
.log-message {
margin: 2px 0;
padding: 4px 8px;
border-bottom: 1px solid #e8e8e8;
}
.log-message.info {
color: #1890ff;
}
.log-message.warning {
color: #faad14;
}
.log-message.error {
color: #f5222d;
}
.no-logs {
text-align: center;
padding: 32px;
color: #999;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
gap: 8px;
background-color: #f5f5f5;
border-top: 1px solid #e0e0e0;
}
.pagination-button {
padding: 6px 12px;
border: 1px solid #d9d9d9;
background-color: white;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #333;
}
.pagination-button:hover {
background-color: #f0f0f0;
}
.pagination-button:disabled {
cursor: not-allowed;
color: #d9d9d9;
background-color: #f5f5f5;
}
.pagination-info {
font-size: 14px;
color: #666;
}
.pagination-actions {
display: flex;
align-items: center;
gap: 8px;
margin-left: 16px;
}
.log-action-button {
padding: 6px;
border: 1px solid #d9d9d9;
background-color: white;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
}
.log-action-button img {
width: 20px;
height: 20px;
}
.log-action-button:hover {
background-color: #f0f0f0;
}
</style>
<div class="run-log-container">
<div class="log-list" id="logList">
<div class="no-logs">暂无日志记录</div>
</div>
</div>
`;
}
setupEventListeners() {
// 移除顶部刷新按钮的事件监听,因为按钮已经被移除
}
loadLogFileList() {
this.fetchLogFiles().then(logFiles => {
this.logFiles = logFiles;
this.renderLogList();
}).catch(error => {
console.error('获取日志文件列表失败:', error);
});
}
async fetchLogFiles() {
try {
// 直接使用固定的日志目录路径
const logDirPath = this.logDir;
// 读取目录下的所有文件
const files = await this.readLogDirectory(logDirPath);
// 过滤出.log文件并获取文件信息
const logFiles = [];
for (const fileName of files) {
if (fileName.endsWith('.log')) {
const filePath = `${logDirPath}/${fileName}`;
const fileStats = await this.getFileStats(filePath);
// 格式化修改时间
const modTime = new Date(fileStats.mtime);
const formattedTime = this.formatDateTime(modTime);
logFiles.push({
name: fileName,
time: formattedTime,
path: filePath
});
}
}
// 按修改时间排序,最新的在前面
return logFiles.sort((a, b) => {
return new Date(b.time) - new Date(a.time);
});
} catch (error) {
console.error('获取日志文件列表失败:', error);
return [];
}
}
// 读取目录内容
async readLogDirectory(dirPath) {
return new Promise((resolve, reject) => {
fetch(`/api/filesystem/readdir?path=${encodeURIComponent(dirPath)}`)
.then(response => {
if (!response.ok) {
throw new Error(`读取目录失败: ${response.status}`);
}
return response.json();
})
.then(data => {
resolve(data.files || []);
})
.catch(error => {
console.error('读取目录请求失败:', error);
reject(error);
});
});
}
// 获取文件状态信息
async getFileStats(filePath) {
return new Promise((resolve, reject) => {
fetch(`/api/filesystem/stat?path=${encodeURIComponent(filePath)}`)
.then(response => {
if (!response.ok) {
throw new Error(`获取文件信息失败: ${response.status}`);
}
return response.json();
})
.then(data => resolve(data))
.catch(error => reject(error));
});
}
// 格式化日期时间
formatDateTime(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
renderLogList() {
const logListElement = this.shadowRoot.getElementById('logList');
if (this.logFiles.length === 0) {
logListElement.innerHTML = '<div class="no-logs">暂无日志记录</div>';
return;
}
// 计算总页数
this.totalPages = Math.ceil(this.logFiles.length / this.pageSize);
// 获取当前页的日志文件
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
const currentPageFiles = this.logFiles.slice(start, end);
// 清空列表
logListElement.innerHTML = '';
// 渲染当前页的日志文件
currentPageFiles.forEach(logFile => {
const logItemElement = document.createElement('div');
logItemElement.className = 'log-item';
logItemElement.dataset.path = logFile.path;
const isExpanded = this.expandedLogs.has(logFile.path);
if (isExpanded) {
logItemElement.classList.add('expanded');
}
const iconSrc = isExpanded ? 'assets/icons/png/chevron-up_b.png' : 'assets/icons/png/chevron-down_b.png';
const iconAlt = isExpanded ? '收起' : '展开';
logItemElement.innerHTML = `
<div class="log-item-header">
<div class="log-item-info">
<div class="log-item-title">${logFile.name}</div>
<div class="log-item-time">${logFile.time}</div>
</div>
<button class="log-item-toggle">
<img src="${iconSrc}" alt="${iconAlt}">
</button>
</div>
<div class="log-item-content"></div>
`;
const toggleBtn = logItemElement.querySelector('.log-item-toggle');
const toggleImg = toggleBtn.querySelector('img');
const content = logItemElement.querySelector('.log-item-content');
toggleBtn.addEventListener('click', () => {
const isExpanded = logItemElement.classList.contains('expanded');
if (isExpanded) {
logItemElement.classList.remove('expanded');
this.expandedLogs.delete(logFile.path);
content.innerHTML = '';
toggleImg.src = 'assets/icons/png/chevron-down_b.png';
toggleImg.alt = '展开';
} else {
logItemElement.classList.add('expanded');
this.expandedLogs.add(logFile.path);
this.loadLogContent(logFile.path, content);
toggleImg.src = 'assets/icons/png/chevron-up_b.png';
toggleImg.alt = '收起';
}
});
if (isExpanded) {
this.loadLogContent(logFile.path, content);
}
logListElement.appendChild(logItemElement);
});
// 添加分页控制器和刷新按钮
const paginationHtml = `
<div class="pagination">
<button class="pagination-button" id="prevPage" ${this.currentPage === 1 ? 'disabled' : ''}>
上一页
</button>
<span class="pagination-info">
${this.currentPage} 页 / 共 ${this.totalPages}
</span>
<button class="pagination-button" id="nextPage" ${this.currentPage === this.totalPages ? 'disabled' : ''}>
下一页
</button>
<div class="pagination-actions">
<button class="log-action-button" id="refreshLog" title="刷新">
<img src="assets/icons/png/refresh_b.png" alt="刷新">
</button>
</div>
</div>
`;
logListElement.insertAdjacentHTML('beforeend', paginationHtml);
// 添加分页按钮事件监听
const prevButton = logListElement.querySelector('#prevPage');
const nextButton = logListElement.querySelector('#nextPage');
const refreshButton = logListElement.querySelector('#refreshLog');
prevButton.addEventListener('click', () => {
if (this.currentPage > 1) {
this.currentPage--;
this.renderLogList();
}
});
nextButton.addEventListener('click', () => {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.renderLogList();
}
});
refreshButton.addEventListener('click', () => {
this.loadLogFileList();
});
}
loadLogContent(logPath, contentElement) {
contentElement.innerHTML = '<div style="padding: 8px;">加载中...</div>';
this.fetchLogContent(logPath).then(content => {
let html = '<div class="log-filters">';
html += '<div class="log-filter active" data-level="all">全部</div>';
html += '<div class="log-filter" data-level="info">信息</div>';
html += '<div class="log-filter" data-level="warning">警告</div>';
html += '<div class="log-filter" data-level="error">错误</div>';
html += '</div><div class="log-content-messages">';
if (!content || content.length === 0) {
html += '<div style="padding: 8px;">日志为空</div>';
} else {
content.forEach(line => {
let className = 'log-message';
if (line.includes('[INFO]')) {
className += ' info';
} else if (line.includes('[WARNING]')) {
className += ' warning';
} else if (line.includes('[ERROR]')) {
className += ' error';
}
html += `<div class="${className}">${line}</div>`;
});
}
html += '</div>';
contentElement.innerHTML = html;
// 设置过滤器事件
const filterElements = contentElement.querySelectorAll('.log-filter');
filterElements.forEach(filter => {
filter.addEventListener('click', () => {
filterElements.forEach(f => f.classList.remove('active'));
filter.classList.add('active');
const level = filter.getAttribute('data-level');
const messagesContainer = contentElement.querySelector('.log-content-messages');
this.filterLogContent(messagesContainer, level);
});
});
}).catch(error => {
contentElement.innerHTML = `<div class="log-message error" style="padding: 8px;">加载日志内容失败: ${error.message}</div>`;
});
}
async fetchLogContent(logPath) {
try {
const response = await fetch(`/api/filesystem/readFile?path=${encodeURIComponent(logPath)}`);
if (!response.ok) {
throw new Error(`读取文件失败: ${response.status}`);
}
const fileContent = await response.text();
// 解析日志内容,按行分割
const lines = fileContent.split('\n')
.filter(line => line.trim() !== '') // 过滤空行
.slice(-500); // 只显示最后500行避免日志过大
return lines;
} catch (error) {
console.error('获取日志内容失败:', error);
return [];
}
}
filterLogContent(contentElement, level) {
const logMessages = contentElement.querySelectorAll('.log-message');
logMessages.forEach(message => {
if (level === 'all') {
message.style.display = 'block';
} else {
message.style.display = message.classList.contains(level) ? 'block' : 'none';
}
});
}
}
customElements.define('run-log', RunLog);