2025-04-28 12:25:20 +08:00

500 lines
18 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'; // 直接设置日志目录路径
}
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-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #e0e0e0;
}
.log-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.log-actions {
display: flex;
gap: 8px;
}
.log-action-button {
background-color: transparent;
color: #333;
border: none;
border-radius: 4px;
padding: 6px;
cursor: pointer;
font-size: 14px;
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: rgba(0,0,0,0.05);
}
.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-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;
}
</style>
<div class="run-log-container">
<div class="log-header">
<div class="log-title">运行日志</div>
<div class="log-actions">
<button class="log-action-button" id="refreshLog">
<img src="assets/icons/png/refresh_b.png" alt="刷新">
</button>
</div>
</div>
<div class="log-list" id="logList">
<div class="no-logs">暂无日志记录</div>
</div>
</div>
`;
}
setupEventListeners() {
// 设置按钮事件
this.shadowRoot.getElementById('refreshLog').addEventListener('click', () => {
this.loadLogFileList();
});
}
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 => 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;
}
logListElement.innerHTML = '';
this.logFiles.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);
});
}
loadLogContent(logPath, contentElement) {
contentElement.innerHTML = '<div style="padding: 8px;">加载中...</div>';
// 模拟从文件读取内容
this.fetchLogContent(logPath).then(content => {
// 构建不含空白的HTML结构
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);