558 lines
20 KiB
JavaScript
558 lines
20 KiB
JavaScript
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-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 => 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);
|