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;