diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 40bb129..5045252 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -1,335 +1,70 @@ -name: Quality CI +name: CI on: push: - branches: [ main, develop ] + branches: [main, develop] pull_request: - branches: [ '**' ] + branches: ['**'] env: - QT_VERSION: '6.5.3' + QT_VERSION: '5.14.2' jobs: - static-analysis: - name: Static Analysis (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - os: windows-latest - job_type: full - - os: ubuntu-latest - job_type: full + lint: + name: Lint + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Install Qt - if: matrix.os == 'windows-latest' || matrix.os == 'ubuntu-latest' - uses: jurplel/install-qt-action@v3 - with: - version: ${{ env.QT_VERSION }} - - # Windows 不再使用 MinGW 路线,移除此步骤 - # Windows/Linux依赖安装 - - name: Install dependencies (Windows) - if: matrix.os == 'windows-latest' - run: | - # 增加超时和重试机制 - choco install -y mingw cmake ninja cppcheck llvm --timeout=600 --execution-timeout=600 - echo "C:\ProgramData\chocolatey\bin;C:\tools\mingw64\bin;C:\ProgramData\chocolatey\lib\cppcheck\tools" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - name: Install dependencies (Linux) - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt-get update - sudo apt-get install -y build-essential cmake qt6-base-dev qt6-tools-dev qt6-tools-dev-tools libgl1-mesa-dev libicu-dev - # clang/clang-tidy/clang-format - sudo apt-get install -y clang clang-tidy clang-format-16 || sudo apt-get install -y clang-format - # 创建符号链接确保使用正确版本 - if command -v clang-format-16 >/dev/null 2>&1; then - sudo ln -sf $(which clang-format-16) /usr/local/bin/clang-format - fi - # Windows/Linux CMake构建 - - name: Configure & Build (Windows) - if: matrix.os == 'windows-latest' - run: | - mkdir build - cd build - cmake .. -A x64 -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - cmake --build . --config Release -- -m:2 - - name: Configure & Build (Linux) - if: matrix.os == 'ubuntu-latest' - run: | - mkdir -p build - cd build - cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - cmake --build . -- -j$(nproc || echo 2) - - name: clang-format Check - shell: bash + - name: clang-format check run: | - # 跨平台兼容的clang-format检查 (容错模式 - 版本差异问题) - if command -v clang-format >/dev/null 2>&1; then - echo "clang-format found, checking format..." - echo "clang-format version: $(clang-format --version)" - git ls-files '*.cpp' '*.h' | xargs clang-format --dry-run || echo "Format issues detected but not failing CI due to version differences" - else - echo "Warning: clang-format not found, skipping format check" - fi + sudo apt-get update && sudo apt-get install -y clang-format + git ls-files '*.cpp' '*.h' | xargs clang-format --dry-run --Werror || true - - name: clang-tidy (fast rules) - shell: bash + - name: cppcheck run: | - if command -v clang-tidy >/dev/null 2>&1; then - echo "Running clang-tidy with fast rules..." - clang-tidy -p build --config-file .clang-tidy.fast $(git ls-files '*.cpp') > clang-tidy-fast.txt || true - else - echo "Warning: clang-tidy not found, skipping analysis" - fi + sudo apt-get install -y cppcheck + cppcheck --enable=warning,style --std=c++17 --language=c++ \ + --suppress=missingInclude --suppress=unusedFunction \ + --quiet --error-exitcode=1 src/ || true - - name: Upload clang-tidy fast report - uses: actions/upload-artifact@v4 - with: - name: clang-tidy-fast-${{ matrix.os }} - path: clang-tidy-fast.txt - if-no-files-found: warn - - - name: clang-tidy (quality rules) - shell: bash - run: | - if command -v clang-tidy >/dev/null 2>&1; then - echo "Running clang-tidy with quality rules..." - clang-tidy -p build --config-file .clang-tidy.quality $(git ls-files '*.cpp') > clang-tidy-quality.txt || true - else - echo "Warning: clang-tidy not found, skipping analysis" - fi - - - name: Upload clang-tidy quality report - uses: actions/upload-artifact@v4 - with: - name: clang-tidy-quality-${{ matrix.os }} - path: clang-tidy-quality.txt - if-no-files-found: warn - - - name: cppcheck Analysis - shell: bash - run: | - # 跨平台兼容的cppcheck检查 - if command -v cppcheck >/dev/null 2>&1; then - bash scripts/run_cppcheck.sh build || true - else - echo "Warning: cppcheck not found, skipping analysis" - fi - - # 预留动态分析及覆盖率任务 - # sanitizers: - # ... - - # sanitizers job - sanitizers: - name: Sanitizers (${{ matrix.os }}) + build: + name: Build (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: + fail-fast: true matrix: os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 - - # Install Qt - - name: Install Qt - uses: jurplel/install-qt-action@v3 - with: - version: ${{ env.QT_VERSION }} - - - name: Install dependencies (Windows) - if: matrix.os == 'windows-latest' - run: | - # 增加超时和重试机制 - choco install -y mingw cmake ninja cppcheck --timeout=600 --execution-timeout=600 - echo "C:\ProgramData\chocolatey\bin;C:\tools\mingw64\bin;C:\ProgramData\chocolatey\lib\cppcheck\tools" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - name: Install dependencies (Linux) - if: runner.os == 'Linux' - run: | - sudo apt update - sudo apt install -y clang clang-tidy lcov - - - name: Install dependencies (macOS) - if: runner.os == 'macOS' - run: | - brew update || true - brew install llvm lcov || true - echo "/opt/homebrew/bin" >> $GITHUB_PATH - echo "/opt/homebrew/opt/llvm/bin" >> $GITHUB_PATH - - - name: Configure with Sanitizers (Linux) - if: runner.os == 'Linux' - run: | - mkdir -p build - cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_C_FLAGS="-fsanitize=address,undefined" \ - -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ - -DENABLE_TESTING=ON - - - name: Configure with Debug (Windows) - if: runner.os == 'Windows' - run: | - mkdir build - cd build - cmake .. -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DENABLE_TESTING=ON - - - name: Build & Tests (Linux) - if: runner.os == 'Linux' - run: | - cd build - make -j$(nproc) - # 运行测试(如果存在) - if [ -f CTestTestfile.cmake ]; then - ctest --output-on-failure || true - else - echo "No tests configured, skipping test execution" - fi - - - name: Build & Tests (Windows) - if: runner.os == 'Windows' - run: | - cd build - cmake --build . -- -j2 - # 运行测试(如果存在) - if (Test-Path "CTestTestfile.cmake") { - Write-Host "Running tests..." - ctest --output-on-failure || Write-Host "Some tests failed but continuing CI" - } else { - Write-Host "No tests configured, skipping test execution" - } - - - name: clang-tidy (fast rules) [sanitizers] - shell: bash - run: | - if command -v clang-tidy >/dev/null 2>&1; then - echo "Running clang-tidy (sanitizers) with fast rules..." - clang-tidy -p build --config-file .clang-tidy.fast $(git ls-files '*.cpp') > clang-tidy-fast-sanitizers.txt || true - else - echo "Warning: clang-tidy not found, skipping analysis" - fi - - - name: Upload clang-tidy fast report [sanitizers] - uses: actions/upload-artifact@v4 - with: - name: clang-tidy-fast-sanitizers-${{ matrix.os }} - path: clang-tidy-fast-sanitizers.txt - if-no-files-found: warn - - - name: clang-tidy (quality rules) [sanitizers] - shell: bash - run: | - if command -v clang-tidy >/dev/null 2>&1; then - echo "Running clang-tidy (sanitizers) with quality rules..." - clang-tidy -p build --config-file .clang-tidy.quality $(git ls-files '*.cpp') > clang-tidy-quality-sanitizers.txt || true - else - echo "Warning: clang-tidy not found, skipping analysis" - fi - - - name: Upload clang-tidy quality report [sanitizers] - uses: actions/upload-artifact@v4 - with: - name: clang-tidy-quality-sanitizers-${{ matrix.os }} - path: clang-tidy-quality-sanitizers.txt - if-no-files-found: warn - coverage: - name: Coverage (Ubuntu) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - # Install Qt - name: Install Qt uses: jurplel/install-qt-action@v3 with: version: ${{ env.QT_VERSION }} - - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y lcov gcc g++ clang clang-tidy - - - name: Configure coverage - run: | - mkdir -p build - cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug \ - -DENABLE_CODE_COVERAGE=ON \ - -DENABLE_TESTING=ON \ - -DENABLE_COVERAGE=ON \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ - -DCMAKE_C_FLAGS="--coverage -fprofile-arcs -ftest-coverage" \ - -DCMAKE_CXX_FLAGS="--coverage -fprofile-arcs -ftest-coverage" - - - name: Build & Tests - run: | - cd build - make -j$(nproc) - # 运行测试以生成coverage数据 - if [ -f CTestTestfile.cmake ]; then - ctest --output-on-failure || true - else - echo "No tests configured, creating dummy coverage data" - # 运行主程序生成一些coverage数据 - timeout 5s ./LogReader --help || true - fi + arch: ${{ matrix.os == 'windows-latest' && 'win64_msvc2017_64' || '' }} - - name: clang-tidy (fast rules) [coverage] + - name: Install build tools shell: bash run: | - if command -v clang-tidy >/dev/null 2>&1; then - echo "Running clang-tidy (coverage) with fast rules..." - clang-tidy -p build --config-file .clang-tidy.fast $(git ls-files '*.cpp') > clang-tidy-fast-coverage.txt || true + if [[ "$RUNNER_OS" == "Linux" ]]; then + sudo apt-get update + sudo apt-get install -y ninja-build libgl1-mesa-dev libicu-dev else - echo "Warning: clang-tidy not found, skipping analysis" + choco install -y ninja fi - - name: Upload clang-tidy fast report [coverage] - uses: actions/upload-artifact@v4 - with: - name: clang-tidy-fast-coverage-${{ runner.os }} - path: clang-tidy-fast-coverage.txt - if-no-files-found: warn + - name: Setup MSVC + if: matrix.os == 'windows-latest' + uses: ilammy/msvc-dev-cmd@v1 - - name: clang-tidy (quality rules) [coverage] - shell: bash - run: | - if command -v clang-tidy >/dev/null 2>&1; then - echo "Running clang-tidy (coverage) with quality rules..." - clang-tidy -p build --config-file .clang-tidy.quality $(git ls-files '*.cpp') > clang-tidy-quality-coverage.txt || true - else - echo "Warning: clang-tidy not found, skipping analysis" - fi + - name: Configure + run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_TESTING=ON - - name: Upload clang-tidy quality report [coverage] - uses: actions/upload-artifact@v4 - with: - name: clang-tidy-quality-coverage-${{ runner.os }} - path: clang-tidy-quality-coverage.txt - if-no-files-found: warn - - - name: Generate lcov report - run: | - cd build - # 确保有coverage数据 - if [ -n "$(find . -name '*.gcda' -print -quit)" ]; then - lcov --capture --directory . --output-file coverage.info - lcov --remove coverage.info '/usr/*' '*/tests/*' '*/build/*' --output-file coverage.info - lcov --list coverage.info - else - echo "No coverage data found (.gcda files), creating empty report" - lcov --capture --directory . --output-file coverage.info --ignore-errors empty || true - echo "Coverage report created with available data" - fi - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - files: build/coverage.info - fail_ci_if_error: false - verbose: true \ No newline at end of file + - name: Build + run: cmake --build build + + - name: Test + env: + QT_QPA_PLATFORM: offscreen + run: cd build && ctest --output-on-failure --verbose diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1121264..1bddc35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,351 +1,170 @@ -name: Release Build +name: Release on: push: - tags: - - 'v*' + tags: ['v*'] workflow_dispatch: permissions: contents: write - actions: read env: - QT_VERSION: '5.15.2' + QT_VERSION: '5.14.2' jobs: - # Windows构建任务 build-windows: - name: Build Windows + name: Windows runs-on: windows-latest - continue-on-error: true - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install MinGW - run: | - choco install mingw --version 8.1.0 --allow-downgrade --force - echo "C:\\tools\\mingw64\\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - - name: Install Qt - uses: jurplel/install-qt-action@v3 - with: - version: ${{ env.QT_VERSION }} - arch: win64_mingw81 - - - name: Compile translations - run: | - Get-ChildItem "translations\*.ts" | ForEach-Object { - Write-Host "Compiling $($_.Name)" - lrelease $_.FullName - } - - - name: Build - run: | - mkdir build - cd build - cmake .. -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release - mingw32-make -j4 - - - name: Deploy Qt - run: | - cd build - # Fix windeployqt environment - set QT_QPA_PLATFORM_PLUGIN_PATH first - $env:QT_QPA_PLATFORM_PLUGIN_PATH = "$env:Qt5_Dir\plugins\platforms" - $env:PATH = "$env:Qt5_Dir\bin;C:\tools\mingw64\bin;$env:PATH" - $env:QTDIR = "$env:Qt5_Dir" - - Write-Host "Qt5_Dir: $env:Qt5_Dir" - Write-Host "QT_QPA_PLATFORM_PLUGIN_PATH: $env:QT_QPA_PLATFORM_PLUGIN_PATH" - Write-Host "Platform plugin exists: $(Test-Path '$env:Qt5_Dir\plugins\platforms\qwindows.dll')" - - # Use windeployqt with corrected environment - & "$env:Qt5_Dir\bin\windeployqt.exe" --release --compiler-runtime --force --verbose 2 LogReader.exe - - # Copy our translations - Copy-Item "..\translations\*.qm" . -ErrorAction SilentlyContinue - - - name: Package Windows - run: | - # Get version from tag - $VERSION = "${{ github.ref_name }}" - if ($VERSION -like "v*") { - $VERSION = $VERSION.Substring(1) - } - - $PACKAGE = "LogReader-$VERSION-Windows-x64" - New-Item -ItemType Directory $PACKAGE -Force - - cd build - Get-ChildItem . -Exclude "CMakeFiles","*.cmake","CMakeCache.txt","*.o","Makefile" | Copy-Item -Destination "..\$PACKAGE\" -Recurse -Force - - cd .. - Copy-Item "README.md" "$PACKAGE\" -ErrorAction SilentlyContinue - - Compress-Archive $PACKAGE "$PACKAGE.zip" - Write-Host "Created: $PACKAGE.zip" - - - name: Upload Windows Artifact - uses: actions/upload-artifact@v4 - with: - name: windows-build - path: "LogReader-*.zip" - retention-days: 7 + - uses: actions/checkout@v4 + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: ${{ env.QT_VERSION }} + arch: win64_msvc2017_64 + + - name: Install Ninja + run: choco install -y ninja + + - name: Setup MSVC + uses: ilammy/msvc-dev-cmd@v1 + + - name: Build + run: | + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_DEPLOY=ON + cmake --build build + + - name: Package + shell: bash + run: | + VERSION="${GITHUB_REF_NAME#v}" + PKG="LogReader-${VERSION}-Windows-x64" + mkdir -p "$PKG" + + # windeployqt ran via ENABLE_DEPLOY, collect outputs + cp build/LogReader.exe "$PKG/" + cp build/*.dll "$PKG/" 2>/dev/null || true + for d in platforms styles imageformats translations; do + [ -d "build/$d" ] && cp -r "build/$d" "$PKG/$d" + done + cp README.md "$PKG/" 2>/dev/null || true + + 7z a "${PKG}.zip" "$PKG" + + - uses: actions/upload-artifact@v4 + with: + name: windows + path: LogReader-*.zip - # Linux构建任务 build-linux: - name: Build Linux + name: Linux runs-on: ubuntu-latest - continue-on-error: true - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y cmake build-essential - - - name: Install Qt - uses: jurplel/install-qt-action@v3 - with: - version: ${{ env.QT_VERSION }} - arch: gcc_64 - - - name: Compile translations - run: | - for ts_file in translations/*.ts; do - echo "Compiling $ts_file" - lrelease "$ts_file" - done - - - name: Build - run: | - mkdir build - cd build - cmake .. -DCMAKE_BUILD_TYPE=Release - make -j$(nproc) - - - name: Package Linux - run: | - # Get version from tag - VERSION="${{ github.ref_name }}" - if [[ $VERSION == v* ]]; then - VERSION=${VERSION#v} - fi - - PACKAGE="LogReader-$VERSION-Linux-x64" - mkdir -p $PACKAGE - - # Copy executable and translations - cp build/LogReader $PACKAGE/ - cp translations/*.qm $PACKAGE/ 2>/dev/null || true - cp README.md $PACKAGE/ - - # Create simple run script - echo '#!/bin/bash' > $PACKAGE/run.sh - echo 'export QT_QPA_PLATFORM_PLUGIN_PATH="."' >> $PACKAGE/run.sh - echo './LogReader "$@"' >> $PACKAGE/run.sh - chmod +x $PACKAGE/run.sh - - tar -czf $PACKAGE.tar.gz $PACKAGE - echo "Created: $PACKAGE.tar.gz" - - - name: Upload Linux Artifact - uses: actions/upload-artifact@v4 - with: - name: linux-build - path: "LogReader-*.tar.gz" - retention-days: 7 + - uses: actions/checkout@v4 + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: ${{ env.QT_VERSION }} + + - name: Install deps + run: sudo apt-get update && sudo apt-get install -y ninja-build libgl1-mesa-dev libicu-dev + + - name: Build + run: | + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_DEPLOY=ON + cmake --build build + + - name: Package + run: | + VERSION="${GITHUB_REF_NAME#v}" + PKG="LogReader-${VERSION}-Linux-x64" + mkdir -p "$PKG" + cp build/LogReader "$PKG/" + cp -r build/translations "$PKG/" 2>/dev/null || true + cp README.md "$PKG/" 2>/dev/null || true + printf '#!/bin/bash\ndir="$(cd "$(dirname "$0")" && pwd)"\nexport QT_QPA_PLATFORM_PLUGIN_PATH="$dir"\n./LogReader "$@"\n' > "$PKG/run.sh" + chmod +x "$PKG/run.sh" + tar czf "${PKG}.tar.gz" "$PKG" + + - uses: actions/upload-artifact@v4 + with: + name: linux + path: LogReader-*.tar.gz - # macOS构建任务 build-macos: - name: Build macOS - runs-on: macos-latest - continue-on-error: true - + name: macOS + runs-on: macos-13 steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Qt - uses: jurplel/install-qt-action@v3 - with: - version: ${{ env.QT_VERSION }} - arch: clang_64 - - - name: Compile translations - run: | - for ts_file in translations/*.ts; do - echo "Compiling $ts_file" - lrelease "$ts_file" - done - - - name: Build - run: | - mkdir build - cd build - cmake .. -DCMAKE_BUILD_TYPE=Release - make -j$(sysctl -n hw.ncpu) - - - name: Create App Bundle and Deploy Qt - run: | - cd build - - # Check if LogReader.app already exists - if [ ! -d "LogReader.app" ]; then - echo "Creating LogReader.app bundle..." + - uses: actions/checkout@v4 + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: ${{ env.QT_VERSION }} + + - name: Build + run: | + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_DEPLOY=ON + cmake --build build + + - name: Create app bundle + run: | + cd build mkdir -p LogReader.app/Contents/{MacOS,Resources} - - # Create Info.plist using echo - echo '' > LogReader.app/Contents/Info.plist - echo '' >> LogReader.app/Contents/Info.plist - echo '' >> LogReader.app/Contents/Info.plist - echo '' >> LogReader.app/Contents/Info.plist - echo ' CFBundleExecutable' >> LogReader.app/Contents/Info.plist - echo ' LogReader' >> LogReader.app/Contents/Info.plist - echo ' CFBundleIdentifier' >> LogReader.app/Contents/Info.plist - echo ' com.example.LogReader' >> LogReader.app/Contents/Info.plist - echo ' CFBundleName' >> LogReader.app/Contents/Info.plist - echo ' LogReader' >> LogReader.app/Contents/Info.plist - echo ' CFBundleVersion' >> LogReader.app/Contents/Info.plist - echo ' 1.0' >> LogReader.app/Contents/Info.plist - echo ' CFBundleShortVersionString' >> LogReader.app/Contents/Info.plist - echo ' 1.0' >> LogReader.app/Contents/Info.plist - echo ' CFBundlePackageType' >> LogReader.app/Contents/Info.plist - echo ' APPL' >> LogReader.app/Contents/Info.plist - echo '' >> LogReader.app/Contents/Info.plist - echo '' >> LogReader.app/Contents/Info.plist - - # Copy executable - if [ -f "LogReader" ]; then - cp LogReader LogReader.app/Contents/MacOS/ - else - echo "Error: LogReader executable not found" - ls -la - exit 1 - fi - + cp LogReader LogReader.app/Contents/MacOS/ chmod +x LogReader.app/Contents/MacOS/LogReader - fi - - # Deploy Qt - macdeployqt LogReader.app - cp ../translations/*.qm LogReader.app/Contents/Resources/ 2>/dev/null || true - - - name: Package macOS - run: | - # Get version from tag - VERSION="${{ github.ref_name }}" - if [[ $VERSION == v* ]]; then - VERSION=${VERSION#v} - fi - - PACKAGE="LogReader-$VERSION-macOS-universal" - mkdir -p $PACKAGE - - cp -R build/LogReader.app $PACKAGE/ - cp README.md $PACKAGE/ - - # Create DMG using built-in tools - hdiutil create -srcfolder $PACKAGE -volname "LogReader $VERSION" $PACKAGE.dmg - echo "Created: $PACKAGE.dmg" - - - name: Upload macOS Artifact - uses: actions/upload-artifact@v4 - with: - name: macos-build - path: "LogReader-*.dmg" - retention-days: 7 + cat > LogReader.app/Contents/Info.plist << 'EOF' + + + + CFBundleExecutableLogReader + CFBundleIdentifiercom.gezip.LogReader + CFBundleNameLogReader + CFBundlePackageTypeAPPL + CFBundleVersion1.0 + NSHighResolutionCapable + + EOF + macdeployqt LogReader.app + cp ../translations/*.qm LogReader.app/Contents/Resources/ 2>/dev/null || true - # 创建Release任务 - create-release: - name: Create Release + - name: Package + run: | + VERSION="${GITHUB_REF_NAME#v}" + PKG="LogReader-${VERSION}-macOS-x64" + mkdir -p "$PKG" + cp -R build/LogReader.app "$PKG/" + cp README.md "$PKG/" 2>/dev/null || true + hdiutil create -srcfolder "$PKG" -volname "LogReader $VERSION" "${PKG}.dmg" + + - uses: actions/upload-artifact@v4 + with: + name: macos + path: LogReader-*.dmg + + release: + name: Publish Release runs-on: ubuntu-latest needs: [build-windows, build-linux, build-macos] if: always() && (needs.build-windows.result == 'success' || needs.build-linux.result == 'success' || needs.build-macos.result == 'success') - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Prepare release assets - run: | - mkdir -p release-assets - find artifacts -name "*.zip" -o -name "*.tar.gz" -o -name "*.dmg" | while read file; do - echo "Found asset: $file" - cp "$file" release-assets/ - done - - ls -la release-assets/ - - - name: Generate release notes - run: | - # Get version from tag - VERSION="${{ github.ref_name }}" - if [[ $VERSION == v* ]]; then - VERSION=${VERSION#v} - fi - - # Create release notes using echo - echo "# LogReader $VERSION" > release-notes.md - echo "" >> release-notes.md - echo "## 📦 可用平台" >> release-notes.md - - # Check which platforms are available - if find artifacts/windows-build -name "*.zip" 2>/dev/null | grep -q .; then - echo "- ✅ **Windows** - 便携版ZIP包" >> release-notes.md - else - echo "- ❌ **Windows** - 构建失败" >> release-notes.md - fi - - if find artifacts/linux-build -name "*.tar.gz" 2>/dev/null | grep -q .; then - echo "- ✅ **Linux** - tar.gz压缩包" >> release-notes.md - else - echo "- ❌ **Linux** - 构建失败" >> release-notes.md - fi - - if find artifacts/macos-build -name "*.dmg" 2>/dev/null | grep -q .; then - echo "- ✅ **macOS** - DMG安装包" >> release-notes.md - else - echo "- ❌ **macOS** - 构建失败" >> release-notes.md - fi - - # Add usage instructions - echo "" >> release-notes.md - echo "## 🚀 如何使用" >> release-notes.md - echo "" >> release-notes.md - echo "1. 根据您的操作系统下载对应的安装包" >> release-notes.md - echo "2. Windows: 解压ZIP文件,运行LogReader.exe" >> release-notes.md - echo "3. Linux: 解压tar.gz文件,运行./run.sh" >> release-notes.md - echo "4. macOS: 打开DMG文件,拖拽LogReader.app到应用程序文件夹" >> release-notes.md - echo "" >> release-notes.md - echo "## 📝 更新内容" >> release-notes.md - echo "" >> release-notes.md - echo "详见提交记录中的更改。" >> release-notes.md - echo "" >> release-notes.md - echo "## 🐛 问题反馈" >> release-notes.md - echo "" >> release-notes.md - echo "如遇到问题,请在GitHub Issues中反馈。" >> release-notes.md - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.ref_name }} - name: LogReader ${{ github.ref_name }} - body_path: release-notes.md - files: release-assets/* - draft: false - prerelease: false - fail_on_unmatched_files: false - generate_release_notes: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Collect assets + run: | + mkdir assets + find artifacts -name "*.zip" -o -name "*.tar.gz" -o -name "*.dmg" | xargs -I{} cp {} assets/ + ls -la assets/ + + - name: Release + uses: softprops/action-gh-release@v2 + with: + files: assets/* + generate_release_notes: true + fail_on_unmatched_files: false diff --git a/CMakeLists.txt b/CMakeLists.txt index b5b289b..86e7527 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,11 @@ else() target_link_libraries(LogReader Qt5::Core Qt5::Gui Qt5::Widgets) endif() +# MSVC: treat source files as UTF-8 +if(MSVC) + target_compile_options(LogReader PRIVATE /utf-8) +endif() + # Release构建:禁用调试输出并启用更激进的优化定义 # 为所有编译器在Release模式下禁用调试输出 target_compile_definitions(LogReader PRIVATE diff --git a/LogReader.pro b/LogReader.pro index d21d0b2..30a015c 100644 --- a/LogReader.pro +++ b/LogReader.pro @@ -2,7 +2,7 @@ QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets -CONFIG += c++11 +CONFIG += c++17 # 隐藏控制台窗口 (Windows) win32 { diff --git a/src/core/logentry.h b/src/core/logentry.h index 86075c9..f85b0bd 100644 --- a/src/core/logentry.h +++ b/src/core/logentry.h @@ -14,8 +14,8 @@ #define LOGENTRY_H #include -#include #include +#include /** * @struct LogEntry @@ -74,6 +74,12 @@ struct LogEntry * content that users search through and analyze. */ QString message; + + bool operator==(const LogEntry& other) const + { + return timestamp == other.timestamp && level == other.level && + module == other.module && message == other.message; + } }; // Declare metatype for queued signal/slot usage across threads diff --git a/src/core/logexporter.cpp b/src/core/logexporter.cpp index 9905c2f..ffa79ba 100644 --- a/src/core/logexporter.cpp +++ b/src/core/logexporter.cpp @@ -207,15 +207,13 @@ bool LogExporter::exportToTxt(const QList& logs, return false; } - // Write UTF-8 BOM to ensure Windows correctly recognizes encoding - file.write("\xEF\xBB\xBF"); - QTextStream out(&file); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) out.setCodec("UTF-8"); #else out.setEncoding(QStringConverter::Utf8); #endif + out.setGenerateByteOrderMark(true); // Write each log entry as a formatted line for (int i = 0; i < logs.size(); ++i) { @@ -223,7 +221,13 @@ bool LogExporter::exportToTxt(const QList& logs, QString line = formatLogEntry(entry, config, ExportConfig::TXT); out << line << "\n"; - emitProgress(i + 1, logs.size()); + if (i % 1000 == 0 || i == logs.size() - 1) + emitProgress(i + 1, logs.size()); + } + + if (out.status() != QTextStream::Ok) { + emit exportFinished(false, tr("写入文件失败: %1").arg(filePath)); + return false; } return true; @@ -249,26 +253,24 @@ bool LogExporter::exportToCsv(const QList& logs, return false; } - // Write UTF-8 BOM to ensure Windows correctly recognizes encoding - file.write("\xEF\xBB\xBF"); - QTextStream out(&file); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) out.setCodec("UTF-8"); #else out.setEncoding(QStringConverter::Utf8); #endif + out.setGenerateByteOrderMark(true); // Write CSV header row based on included fields QStringList headers; if (config.includeTimestamp) - headers << "时间戳"; + headers << QObject::tr("时间戳"); if (config.includeLevel) - headers << "日志等级"; + headers << QObject::tr("日志等级"); if (config.includeModule) - headers << "模块"; + headers << QObject::tr("模块"); if (config.includeContent) - headers << "内容"; + headers << QObject::tr("内容"); out << headers.join(",") << "\n"; @@ -278,7 +280,13 @@ bool LogExporter::exportToCsv(const QList& logs, QString line = formatLogEntry(entry, config, ExportConfig::CSV); out << line << "\n"; - emitProgress(i + 1, logs.size()); + if (i % 1000 == 0 || i == logs.size() - 1) + emitProgress(i + 1, logs.size()); + } + + if (out.status() != QTextStream::Ok) { + emit exportFinished(false, tr("写入文件失败: %1").arg(filePath)); + return false; } return true; @@ -298,45 +306,71 @@ bool LogExporter::exportToJson(const QList& logs, const ExportConfig& config, const QString& filePath) { - QJsonArray logArray; + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + emit exportFinished(false, tr("无法创建文件: %1").arg(filePath)); + return false; + } - // Convert each log entry to JSON object + // JSON must NOT have BOM per RFC 8259 + QTextStream out(&file); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + out.setCodec("UTF-8"); +#else + out.setEncoding(QStringConverter::Utf8); +#endif + + // Stream JSON array to avoid building entire document in memory + out << "[\n"; for (int i = 0; i < logs.size(); ++i) { const LogEntry& entry = logs[i]; - QJsonObject logObject; - // Add fields based on configuration + if (i > 0) + out << ",\n"; + out << " {\n"; + + // Wrap in QJsonArray to get proper JSON string escaping + auto jsonEscape = [](const QString& s) -> QString { + QJsonArray arr; + arr.append(s); + QString json = QJsonDocument(arr).toJson(QJsonDocument::Compact); + return json.mid(1, json.size() - 2); // strip [ and ] + }; + + bool first = true; + auto writeField = [&](const QString& key, const QString& value) { + if (!first) + out << ",\n"; + first = false; + out << " " << jsonEscape(key) << ": " << jsonEscape(value); + }; + if (config.includeTimestamp) { - logObject["timestamp"] = - entry.timestamp.toString("yyyy-MM-dd HH:mm:ss.zzz"); + writeField("timestamp", + entry.timestamp.toString("yyyy-MM-dd HH:mm:ss.zzz")); } if (config.includeLevel) { - logObject["level"] = entry.level; + writeField("level", entry.level); } if (config.includeModule) { - logObject["module"] = entry.module; + writeField("module", entry.module); } if (config.includeContent) { - logObject["content"] = entry.message; + writeField("content", entry.message); } - logArray.append(logObject); - emitProgress(i + 1, logs.size()); - } + out << "\n }"; - // Write JSON document to file - QJsonDocument doc(logArray); + if (i % 1000 == 0 || i == logs.size() - 1) + emitProgress(i + 1, logs.size()); + } + out << "\n]\n"; - QFile file(filePath); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - emit exportFinished(false, tr("无法创建文件: %1").arg(filePath)); + if (out.status() != QTextStream::Ok) { + emit exportFinished(false, tr("写入文件失败: %1").arg(filePath)); return false; } - // Write UTF-8 BOM for consistency - file.write("\xEF\xBB\xBF"); - file.write(doc.toJson(QJsonDocument::Indented)); - return true; } @@ -420,21 +454,6 @@ QString LogExporter::escapeForCsv(const QString& field) return escaped; } -/** - * @brief Escape special characters for JSON format - * @param field Text field to escape - * @return Properly escaped text safe for JSON - * @details Handles JSON escaping according to JSON standards: - * - Escapes quotes, backslashes, and control characters - * - Note: Qt's JSON classes handle this automatically - */ -QString LogExporter::escapeForJson(const QString& field) -{ - // Qt's JSON classes handle escaping automatically - // This method is provided for completeness and future use - return field; -} - /** * @brief Emit progress signal with calculated percentage * @param current Current item being processed (1-based) diff --git a/src/core/logexporter.h b/src/core/logexporter.h index 071cd69..6da3766 100644 --- a/src/core/logexporter.h +++ b/src/core/logexporter.h @@ -280,15 +280,6 @@ class LogExporter : public QObject */ QString escapeForCsv(const QString& field); - /** - * @brief Escape special characters for JSON format - * @param field Text field to escape - * @return Properly escaped text safe for JSON format - * @details Handles quote, backslash, and control character escaping - * according to JSON standards, ensuring valid JSON output. - */ - QString escapeForJson(const QString& field); - /** * @brief Emit progress signal with calculated percentage * @param current Current item being processed diff --git a/src/core/logloader.cpp b/src/core/logloader.cpp index 364d953..35d6868 100644 --- a/src/core/logloader.cpp +++ b/src/core/logloader.cpp @@ -10,8 +10,12 @@ #include #endif -LogLoader::LogLoader(const QString& filePath, const QString& encoding, int chunkSize, QObject* parent) - : QObject(parent), m_filePath(filePath), m_encoding(encoding), m_chunkSize(chunkSize) +LogLoader::LogLoader(const QString& filePath, const QString& encoding, + int chunkSize, QObject* parent) + : QObject(parent), + m_filePath(filePath), + m_encoding(encoding), + m_chunkSize(chunkSize) { } @@ -19,7 +23,7 @@ void LogLoader::process() { QFile file(m_filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - emit error(QObject::tr("无法打开日志文件。")); + emit error(QObject::tr("无法打开日志文件:%1").arg(m_filePath)); emit finished(); return; } @@ -38,13 +42,15 @@ void LogLoader::process() if (enc.has_value()) { in.setEncoding(*enc); } else { - emit error(QObject::tr("不支持的编码格式:%1,已回退为UTF-8").arg(m_encoding)); + emit error( + QObject::tr("不支持的编码格式:%1,已回退为UTF-8").arg(m_encoding)); in.setEncoding(QStringConverter::Utf8); } #endif // More tolerant spacing: allow variable spaces around tokens and colon - QRegularExpression regex(R"((?:\[\s*(.*?)\s*\])\s*(?:\[\s*(.*?)\s*\])\s*(?:\[\s*(.*?)\s*\])\s*:\s*(.*)$)"); + QRegularExpression regex( + R"((?:\[\s*(.*?)\s*\])\s*(?:\[\s*(.*?)\s*\])\s*(?:\[\s*(.*?)\s*\])\s*:\s*(.*)$)"); regex.optimize(); QVector buffer; buffer.reserve(m_chunkSize); @@ -69,9 +75,11 @@ void LogLoader::process() QRegularExpressionMatch match = regex.match(line); if (match.hasMatch()) { LogEntry entry; - entry.timestamp = QDateTime::fromString(match.captured(1), "yyyy-MM-dd HH:mm:ss.zzz"); + entry.timestamp = QDateTime::fromString(match.captured(1), + "yyyy-MM-dd HH:mm:ss.zzz"); if (!entry.timestamp.isValid()) { - entry.timestamp = QDateTime::fromString(match.captured(1), "yyyy-MM-dd HH:mm:ss"); + entry.timestamp = QDateTime::fromString(match.captured(1), + "yyyy-MM-dd HH:mm:ss"); } entry.level = match.captured(2).trimmed(); entry.module = match.captured(3).trimmed(); @@ -82,8 +90,10 @@ void LogLoader::process() minTime = maxTime = entry.timestamp; hasTime = true; } else { - if (entry.timestamp < minTime) minTime = entry.timestamp; - if (entry.timestamp > maxTime) maxTime = entry.timestamp; + if (entry.timestamp < minTime) + minTime = entry.timestamp; + if (entry.timestamp > maxTime) + maxTime = entry.timestamp; } } modulesSet.insert(entry.module); @@ -95,7 +105,8 @@ void LogLoader::process() buffer.clear(); // 仅在块处理完成时计算和发射进度,减少计算频率 if (totalBytes > 0) { - int percent = static_cast((processedBytes * 100) / totalBytes); + int percent = + static_cast((processedBytes * 100) / totalBytes); emit progress(percent); } } @@ -118,5 +129,3 @@ void LogLoader::process() emit progress(100); emit finished(); } - - diff --git a/src/core/logloader.h b/src/core/logloader.h index d32a923..099a184 100644 --- a/src/core/logloader.h +++ b/src/core/logloader.h @@ -1,12 +1,12 @@ #ifndef LOGLOADER_H #define LOGLOADER_H -#include #include #include -#include +#include #include #include +#include #include "logentry.h" @@ -19,10 +19,8 @@ class LogLoader : public QObject Q_OBJECT public: - explicit LogLoader(const QString& filePath, - const QString& encoding, - int chunkSize = 5000, - QObject* parent = nullptr); + explicit LogLoader(const QString& filePath, const QString& encoding, + int chunkSize = 5000, QObject* parent = nullptr); public slots: void process(); @@ -30,10 +28,8 @@ public slots: signals: void chunkReady(QVector chunk); void progress(int percentage); - void summaryReady(const QDateTime& minTime, - const QDateTime& maxTime, - const QStringList& modules, - const QStringList& levels); + void summaryReady(const QDateTime& minTime, const QDateTime& maxTime, + const QStringList& modules, const QStringList& levels); void finished(); void error(const QString& message); @@ -44,5 +40,3 @@ public slots: }; #endif // LOGLOADER_H - - diff --git a/src/main.cpp b/src/main.cpp index 1d687de..8e26cba 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,10 +13,12 @@ #include #include #include +#include #include #include #include +#include "core/logentry.h" #include "ui/logviewer.h" #include "utils/languagemanager.h" @@ -65,6 +67,10 @@ int main(int argc, char* argv[]) qDebug() << "Initializing language manager..."; #endif + // Register metatypes for cross-thread signal-slot connections + qRegisterMetaType("LogEntry"); + qRegisterMetaType>("QVector"); + // Initialize language management system LanguageManager::instance().initialize(); diff --git a/src/ui/logtablemodel.cpp b/src/ui/logtablemodel.cpp index 62851e1..e26c08f 100644 --- a/src/ui/logtablemodel.cpp +++ b/src/ui/logtablemodel.cpp @@ -102,6 +102,15 @@ void LogTableModel::appendRows(const QVector& rows) endInsertRows(); } +void LogTableModel::appendRows(QVector&& rows) +{ + if (rows.isEmpty()) + return; + beginInsertRows(QModelIndex(), m_entries.size(), m_entries.size() + rows.size() - 1); + m_entries += rows; // Qt containers handle move when source is rvalue + endInsertRows(); +} + const LogEntry& LogTableModel::at(int row) const { return m_entries.at(row); diff --git a/src/ui/logtablemodel.h b/src/ui/logtablemodel.h index decb55e..d8f95cc 100644 --- a/src/ui/logtablemodel.h +++ b/src/ui/logtablemodel.h @@ -49,6 +49,7 @@ class LogTableModel : public QAbstractTableModel // Data operations void clear(); void appendRows(const QVector& rows); + void appendRows(QVector&& rows); const LogEntry& at(int row) const; int size() const { return m_entries.size(); } diff --git a/src/ui/logviewer.cpp b/src/ui/logviewer.cpp index bde00ee..32dd9ec 100644 --- a/src/ui/logviewer.cpp +++ b/src/ui/logviewer.cpp @@ -32,17 +32,18 @@ #include #include #include +#include +#include #include #include #include #include "exportdialog.h" +#include "highlightdelegate.h" #include "logfilterproxymodel.h" #include "logtablemodel.h" -#include "highlightdelegate.h" + #include "../core/logloader.h" -#include -#include #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include #else @@ -54,7 +55,6 @@ #include #include #include -#include #include #include #include @@ -64,7 +64,9 @@ #include #include #include +#include #include +#include /** * @brief Constructor for LogViewer main window @@ -75,15 +77,18 @@ * functionality. */ LogViewer::LogViewer(QWidget* parent) - : QMainWindow(parent), sourceModel(nullptr), proxyModel(nullptr), highlightDelegate(nullptr), currentSearchIndex(-1) + : QMainWindow(parent), + sourceModel(nullptr), + proxyModel(nullptr), + highlightDelegate(nullptr), + currentSearchIndex(-1), + searchDebounceTimer(nullptr) { setupUI(); - // Set up language manager callback function for dynamic UI updates - LanguageManager::instance().setLanguageChangeCallback( - [this](LanguageManager::Language language) { - this->onLanguageManagerChanged(language); - }); + // Connect language manager signal for dynamic UI updates + connect(&LanguageManager::instance(), &LanguageManager::languageChanged, + this, &LogViewer::onLanguageManagerChanged); } /** @@ -169,7 +174,7 @@ void LogViewer::setupUI() } )"; - qApp->setStyleSheet(qss); + this->setStyleSheet(qss); QWidget* centralWidget = new QWidget(this); setCentralWidget(centralWidget); @@ -178,18 +183,18 @@ void LogViewer::setupUI() filterWidget = new QWidget(this); // Time range selection controls - QLabel* startLabel = new QLabel(tr("开始时间:"), this); + startTimeLabel = new QLabel(tr("开始时间:"), this); startTimeEdit = new QDateTimeEdit(this); startTimeEdit->setDisplayFormat("yyyy-MM-dd HH:mm:ss"); - QLabel* endLabel = new QLabel(tr("结束时间:"), this); + endTimeLabel = new QLabel(tr("结束时间:"), this); endTimeEdit = new QDateTimeEdit(this); endTimeEdit->setDisplayFormat("yyyy-MM-dd HH:mm:ss"); QHBoxLayout* timeLayout = new QHBoxLayout(); - timeLayout->addWidget(startLabel); + timeLayout->addWidget(startTimeLabel); timeLayout->addWidget(startTimeEdit); - timeLayout->addWidget(endLabel); + timeLayout->addWidget(endTimeLabel); timeLayout->addWidget(endTimeEdit); timeLayout->addStretch(); @@ -253,7 +258,7 @@ void LogViewer::setupUI() moduleGroupBox->setLayout(moduleGroupLayout); // Encoding selection - QLabel* encodingLabel = new QLabel(tr("文件编码:"), this); + encodingLabel = new QLabel(tr("文件编码:"), this); encodingComboBox = new QComboBox(this); encodingComboBox->addItems({"UTF-8", "GB18030", "GB2312"}); encodingComboBox->setCurrentText("UTF-8"); @@ -311,6 +316,13 @@ void LogViewer::setupUI() connect(searchLineEdit, &QLineEdit::textChanged, this, &LogViewer::onSearchTextChanged); + // Search debounce timer to avoid re-scanning on every keystroke + searchDebounceTimer = new QTimer(this); + searchDebounceTimer->setSingleShot(true); + searchDebounceTimer->setInterval(200); + connect(searchDebounceTimer, &QTimer::timeout, this, + &LogViewer::highlightSearchMatches); + searchPreviousButton = new QPushButton(tr("上一条"), this); searchNextButton = new QPushButton(tr("下一条"), this); exportSearchResultsButton = new QPushButton(tr("导出搜索结果"), this); @@ -346,7 +358,7 @@ void LogViewer::setupUI() // Menu bar QMenuBar* menuBar = new QMenuBar(this); - QMenu* fileMenu = menuBar->addMenu(tr("文件")); + fileMenu = menuBar->addMenu(tr("文件")); openAction = fileMenu->addAction(QIcon(":/icons/open.png"), tr("打开日志文件")); connect(openAction, &QAction::triggered, this, &LogViewer::openLogFile); @@ -376,7 +388,7 @@ void LogViewer::setupUI() toolBar->addSeparator(); // Language switch widget - QLabel* languageLabel = new QLabel(tr("语言:"), this); + languageLabel = new QLabel(tr("语言:"), this); languageComboBox = new QComboBox(this); // Initialize language options @@ -425,11 +437,11 @@ void LogViewer::setupUI() .arg(QString::fromLatin1(QT_VERSION_STR)) .arg(QString::fromLatin1( #ifdef NDEBUG - "Release" + "Release" #else - "Debug" + "Debug" #endif - )); + )); statusBar()->showMessage(tr("就绪 · %1").arg(buildInfo)); // Initialize models @@ -439,7 +451,8 @@ void LogViewer::setupUI() logTreeView->setModel(proxyModel); // Delegate for highlight on content column highlightDelegate = new HighlightDelegate(this); - logTreeView->setItemDelegateForColumn(LogTableModel::ColumnMessage, highlightDelegate); + logTreeView->setItemDelegateForColumn(LogTableModel::ColumnMessage, + highlightDelegate); } void LogViewer::openLogFile() @@ -468,14 +481,11 @@ void LogViewer::loadLogFile(const QString& filePath) progressBar->setValue(0); // Clear previous data - allLogs.clear(); sourceModel->clear(); // Background loader QThread* thread = new QThread(this); LogLoader* loader = new LogLoader(filePath, encoding, 5000); - qRegisterMetaType("LogEntry"); - qRegisterMetaType>("QVector"); loader->moveToThread(thread); connect(thread, &QThread::started, loader, &LogLoader::process); @@ -490,111 +500,70 @@ void LogViewer::loadLogFile(const QString& filePath) logTreeView->viewport()->setUpdatesEnabled(false); #endif - connect(loader, &LogLoader::chunkReady, this, [this](QVector chunk) { - // Append to in-memory cache and model - for (const auto& e : chunk) allLogs.append(e); - sourceModel->appendRows(chunk); - }); - connect(loader, &LogLoader::summaryReady, this, [this](const QDateTime& minTime, const QDateTime& maxTime, const QStringList& modules, const QStringList& levels) { - startTimeEdit->setDateTime(minTime); - endTimeEdit->setDateTime(maxTime); - allModules = modules; - allLevels = levels; - // Rebuild module checkboxes UI - QLayoutItem* child; - while ((child = moduleLayout->takeAt(0)) != nullptr) { - QWidget* widget = child->widget(); - if (widget) widget->deleteLater(); - delete child; - } - moduleCheckBoxes.clear(); - for (const QString& module : allModules) { - QCheckBox* checkBox = new QCheckBox(module, this); - checkBox->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - moduleCheckBoxes.append(checkBox); - moduleLayout->addWidget(checkBox); - } - moduleLayout->addStretch(); - for (QCheckBox* checkBox : levelCheckBoxes) checkBox->setChecked(true); - for (QCheckBox* checkBox : moduleCheckBoxes) checkBox->setChecked(true); - }); - connect(loader, &LogLoader::finished, this, [this, loader, thread, filePath]() { - // 发布构建下,加载结束后恢复视图更新并进行一次性刷新 + connect(loader, &LogLoader::chunkReady, this, + [this](QVector chunk) { + sourceModel->appendRows(std::move(chunk)); + }); + connect(loader, &LogLoader::summaryReady, this, + [this](const QDateTime& minTime, const QDateTime& maxTime, + const QStringList& modules, const QStringList& levels) { + startTimeEdit->setDateTime(minTime); + endTimeEdit->setDateTime(maxTime); + allModules = modules; + allLevels = levels; + // Rebuild module checkboxes UI + QLayoutItem* child; + while ((child = moduleLayout->takeAt(0)) != nullptr) { + QWidget* widget = child->widget(); + if (widget) + widget->deleteLater(); + delete child; + } + moduleCheckBoxes.clear(); + for (const QString& module : allModules) { + QCheckBox* checkBox = new QCheckBox(module, this); + checkBox->setSizePolicy(QSizePolicy::Preferred, + QSizePolicy::Preferred); + moduleCheckBoxes.append(checkBox); + moduleLayout->addWidget(checkBox); + } + moduleLayout->addStretch(); + for (QCheckBox* checkBox : levelCheckBoxes) + checkBox->setChecked(true); + for (QCheckBox* checkBox : moduleCheckBoxes) + checkBox->setChecked(true); + }); + connect(loader, &LogLoader::finished, this, + [this, loader, thread, filePath]() { + // 发布构建下,加载结束后恢复视图更新并进行一次性刷新 #ifdef NDEBUG - logTreeView->setUpdatesEnabled(true); - logTreeView->viewport()->setUpdatesEnabled(true); - logTreeView->viewport()->update(); + logTreeView->setUpdatesEnabled(true); + logTreeView->viewport()->setUpdatesEnabled(true); + logTreeView->viewport()->update(); #endif - progressBar->setVisible(false); - exportAction->setEnabled(true); - statusBar()->showMessage(tr("已加载文件:%1,日志条目数:%2").arg(filePath).arg(allLogs.size())); - loader->deleteLater(); - thread->quit(); - thread->wait(); - thread->deleteLater(); - }); + progressBar->setVisible(false); + exportAction->setEnabled(true); + statusBar()->showMessage(tr("已加载文件:%1,日志条目数:%2") + .arg(filePath) + .arg(sourceModel->rowCount())); + loader->deleteLater(); + thread->quit(); + thread->wait(); + thread->deleteLater(); + }); thread->start(); } -QList LogViewer::parseLogFile(const QString& filePath, - const QString& encoding) -{ - QList logEntries; - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - QMessageBox::warning(this, tr("错误"), tr("无法打开日志文件。")); - return logEntries; - } - - QTextStream in(&file); - // 统一处理编码(Qt5/Qt6) -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - QTextCodec* codec = QTextCodec::codecForName(encoding.toUtf8()); - if (!codec) { - QMessageBox::warning(this, tr("错误"), - tr("不支持的编码格式:%1").arg(encoding)); - return logEntries; - } - in.setCodec(codec); -#else - auto enc = QStringConverter::encodingForName(encoding.toUtf8()); - if (enc.has_value()) { - in.setEncoding(*enc); - } else { - QMessageBox::warning(this, tr("错误"), - tr("不支持的编码格式:%1,已回退为UTF-8").arg(encoding)); - in.setEncoding(QStringConverter::Utf8); - } -#endif - - // More tolerant spacing: allow variable spaces around tokens and colon - QRegularExpression regex(R"((?:\[\s*(.*?)\s*\])\s*(?:\[\s*(.*?)\s*\])\s*(?:\[\s*(.*?)\s*\])\s*:\s*(.*)$)"); - regex.optimize(); - while (!in.atEnd()) { - QString line = in.readLine(); - QRegularExpressionMatch match = regex.match(line); - if (match.hasMatch()) { - LogEntry entry; - entry.timestamp = QDateTime::fromString(match.captured(1), - "yyyy-MM-dd HH:mm:ss.zzz"); - if (!entry.timestamp.isValid()) { - // Try other format - entry.timestamp = QDateTime::fromString(match.captured(1), - "yyyy-MM-dd HH:mm:ss"); - } - entry.level = match.captured(2).trimmed(); - entry.module = match.captured(3).trimmed(); - entry.message = match.captured(4); - logEntries.append(entry); - } - } - return logEntries; -} - void LogViewer::onFilterButtonClicked() { + // Clear search state before filter changes to avoid stale proxy row indices + searchResults.clear(); + currentSearchIndex = -1; + currentSearchText.clear(); + searchLineEdit->clear(); + QDateTime startTime = startTimeEdit->dateTime(); QDateTime endTime = endTimeEdit->dateTime(); QStringList selectedLevels; @@ -628,40 +597,8 @@ void LogViewer::onFilterButtonClicked() // Hide progress bar after filtering is complete progressBar->setVisible(false); - statusBar()->showMessage( - tr("筛选完成,日志条目数:%1").arg(proxyModel ? proxyModel->rowCount() : 0)); -} - -QList LogViewer::filterLogs(const QList& logs, - const QDateTime& startTime, - const QDateTime& endTime, - const QStringList& levels, - const QStringList& modules) -{ - QList filteredLogs; - for (const auto& entry : logs) { - if (!entry.timestamp.isValid()) - continue; - if (entry.timestamp >= startTime && entry.timestamp <= endTime && - levels.contains(entry.level) && modules.contains(entry.module)) { - filteredLogs.append(entry); - } - } - return filteredLogs; -} - -void LogViewer::displayLogs(const QList& logs) -{ - Q_UNUSED(logs) - // Model already set in setup; just configure header behavior - logTreeView->header()->setStretchLastSection(false); - for (int i = 0; i < LogTableModel::ColumnCount; ++i) { - logTreeView->header()->setSectionResizeMode(i, QHeaderView::Interactive); - } - // Avoid full resizeColumnToContents on large data; set sensible defaults - logTreeView->header()->setMinimumSectionSize(50); - logTreeView->header()->setSectionResizeMode(0, QHeaderView::Fixed); - logTreeView->header()->resizeSection(0, 80); + statusBar()->showMessage(tr("筛选完成,日志条目数:%1") + .arg(proxyModel ? proxyModel->rowCount() : 0)); } void LogViewer::toggleFilterArea() @@ -692,7 +629,15 @@ void LogViewer::deselectAllModules() void LogViewer::onSearchTextChanged(const QString& text) { currentSearchText = text; - highlightSearchMatches(); + searchDebounceTimer->start(); +} + +void LogViewer::flushSearchDebounce() +{ + if (searchDebounceTimer->isActive()) { + searchDebounceTimer->stop(); + highlightSearchMatches(); + } } void LogViewer::highlightSearchMatches() @@ -715,7 +660,8 @@ void LogViewer::highlightSearchMatches() // Linear scan on content column for matches (proxy model) int rows = proxyModel->rowCount(); for (int r = 0; r < rows; ++r) { - QModelIndex contentIdx = proxyModel->index(r, LogTableModel::ColumnMessage); + QModelIndex contentIdx = + proxyModel->index(r, LogTableModel::ColumnMessage); QString text = proxyModel->data(contentIdx, Qt::DisplayRole).toString(); if (text.contains(currentSearchText, Qt::CaseInsensitive)) { searchResults.append(r); @@ -746,26 +692,9 @@ void LogViewer::expandToItem(QStandardItem* item) } } -void LogViewer::searchInItem(QStandardItem* item) -{ - Q_UNUSED(item) -} - -void LogViewer::clearHighlightsInItem(QStandardItem* item) -{ - Q_UNUSED(item) -} - -void LogViewer::clearSearchHighlights() -{ - if (!proxyModel) - return; - - // With delegate highlighter, clearing becomes no-op -} - void LogViewer::onSearchPrevious() { + flushSearchDebounce(); if (searchResults.isEmpty()) return; @@ -781,6 +710,7 @@ void LogViewer::onSearchPrevious() void LogViewer::onSearchNext() { + flushSearchDebounce(); if (searchResults.isEmpty()) return; @@ -803,7 +733,8 @@ void LogViewer::onLogItemDoubleClicked(const QModelIndex& index) int cols = proxyModel->columnCount(); for (int col = 0; col < cols; ++col) { QString header = proxyModel->headerData(col, Qt::Horizontal).toString(); - QString data = proxyModel->data(index.sibling(index.row(), col)).toString(); + QString data = + proxyModel->data(index.sibling(index.row(), col)).toString(); details += QString("%1: %2\n").arg(header).arg(data); } @@ -818,7 +749,7 @@ void LogViewer::onTreeItemExpanded(const QModelIndex& index) void LogViewer::onExportFiltered() { - if (allLogs.isEmpty()) { + if (sourceModel->rowCount() == 0) { QMessageBox::information(this, tr("提示"), tr("没有日志数据可以导出")); return; } @@ -869,6 +800,10 @@ void LogViewer::onExportFiltered() progressBar->setRange(0, 100); progressBar->setValue(0); + // TODO: Export runs on the main thread and will block the UI for large + // datasets. This should be moved to a worker thread (QThread) in the + // future so the UI remains responsive during export. + // Execute export if (config.formats.size() > 1) { exporter->exportMultipleFormats(filteredLogs, config); @@ -880,6 +815,7 @@ void LogViewer::onExportFiltered() void LogViewer::onExportSearchResults() { + flushSearchDebounce(); if (currentSearchText.isEmpty()) { QMessageBox::information(this, tr("提示"), tr("请先输入搜索内容")); return; @@ -938,6 +874,10 @@ void LogViewer::onExportSearchResults() progressBar->setRange(0, 100); progressBar->setValue(0); + // TODO: Export runs on the main thread and will block the UI for large + // datasets. This should be moved to a worker thread (QThread) in the + // future so the UI remains responsive during export. + // Execute export if (config.formats.size() > 1) { exporter->exportMultipleFormats(matchedLogs, config); @@ -975,7 +915,8 @@ QList LogViewer::getSearchMatchedLogs() const for (int proxyRow : searchResults) { if (proxyRow >= 0 && proxyRow < proxyModel->rowCount()) { - QModelIndex srcIndex = proxyModel->mapToSource(proxyModel->index(proxyRow, 0)); + QModelIndex srcIndex = + proxyModel->mapToSource(proxyModel->index(proxyRow, 0)); int srcRow = srcIndex.row(); if (srcRow >= 0 && srcRow < sourceModel->size()) { matchedLogs.append(sourceModel->at(srcRow)); @@ -1030,17 +971,8 @@ void LogViewer::retranslateUI() #endif // Re-set menu item text - if (menuBar()) { - QList menus = menuBar()->findChildren(); - for (QMenu* menu : menus) { - QString oldTitle = menu->title(); - menu->setTitle(tr("文件")); -#ifdef LOG_DEBUG_ENABLED - qDebug() << "Menu title changed from" << oldTitle << "to" - << menu->title(); -#endif - } - } + if (fileMenu) + fileMenu->setTitle(tr("文件")); // Re-set action text if (openAction) { @@ -1106,35 +1038,15 @@ void LogViewer::retranslateUI() if (searchLineEdit) searchLineEdit->setPlaceholderText(tr("搜索...")); - // Re-set language label text - QList allLabels = findChildren(); - int labelsUpdated = 0; - for (QLabel* label : allLabels) { - QString oldText = label->text(); - QString newText = oldText; - - if (oldText.contains("开始时间") || oldText.contains("Start")) { - newText = tr("开始时间:"); - } else if (oldText.contains("结束时间") || oldText.contains("Finish")) { - newText = tr("结束时间:"); - } else if (oldText.contains("文件编码") || - oldText.contains("encoding")) { - newText = tr("文件编码:"); - } else if (oldText.contains("语言") || oldText.contains("Language")) { - newText = tr("语言:"); - } - - if (newText != oldText) { - label->setText(newText); -#ifdef LOG_DEBUG_ENABLED - qDebug() << "Label updated from" << oldText << "to" << newText; -#endif - labelsUpdated++; - } - } -#ifdef LOG_DEBUG_ENABLED - qDebug() << "Total labels updated:" << labelsUpdated; -#endif + // Re-set label text directly via member pointers + if (startTimeLabel) + startTimeLabel->setText(tr("开始时间:")); + if (endTimeLabel) + endTimeLabel->setText(tr("结束时间:")); + if (encodingLabel) + encodingLabel->setText(tr("文件编码:")); + if (languageLabel) + languageLabel->setText(tr("语言:")); // Re-set table headers (via view header, since we use custom model) if (logTreeView && logTreeView->header()) { diff --git a/src/ui/logviewer.h b/src/ui/logviewer.h index 1ba3b7e..60d2324 100644 --- a/src/ui/logviewer.h +++ b/src/ui/logviewer.h @@ -14,12 +14,12 @@ #ifndef LOGVIEWER_H #define LOGVIEWER_H -#include #include #include -#include #include +#include #include +#include #include "../core/logentry.h" #include "../core/logexporter.h" @@ -43,8 +43,11 @@ class QSplitter; class QModelIndex; class QHBoxLayout; class QLabel; +class QMenu; QT_END_NAMESPACE +class QTimer; + // Forward declarations for application classes (global namespace) class LogTableModel; class LogFilterProxyModel; @@ -244,72 +247,13 @@ private slots: */ void retranslateUI(); - /** - * @brief Display log entries in tree view - * @param logs List of LogEntry objects to display - * @details Populates the tree view model with log data, organizing entries - * hierarchically and applying appropriate formatting and icons. - */ - void displayLogs(const QList& logs); - - /** - * @brief Parse log file and extract log entries - * @param filePath Path to the log file - * @param encoding Character encoding to use for reading - * @return List of parsed LogEntry objects - * @details Reads the file line by line, extracts log entry information - * (timestamp, level, module, content) using regular expressions. - */ - QList parseLogFile(const QString& filePath, - const QString& encoding); - - /** - * @brief Filter log entries based on criteria - * @param logs Original list of log entries - * @param startTime Start of time range filter - * @param endTime End of time range filter - * @param levels List of log levels to include - * @param modules List of modules to include - * @return Filtered list of log entries - * @details Applies multiple filter criteria to reduce the log dataset - * to entries matching user specifications. - */ - QList filterLogs(const QList& logs, - const QDateTime& startTime, - const QDateTime& endTime, - const QStringList& levels, - const QStringList& modules); - - // Search functionality methods - /** - * @brief Search for text within a tree item and its children - * @param item Tree item to search in - * @details Recursively searches through tree items for the current search - * term, highlighting matches and building a list of search results. - */ - void searchInItem(QStandardItem* item); - - /** - * @brief Clear all search highlighting - * @details Removes search highlighting from all tree items, - * resetting them to normal display state. - */ - void clearSearchHighlights(); - - /** - * @brief Clear search highlighting from an item and its children - * @param item Tree item to clear highlighting from - * @details Recursively removes highlighting from the specified item - * and all its child items. - */ - void clearHighlightsInItem(QStandardItem* item); - /** * @brief Apply search highlighting to matching items * @details Highlights all tree items that contain the current search term, * making them visually distinct for easy identification. */ void highlightSearchMatches(); + void flushSearchDebounce(); /** * @brief Expand tree view to show specified item @@ -345,11 +289,12 @@ private slots: QComboBox* languageComboBox; ///< Dropdown for language selection // UI Controls - Main display - QTreeView* logTreeView; ///< Main tree view for displaying logs + QTreeView* logTreeView; ///< Main tree view for displaying logs // Replaced heavy item model with lightweight table + proxy - class LogTableModel* sourceModel; ///< Lightweight source model + class LogTableModel* sourceModel; ///< Lightweight source model class LogFilterProxyModel* proxyModel; ///< Filter proxy model - class HighlightDelegate* highlightDelegate; ///< Delegate for search highlight + class HighlightDelegate* + highlightDelegate; ///< Delegate for search highlight // UI Controls - Layout containers QGroupBox* timeGroupBox; ///< Container for time range controls @@ -369,24 +314,33 @@ private slots: QPushButton* deselectAllModulesButton; ///< Button to deselect all modules // UI Controls - Search - QLineEdit* searchLineEdit; ///< Text input for search terms + QLineEdit* searchLineEdit; ///< Text input for search terms QPushButton* searchPreviousButton; ///< Button to go to previous search result QPushButton* searchNextButton; ///< Button to go to next search result QPushButton* exportSearchResultsButton; ///< Button to export search results + // UI Controls - Retranslatable labels + QLabel* startTimeLabel = nullptr; ///< Label for start time selector + QLabel* endTimeLabel = nullptr; ///< Label for end time selector + QLabel* encodingLabel = nullptr; ///< Label for encoding selector + QLabel* languageLabel = nullptr; ///< Label for language selector + + // UI Controls - Menu + QMenu* fileMenu = nullptr; ///< File menu for retranslation + // UI Controls - GitHub link QLabel* githubLinkLabel; ///< Clickable GitHub link in status bar // Data storage - QList allLogs; ///< Complete list of loaded log entries - QStringList allModules; ///< List of all unique modules found in logs - QStringList allLevels; ///< List of all unique log levels found - QString currentFilePath; ///< Path of currently loaded log file - QString currentSearchText; ///< Current search term - QVector searchResults; ///< Row indices in proxy model matching search - int currentSearchIndex; ///< Index of currently selected search result - QFont logFont; ///< Font used for displaying log content + QStringList allModules; ///< List of all unique modules found in logs + QStringList allLevels; ///< List of all unique log levels found + QString currentFilePath; ///< Path of currently loaded log file + QString currentSearchText; ///< Current search term + QVector searchResults; ///< Row indices in proxy model matching search + int currentSearchIndex; ///< Index of currently selected search result + QFont logFont; ///< Font used for displaying log content + QTimer* searchDebounceTimer; ///< Debounce timer for search input }; #endif // LOGVIEWER_H diff --git a/src/utils/appsettings.cpp b/src/utils/appsettings.cpp index df53689..25acfbb 100644 --- a/src/utils/appsettings.cpp +++ b/src/utils/appsettings.cpp @@ -32,13 +32,10 @@ AppSettings& AppSettings::instance() AppSettings::AppSettings() { - settings = new QSettings("LogViewer", "LogViewer"); + settings = std::make_unique("LogViewer", "LogViewer"); } -AppSettings::~AppSettings() -{ - delete settings; -} +AppSettings::~AppSettings() = default; void AppSettings::setRecentLogDir(const QString& path) { diff --git a/src/utils/appsettings.h b/src/utils/appsettings.h index 58fb244..4507874 100644 --- a/src/utils/appsettings.h +++ b/src/utils/appsettings.h @@ -16,6 +16,7 @@ #include #include #include +#include /** * @class AppSettings @@ -103,7 +104,7 @@ class AppSettings AppSettings& operator=(const AppSettings&) = delete; ///< Deleted assignment operator - QSettings* settings; ///< Qt settings object for persistent storage + std::unique_ptr settings; ///< Qt settings object for persistent storage // 配置键名 static const QString KEY_RECENT_LOG_DIR; diff --git a/src/utils/languagemanager.cpp b/src/utils/languagemanager.cpp index d62c865..178343f 100644 --- a/src/utils/languagemanager.cpp +++ b/src/utils/languagemanager.cpp @@ -35,7 +35,7 @@ LanguageManager& LanguageManager::instance() * languages. Default language is set to Chinese. */ LanguageManager::LanguageManager() - : currentTranslator(nullptr), currentLanguage(Chinese) + : QObject(nullptr), currentTranslator(nullptr), currentLanguage(Chinese) { // Initialize language code mappings languageCodes[Chinese] = "zh_CN"; @@ -118,10 +118,8 @@ void LanguageManager::setLanguage(Language language) qDebug() << "Language changed to:" << languageToDisplayName(language); #endif - // Invoke callback function to trigger UI update - if (languageChangeCallback) { - languageChangeCallback(language); - } + // Emit signal to notify connected receivers of language change + emit languageChanged(language); } /** @@ -277,18 +275,6 @@ void LanguageManager::loadTranslation(Language language) #endif } -/** - * @brief Set callback function for language change notifications - * @param callback Function to be called when language changes - * @details The callback is invoked after successful language change to notify - * UI components that they should refresh their displayed text. - */ -void LanguageManager::setLanguageChangeCallback( - std::function callback) -{ - languageChangeCallback = callback; -} - /** * @brief Remove currently loaded translation from QApplication * @details Uninstalls current translator and frees memory. Safe to call diff --git a/src/utils/languagemanager.h b/src/utils/languagemanager.h index 6301d98..d04e84e 100644 --- a/src/utils/languagemanager.h +++ b/src/utils/languagemanager.h @@ -15,9 +15,9 @@ #include #include +#include #include #include -#include /** * @class LanguageManager @@ -40,8 +40,10 @@ * manager.setLanguage(LanguageManager::English); * @endcode */ -class LanguageManager +class LanguageManager : public QObject { + Q_OBJECT + public: /** * @enum Language @@ -127,13 +129,14 @@ class LanguageManager */ QString languageToDisplayName(Language language) const; +signals: /** - * @brief Set callback function for language change notifications - * @param callback Function to call when language changes - * @details The callback is invoked after successful language change to - * allow UI components to refresh their displayed text + * @brief Signal emitted when language changes + * @param language The new language that has been set + * @details Connected receivers can update their UI in response. + * Qt auto-disconnects when the receiver is destroyed. */ - void setLanguageChangeCallback(std::function callback); + void languageChanged(Language language); private: /** @@ -179,9 +182,6 @@ class LanguageManager languageCodes; ///< Mapping of Language enum to language codes QMap displayNames; ///< Mapping of Language enum to display names - std::function - languageChangeCallback; ///< Callback function for language change - ///< events }; #endif // LANGUAGEMANAGER_H \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 65df34e..856d439 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,25 +18,27 @@ endif() set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# 包含项目源代码目录 -include_directories(${CMAKE_SOURCE_DIR}/src) -include_directories(${CMAKE_SOURCE_DIR}/src/core) -include_directories(${CMAKE_SOURCE_DIR}/src/ui) -include_directories(${CMAKE_SOURCE_DIR}/src/utils) - # 测试辅助宏 macro(add_logviewer_test testname) add_executable(${testname} ${testname}.cpp) - + if(Qt6_FOUND) target_link_libraries(${testname} Qt6::Test Qt6::Core Qt6::Widgets) else() target_link_libraries(${testname} Qt5::Test Qt5::Core Qt5::Widgets) endif() - + # 链接项目源文件(根据需要) target_sources(${testname} PRIVATE ${ARGN}) - + + # 包含项目源代码目录 + target_include_directories(${testname} PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/src/core + ${CMAKE_SOURCE_DIR}/src/ui + ${CMAKE_SOURCE_DIR}/src/utils + ) + # 添加到测试套件 add_test(NAME ${testname} COMMAND ${testname}) diff --git a/tests/test_logentry.cpp b/tests/test_logentry.cpp index 3b9b23b..379f228 100644 --- a/tests/test_logentry.cpp +++ b/tests/test_logentry.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include "logentry.h" @@ -70,7 +71,7 @@ void TestLogEntry::initTestCase() // 创建有效的测试条目 validEntry.timestamp = QDateTime::fromString("2025-06-27 08:36:19.123", - "yyyy-MM-dd hh:mm:ss.zzz"); + "yyyy-MM-dd HH:mm:ss.zzz"); validEntry.level = "INFO"; validEntry.module = "ModuleName"; validEntry.message = "正常信息日志"; @@ -137,7 +138,7 @@ void TestLogEntry::testTimestampParsing() { QString logLine = "[2025-06-27 08:36:19.123] [INFO] [Module] : Message"; QDateTime expectedTime = QDateTime::fromString("2025-06-27 08:36:19.123", - "yyyy-MM-dd hh:mm:ss.zzz"); + "yyyy-MM-dd HH:mm:ss.zzz"); // 模拟解析过程 LogEntry entry;