|
440 | 440 | let lastContainerLogTime = '' |
441 | 441 |
|
442 | 442 | const appendLog = (refElem, data, refLineNum, logBlocksRef, lastLogTimeRef) => { |
| 443 | + if (!refElem.value) return; |
| 444 | + |
443 | 445 | // ANSI 转 HTML 简单实现 |
444 | 446 | function ansiToHtml(text) { |
445 | 447 | const ansiMap = { |
|
454 | 456 | }; |
455 | 457 | return text.replace(/\x1b\[(\d+)m/g, (match, code) => ansiMap[code] || ''); |
456 | 458 | } |
457 | | - // 提取时间戳(如 [003s] 或 [0107]) |
458 | | - const timeMatch = data.match(/\[(\d+s?|\d{4})\]/) |
459 | | - let timeStr = '' |
460 | | - if (timeMatch) { |
461 | | - timeStr = timeMatch[1] |
462 | | - // 格式化为 03s 或 0107 |
| 459 | +
|
| 460 | + /** |
| 461 | + * 将 [0004] 转换为 [00:04] 格式 |
| 462 | + */ |
| 463 | + function formatTime(timeStr) { |
| 464 | + if (!timeStr) return ''; |
| 465 | + |
| 466 | + // 处理秒格式 (如 "03s") |
463 | 467 | if (/^\d+s$/.test(timeStr)) { |
464 | | - timeStr = timeStr.padStart(3, '0') |
| 468 | + const seconds = timeStr.replace('s', '').padStart(2, '0'); |
| 469 | + return `00:${seconds}`; |
| 470 | + } |
| 471 | + |
| 472 | + // 处理纯数字格式 (如 "0107") |
| 473 | + if (/^\d{4}$/.test(timeStr)) { |
| 474 | + return `${timeStr.slice(0, 2)}:${timeStr.slice(2)}`; |
465 | 475 | } |
| 476 | + |
| 477 | + return timeStr; |
466 | 478 | } |
467 | | - // 处理内容,去掉时间戳部分 |
468 | | - let content = data.replace(/\[(\d+s?|\d{4})\]\s*/, '') |
469 | | - content = ansiToHtml(content.replace(/\\r/g, '<br>')) |
470 | | - // 合并到同一时间块 |
471 | | - if (timeStr && lastLogTimeRef.value === timeStr && logBlocksRef.value.length > 0) { |
472 | | - logBlocksRef.value[logBlocksRef.value.length - 1].lines.push(content) |
473 | | - } else if (timeStr) { |
474 | | - logBlocksRef.value.push({ time: timeStr, lines: [content] }) |
475 | | - lastLogTimeRef.value = timeStr |
476 | | - } else { |
477 | | - // 没有时间戳,单独作为一块 |
478 | | - logBlocksRef.value.push({ time: '', lines: [content] }) |
479 | | - lastLogTimeRef.value = '' |
| 479 | +
|
| 480 | + /** |
| 481 | + * 过滤不需要的前缀如"build-" |
| 482 | + */ |
| 483 | + function filterPrefixes(text) { |
| 484 | + return text.replace(/(build|platform)-\d+\s*\|\s*/g, ''); |
| 485 | + } |
| 486 | +
|
| 487 | + // 新增:在日志级别后添加空格的函数 |
| 488 | + function addSpaceAfterLogLevel(text) { |
| 489 | + return text.replace(/(INFO|WARN|ERROR|DEBUG|TRACE|FATAL)([^\s])/g, '$1 $2'); |
480 | 490 | } |
| 491 | +
|
| 492 | + // 处理换行符 |
| 493 | + const processedData = data.replace(/\\n/g, '\n'); |
| 494 | +
|
| 495 | + // 按行处理日志内容 |
| 496 | + processedData.split('\n').forEach(line => { |
| 497 | + if (!line.trim()) return; |
| 498 | +
|
| 499 | + // 转换时间格式 [0004] → [00:04] |
| 500 | + line = line.replace(/\[(\d{4})\]/g, (_, time) => `[${time.slice(0, 2)}:${time.slice(2)}]`) |
| 501 | + .replace(/\[(\d+)s\]/g, (_, sec) => `[00:${sec.padStart(2, '0')}]`); |
| 502 | +
|
| 503 | + // 处理内容 |
| 504 | + let content = line; |
| 505 | + |
| 506 | + // 过滤不需要的前缀 |
| 507 | + content = filterPrefixes(content); |
| 508 | + |
| 509 | + // 在日志级别后添加空格 |
| 510 | + content = addSpaceAfterLogLevel(content); |
| 511 | + |
| 512 | + content = ansiToHtml(content.replace(/\\r/g, '<br>')); |
| 513 | +
|
| 514 | + // 合并到同一时间块(不再使用时间块分组) |
| 515 | + logBlocksRef.value.push({ |
| 516 | + time: '', // 置空时间,不在渲染时显示单独的时间行 |
| 517 | + lines: [content] |
| 518 | + }); |
| 519 | + }); |
| 520 | +
|
481 | 521 | // 重新渲染 |
482 | 522 | renderLogBlocks(refElem, logBlocksRef) |
483 | 523 | refLineNum.value = logBlocksRef.value.reduce((acc, b) => acc + b.lines.length, 0) |
|
486 | 526 | }) |
487 | 527 | } |
488 | 528 |
|
489 | | - // 渲染日志块 |
490 | 529 | function renderLogBlocks(refElem, logBlocksRef) { |
491 | | - if (!refElem.value) return |
492 | | - refElem.value.innerHTML = '' |
493 | | - let lineNum = 0 |
494 | | - logBlocksRef.value.forEach(block => { |
495 | | - const blockDiv = document.createElement('div') |
496 | | - blockDiv.className = 'mb-2' |
497 | | - if (block.time) { |
498 | | - const timeDiv = document.createElement('div') |
499 | | - timeDiv.className = 'text-base text-gray-400 mb-1 flex items-center' |
500 | | - timeDiv.innerHTML = `⏱ <span class='ml-1'>${block.time}</span>` |
501 | | - blockDiv.appendChild(timeDiv) |
502 | | - } |
503 | | - block.lines.forEach(line => { |
504 | | - const lineDiv = document.createElement('div') |
505 | | - lineDiv.className = 'flex items-start' |
506 | | - // 在INFO/WARN/ERROR等类型和内容之间加4px间距 |
507 | | - let lineHtml = line |
508 | | - // 匹配以INFO/WARN/ERROR/DEBUG等大写单词开头的内容,后跟内容 |
509 | | - lineHtml = lineHtml.replace(/^(<span[^>]*>)*(INFO|WARN|ERROR|DEBUG|TRACE|FATAL|NOTICE|WARNING)(<\/span[^>]*>)*([\s\S]*?)(?=<|$)/, (match, p1, type, p3, rest) => { |
510 | | - return `${p1 || ''}${type}${p3 || ''}<span style='display:inline-block;width:4px'></span>${rest || ''}` |
| 530 | + if (!refElem.value) return |
| 531 | + refElem.value.innerHTML = '' |
| 532 | + let lineNum = 0 |
| 533 | + logBlocksRef.value.forEach(block => { |
| 534 | + const blockDiv = document.createElement('div') |
| 535 | + blockDiv.className = 'mb-2' |
| 536 | + if (block.time) { |
| 537 | + const timeDiv = document.createElement('div') |
| 538 | + timeDiv.className = 'text-base text-gray-400 mb-1 flex items-center' |
| 539 | + timeDiv.innerHTML = `⏱ <span class='ml-1'>${block.time}</span>` |
| 540 | + blockDiv.appendChild(timeDiv) |
| 541 | + } |
| 542 | + block.lines.forEach(line => { |
| 543 | + const lineDiv = document.createElement('div') |
| 544 | + lineDiv.className = 'flex items-start' |
| 545 | + // 在INFO/WARN/ERROR等类型和内容之间加4px间距 |
| 546 | + let lineHtml = line |
| 547 | + // 匹配以INFO/WARN/ERROR/DEBUG等大写单词开头的内容,后跟内容 |
| 548 | + lineHtml = lineHtml.replace(/^(<span[^>]*>)*(INFO|WARN|ERROR|DEBUG|TRACE|FATAL|NOTICE|WARNING)(<\/span[^>]*>)*([\s\S]*?)(?=<|$)/, (match, p1, type, p3, rest) => { |
| 549 | + return `${p1 || ''}${type}${p3 || ''}<span style='display:inline-block;width:4px'></span>${rest || ''}` |
| 550 | + }) |
| 551 | + lineDiv.innerHTML = `<span class='mr-1 text-base text-gray-400' style='min-width:2.5em;display:inline-block;'>${lineNum}:</span><span class='mr-2 text-base'>•</span><span class='text-base'>${lineHtml}</span>` |
| 552 | + blockDiv.appendChild(lineDiv) |
| 553 | + lineNum++ |
511 | 554 | }) |
512 | | - lineDiv.innerHTML = `<span class='mr-1 text-base text-gray-400' style='min-width:2.5em;display:inline-block;'>${lineNum}:</span><span class='mr-2 text-base'>•</span><span class='text-base'>${lineHtml}</span>` |
513 | | - blockDiv.appendChild(lineDiv) |
514 | | - lineNum++ |
| 555 | + refElem.value.appendChild(blockDiv) |
515 | 556 | }) |
516 | | - refElem.value.appendChild(blockDiv) |
517 | | - }) |
518 | 557 | } |
519 | 558 |
|
520 | 559 | const downloadLog = () => { |
521 | | - const targetDiv = isBuildLogTab.value ? buildLogDiv : containerLogDiv |
522 | | - if (!targetDiv.value) return |
| 560 | + const logBlocks = isBuildLogTab.value ? buildLogBlocks : containerLogBlocks |
| 561 | + |
| 562 | + if (!logBlocks.value.length) { |
| 563 | + ElMessage.warning(t('application_spaces.errorPage.downloadNull')) |
| 564 | + return |
| 565 | + } |
523 | 566 |
|
524 | | - const logElements = targetDiv.value.querySelectorAll('p') |
525 | | - let logContent = '' |
526 | | - logElements.forEach((element) => { |
527 | | - logContent += element.textContent + '\n' |
528 | | - }) |
| 567 | + // 时间格式转换函数 |
| 568 | + const formatTime = (timeStr) => { |
| 569 | + if (!timeStr) return '' |
| 570 | + |
| 571 | + // 处理秒格式 (如 "03s") |
| 572 | + if (/^\d+s$/.test(timeStr)) { |
| 573 | + const seconds = timeStr.replace('s', '').padStart(2, '0') |
| 574 | + return `00:${seconds}` |
| 575 | + } |
| 576 | + |
| 577 | + // 处理纯数字格式 (如 "0107") |
| 578 | + if (/^\d{4}$/.test(timeStr)) { |
| 579 | + return `${timeStr.slice(0, 2)}:${timeStr.slice(2)}` |
| 580 | + } |
| 581 | + |
| 582 | + return timeStr |
| 583 | + } |
529 | 584 |
|
530 | | - const blob = new Blob([logContent], { type: 'text/plain' }) |
531 | | - const link = document.createElement('a') |
532 | | - link.href = URL.createObjectURL(blob) |
533 | | - link.download = isBuildLogTab.value ? 'build_log.txt' : 'container_log.txt' |
534 | | - link.click() |
535 | | - URL.revokeObjectURL(link.href) |
536 | | - } |
| 585 | + const logContent = logBlocks.value.map(block => { |
| 586 | + // 转换时间格式 [0004] → [00:04] |
| 587 | + const timeHeader = block.time ? `[${formatTime(block.time)}]\n` : '' |
| 588 | + return timeHeader + block.lines.map(line => { |
| 589 | + const tempDiv = document.createElement('div') |
| 590 | + tempDiv.innerHTML = line |
| 591 | + let plainText = tempDiv.textContent || tempDiv.innerText || '' |
| 592 | + |
| 593 | + // 1. 日志级别后添加空格 (INFO/WARN/ERROR等) |
| 594 | + plainText = plainText.replace(/^(INFO|WARN|ERROR|DEBUG|TRACE|FATAL|NOTICE|WARNING)([^\s])/, '$1 $2') |
| 595 | + |
| 596 | + // 2. 移除ANSI颜色代码 |
| 597 | + plainText = plainText.replace(/\x1b\[[0-9;]*m/g, '') |
| 598 | + |
| 599 | + // 3. 处理换行符 (\r → \n) |
| 600 | + plainText = plainText.replace(/\\r/g, '\n') |
| 601 | + |
| 602 | + // 4. 处理换行符 (\n → 实际换行) |
| 603 | + plainText = plainText.replace(/\\n/g, '\n') |
| 604 | + |
| 605 | + return plainText |
| 606 | + }).join('\n') |
| 607 | + }).join('\n\n') |
| 608 | +
|
| 609 | + try { |
| 610 | + const blob = new Blob([logContent], { type: 'text/plain;charset=utf-8' }) |
| 611 | + const link = document.createElement('a') |
| 612 | + link.href = URL.createObjectURL(blob) |
| 613 | + link.download = `${props.repoName}_${isBuildLogTab.value ? 'build' : 'container'}_${new Date().toISOString().slice(0,10)}.log` |
| 614 | + |
| 615 | + document.body.appendChild(link) |
| 616 | + link.click() |
| 617 | + |
| 618 | + setTimeout(() => { |
| 619 | + document.body.removeChild(link) |
| 620 | + URL.revokeObjectURL(link.href) |
| 621 | + }, 100) |
| 622 | + } catch (err) { |
| 623 | + console.error('日志下载失败:', err) |
| 624 | + ElMessage.error(t('application_spaces.errorPage.downloadError')) |
| 625 | + } |
| 626 | +} |
537 | 627 |
|
538 | 628 | const syncSpaceStatus = () => { |
539 | 629 | fetchEventSource( |
|
0 commit comments