初始化
97
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
name: 🧀 打包所有平台
|
||||
|
||||
on:
|
||||
# 手动触发
|
||||
workflow_dispatch:
|
||||
# 推送 tag 时触发
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Windows 64位
|
||||
- os: windows-latest
|
||||
platform: win64
|
||||
artifact_name: CheeseCloudTools-Windows-x64
|
||||
|
||||
# Windows 32位 (使用 32位 Python)
|
||||
# - os: windows-latest
|
||||
# platform: win32
|
||||
# artifact_name: CheeseCloudTools-Windows-x86
|
||||
# python_arch: x86
|
||||
|
||||
# macOS Intel
|
||||
- os: macos-13
|
||||
platform: mac_x64
|
||||
artifact_name: CheeseCloudTools-macOS-Intel
|
||||
|
||||
# macOS Apple Silicon
|
||||
- os: macos-latest
|
||||
platform: mac_arm64
|
||||
artifact_name: CheeseCloudTools-macOS-AppleSilicon
|
||||
|
||||
# Linux 64位
|
||||
- os: ubuntu-latest
|
||||
platform: linux64
|
||||
artifact_name: CheeseCloudTools-Linux-x64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: 📥 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🐍 设置 Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
architecture: ${{ matrix.python_arch || 'x64' }}
|
||||
|
||||
- name: 📦 安装依赖
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
|
||||
- name: 🔨 执行打包
|
||||
run: python build_app.py --platform ${{ matrix.platform }}
|
||||
|
||||
- name: 📤 上传构建产物
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: dist/${{ matrix.platform }}/
|
||||
retention-days: 30
|
||||
|
||||
# 创建 Release(仅在推送 tag 时)
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
steps:
|
||||
- name: 📥 下载所有构建产物
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts/
|
||||
|
||||
- name: 📦 打包为 ZIP
|
||||
run: |
|
||||
cd artifacts
|
||||
for dir in */; do
|
||||
zip -r "${dir%/}.zip" "$dir"
|
||||
done
|
||||
|
||||
- name: 🚀 创建 Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: artifacts/*.zip
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/dist/
|
||||
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/nly-tool.iml" filepath="$PROJECT_DIR$/.idea/nly-tool.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/nly-tool.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
319
README.md
Normal file
@@ -0,0 +1,319 @@
|
||||
# 🧀 奶酪云工具箱
|
||||
|
||||
> 多功能图片/PDF/Excel处理桌面应用
|
||||
|
||||
基于 **PySide6** 开发的跨平台桌面工具箱,支持 Windows 7/10/11 和 macOS。
|
||||
|
||||
---
|
||||
|
||||
## ✨ 功能列表
|
||||
|
||||
### 🖼️ 图片工具
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **图片压缩** | 智能压缩,视觉无损/均衡/极致压缩模式 |
|
||||
| **格式转换** | JPG ↔ PNG ↔ WebP ↔ ICO ↔ PDF |
|
||||
| **添加水印** | 文字水印/图片水印,支持位置、透明度调整 |
|
||||
|
||||
### 📄 PDF工具
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **PDF拆分** | 按页码范围拆分PDF |
|
||||
| **PDF合并** | 多个PDF合并为一个 |
|
||||
| **PDF转Word** | 保持排版转换为Word文档 |
|
||||
|
||||
### 📊 Excel工具
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **Excel预览** | 快速预览Excel内容 |
|
||||
| **生成图表** | 根据数据生成可视化图表 |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 运行方式
|
||||
|
||||
### 方式一:源码运行
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 2. 运行程序
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 方式二:打包后运行
|
||||
|
||||
直接双击 `CheeseCloudTools.exe` (Windows) 或 `CheeseCloudTools.app` (macOS)
|
||||
|
||||
---
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
nly-tool/
|
||||
├── main.py # 程序入口
|
||||
├── requirements.txt # 依赖列表
|
||||
├── README.md # 项目说明
|
||||
│
|
||||
├── core/ # 核心模块
|
||||
│ ├── config.py # 全局配置管理
|
||||
│ ├── logger.py # 日志系统
|
||||
│ └── error_handler.py # 错误处理
|
||||
│
|
||||
├── ui/ # 界面模块
|
||||
│ ├── main_window.py # 主窗口
|
||||
│ ├── sidebar.py # 侧边栏
|
||||
│ ├── tool_list.py # 工具列表
|
||||
│ ├── workspace.py # 工作区
|
||||
│ ├── settings.py # 设置页面
|
||||
│ ├── image_preview.py # 图片预览组件
|
||||
│ └── animations.py # 动画效果
|
||||
│
|
||||
├── tools/ # 工具实现
|
||||
│ ├── image/ # 图片工具
|
||||
│ │ ├── compress.py # 压缩
|
||||
│ │ ├── convert.py # 格式转换
|
||||
│ │ └── watermark.py # 水印
|
||||
│ ├── pdf/ # PDF工具
|
||||
│ │ ├── split.py # 拆分
|
||||
│ │ ├── merge.py # 合并
|
||||
│ │ └── to_word.py # 转Word
|
||||
│ └── excel/ # Excel工具
|
||||
│ ├── preview.py # 预览
|
||||
│ └── chart.py # 图表
|
||||
│
|
||||
├── resources/ # 资源文件
|
||||
│ └── style.qss # 样式表
|
||||
│
|
||||
├── image/ # 图片资源
|
||||
│ └── 生成奶酪商城官方店介绍.png # 应用图标
|
||||
│
|
||||
├── config/ # 配置文件(运行时生成)
|
||||
├── logs/ # 日志文件(运行时生成)
|
||||
│
|
||||
├── build_app.py # 打包脚本
|
||||
├── build.bat # Windows打包快捷方式
|
||||
├── build.sh # macOS/Linux打包快捷方式
|
||||
│
|
||||
└── .github/
|
||||
└── workflows/
|
||||
└── build.yml # GitHub Actions 自动打包
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 一键打包所有平台(GitHub Actions)
|
||||
|
||||
> **推荐方式**:使用 GitHub Actions 云端打包,可在 Windows 上一次性打包所有平台!
|
||||
|
||||
### 使用方法
|
||||
|
||||
1. **将代码推送到 GitHub 仓库**
|
||||
|
||||
2. **手动触发打包**:
|
||||
- 进入仓库 → `Actions` → `🧀 打包所有平台`
|
||||
- 点击 `Run workflow` → `Run workflow`
|
||||
|
||||
3. **自动触发打包**(推送版本标签时):
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
### 打包产物
|
||||
|
||||
| 平台 | 文件名 |
|
||||
|------|--------|
|
||||
| Windows 64位 | `CheeseCloudTools-Windows-x64.zip` |
|
||||
| macOS Intel | `CheeseCloudTools-macOS-Intel.zip` |
|
||||
| macOS Apple Silicon | `CheeseCloudTools-macOS-AppleSilicon.zip` |
|
||||
| Linux 64位 | `CheeseCloudTools-Linux-x64.zip` |
|
||||
|
||||
### 下载位置
|
||||
|
||||
- **手动触发**:Actions → 对应工作流 → Artifacts
|
||||
- **Tag 触发**:自动创建 Release,在 Releases 页面下载
|
||||
|
||||
---
|
||||
|
||||
## 📦 本地打包命令
|
||||
|
||||
### Windows 用户
|
||||
|
||||
```bash
|
||||
# 方式1:双击运行
|
||||
build.bat
|
||||
|
||||
# 方式2:命令行 - 打包当前平台
|
||||
python build_app.py
|
||||
|
||||
# 方式3:指定平台
|
||||
python build_app.py --platform win64 # Windows 64位
|
||||
python build_app.py --platform win32 # Windows 32位
|
||||
|
||||
# 一键打包所有支持的版本
|
||||
python build_app.py --all
|
||||
|
||||
# 清理构建缓存
|
||||
python build_app.py --clean
|
||||
```
|
||||
|
||||
### macOS 用户
|
||||
|
||||
```bash
|
||||
# 添加执行权限
|
||||
chmod +x build.sh
|
||||
|
||||
# 方式1:快捷脚本
|
||||
./build.sh
|
||||
|
||||
# 方式2:命令行
|
||||
python3 build_app.py # 自动识别
|
||||
python3 build_app.py --platform mac_x64 # Intel Mac
|
||||
python3 build_app.py --platform mac_arm64 # Apple Silicon (M1/M2/M3)
|
||||
|
||||
# 一键打包
|
||||
python3 build_app.py --all
|
||||
|
||||
# 清理构建缓存
|
||||
python3 build_app.py --clean
|
||||
```
|
||||
|
||||
### Linux 用户
|
||||
|
||||
```bash
|
||||
chmod +x build.sh
|
||||
./build.sh
|
||||
|
||||
# 或
|
||||
python3 build_app.py --platform linux64
|
||||
```
|
||||
|
||||
### 打包参数说明
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--platform win64` | Windows 64位 |
|
||||
| `--platform win32` | Windows 32位 |
|
||||
| `--platform mac_x64` | macOS Intel |
|
||||
| `--platform mac_arm64` | macOS Apple Silicon |
|
||||
| `--platform linux64` | Linux 64位 |
|
||||
| `--platform current` | 当前系统(默认) |
|
||||
| `--all` / `-a` | 打包当前系统所有支持的架构 |
|
||||
| `--clean` / `-c` | 清理构建缓存 |
|
||||
|
||||
---
|
||||
|
||||
## 📂 打包结果
|
||||
|
||||
打包完成后,可执行文件位于 `dist/` 目录:
|
||||
|
||||
```
|
||||
dist/
|
||||
├── win64/ # Windows 64位
|
||||
│ └── CheeseCloudTools/
|
||||
│ ├── CheeseCloudTools.exe ← 主程序
|
||||
│ └── _internal/ ← 依赖文件
|
||||
│ ├── image/
|
||||
│ ├── resources/
|
||||
│ └── ...
|
||||
│
|
||||
├── win32/ # Windows 32位
|
||||
│ └── CheeseCloudTools/
|
||||
│
|
||||
├── mac_x64/ # macOS Intel
|
||||
│ └── CheeseCloudTools.app/
|
||||
│
|
||||
├── mac_arm64/ # macOS Apple Silicon
|
||||
│ └── CheeseCloudTools.app/
|
||||
│
|
||||
└── linux64/ # Linux 64位
|
||||
└── CheeseCloudTools/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 跨平台打包
|
||||
- ❌ **无法在 Windows 上打包 macOS 版本**
|
||||
- ❌ **无法在 macOS 上打包 Windows 版本**
|
||||
- ✅ 每个平台需要在对应系统上执行打包命令
|
||||
|
||||
### Windows 兼容性
|
||||
| 版本 | 32位 | 64位 |
|
||||
|------|------|------|
|
||||
| Windows 7 | ✅ | ✅ |
|
||||
| Windows 10 | ✅ | ✅ |
|
||||
| Windows 11 | ❌ | ✅ |
|
||||
|
||||
> **提示**:Windows 7 可能需要安装 Visual C++ Redistributable
|
||||
|
||||
### macOS 兼容性
|
||||
| 芯片 | 打包命令 |
|
||||
|------|------|
|
||||
| Intel (x86_64) | `--platform mac_x64` |
|
||||
| Apple Silicon (M1/M2/M3) | `--platform mac_arm64` |
|
||||
|
||||
### 32位版本打包
|
||||
Windows 32位版本需要在 **32位 Python 环境** 中打包:
|
||||
|
||||
```bash
|
||||
# 1. 安装32位Python
|
||||
# 2. 使用32位Python运行打包
|
||||
C:\Python312-32\python.exe build_app.py --platform win32
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 文件清单
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `main.py` | 程序主入口 |
|
||||
| `requirements.txt` | Python依赖列表 |
|
||||
| `README.md` | 项目说明文档 |
|
||||
| `build_app.py` | 跨平台打包脚本 |
|
||||
| `build.bat` | Windows快捷打包 |
|
||||
| `build.sh` | macOS/Linux快捷打包 |
|
||||
| `.github/workflows/build.yml` | **GitHub Actions 云端打包配置** |
|
||||
| `resources/style.qss` | 界面样式表 |
|
||||
| `image/生成奶酪商城官方店介绍.png` | 应用图标 |
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q: 打包后运行闪退
|
||||
**A:** 在命令行运行 exe 查看错误信息:
|
||||
```bash
|
||||
cd dist\win64\CheeseCloudTools
|
||||
CheeseCloudTools.exe
|
||||
```
|
||||
|
||||
### Q: 图标不显示
|
||||
**A:** 确保 `image/生成奶酪商城官方店介绍.png` 文件存在
|
||||
|
||||
### Q: 打包文件太大
|
||||
**A:** 正常现象,PySide6 和依赖库较大(约200-400MB)
|
||||
|
||||
### Q: Windows Defender 报警
|
||||
**A:** 这是 PyInstaller 打包的常见误报,可以添加白名单
|
||||
|
||||
### Q: macOS 提示"无法验证开发者"
|
||||
**A:** 右键点击应用 → 打开,或在系统设置中允许
|
||||
|
||||
---
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
- **GUI框架**: PySide6 (LGPL,可商用)
|
||||
- **项目**: MIT License
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 作者
|
||||
|
||||
**奶酪源码** - 让工具更简单
|
||||
|
||||
461
UI设计.html
Normal file
@@ -0,0 +1,461 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>奶酪云工具箱 | Cheese Cloud Tools</title>
|
||||
<!-- 引入 Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- 引入 Phosphor Icons 图标库 -->
|
||||
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
||||
<!-- Google Fonts: Inter -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
cheese: {
|
||||
50: '#fffbeb',
|
||||
100: '#fef3c7',
|
||||
200: '#fde68a',
|
||||
300: '#fcd34d',
|
||||
400: '#fbbf24', // 主色
|
||||
500: '#f59e0b', // 深色
|
||||
600: '#d97706',
|
||||
900: '#78350f',
|
||||
},
|
||||
darkbg: {
|
||||
900: '#0f172a', // 主背景
|
||||
800: '#1e293b', // 侧边栏
|
||||
700: '#334155', // 边框/悬浮
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.3s ease-out',
|
||||
'slide-up': 'slideUp 0.4s ease-out',
|
||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
||||
|
||||
.glass-panel {
|
||||
background: rgba(30, 41, 59, 0.7);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.active-nav-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 60%;
|
||||
width: 3px;
|
||||
background-color: #fbbf24;
|
||||
border-radius: 0 4px 4px 0;
|
||||
box-shadow: 0 0 10px #fbbf24;
|
||||
}
|
||||
|
||||
/* 模拟 PDF 页面选中效果 */
|
||||
.pdf-page-selected {
|
||||
border-color: #fbbf24;
|
||||
box-shadow: 0 0 0 2px rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
.pdf-page-selected .check-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-darkbg-900 text-slate-300 font-sans h-screen w-screen overflow-hidden flex selection:bg-cheese-500 selection:text-white">
|
||||
|
||||
<!-- 1. 左侧一级菜单 -->
|
||||
<aside class="w-20 bg-darkbg-800 flex flex-col items-center py-6 border-r border-darkbg-700 z-20 flex-shrink-0 shadow-2xl">
|
||||
<div class="mb-8 relative group cursor-pointer">
|
||||
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-cheese-400 to-cheese-600 flex items-center justify-center shadow-lg shadow-cheese-500/20 transform group-hover:scale-105 transition-all duration-300 overflow-hidden">
|
||||
<i class="ph-fill ph-cheese text-2xl text-white"></i>
|
||||
</div>
|
||||
<span class="absolute left-14 top-1/2 -translate-y-1/2 bg-black text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition pointer-events-none whitespace-nowrap z-50">奶酪云</span>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 w-full space-y-4">
|
||||
<div onclick="switchCategory('image')" id="nav-image" class="group relative w-full flex items-center justify-center cursor-pointer active-nav-item">
|
||||
<div class="w-10 h-10 rounded-xl bg-darkbg-700/50 group-hover:bg-cheese-500/20 text-cheese-400 flex items-center justify-center transition-all duration-300">
|
||||
<i class="ph ph-image text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div onclick="switchCategory('pdf')" id="nav-pdf" class="group relative w-full flex items-center justify-center cursor-pointer">
|
||||
<div class="w-10 h-10 rounded-xl group-hover:bg-darkbg-700 text-slate-400 group-hover:text-cheese-400 flex items-center justify-center transition-all duration-300">
|
||||
<i class="ph ph-file-pdf text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div onclick="switchCategory('excel')" id="nav-excel" class="group relative w-full flex items-center justify-center cursor-pointer">
|
||||
<div class="w-10 h-10 rounded-xl group-hover:bg-darkbg-700 text-slate-400 group-hover:text-cheese-400 flex items-center justify-center transition-all duration-300">
|
||||
<i class="ph ph-file-xls text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mt-auto">
|
||||
<div class="w-10 h-10 rounded-xl hover:bg-darkbg-700 text-slate-500 hover:text-white flex items-center justify-center transition-all cursor-pointer">
|
||||
<i class="ph ph-gear text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 2. 中间二级菜单 -->
|
||||
<div class="w-64 bg-darkbg-800/50 backdrop-blur-md border-r border-darkbg-700 flex flex-col z-10 flex-shrink-0 transition-all duration-300" id="secondary-sidebar">
|
||||
<div class="h-20 flex items-center px-6 border-b border-darkbg-700/50">
|
||||
<h2 class="text-xl font-semibold text-white tracking-wide" id="category-title">图片工具</h2>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<div class="relative group">
|
||||
<i class="ph ph-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 text-slate-500 group-focus-within:text-cheese-400 transition-colors"></i>
|
||||
<input type="text" placeholder="搜索功能..." class="w-full bg-darkbg-900 border border-darkbg-700 text-sm text-slate-200 rounded-lg pl-9 pr-3 py-2.5 focus:outline-none focus:border-cheese-500/50 focus:ring-1 focus:ring-cheese-500/50 transition-all placeholder-slate-600">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-3 pb-4 space-y-1" id="tool-list">
|
||||
<!-- 动态生成内容 -->
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-darkbg-700/50 bg-darkbg-800/30">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-500 to-indigo-500 flex items-center justify-center text-xs font-bold text-white shadow-inner">SV</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<p class="text-xs font-medium text-white truncate">超级会员</p>
|
||||
<p class="text-[10px] text-slate-400 truncate">有效期至 2026-10</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 右侧大区域 -->
|
||||
<main class="flex-1 bg-darkbg-900 relative flex flex-col h-full overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[10%] -right-[10%] w-[500px] h-[500px] bg-cheese-500/10 rounded-full blur-[100px]"></div>
|
||||
<div class="absolute top-[20%] left-[10%] w-[300px] h-[300px] bg-purple-500/5 rounded-full blur-[80px]"></div>
|
||||
</div>
|
||||
|
||||
<header class="h-20 flex items-center justify-between px-8 border-b border-darkbg-700/30 z-10 glass-panel bg-opacity-30 flex-shrink-0">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-2 text-xs text-slate-500 mb-1">
|
||||
<span id="breadcrumb-category">首页</span>
|
||||
<i class="ph ph-caret-right"></i>
|
||||
<span id="breadcrumb-tool" class="text-cheese-400">控制台</span>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-white flex items-center gap-2 animate-slide-up" id="workspace-title">欢迎回来</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button class="px-4 py-2 text-sm rounded-lg border border-darkbg-700 text-slate-300 hover:bg-darkbg-700 transition flex items-center gap-2">
|
||||
<i class="ph ph-clock-counter-clockwise"></i> 历史
|
||||
</button>
|
||||
<button class="px-4 py-2 text-sm rounded-lg bg-cheese-500 hover:bg-cheese-400 text-white shadow-lg shadow-cheese-500/20 transition transform hover:scale-105 flex items-center gap-2 font-medium">
|
||||
<i class="ph ph-export"></i> 导出
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 p-8 overflow-y-auto z-10 scroll-smooth" id="workspace-content">
|
||||
<!-- 默认欢迎页 -->
|
||||
<div class="h-full flex flex-col items-center justify-center text-center animate-fade-in">
|
||||
<div class="w-32 h-32 mb-6 rounded-full bg-darkbg-800 border border-darkbg-700 flex items-center justify-center shadow-2xl relative">
|
||||
<div class="absolute inset-0 rounded-full bg-cheese-500/20 blur-xl animate-pulse-slow"></div>
|
||||
<i class="ph-duotone ph-cheese text-6xl text-cheese-400"></i>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-white mb-3">开始你的创作</h2>
|
||||
<p class="text-slate-400 max-w-md mx-auto mb-8">
|
||||
从左侧选择一个工具。无论是压缩图片、拆分PDF还是处理数据,我们都能搞定。
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 w-full max-w-3xl">
|
||||
<div onclick="simulateClickTool('img-compress', 'image')" class="p-4 rounded-xl bg-darkbg-800/50 border border-darkbg-700 hover:border-cheese-500/50 hover:bg-darkbg-800 transition cursor-pointer group text-left">
|
||||
<div class="w-10 h-10 rounded-lg bg-blue-500/10 text-blue-400 flex items-center justify-center mb-3 group-hover:scale-110 transition"><i class="ph ph-image-square text-xl"></i></div>
|
||||
<h3 class="text-white font-medium mb-1">图片压缩</h3>
|
||||
<p class="text-xs text-slate-500">智能无损压缩</p>
|
||||
</div>
|
||||
<div onclick="simulateClickTool('pdf-split', 'pdf')" class="p-4 rounded-xl bg-darkbg-800/50 border border-darkbg-700 hover:border-cheese-500/50 hover:bg-darkbg-800 transition cursor-pointer group text-left">
|
||||
<div class="w-10 h-10 rounded-lg bg-red-500/10 text-red-400 flex items-center justify-center mb-3 group-hover:scale-110 transition"><i class="ph ph-scissors text-xl"></i></div>
|
||||
<h3 class="text-white font-medium mb-1">PDF 拆分</h3>
|
||||
<p class="text-xs text-slate-500">提取特定页面</p>
|
||||
</div>
|
||||
<div onclick="simulateClickTool('img-convert', 'image')" class="p-4 rounded-xl bg-darkbg-800/50 border border-darkbg-700 hover:border-cheese-500/50 hover:bg-darkbg-800 transition cursor-pointer group text-left">
|
||||
<div class="w-10 h-10 rounded-lg bg-green-500/10 text-green-400 flex items-center justify-center mb-3 group-hover:scale-110 transition"><i class="ph ph-arrows-left-right text-xl"></i></div>
|
||||
<h3 class="text-white font-medium mb-1">格式转换</h3>
|
||||
<p class="text-xs text-slate-500">格式互转工具</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const toolsData = {
|
||||
image: {
|
||||
title: "图片工具",
|
||||
items: [
|
||||
{ id: "img-compress", name: "图片压缩", icon: "ph-arrows-in-line-horizontal", desc: "智能无损压缩" },
|
||||
{ id: "img-convert", name: "格式转换", icon: "ph-arrows-left-right", desc: "JPG/PNG/WEBP" },
|
||||
{ id: "img-watermark", name: "图片加水印", icon: "ph-stamp", desc: "批量添加水印" },
|
||||
]
|
||||
},
|
||||
pdf: {
|
||||
title: "PDF 工具箱",
|
||||
items: [
|
||||
{ id: "pdf-split", name: "PDF 拆分", icon: "ph-scissors", desc: "提取指定页面" },
|
||||
{ id: "pdf-merge", name: "PDF 合并", icon: "ph-files", desc: "多文件合并" },
|
||||
{ id: "pdf-word", name: "PDF 转 Word", icon: "ph-microsoft-word-logo", desc: "保持排版转换" },
|
||||
]
|
||||
},
|
||||
excel: {
|
||||
title: "Excel 表格",
|
||||
items: [
|
||||
{ id: "xls-view", name: "Excel 预览", icon: "ph-eye", desc: "在线查看表格" },
|
||||
{ id: "xls-chart", name: "图表生成", icon: "ph-chart-bar", desc: "数据可视化" },
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
let currentCategory = 'image';
|
||||
let currentToolId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => { renderToolList('image'); });
|
||||
|
||||
function switchCategory(cat) {
|
||||
currentCategory = cat;
|
||||
document.querySelectorAll('.active-nav-item').forEach(el => {
|
||||
el.classList.remove('active-nav-item');
|
||||
el.querySelector('div').classList.remove('bg-darkbg-700/50', 'text-cheese-400');
|
||||
el.querySelector('div').classList.add('text-slate-400');
|
||||
});
|
||||
const activeBtn = document.getElementById(`nav-${cat}`);
|
||||
activeBtn.classList.add('active-nav-item');
|
||||
activeBtn.querySelector('div').classList.replace('text-slate-400', 'text-cheese-400');
|
||||
activeBtn.querySelector('div').classList.add('bg-darkbg-700/50');
|
||||
document.getElementById('category-title').innerText = toolsData[cat].title;
|
||||
renderToolList(cat);
|
||||
const sidebar = document.getElementById('secondary-sidebar');
|
||||
sidebar.classList.remove('opacity-100'); sidebar.classList.add('opacity-0');
|
||||
setTimeout(() => { sidebar.classList.remove('opacity-0'); sidebar.classList.add('opacity-100'); }, 50);
|
||||
}
|
||||
|
||||
function renderToolList(cat) {
|
||||
const listContainer = document.getElementById('tool-list');
|
||||
listContainer.innerHTML = '';
|
||||
toolsData[cat].items.forEach(tool => {
|
||||
const item = document.createElement('div');
|
||||
item.className = `p-3 rounded-lg cursor-pointer transition-all duration-200 group hover:bg-darkbg-700/50 ${currentToolId === tool.id ? 'bg-darkbg-700 border-l-2 border-cheese-400' : ''}`;
|
||||
item.onclick = () => selectTool(tool);
|
||||
item.id = `tool-item-${tool.id}`;
|
||||
item.innerHTML = `
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-md bg-darkbg-900 border border-darkbg-700 flex items-center justify-center text-slate-400 group-hover:text-cheese-400 group-hover:border-cheese-500/30 transition-colors">
|
||||
<i class="ph ${tool.icon} text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-slate-200 group-hover:text-white">${tool.name}</h4>
|
||||
<p class="text-[10px] text-slate-500 truncate w-32">${tool.desc}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
listContainer.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function selectTool(tool) {
|
||||
currentToolId = tool.id;
|
||||
renderToolList(currentCategory);
|
||||
const workspace = document.getElementById('workspace-content');
|
||||
document.getElementById('breadcrumb-category').innerText = toolsData[currentCategory].title;
|
||||
document.getElementById('breadcrumb-tool').innerText = tool.name;
|
||||
document.getElementById('workspace-title').innerHTML = `${tool.name} <span class="text-sm font-normal text-slate-500 ml-2 bg-darkbg-800 px-2 py-0.5 rounded border border-darkbg-700">v2.1</span>`;
|
||||
|
||||
workspace.innerHTML = `<div class="h-full flex items-center justify-center"><i class="ph ph-spinner animate-spin text-4xl text-cheese-400"></i></div>`;
|
||||
setTimeout(() => {
|
||||
let contentHTML = '';
|
||||
// 现在所有功能都先生成头部,再生成各自独特的内容
|
||||
if (tool.id === 'img-compress') contentHTML = renderCompressUI(tool);
|
||||
else if (tool.id === 'img-convert') contentHTML = renderConvertUI(tool);
|
||||
else if (tool.id === 'pdf-split') contentHTML = renderPDFSplitUI(tool);
|
||||
else if (tool.id === 'pdf-merge') contentHTML = renderPDFMergeUI(tool);
|
||||
else contentHTML = renderDefaultUI(tool);
|
||||
|
||||
workspace.innerHTML = `<div class="animate-slide-up max-w-6xl mx-auto flex flex-col gap-6">${contentHTML}</div>`;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function simulateClickTool(toolId, cat) {
|
||||
switchCategory(cat);
|
||||
setTimeout(() => {
|
||||
const tool = toolsData[cat].items.find(t => t.id === toolId);
|
||||
if (tool) selectTool(tool);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// ==================== 统一的上传区域组件 ====================
|
||||
function getUploadAreaHTML(toolName) {
|
||||
return `
|
||||
<div class="w-full h-52 border-2 border-dashed border-darkbg-700 rounded-2xl bg-darkbg-800/30 flex flex-col items-center justify-center hover:border-cheese-500/50 hover:bg-darkbg-800/50 transition-all cursor-pointer group relative overflow-hidden flex-shrink-0">
|
||||
<div class="absolute inset-0 bg-gradient-to-tr from-cheese-500/5 to-transparent opacity-0 group-hover:opacity-100 transition duration-500"></div>
|
||||
<div class="z-10 flex flex-col items-center">
|
||||
<div class="w-14 h-14 rounded-full bg-darkbg-700 flex items-center justify-center mb-3 group-hover:scale-110 transition duration-300 shadow-lg">
|
||||
<i class="ph ph-cloud-arrow-up text-3xl text-cheese-400"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-white mb-1">点击或拖拽文件到此处</h3>
|
||||
<p class="text-sm text-slate-500">支持批量上传 (${toolName})</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/* ==================== 1. 图片压缩 UI ==================== */
|
||||
function renderCompressUI(tool) {
|
||||
// 下方功能区:左侧预览,右侧设置
|
||||
const specificContent = `
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 flex-1">
|
||||
<!-- 预览区 -->
|
||||
<div class="lg:col-span-2 bg-darkbg-800/50 border border-darkbg-700 rounded-2xl p-4 flex items-center justify-center relative min-h-[300px]">
|
||||
<div class="text-center">
|
||||
<i class="ph ph-image text-5xl text-slate-600 mb-3"></i>
|
||||
<p class="text-slate-400 text-sm">上传图片后在此处预览效果</p>
|
||||
</div>
|
||||
<span class="absolute top-4 left-4 bg-black/50 text-white text-xs px-2 py-1 rounded backdrop-blur">原始预览</span>
|
||||
</div>
|
||||
|
||||
<!-- 设置区 -->
|
||||
<div class="bg-darkbg-800 border border-darkbg-700 rounded-2xl p-6 h-fit">
|
||||
<h3 class="font-bold text-white mb-4 flex items-center gap-2"><i class="ph ph-sliders"></i> 压缩设置</h3>
|
||||
<div class="mb-6">
|
||||
<div class="flex justify-between mb-2">
|
||||
<span class="text-sm text-slate-300">压缩强度</span>
|
||||
<span class="text-sm text-cheese-400 font-bold">75%</span>
|
||||
</div>
|
||||
<input type="range" class="w-full h-2 bg-darkbg-900 rounded-lg appearance-none cursor-pointer accent-cheese-500" value="75">
|
||||
</div>
|
||||
<button class="w-full py-3 bg-cheese-500 hover:bg-cheese-400 text-white rounded-xl font-bold shadow-lg shadow-cheese-500/20 flex items-center justify-center gap-2">
|
||||
<i class="ph ph-download-simple"></i> 开始压缩
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return getUploadAreaHTML(tool.name) + specificContent;
|
||||
}
|
||||
|
||||
/* ==================== 2. 格式转换 UI ==================== */
|
||||
function renderConvertUI(tool) {
|
||||
const formats = ['JPG', 'PNG', 'WEBP', 'PDF', 'ICO'];
|
||||
const specificContent = `
|
||||
<div class="bg-darkbg-800/50 backdrop-blur border border-darkbg-700 rounded-2xl p-8 text-center mt-2">
|
||||
<h3 class="text-lg text-white mb-6">选择目标格式</h3>
|
||||
<div class="flex flex-wrap justify-center gap-4 mb-8">
|
||||
${formats.map(fmt => `
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="format" class="peer sr-only" ${fmt === 'WEBP' ? 'checked' : ''}>
|
||||
<div class="px-6 py-3 rounded-xl border border-darkbg-600 bg-darkbg-900 text-slate-400 hover:bg-darkbg-700 peer-checked:bg-cheese-500 peer-checked:text-white peer-checked:border-cheese-500 peer-checked:shadow-lg transition-all text-sm font-medium">
|
||||
${fmt}
|
||||
</div>
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="border-t border-darkbg-700 pt-6 flex justify-end">
|
||||
<button class="px-8 py-2.5 bg-white text-darkbg-900 rounded-lg font-bold hover:bg-slate-200 transition">开始转换</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return getUploadAreaHTML(tool.name) + specificContent;
|
||||
}
|
||||
|
||||
/* ==================== 3. PDF 拆分 UI ==================== */
|
||||
function renderPDFSplitUI(tool) {
|
||||
let pagesHTML = '';
|
||||
for(let i=1; i<=12; i++) {
|
||||
pagesHTML += `
|
||||
<div class="relative group cursor-pointer" onclick="this.querySelector('.page-card').classList.toggle('pdf-page-selected')">
|
||||
<div class="page-card aspect-[1/1.4] bg-white rounded-lg border-2 border-transparent hover:shadow-lg transition-all relative overflow-hidden flex items-center justify-center">
|
||||
<div class="text-xs text-slate-400">Page ${i}</div>
|
||||
<div class="absolute inset-0 bg-cheese-500/10 hidden group-hover:block"></div>
|
||||
<div class="check-icon absolute top-2 right-2 w-5 h-5 bg-cheese-500 rounded-full flex items-center justify-center text-white text-xs opacity-0 transition-opacity shadow"><i class="ph-bold ph-check"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const specificContent = `
|
||||
<div class="flex flex-col h-full mt-2">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-bold text-white">选择页面</h3>
|
||||
<div class="flex gap-3">
|
||||
<button class="text-xs text-cheese-400 hover:text-cheese-300">全选</button>
|
||||
<button class="text-xs text-slate-400 hover:text-white">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-darkbg-800/30 border border-darkbg-700 rounded-2xl p-6 overflow-y-auto max-h-[400px]">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 gap-6">
|
||||
${pagesHTML}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button class="px-6 py-2 bg-cheese-500 hover:bg-cheese-400 text-white rounded-lg font-bold shadow-lg shadow-cheese-500/20">拆分选定页</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return getUploadAreaHTML(tool.name) + specificContent;
|
||||
}
|
||||
|
||||
/* ==================== 4. PDF 合并 UI ==================== */
|
||||
function renderPDFMergeUI(tool) {
|
||||
const files = [
|
||||
{ name: '2023年度财务报表.pdf', size: '2.4 MB' },
|
||||
{ name: 'Q4 销售数据分析.pdf', size: '1.1 MB' },
|
||||
];
|
||||
|
||||
const specificContent = `
|
||||
<div class="mt-4">
|
||||
<h3 class="text-lg font-bold text-white mb-4">待合并文件列表 (可拖拽排序)</h3>
|
||||
<div class="space-y-3 mb-8">
|
||||
${files.map(file => `
|
||||
<div class="bg-darkbg-800 border border-darkbg-700 rounded-lg p-3 flex items-center gap-4 hover:border-cheese-500/30 transition group">
|
||||
<div class="cursor-grab text-slate-600 hover:text-slate-300 px-2"><i class="ph ph-dots-six-vertical text-xl"></i></div>
|
||||
<div class="w-10 h-10 bg-red-500/10 rounded text-red-400 flex items-center justify-center"><i class="ph ph-file-pdf text-xl"></i></div>
|
||||
<div class="flex-1"><h4 class="text-sm text-slate-200 font-medium">${file.name}</h4><p class="text-xs text-slate-500">${file.size}</p></div>
|
||||
<button class="p-2 hover:bg-darkbg-700 rounded text-slate-400 hover:text-red-400"><i class="ph ph-trash"></i></button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<button class="w-full py-3 bg-cheese-500 hover:bg-cheese-400 text-white rounded-xl font-bold text-lg shadow-lg shadow-cheese-500/20">合并为单个 PDF</button>
|
||||
</div>
|
||||
`;
|
||||
return getUploadAreaHTML(tool.name) + specificContent;
|
||||
}
|
||||
|
||||
/* ==================== 5. 默认 UI ==================== */
|
||||
function renderDefaultUI(tool) {
|
||||
return getUploadAreaHTML(tool.name);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
31
build.bat
Normal file
@@ -0,0 +1,31 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo.
|
||||
echo ╔══════════════════════════════════════════════╗
|
||||
echo ║ 奶酪云工具箱 - Windows 打包 ║
|
||||
echo ╚══════════════════════════════════════════════╝
|
||||
echo.
|
||||
|
||||
:: 检查 Python
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ❌ 未找到 Python,请先安装 Python 3.8+
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 安装依赖
|
||||
echo 📦 安装/更新依赖...
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
|
||||
:: 执行打包
|
||||
echo.
|
||||
echo 🔨 开始打包...
|
||||
python build_app.py --platform current
|
||||
|
||||
echo.
|
||||
echo ✅ 打包完成!
|
||||
echo 📁 输出目录: dist\
|
||||
pause
|
||||
|
||||
28
build.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════╗"
|
||||
echo "║ 奶酪云工具箱 - macOS/Linux 打包 ║"
|
||||
echo "╚══════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# 检查 Python
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "❌ 未找到 Python3,请先安装"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 安装依赖
|
||||
echo "📦 安装/更新依赖..."
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install pyinstaller
|
||||
|
||||
# 执行打包
|
||||
echo ""
|
||||
echo "🔨 开始打包..."
|
||||
python3 build_app.py --platform current
|
||||
|
||||
echo ""
|
||||
echo "✅ 打包完成!"
|
||||
echo "📁 输出目录: dist/"
|
||||
|
||||
46
build/CheeseCloudTools.spec
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['D:\\workers\\project\\ai\\nly-tool\\main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('D:\\workers\\project\\ai\\nly-tool\\resources', 'resources'), ('D:\\workers\\project\\ai\\nly-tool\\image', 'image')],
|
||||
hiddenimports=['PySide6.QtSvg', 'PySide6.QtSvgWidgets', 'PIL', 'PIL.Image', 'fitz', 'pandas', 'openpyxl', 'matplotlib', 'matplotlib.backends.backend_qtagg'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='CheeseCloudTools',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
version='D:\\workers\\project\\ai\\nly-tool\\version_info.txt',
|
||||
icon=['D:\\workers\\project\\ai\\nly-tool\\build_icons\\app.ico'],
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='CheeseCloudTools',
|
||||
)
|
||||
13648
build/win64/CheeseCloudTools/Analysis-00.toc
Normal file
6493
build/win64/CheeseCloudTools/COLLECT-00.toc
Normal file
BIN
build/win64/CheeseCloudTools/CheeseCloudTools.exe
Normal file
BIN
build/win64/CheeseCloudTools/CheeseCloudTools.pkg
Normal file
84
build/win64/CheeseCloudTools/EXE-00.toc
Normal file
@@ -0,0 +1,84 @@
|
||||
('D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\CheeseCloudTools.exe',
|
||||
False,
|
||||
False,
|
||||
True,
|
||||
['D:\\workers\\project\\ai\\nly-tool\\build_icons\\app.ico'],
|
||||
versioninfo.VSVersionInfo(ffi=versioninfo.FixedFileInfo(filevers=(1, 0, 0, 0), prodvers=(1, 0, 0, 0), mask=0x3f, flags=0x0, OS=0x40004, fileType=1, subtype=0x0, date=(0, 0)), kids=[versioninfo.StringFileInfo([versioninfo.StringTable('080404b0', [versioninfo.StringStruct('CompanyName', '奶酪源码'), versioninfo.StringStruct('FileDescription', '多功能图片/PDF/Excel处理工具'), versioninfo.StringStruct('FileVersion', '1.0.0'), versioninfo.StringStruct('InternalName', 'CheeseCloudTools'), versioninfo.StringStruct('LegalCopyright', 'Copyright (C) 2024 奶酪源码'), versioninfo.StringStruct('OriginalFilename', 'CheeseCloudTools.exe'), versioninfo.StringStruct('ProductName', '奶酪云工具箱'), versioninfo.StringStruct('ProductVersion', '1.0.0')])]), versioninfo.VarFileInfo([versioninfo.VarStruct('Translation', [2052, 1200])])]),
|
||||
False,
|
||||
False,
|
||||
b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<assembly xmlns='
|
||||
b'"urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">\n <trustInfo x'
|
||||
b'mlns="urn:schemas-microsoft-com:asm.v3">\n <security>\n <requested'
|
||||
b'Privileges>\n <requestedExecutionLevel level="asInvoker" uiAccess='
|
||||
b'"false"/>\n </requestedPrivileges>\n </security>\n </trustInfo>\n '
|
||||
b'<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">\n <'
|
||||
b'application>\n <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f'
|
||||
b'0}"/>\n <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>\n '
|
||||
b' <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>\n <s'
|
||||
b'upportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>\n <supporte'
|
||||
b'dOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>\n </application>\n <'
|
||||
b'/compatibility>\n <application xmlns="urn:schemas-microsoft-com:asm.v3">'
|
||||
b'\n <windowsSettings>\n <longPathAware xmlns="http://schemas.micros'
|
||||
b'oft.com/SMI/2016/WindowsSettings">true</longPathAware>\n </windowsSett'
|
||||
b'ings>\n </application>\n <dependency>\n <dependentAssembly>\n <ass'
|
||||
b'emblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version='
|
||||
b'"6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" langua'
|
||||
b'ge="*"/>\n </dependentAssembly>\n </dependency>\n</assembly>',
|
||||
True,
|
||||
False,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\CheeseCloudTools.pkg',
|
||||
[('pyi-contents-directory _internal', '', 'OPTION'),
|
||||
('PYZ-00.pyz',
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\PYZ-00.pyz',
|
||||
'PYZ'),
|
||||
('struct',
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\localpycs\\struct.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod01_archive',
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\localpycs\\pyimod01_archive.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod02_importers',
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\localpycs\\pyimod02_importers.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod03_ctypes',
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\localpycs\\pyimod03_ctypes.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod04_pywin32',
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\localpycs\\pyimod04_pywin32.pyc',
|
||||
'PYMODULE'),
|
||||
('pyiboot01_bootstrap',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_inspect',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_pkgutil',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_multiprocessing',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_pyside6',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pyside6.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_mplconfig',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_mplconfig.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_setuptools',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_setuptools.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth__tkinter',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth__tkinter.py',
|
||||
'PYSOURCE'),
|
||||
('main', 'D:\\workers\\project\\ai\\nly-tool\\main.py', 'PYSOURCE')],
|
||||
[],
|
||||
False,
|
||||
False,
|
||||
1765416156,
|
||||
[('runw.exe',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\bootloader\\Windows-64bit-intel\\runw.exe',
|
||||
'EXECUTABLE')],
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\python312.dll')
|
||||
62
build/win64/CheeseCloudTools/PKG-00.toc
Normal file
@@ -0,0 +1,62 @@
|
||||
('D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\CheeseCloudTools.pkg',
|
||||
{'BINARY': True,
|
||||
'DATA': True,
|
||||
'EXECUTABLE': True,
|
||||
'EXTENSION': True,
|
||||
'PYMODULE': True,
|
||||
'PYSOURCE': True,
|
||||
'PYZ': False,
|
||||
'SPLASH': True,
|
||||
'SYMLINK': False},
|
||||
[('pyi-contents-directory _internal', '', 'OPTION'),
|
||||
('PYZ-00.pyz',
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\PYZ-00.pyz',
|
||||
'PYZ'),
|
||||
('struct',
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\localpycs\\struct.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod01_archive',
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\localpycs\\pyimod01_archive.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod02_importers',
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\localpycs\\pyimod02_importers.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod03_ctypes',
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\localpycs\\pyimod03_ctypes.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod04_pywin32',
|
||||
'D:\\workers\\project\\ai\\nly-tool\\build\\win64\\CheeseCloudTools\\localpycs\\pyimod04_pywin32.pyc',
|
||||
'PYMODULE'),
|
||||
('pyiboot01_bootstrap',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_inspect',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_pkgutil',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_multiprocessing',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_pyside6',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pyside6.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_mplconfig',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_mplconfig.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_setuptools',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_setuptools.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth__tkinter',
|
||||
'C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth__tkinter.py',
|
||||
'PYSOURCE'),
|
||||
('main', 'D:\\workers\\project\\ai\\nly-tool\\main.py', 'PYSOURCE')],
|
||||
'python312.dll',
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
None,
|
||||
None,
|
||||
None)
|
||||
BIN
build/win64/CheeseCloudTools/PYZ-00.pyz
Normal file
6596
build/win64/CheeseCloudTools/PYZ-00.toc
Normal file
BIN
build/win64/CheeseCloudTools/base_library.zip
Normal file
400
build/win64/CheeseCloudTools/warn-CheeseCloudTools.txt
Normal file
@@ -0,0 +1,400 @@
|
||||
|
||||
This file lists modules PyInstaller was not able to find. This does not
|
||||
necessarily mean this module is required for running your program. Python and
|
||||
Python 3rd-party packages include a lot of conditional or optional modules. For
|
||||
example the module 'ntpath' only exists on Windows, whereas the module
|
||||
'posixpath' only exists on Posix systems.
|
||||
|
||||
Types if import:
|
||||
* top-level: imported at the top-level - look at these first
|
||||
* conditional: imported within an if-statement
|
||||
* delayed: imported within a function
|
||||
* optional: imported within a try-except-statement
|
||||
|
||||
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
|
||||
tracking down the missing module yourself. Thanks!
|
||||
|
||||
missing module named urllib.urlopen - imported by urllib (delayed, optional), lxml.html (delayed, optional)
|
||||
missing module named urllib.urlencode - imported by urllib (delayed, optional), lxml.html (delayed, optional)
|
||||
missing module named pwd - imported by posixpath (delayed, conditional, optional), subprocess (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), http.server (delayed, optional), netrc (delayed, conditional), getpass (delayed), setuptools._distutils.util (delayed, conditional, optional), setuptools._vendor.backports.tarfile (optional), setuptools._distutils.archive_util (optional)
|
||||
missing module named grp - imported by subprocess (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), setuptools._vendor.backports.tarfile (optional), setuptools._distutils.archive_util (optional)
|
||||
missing module named posix - imported by os (conditional, optional), posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional)
|
||||
missing module named resource - imported by posix (top-level)
|
||||
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
|
||||
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
|
||||
missing module named _posixsubprocess - imported by subprocess (conditional), multiprocessing.util (delayed)
|
||||
missing module named fcntl - imported by subprocess (optional), fire.console.console_attr_os (delayed)
|
||||
missing module named _posixshmem - imported by multiprocessing.resource_tracker (conditional), multiprocessing.shared_memory (conditional)
|
||||
missing module named multiprocessing.set_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
|
||||
missing module named multiprocessing.get_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
|
||||
missing module named multiprocessing.get_context - imported by multiprocessing (top-level), multiprocessing.pool (top-level), multiprocessing.managers (top-level), multiprocessing.sharedctypes (top-level)
|
||||
missing module named multiprocessing.TimeoutError - imported by multiprocessing (top-level), multiprocessing.pool (top-level)
|
||||
missing module named _scproxy - imported by urllib.request (conditional)
|
||||
missing module named termios - imported by tty (top-level), getpass (optional), fire.console.console_attr_os (delayed)
|
||||
missing module named multiprocessing.BufferTooShort - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
|
||||
missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
|
||||
missing module named multiprocessing.cpu_count - imported by multiprocessing (top-level), pdf2docx.converter (top-level)
|
||||
missing module named multiprocessing.Pool - imported by multiprocessing (delayed, conditional), scipy._lib._util (delayed, conditional), pdf2docx.converter (top-level)
|
||||
missing module named asyncio.DefaultEventLoopPolicy - imported by asyncio (delayed, conditional), asyncio.events (delayed, conditional)
|
||||
missing module named annotationlib - imported by typing_extensions (conditional)
|
||||
missing module named vms_lib - imported by platform (delayed, optional)
|
||||
missing module named 'java.lang' - imported by platform (delayed, optional)
|
||||
missing module named java - imported by platform (delayed)
|
||||
missing module named _winreg - imported by platform (delayed, optional)
|
||||
missing module named usercustomize - imported by site (delayed, optional)
|
||||
missing module named sitecustomize - imported by site (delayed, optional)
|
||||
missing module named readline - imported by cmd (delayed, conditional, optional), code (delayed, conditional, optional), pdb (delayed, optional), site (delayed, optional), rlcompleter (optional)
|
||||
missing module named _typeshed - imported by numpy.random.bit_generator (top-level), setuptools._distutils.dist (conditional), setuptools.glob (conditional), setuptools.compat.py311 (conditional)
|
||||
missing module named _manylinux - imported by packaging._manylinux (delayed, optional), setuptools._vendor.packaging._manylinux (delayed, optional), setuptools._vendor.wheel.vendored.packaging._manylinux (delayed, optional)
|
||||
missing module named importlib_resources - imported by setuptools._vendor.jaraco.text (optional)
|
||||
missing module named trove_classifiers - imported by setuptools.config._validate_pyproject.formats (optional)
|
||||
missing module named pyimod02_importers - imported by C:\Users\Lenovo\AppData\Local\Programs\Python\Python312\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgutil.py (delayed)
|
||||
missing module named 'tornado.template' - imported by matplotlib.backends.backend_webagg (delayed)
|
||||
missing module named jinja2 - imported by pyparsing.diagram (top-level), pandas.io.formats.style (top-level)
|
||||
missing module named railroad - imported by pyparsing.diagram (top-level)
|
||||
missing module named pyparsing.Word - imported by pyparsing (delayed), pyparsing.unicode (delayed)
|
||||
missing module named _dummy_thread - imported by numpy._core.arrayprint (optional), cffi.lock (conditional, optional)
|
||||
missing module named numpy._core.void - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.vecmat - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ushort - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.unsignedinteger - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ulonglong - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ulong - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.uintp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.uintc - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.uint64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.uint32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.uint16 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.uint - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ubyte - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.trunc - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.true_divide - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.timedelta64 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.tanh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.tan - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.subtract - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.str_ - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.square - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.spacing - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.sinh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.signedinteger - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.short - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.rint - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.right_shift - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.remainder - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.radians - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.rad2deg - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.power - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.positive - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.pi - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.not_equal - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.negative - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.modf - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.mod - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.minimum - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.maximum - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.matvec - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.longdouble - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.long - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logical_xor - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logical_or - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logical_not - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logical_and - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logaddexp2 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logaddexp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.log2 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.log1p - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.log - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.less_equal - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.less - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.left_shift - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ldexp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.lcm - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.integer - imported by numpy._core (conditional), numpy (conditional), numpy.fft._helper (top-level)
|
||||
missing module named numpy._core.int8 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.int64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.int32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.int16 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.hypot - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.heaviside - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.half - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.greater_equal - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.greater - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.gcd - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.frompyfunc - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.frexp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.fmod - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.fmin - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.fmax - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.floor_divide - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.floor - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.floating - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.float_power - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.float16 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.fabs - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.expm1 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.exp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.euler_gamma - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.equal - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.e - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.divmod - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.degrees - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.deg2rad - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.datetime64 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.cosh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.cos - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.copysign - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.conjugate - imported by numpy._core (conditional), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.conj - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.complex64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.clongdouble - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.character - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ceil - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.cbrt - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bytes_ - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.byte - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bool_ - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bitwise_xor - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bitwise_or - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bitwise_count - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bitwise_and - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arctanh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arctan2 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arctan - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arcsinh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arcsin - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arccosh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arccos - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ones - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.hstack - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.atleast_1d - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.atleast_3d - imported by numpy._core (top-level), numpy.lib._shape_base_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.vstack - imported by numpy._core (top-level), numpy.lib._shape_base_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.linspace - imported by numpy._core (top-level), numpy.lib._index_tricks_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.result_type - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.number - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.max - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.array2string - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.signbit - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.isscalar - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.isnat - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional)
|
||||
missing module named numpy._core.array_repr - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional)
|
||||
missing module named numpy._core.arange - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy.fft._helper (top-level)
|
||||
missing module named numpy._core.float32 - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.vecdot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.matrix_transpose - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.matmul - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.tensordot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.outer - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.cross - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.trace - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.diagonal - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.reciprocal - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.sort - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.argsort - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.sign - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.isnan - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.count_nonzero - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.divide - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.swapaxes - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.object_ - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.asanyarray - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.intp - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.atleast_2d - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.prod - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.amax - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.amin - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.moveaxis - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.errstate - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.finfo - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.isfinite - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.sum - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.sqrt - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.multiply - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.add - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.dot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.inf - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.all - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.newaxis - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.complexfloating - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.inexact - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.cdouble - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.csingle - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.double - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.single - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.intc - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.empty_like - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.empty - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy.fft._helper (top-level)
|
||||
missing module named numpy._core.zeros - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.array - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.iinfo - imported by numpy._core (top-level), numpy.lib._twodim_base_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.transpose - imported by numpy._core (top-level), numpy.lib._function_base_impl (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.ndarray - imported by numpy._core (top-level), numpy.lib._utils_impl (top-level), numpy.testing._private.utils (top-level), numpy (conditional)
|
||||
missing module named numpy._core.asarray - imported by numpy._core (top-level), numpy.lib._array_utils_impl (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level), numpy.fft._helper (top-level)
|
||||
missing module named threadpoolctl - imported by numpy.lib._utils_impl (delayed, optional)
|
||||
missing module named psutil - imported by numpy.testing._private.utils (delayed, optional), scipy._lib._testutils (delayed, optional)
|
||||
missing module named six.moves.range - imported by six.moves (top-level), dateutil.rrule (top-level)
|
||||
runtime module named six.moves - imported by dateutil.tz.tz (top-level), dateutil.tz._factories (top-level), dateutil.tz.win (top-level), dateutil.rrule (top-level)
|
||||
missing module named StringIO - imported by six (conditional)
|
||||
missing module named dateutil.tz.tzfile - imported by dateutil.tz (top-level), dateutil.zoneinfo (top-level)
|
||||
missing module named gi - imported by matplotlib.cbook (delayed, conditional)
|
||||
missing module named numpy.VisibleDeprecationWarning - imported by numpy (conditional), scipy._lib._util (conditional), matplotlib.cbook (optional)
|
||||
missing module named numpy.ComplexWarning - imported by numpy (conditional), scipy._lib._util (conditional)
|
||||
missing module named numpy.AxisError - imported by numpy (conditional), scipy._lib._util (conditional)
|
||||
missing module named 'numpy_distutils.cpuinfo' - imported by numpy.f2py.diagnose (delayed, conditional, optional)
|
||||
missing module named 'numpy_distutils.fcompiler' - imported by numpy.f2py.diagnose (delayed, conditional, optional)
|
||||
missing module named 'numpy_distutils.command' - imported by numpy.f2py.diagnose (delayed, conditional, optional)
|
||||
missing module named numpy_distutils - imported by numpy.f2py.diagnose (delayed, optional)
|
||||
missing module named numpy.random.RandomState - imported by numpy.random (top-level), numpy.random._generator (top-level)
|
||||
missing module named yaml - imported by numpy.__config__ (delayed), scipy.__config__ (delayed)
|
||||
missing module named numpy._distributor_init_local - imported by numpy (optional), numpy._distributor_init (optional)
|
||||
missing module named 'tornado.websocket' - imported by matplotlib.backends.backend_webagg (optional)
|
||||
missing module named 'tornado.ioloop' - imported by matplotlib.backends.backend_webagg (optional)
|
||||
missing module named tornado - imported by matplotlib.backends.backend_webagg (optional), matplotlib.backends.backend_webagg_core (delayed)
|
||||
missing module named shiboken2 - imported by matplotlib.backends.qt_compat (delayed, conditional, optional)
|
||||
missing module named sip - imported by matplotlib.backends.qt_compat (delayed, conditional)
|
||||
missing module named setuptools_scm - imported by matplotlib (delayed, conditional, optional)
|
||||
missing module named 'defusedxml.ElementTree' - imported by openpyxl.xml.functions (conditional)
|
||||
missing module named htmlentitydefs - imported by lxml.html.soupparser (optional)
|
||||
missing module named BeautifulSoup - imported by lxml.html.soupparser (optional)
|
||||
missing module named bs4 - imported by pandas.io.html (delayed), lxml.html.soupparser (optional)
|
||||
missing module named urlparse - imported by lxml.ElementInclude (optional), lxml.html.html5parser (optional)
|
||||
missing module named urllib2 - imported by lxml.ElementInclude (optional), lxml.html.html5parser (optional)
|
||||
missing module named 'html5lib.treebuilders' - imported by lxml.html.html5parser (top-level)
|
||||
missing module named html5lib - imported by lxml.html._html5builder (top-level), lxml.html.html5parser (top-level)
|
||||
missing module named lxml_html_clean - imported by lxml.html.clean (optional)
|
||||
missing module named cssselect - imported by lxml.cssselect (optional)
|
||||
missing module named openpyxl.tests - imported by openpyxl.reader.excel (optional)
|
||||
missing module named defusedxml - imported by PIL.Image (optional), openpyxl.xml (delayed, optional)
|
||||
missing module named numexpr - imported by pandas.core.computation.expressions (conditional), pandas.core.computation.engines (delayed)
|
||||
missing module named numba - imported by pandas.core._numba.executor (delayed, conditional), pandas.core.util.numba_ (delayed, conditional), pandas.core.window.numba_ (delayed, conditional), pandas.core.window.online (delayed, conditional), pandas.core._numba.kernels.mean_ (top-level), pandas.core._numba.kernels.shared (top-level), pandas.core._numba.kernels.sum_ (top-level), pandas.core._numba.kernels.min_max_ (top-level), pandas.core._numba.kernels.var_ (top-level), pandas.core.groupby.numba_ (delayed, conditional), pandas.core._numba.extensions (top-level)
|
||||
missing module named 'numba.extending' - imported by pandas.core._numba.kernels.sum_ (top-level)
|
||||
missing module named 'pyarrow.compute' - imported by pandas.core.arrays._arrow_string_mixins (conditional), pandas.core.arrays.string_arrow (conditional), pandas.core.reshape.merge (delayed, conditional), pandas.core.arrays.arrow.array (conditional), pandas.core.arrays.arrow.accessors (conditional)
|
||||
missing module named 'numba.typed' - imported by pandas.core._numba.extensions (delayed)
|
||||
missing module named 'numba.core' - imported by pandas.core._numba.extensions (top-level)
|
||||
missing module named pyarrow - imported by pandas.core.arrays._arrow_string_mixins (conditional), pandas.core.arrays.masked (delayed), pandas.core.arrays.boolean (delayed, conditional), pandas.core.arrays.numeric (delayed, conditional), pandas.core.arrays.arrow._arrow_utils (top-level), pandas.core.interchange.utils (delayed, conditional), pandas.core.strings.accessor (delayed, conditional), pandas.io._util (conditional), pandas.io.parsers.base_parser (delayed, conditional), pandas.core.arrays.interval (delayed), pandas.core.arrays.arrow.extension_types (top-level), pandas.core.arrays.period (delayed), pandas.core.methods.describe (delayed, conditional), pandas.io.sql (delayed, conditional), pandas.core.arrays.string_arrow (conditional), pandas.core.reshape.merge (delayed, conditional), pandas.core.arrays.arrow.array (conditional), pandas.core.interchange.buffer (conditional), pandas.io.feather_format (delayed), pandas.core.indexes.base (delayed, conditional), pandas.core.dtypes.cast (delayed, conditional), pandas.core.arrays.string_ (delayed, conditional), pandas.core.arrays.arrow.accessors (conditional), pandas.core.dtypes.dtypes (delayed, conditional), pandas.compat.pyarrow (optional), pandas.core.reshape.encoding (delayed, conditional), pandas._testing (conditional)
|
||||
missing module named pytest - imported by scipy._lib._testutils (delayed), pandas._testing._io (delayed), pandas._testing (delayed)
|
||||
missing module named cupy_backends - imported by scipy._lib.array_api_compat.common._helpers (delayed)
|
||||
missing module named 'cupy.cuda' - imported by scipy._lib.array_api_compat.cupy._typing (top-level), scipy._lib.array_api_compat.common._helpers (delayed)
|
||||
missing module named 'jax.experimental' - imported by scipy._lib.array_api_compat.common._helpers (delayed, conditional)
|
||||
missing module named 'jax.numpy' - imported by scipy._lib.array_api_compat.common._helpers (delayed, conditional)
|
||||
missing module named 'dask.array' - imported by scipy._lib.array_api_compat.dask.array (top-level), scipy._lib.array_api_compat.dask.array._aliases (top-level), scipy._lib.array_api_compat.common._helpers (delayed, conditional)
|
||||
missing module named sparse - imported by scipy._lib.array_api_compat.common._helpers (delayed, conditional), scipy.sparse.linalg._expm_multiply (delayed, conditional), scipy.sparse.linalg._matfuncs (delayed, conditional)
|
||||
missing module named dask - imported by scipy._lib.array_api_compat.common._helpers (delayed)
|
||||
missing module named ndonnx - imported by scipy._lib.array_api_compat.common._helpers (delayed)
|
||||
missing module named torch - imported by scipy._lib.array_api_compat.common._helpers (delayed, conditional), scipy._lib.array_api_compat.torch (top-level), scipy._lib.array_api_compat.torch._info (top-level), scipy._lib.array_api_compat.torch._aliases (top-level), scipy._lib._array_api (delayed, conditional)
|
||||
missing module named cupy - imported by scipy._lib.array_api_compat.common._helpers (delayed, conditional), scipy._lib.array_api_compat.cupy (top-level), scipy._lib.array_api_compat.cupy._aliases (top-level), scipy._lib.array_api_compat.cupy._info (top-level), scipy._lib.array_api_compat.cupy._typing (top-level), scipy._lib._array_api (delayed, conditional)
|
||||
missing module named jax - imported by scipy._lib.array_api_compat.common._helpers (delayed), scipy._lib._array_api (delayed, conditional)
|
||||
missing module named Cython - imported by scipy._lib._testutils (optional)
|
||||
missing module named cython - imported by fontTools.varLib.iup (optional), fontTools.misc.bezierTools (optional), scipy._lib._testutils (optional)
|
||||
missing module named sphinx - imported by scipy._lib._docscrape (delayed, conditional)
|
||||
missing module named cupyx - imported by scipy._lib._array_api (delayed, conditional)
|
||||
missing module named scipy.sparse.issparse - imported by scipy.sparse (delayed), scipy._lib._array_api (delayed), scipy.sparse.linalg._interface (top-level), scipy.optimize._numdiff (top-level), scipy.optimize._constraints (top-level), scipy.optimize._trustregion_constr.projections (top-level), scipy.optimize._lsq.least_squares (top-level), scipy.optimize._lsq.common (top-level), scipy.optimize._lsq.lsq_linear (top-level), scipy.optimize._linprog_highs (top-level), scipy.optimize._differentialevolution (top-level), scipy.sparse.csgraph._laplacian (top-level), scipy.optimize._milp (top-level), scipy.integrate._ivp.bdf (top-level), scipy.integrate._ivp.radau (top-level), scipy.sparse.linalg._dsolve.linsolve (top-level), scipy.sparse.linalg._eigen.arpack.arpack (top-level), scipy.sparse.linalg._eigen.lobpcg.lobpcg (top-level), scipy.sparse.linalg._norm (top-level), pandas.core.dtypes.common (delayed, conditional, optional), scipy.sparse.csgraph._validation (top-level)
|
||||
missing module named scipy.sparse.linalg.LinearOperator - imported by scipy.sparse.linalg (top-level), scipy.optimize._optimize (top-level), scipy.optimize._numdiff (top-level), scipy.optimize._differentiable_functions (top-level), scipy.optimize._trustregion_constr.minimize_trustregion_constr (top-level), scipy.optimize._trustregion_constr.projections (top-level), scipy.optimize._trustregion_constr.tr_interior_point (top-level), scipy.optimize._lbfgsb_py (top-level), scipy.optimize._lsq.least_squares (top-level), scipy.optimize._lsq.common (top-level), scipy.optimize._lsq.dogbox (top-level), scipy.optimize._lsq.lsq_linear (top-level), scipy.linalg.interpolative (delayed), scipy.sparse.csgraph._laplacian (top-level), scipy.sparse.linalg._eigen.lobpcg.lobpcg (top-level), scipy.sparse.linalg._special_sparse_arrays (top-level)
|
||||
missing module named scipy.linalg._fblas_64 - imported by scipy.linalg (optional), scipy.linalg.blas (optional)
|
||||
missing module named scipy.linalg._cblas - imported by scipy.linalg (optional), scipy.linalg.blas (optional)
|
||||
missing module named scipy.linalg._flapack_64 - imported by scipy.linalg (optional), scipy.linalg.lapack (optional)
|
||||
missing module named scipy.linalg._clapack - imported by scipy.linalg (optional), scipy.linalg.lapack (optional)
|
||||
missing module named scipy.linalg.qr_insert - imported by scipy.linalg (top-level), scipy.sparse.linalg._isolve._gcrotmk (top-level)
|
||||
missing module named scipy.special.gammaincinv - imported by scipy.special (top-level), scipy.stats._qmvnt (top-level)
|
||||
missing module named scipy.special.ive - imported by scipy.special (top-level), scipy.stats._multivariate (top-level)
|
||||
missing module named scipy.special.betaln - imported by scipy.special (top-level), scipy.stats._discrete_distns (top-level), scipy.stats._multivariate (top-level)
|
||||
missing module named scipy.special.beta - imported by scipy.special (top-level), scipy.stats._tukeylambda_stats (top-level)
|
||||
missing module named scipy.special.loggamma - imported by scipy.special (top-level), scipy.fft._fftlog_backend (top-level), scipy.stats._multivariate (top-level)
|
||||
missing module named scipy.interpolate.PPoly - imported by scipy.interpolate (top-level), scipy.interpolate._cubic (top-level), scipy.spatial.transform._rotation_spline (delayed), scipy.integrate._bvp (delayed)
|
||||
missing module named scikits - imported by scipy.optimize._linprog_ip (optional)
|
||||
missing module named 'sksparse.cholmod' - imported by scipy.optimize._linprog_ip (optional)
|
||||
missing module named sksparse - imported by scipy.optimize._trustregion_constr.projections (optional), scipy.optimize._linprog_ip (optional)
|
||||
missing module named scipy.special.airy - imported by scipy.special (top-level), scipy.special._orthogonal (top-level)
|
||||
missing module named scipy.linalg.orthogonal_procrustes - imported by scipy.linalg (top-level), scipy.spatial._procrustes (top-level)
|
||||
missing module named uarray - imported by scipy._lib.uarray (conditional, optional)
|
||||
missing module named 'scikits.umfpack' - imported by scipy.sparse.linalg._dsolve.linsolve (optional)
|
||||
missing module named scipy.sparse.linalg.splu - imported by scipy.sparse.linalg (top-level), scipy.integrate._bvp (top-level), scipy.integrate._ivp.bdf (top-level), scipy.integrate._ivp.radau (top-level), scipy.sparse.linalg._eigen.arpack.arpack (top-level)
|
||||
missing module named scipy.sparse.linalg.aslinearoperator - imported by scipy.sparse.linalg (top-level), scipy.optimize._lsq.common (top-level), scipy.optimize._lsq.dogbox (top-level), scipy.linalg.interpolative (delayed), scipy.sparse.linalg._svdp (top-level), scipy.sparse.linalg._expm_multiply (top-level), scipy.sparse.linalg._onenormest (top-level)
|
||||
missing module named scipy.sparse.linalg.lsmr - imported by scipy.sparse.linalg (top-level), scipy.optimize._lsq.trf (top-level), scipy.optimize._lsq.dogbox (top-level), scipy.optimize._lsq.lsq_linear (top-level), scipy.optimize._lsq.trf_linear (top-level)
|
||||
missing module named scipy.sparse.linalg.onenormest - imported by scipy.sparse.linalg (top-level), scipy.linalg._matfuncs_inv_ssq (top-level)
|
||||
missing module named scipy.sparse.diags - imported by scipy.sparse (delayed), scipy.sparse.linalg._special_sparse_arrays (delayed)
|
||||
missing module named scipy.sparse.spdiags - imported by scipy.sparse (delayed), scipy.sparse.linalg._special_sparse_arrays (delayed)
|
||||
missing module named scipy.sparse.dia_array - imported by scipy.sparse (top-level), scipy.sparse.linalg._special_sparse_arrays (top-level)
|
||||
missing module named scipy.sparse.kron - imported by scipy.sparse (top-level), scipy.sparse.linalg._special_sparse_arrays (top-level)
|
||||
missing module named scipy.sparse.diags_array - imported by scipy.sparse (top-level), scipy.sparse.linalg._dsolve.linsolve (top-level)
|
||||
missing module named scipy.sparse.eye_array - imported by scipy.sparse (top-level), scipy.sparse.linalg._dsolve.linsolve (top-level)
|
||||
missing module named scipy.sparse.SparseEfficiencyWarning - imported by scipy.sparse (top-level), scipy.sparse.linalg._dsolve.linsolve (top-level)
|
||||
missing module named scipy.sparse.csr_array - imported by scipy.sparse (top-level), scipy.interpolate._bsplines (top-level), scipy.interpolate._ndbspline (top-level), scipy.sparse.linalg._dsolve.linsolve (top-level)
|
||||
missing module named scipy.sparse.csc_array - imported by scipy.sparse (top-level), scipy.optimize._milp (top-level), scipy.sparse.linalg._dsolve.linsolve (top-level)
|
||||
missing module named scipy.sparse.vstack - imported by scipy.sparse (top-level), scipy.optimize._linprog_highs (top-level), scipy.optimize._milp (top-level)
|
||||
missing module named scipy.sparse.bmat - imported by scipy.sparse (top-level), scipy.optimize._trustregion_constr.projections (top-level), scipy.optimize._trustregion_constr.qp_subproblem (top-level)
|
||||
missing module named scipy.sparse.eye - imported by scipy.sparse (top-level), scipy.optimize._trustregion_constr.equality_constrained_sqp (top-level), scipy.optimize._trustregion_constr.projections (top-level), scipy.integrate._ivp.bdf (top-level), scipy.integrate._ivp.radau (top-level), scipy.sparse.linalg._eigen.arpack.arpack (top-level), scipy.sparse.linalg._special_sparse_arrays (top-level)
|
||||
missing module named scipy.sparse.find - imported by scipy.sparse (top-level), scipy.optimize._numdiff (top-level), scipy.integrate._ivp.common (top-level)
|
||||
missing module named scipy.sparse.coo_matrix - imported by scipy.sparse (top-level), scipy.optimize._numdiff (top-level), scipy.integrate._bvp (top-level), scipy.integrate._ivp.common (top-level), scipy.stats._crosstab (top-level), pandas.core.arrays.sparse.accessor (delayed)
|
||||
missing module named scipy.sparse.csr_matrix - imported by scipy.sparse (top-level), scipy.optimize._numdiff (top-level), scipy.optimize._lsq.lsq_linear (top-level)
|
||||
missing module named scipy.sparse.csc_matrix - imported by scipy.sparse (top-level), scipy.optimize._numdiff (top-level), scipy.optimize._trustregion_constr.projections (top-level), scipy.optimize._trustregion_constr.qp_subproblem (top-level), scipy.optimize._linprog_highs (top-level), scipy.integrate._bvp (top-level), scipy.integrate._ivp.bdf (top-level), scipy.integrate._ivp.radau (top-level), scipy.linalg._sketches (top-level)
|
||||
missing module named scipy.stats.iqr - imported by scipy.stats (delayed), scipy.stats._hypotests (delayed)
|
||||
missing module named 'setuptools._distutils.msvc9compiler' - imported by cffi._shimmed_dist_utils (conditional, optional)
|
||||
missing module named imp - imported by cffi.verifier (conditional), cffi._imp_emulation (optional)
|
||||
missing module named collections.Callable - imported by collections (optional), cffi.api (optional)
|
||||
missing module named dummy_thread - imported by cffi.lock (conditional, optional)
|
||||
missing module named thread - imported by cffi.lock (conditional, optional), cffi.cparser (conditional, optional)
|
||||
missing module named cStringIO - imported by cffi.ffiplatform (optional)
|
||||
missing module named cPickle - imported by pycparser.ply.yacc (delayed, optional)
|
||||
missing module named cffi._pycparser - imported by cffi (optional), cffi.cparser (optional)
|
||||
missing module named scipy._distributor_init_local - imported by scipy (optional), scipy._distributor_init (optional)
|
||||
missing module named traitlets - imported by pandas.io.formats.printing (delayed, conditional)
|
||||
missing module named 'IPython.core' - imported by pandas.io.formats.printing (delayed, conditional), fire.inspectutils (delayed, optional)
|
||||
missing module named IPython - imported by pandas.io.formats.printing (delayed), fire.interact (delayed)
|
||||
missing module named xlsxwriter - imported by pandas.io.excel._xlsxwriter (delayed)
|
||||
missing module named 'odf.config' - imported by pandas.io.excel._odswriter (delayed)
|
||||
missing module named 'odf.style' - imported by pandas.io.excel._odswriter (delayed)
|
||||
missing module named 'odf.text' - imported by pandas.io.excel._odfreader (delayed), pandas.io.excel._odswriter (delayed)
|
||||
missing module named 'odf.table' - imported by pandas.io.excel._odfreader (delayed), pandas.io.excel._odswriter (delayed)
|
||||
missing module named 'odf.opendocument' - imported by pandas.io.excel._odfreader (delayed), pandas.io.excel._odswriter (delayed)
|
||||
missing module named xlrd - imported by pandas.io.excel._xlrd (delayed, conditional), pandas.io.excel._base (delayed, conditional)
|
||||
missing module named pyxlsb - imported by pandas.io.excel._pyxlsb (delayed, conditional)
|
||||
missing module named 'odf.office' - imported by pandas.io.excel._odfreader (delayed)
|
||||
missing module named 'odf.element' - imported by pandas.io.excel._odfreader (delayed)
|
||||
missing module named 'odf.namespaces' - imported by pandas.io.excel._odfreader (delayed)
|
||||
missing module named odf - imported by pandas.io.excel._odfreader (conditional)
|
||||
missing module named python_calamine - imported by pandas.io.excel._calamine (delayed, conditional)
|
||||
missing module named markupsafe - imported by pandas.io.formats.style_render (top-level)
|
||||
missing module named botocore - imported by pandas.io.common (delayed, conditional, optional)
|
||||
missing module named sets - imported by pytz.tzinfo (optional)
|
||||
missing module named collections.Mapping - imported by collections (optional), pytz.lazy (optional)
|
||||
missing module named UserDict - imported by pytz.lazy (optional)
|
||||
missing module named pandas.core.internals.Block - imported by pandas.core.internals (conditional), pandas.io.pytables (conditional)
|
||||
missing module named Foundation - imported by pandas.io.clipboard (delayed, conditional, optional)
|
||||
missing module named AppKit - imported by pandas.io.clipboard (delayed, conditional, optional)
|
||||
missing module named PyQt4 - imported by pandas.io.clipboard (delayed, conditional, optional)
|
||||
missing module named qtpy - imported by pandas.io.clipboard (delayed, conditional, optional)
|
||||
missing module named 'sqlalchemy.engine' - imported by pandas.io.sql (delayed)
|
||||
missing module named 'sqlalchemy.types' - imported by pandas.io.sql (delayed, conditional)
|
||||
missing module named 'sqlalchemy.schema' - imported by pandas.io.sql (delayed)
|
||||
missing module named 'sqlalchemy.sql' - imported by pandas.io.sql (conditional)
|
||||
missing module named sqlalchemy - imported by pandas.io.sql (delayed, conditional)
|
||||
missing module named tables - imported by pandas.io.pytables (delayed, conditional)
|
||||
missing module named 'pyarrow.fs' - imported by pandas.io.orc (conditional)
|
||||
missing module named fsspec - imported by pandas.io.orc (conditional)
|
||||
missing module named 'pyarrow.parquet' - imported by pandas.io.parquet (delayed)
|
||||
missing module named google - imported by pandas.io.gbq (conditional)
|
||||
missing module named fontTools.ttLib.getSearchRange - imported by fontTools.ttLib (top-level), fontTools.ttLib.tables.otConverters (top-level), fontTools.ttLib.tables._c_m_a_p (top-level), fontTools.ttLib.tables._k_e_r_n (top-level), fontTools.ttLib.woff2 (top-level), fontTools.ttLib.sfnt (delayed, conditional)
|
||||
missing module named zopfli - imported by fontTools.ttLib.sfnt (delayed, conditional)
|
||||
missing module named unicodedata2 - imported by fontTools.unicode (delayed, optional), fontTools.unicodedata (optional)
|
||||
missing module named xattr - imported by fontTools.misc.macCreatorType (optional)
|
||||
missing module named brotli - imported by fontTools.ttLib.woff2 (optional)
|
||||
missing module named brotlicffi - imported by fontTools.ttLib.woff2 (optional)
|
||||
missing module named fontTools.ttLib.getClassTag - imported by fontTools.ttLib (top-level), fontTools.ttLib.tables.DefaultTable (top-level)
|
||||
missing module named fontTools.ttLib.getTableClass - imported by fontTools.ttLib (top-level), fontTools.ttLib.woff2 (top-level)
|
||||
missing module named fontTools.ttLib.getTableModule - imported by fontTools.ttLib (top-level), fontTools.ttLib.woff2 (top-level)
|
||||
missing module named fontTools.ttLib.TTFont - imported by fontTools.ttLib (top-level), fontTools.varLib (top-level), fontTools.cffLib (top-level), fontTools.cffLib.CFFToCFF2 (top-level), fontTools.cffLib.CFF2ToCFF (top-level), fontTools.cffLib.specializer (conditional), fontTools.cffLib.width (top-level), fontTools.ttLib.ttVisitor (top-level), fontTools.varLib.varStore (delayed), fontTools.otlLib.optimize.gpos (top-level), fontTools.otlLib.optimize (top-level), fontTools.varLib.stat (top-level), fontTools.colorLib.unbuilder (conditional), fontTools.ttx (top-level), fontTools.ttLib.woff2 (top-level), pdf2docx.font.Fonts (top-level)
|
||||
missing module named fontTools.ttLib.newTable - imported by fontTools.ttLib (top-level), fontTools.varLib (top-level), fontTools.cffLib.CFFToCFF2 (top-level), fontTools.cffLib.CFF2ToCFF (top-level), fontTools.ttLib.tables._n_a_m_e (top-level), fontTools.varLib.featureVars (top-level), fontTools.varLib.cff (top-level)
|
||||
missing module named pathops - imported by fontTools.ttLib.removeOverlaps (top-level)
|
||||
missing module named uharfbuzz - imported by fontTools.ttLib.tables.otBase (optional)
|
||||
missing module named 'lz4.block' - imported by fontTools.ttLib.tables.grUtils (optional)
|
||||
missing module named lz4 - imported by fontTools.ttLib.tables.grUtils (optional)
|
||||
missing module named cppyy - imported by pymupdf (delayed, conditional)
|
||||
missing module named pymupdf_fonts - imported by pymupdf (delayed, conditional, optional)
|
||||
missing module named mupdf - imported by pymupdf (conditional, optional), pymupdf.utils (optional)
|
||||
missing module named __builtin__ - imported by pymupdf.extra (optional), pymupdf.mupdf (optional)
|
||||
missing module named mupdf_cppyy - imported by pymupdf (conditional)
|
||||
missing module named olefile - imported by PIL.FpxImagePlugin (top-level), PIL.MicImagePlugin (top-level)
|
||||
missing module named PIL._avif - imported by PIL (optional), PIL.AvifImagePlugin (optional)
|
||||
missing module named collections.Iterable - imported by collections (optional), pdf2docx.text.Line (optional)
|
||||
82040
build/win64/CheeseCloudTools/xref-CheeseCloudTools.html
Normal file
391
build_app.py
Normal file
@@ -0,0 +1,391 @@
|
||||
"""
|
||||
奶酪云工具箱 - 打包脚本
|
||||
支持 Windows (7/10/11) 32位/64位, macOS 打包
|
||||
|
||||
使用方法:
|
||||
python build_app.py # 打包当前平台
|
||||
python build_app.py --all # 尝试打包所有平台(需要对应环境)
|
||||
python build_app.py --platform win64 # 指定平台
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import platform
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# 项目配置
|
||||
APP_NAME = "奶酪云工具箱"
|
||||
APP_NAME_EN = "CheeseCloudTools"
|
||||
VERSION = "1.0.0"
|
||||
AUTHOR = "奶酪源码"
|
||||
DESCRIPTION = "多功能图片/PDF/Excel处理工具"
|
||||
|
||||
# 路径配置
|
||||
PROJECT_ROOT = Path(__file__).parent
|
||||
DIST_DIR = PROJECT_ROOT / "dist"
|
||||
BUILD_DIR = PROJECT_ROOT / "build"
|
||||
ICON_SOURCE = PROJECT_ROOT / "image" / "生成奶酪商城官方店介绍.png"
|
||||
ICON_DIR = PROJECT_ROOT / "build_icons"
|
||||
|
||||
# 需要包含的数据文件
|
||||
DATA_FILES = [
|
||||
("resources", "resources"),
|
||||
("image", "image"),
|
||||
]
|
||||
|
||||
|
||||
def create_icon_dir():
|
||||
"""创建图标目录"""
|
||||
ICON_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def convert_png_to_ico(png_path: Path, ico_path: Path, sizes=None):
|
||||
"""将PNG转换为ICO格式(Windows图标)"""
|
||||
if sizes is None:
|
||||
sizes = [(256, 256), (128, 128), (64, 64), (48, 48), (32, 32), (16, 16)]
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
img = Image.open(png_path)
|
||||
|
||||
# 创建不同尺寸的图标
|
||||
icon_images = []
|
||||
for size in sizes:
|
||||
resized = img.copy()
|
||||
resized.thumbnail(size, Image.Resampling.LANCZOS)
|
||||
# 确保尺寸正确
|
||||
if resized.size != size:
|
||||
new_img = Image.new('RGBA', size, (0, 0, 0, 0))
|
||||
offset = ((size[0] - resized.size[0]) // 2,
|
||||
(size[1] - resized.size[1]) // 2)
|
||||
new_img.paste(resized, offset)
|
||||
resized = new_img
|
||||
icon_images.append(resized)
|
||||
|
||||
# 保存为ICO
|
||||
icon_images[0].save(
|
||||
ico_path,
|
||||
format='ICO',
|
||||
sizes=[(img.size[0], img.size[1]) for img in icon_images]
|
||||
)
|
||||
print(f"✅ 已创建 Windows 图标: {ico_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 创建 ICO 失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def convert_png_to_icns(png_path: Path, icns_path: Path):
|
||||
"""将PNG转换为ICNS格式(macOS图标)"""
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
img = Image.open(png_path)
|
||||
|
||||
# macOS 需要特定尺寸
|
||||
sizes = [16, 32, 64, 128, 256, 512, 1024]
|
||||
|
||||
# 创建临时 iconset 目录
|
||||
iconset_dir = icns_path.parent / f"{icns_path.stem}.iconset"
|
||||
iconset_dir.mkdir(exist_ok=True)
|
||||
|
||||
for size in sizes:
|
||||
# 标准分辨率
|
||||
resized = img.copy()
|
||||
resized.thumbnail((size, size), Image.Resampling.LANCZOS)
|
||||
resized.save(iconset_dir / f"icon_{size}x{size}.png")
|
||||
|
||||
# 2x 分辨率 (Retina)
|
||||
if size <= 512:
|
||||
resized_2x = img.copy()
|
||||
resized_2x.thumbnail((size * 2, size * 2), Image.Resampling.LANCZOS)
|
||||
resized_2x.save(iconset_dir / f"icon_{size}x{size}@2x.png")
|
||||
|
||||
# 使用 iconutil 转换(仅macOS可用)
|
||||
if platform.system() == "Darwin":
|
||||
subprocess.run(["iconutil", "-c", "icns", str(iconset_dir), "-o", str(icns_path)])
|
||||
print(f"✅ 已创建 macOS 图标: {icns_path}")
|
||||
else:
|
||||
# 在非macOS上,复制PNG作为替代
|
||||
shutil.copy(png_path, icns_path.with_suffix('.png'))
|
||||
print(f"⚠️ 非macOS环境,已复制PNG: {icns_path.with_suffix('.png')}")
|
||||
|
||||
# 清理临时目录
|
||||
shutil.rmtree(iconset_dir, ignore_errors=True)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 创建 ICNS 失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def prepare_icons():
|
||||
"""准备各平台图标"""
|
||||
create_icon_dir()
|
||||
|
||||
ico_path = ICON_DIR / "app.ico"
|
||||
icns_path = ICON_DIR / "app.icns"
|
||||
|
||||
if ICON_SOURCE.exists():
|
||||
convert_png_to_ico(ICON_SOURCE, ico_path)
|
||||
convert_png_to_icns(ICON_SOURCE, icns_path)
|
||||
else:
|
||||
print(f"⚠️ 图标源文件不存在: {ICON_SOURCE}")
|
||||
|
||||
return ico_path, icns_path
|
||||
|
||||
|
||||
def get_pyinstaller_args(target_platform: str, ico_path: Path, icns_path: Path):
|
||||
"""获取 PyInstaller 参数"""
|
||||
|
||||
# 基础参数
|
||||
args = [
|
||||
"pyinstaller",
|
||||
"--noconfirm",
|
||||
"--clean",
|
||||
"--name", f"{APP_NAME_EN}",
|
||||
"--windowed", # GUI应用,不显示控制台
|
||||
"--onedir", # 打包为目录(更稳定)
|
||||
]
|
||||
|
||||
# 添加数据文件
|
||||
for src, dst in DATA_FILES:
|
||||
src_path = PROJECT_ROOT / src
|
||||
if src_path.exists():
|
||||
args.extend(["--add-data", f"{src_path}{os.pathsep}{dst}"])
|
||||
|
||||
# 隐藏导入
|
||||
hidden_imports = [
|
||||
"PySide6.QtSvg",
|
||||
"PySide6.QtSvgWidgets",
|
||||
"PIL",
|
||||
"PIL.Image",
|
||||
"fitz",
|
||||
"pandas",
|
||||
"openpyxl",
|
||||
"matplotlib",
|
||||
"matplotlib.backends.backend_qtagg",
|
||||
]
|
||||
for hi in hidden_imports:
|
||||
args.extend(["--hidden-import", hi])
|
||||
|
||||
# 平台特定参数
|
||||
if target_platform.startswith("win"):
|
||||
if ico_path.exists():
|
||||
args.extend(["--icon", str(ico_path)])
|
||||
# Windows 版本信息
|
||||
args.extend([
|
||||
"--version-file", str(PROJECT_ROOT / "version_info.txt"),
|
||||
])
|
||||
elif target_platform.startswith("mac"):
|
||||
if icns_path.exists():
|
||||
args.extend(["--icon", str(icns_path)])
|
||||
# macOS bundle 标识符
|
||||
args.extend([
|
||||
"--osx-bundle-identifier", "com.naiyuanma.cheesetools",
|
||||
])
|
||||
|
||||
# 输出目录
|
||||
output_dir = DIST_DIR / target_platform
|
||||
args.extend(["--distpath", str(output_dir)])
|
||||
args.extend(["--workpath", str(BUILD_DIR / target_platform)])
|
||||
args.extend(["--specpath", str(BUILD_DIR)])
|
||||
|
||||
# 主入口文件
|
||||
args.append(str(PROJECT_ROOT / "main.py"))
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def create_version_info():
|
||||
"""创建 Windows 版本信息文件"""
|
||||
version_parts = VERSION.split(".")
|
||||
while len(version_parts) < 4:
|
||||
version_parts.append("0")
|
||||
|
||||
version_tuple = ", ".join(version_parts)
|
||||
|
||||
content = f'''# UTF-8
|
||||
VSVersionInfo(
|
||||
ffi=FixedFileInfo(
|
||||
filevers=({version_tuple}),
|
||||
prodvers=({version_tuple}),
|
||||
mask=0x3f,
|
||||
flags=0x0,
|
||||
OS=0x40004,
|
||||
fileType=0x1,
|
||||
subtype=0x0,
|
||||
date=(0, 0)
|
||||
),
|
||||
kids=[
|
||||
StringFileInfo(
|
||||
[
|
||||
StringTable(
|
||||
u'080404b0',
|
||||
[
|
||||
StringStruct(u'CompanyName', u'{AUTHOR}'),
|
||||
StringStruct(u'FileDescription', u'{DESCRIPTION}'),
|
||||
StringStruct(u'FileVersion', u'{VERSION}'),
|
||||
StringStruct(u'InternalName', u'{APP_NAME_EN}'),
|
||||
StringStruct(u'LegalCopyright', u'Copyright (C) 2024 {AUTHOR}'),
|
||||
StringStruct(u'OriginalFilename', u'{APP_NAME_EN}.exe'),
|
||||
StringStruct(u'ProductName', u'{APP_NAME}'),
|
||||
StringStruct(u'ProductVersion', u'{VERSION}'),
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
VarFileInfo([VarStruct(u'Translation', [2052, 1200])])
|
||||
]
|
||||
)
|
||||
'''
|
||||
|
||||
version_file = PROJECT_ROOT / "version_info.txt"
|
||||
with open(version_file, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print(f"✅ 已创建版本信息文件: {version_file}")
|
||||
|
||||
|
||||
def build_for_platform(target_platform: str):
|
||||
"""为指定平台打包"""
|
||||
print(f"\n{'='*50}")
|
||||
print(f"🔨 开始打包: {target_platform}")
|
||||
print(f"{'='*50}")
|
||||
|
||||
# 准备图标
|
||||
ico_path, icns_path = prepare_icons()
|
||||
|
||||
# 创建版本信息(Windows)
|
||||
if target_platform.startswith("win"):
|
||||
create_version_info()
|
||||
|
||||
# 获取 PyInstaller 参数
|
||||
args = get_pyinstaller_args(target_platform, ico_path, icns_path)
|
||||
|
||||
print(f"📦 执行命令: {' '.join(args)}")
|
||||
|
||||
# 执行打包
|
||||
try:
|
||||
result = subprocess.run(args, check=True)
|
||||
print(f"\n✅ {target_platform} 打包成功!")
|
||||
print(f"📁 输出目录: {DIST_DIR / target_platform}")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"\n❌ {target_platform} 打包失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_current_platform():
|
||||
"""获取当前平台标识"""
|
||||
system = platform.system().lower()
|
||||
machine = platform.machine().lower()
|
||||
|
||||
if system == "windows":
|
||||
if machine in ["amd64", "x86_64"]:
|
||||
return "win64"
|
||||
else:
|
||||
return "win32"
|
||||
elif system == "darwin":
|
||||
if machine == "arm64":
|
||||
return "mac_arm64"
|
||||
else:
|
||||
return "mac_x64"
|
||||
elif system == "linux":
|
||||
if machine in ["amd64", "x86_64"]:
|
||||
return "linux64"
|
||||
else:
|
||||
return "linux32"
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
def clean_build():
|
||||
"""清理构建目录"""
|
||||
print("🧹 清理构建目录...")
|
||||
if BUILD_DIR.exists():
|
||||
shutil.rmtree(BUILD_DIR)
|
||||
if ICON_DIR.exists():
|
||||
shutil.rmtree(ICON_DIR)
|
||||
print("✅ 清理完成")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="奶酪云工具箱打包脚本")
|
||||
parser.add_argument("--platform", "-p",
|
||||
choices=["win32", "win64", "mac_x64", "mac_arm64", "current"],
|
||||
default="current",
|
||||
help="目标平台")
|
||||
parser.add_argument("--all", "-a", action="store_true",
|
||||
help="打包所有平台(需要对应环境)")
|
||||
parser.add_argument("--clean", "-c", action="store_true",
|
||||
help="清理构建目录")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"""
|
||||
╔══════════════════════════════════════════════╗
|
||||
║ 奶酪云工具箱 - 打包工具 ║
|
||||
║ 版本: {VERSION} ║
|
||||
╚══════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
if args.clean:
|
||||
clean_build()
|
||||
return
|
||||
|
||||
# 检查 PyInstaller
|
||||
try:
|
||||
import PyInstaller
|
||||
print(f"✅ PyInstaller 版本: {PyInstaller.__version__}")
|
||||
except ImportError:
|
||||
print("❌ 未安装 PyInstaller,正在安装...")
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "pyinstaller"])
|
||||
|
||||
# 确定要打包的平台
|
||||
if args.all:
|
||||
# 打包当前系统支持的所有架构
|
||||
system = platform.system().lower()
|
||||
if system == "windows":
|
||||
# Windows 可以同时打包 32位和64位(如果有对应Python)
|
||||
platforms = ["win64"] # 默认打包64位
|
||||
print(f"📋 Windows 系统将打包: {platforms}")
|
||||
print(f"⚠️ 32位版本需要在32位Python环境中单独打包")
|
||||
elif system == "darwin":
|
||||
# macOS
|
||||
machine = platform.machine().lower()
|
||||
if machine == "arm64":
|
||||
platforms = ["mac_arm64"]
|
||||
else:
|
||||
platforms = ["mac_x64"]
|
||||
print(f"📋 macOS 系统将打包: {platforms}")
|
||||
else:
|
||||
platforms = [get_current_platform()]
|
||||
print(f"📋 当前平台: {platforms}")
|
||||
elif args.platform == "current":
|
||||
platforms = [get_current_platform()]
|
||||
else:
|
||||
platforms = [args.platform]
|
||||
|
||||
# 创建输出目录
|
||||
DIST_DIR.mkdir(exist_ok=True)
|
||||
BUILD_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# 打包
|
||||
success_count = 0
|
||||
for plat in platforms:
|
||||
if build_for_platform(plat):
|
||||
success_count += 1
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"📊 打包完成: {success_count}/{len(platforms)} 成功")
|
||||
print(f"📁 输出目录: {DIST_DIR}")
|
||||
print(f"{'='*50}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
BIN
build_icons/app.ico
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
build_icons/app.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
9
config/settings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"output_directory": "D:/workers/project/ai/nly-tool/gen",
|
||||
"auto_save_to_default": true,
|
||||
"image_quality": 10,
|
||||
"enable_lossless_compression": true,
|
||||
"show_preview": true,
|
||||
"animation_enabled": true,
|
||||
"animation_duration": 300
|
||||
}
|
||||
18
core/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
核心模块
|
||||
- 配置管理
|
||||
- 日志记录
|
||||
- 错误处理
|
||||
"""
|
||||
from .config import config, Config
|
||||
from .logger import setup_logging, get_all_log_files, read_log_file
|
||||
from .error_handler import ErrorHandler
|
||||
|
||||
__all__ = [
|
||||
'config',
|
||||
'Config',
|
||||
'setup_logging',
|
||||
'get_all_log_files',
|
||||
'read_log_file',
|
||||
'ErrorHandler'
|
||||
]
|
||||
101
core/config.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
全局配置管理模块
|
||||
- 保存/读取用户配置
|
||||
- 配置全局文件保存位置
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class Config:
|
||||
"""全局配置管理器"""
|
||||
|
||||
_instance = None
|
||||
_config_file = None
|
||||
_config_data = {}
|
||||
|
||||
# 默认配置
|
||||
DEFAULT_CONFIG = {
|
||||
"output_directory": "", # 空字符串表示每次询问
|
||||
"auto_save_to_default": False, # 是否自动保存到默认目录
|
||||
"image_quality": 85, # 默认图片质量
|
||||
"enable_lossless_compression": True, # 启用无损压缩
|
||||
"show_preview": True, # 显示预览
|
||||
"animation_enabled": True, # 启用动画
|
||||
"animation_duration": 300, # 动画时长(ms)
|
||||
}
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
# 配置文件路径
|
||||
app_root = Path(__file__).parent.parent
|
||||
config_dir = app_root / "config"
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
self._config_file = config_dir / "settings.json"
|
||||
|
||||
# 加载配置
|
||||
self._load_config()
|
||||
self._initialized = True
|
||||
|
||||
def _load_config(self):
|
||||
"""加载配置文件"""
|
||||
if self._config_file.exists():
|
||||
try:
|
||||
with open(self._config_file, "r", encoding="utf-8") as f:
|
||||
self._config_data = json.load(f)
|
||||
logging.info(f"已加载配置文件: {self._config_file}")
|
||||
except Exception as e:
|
||||
logging.error(f"加载配置文件失败: {e}")
|
||||
self._config_data = {}
|
||||
else:
|
||||
self._config_data = {}
|
||||
|
||||
# 合并默认配置
|
||||
for key, value in self.DEFAULT_CONFIG.items():
|
||||
if key not in self._config_data:
|
||||
self._config_data[key] = value
|
||||
|
||||
def _save_config(self):
|
||||
"""保存配置文件"""
|
||||
try:
|
||||
with open(self._config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(self._config_data, f, ensure_ascii=False, indent=2)
|
||||
logging.info("配置已保存")
|
||||
except Exception as e:
|
||||
logging.error(f"保存配置失败: {e}")
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""获取配置值"""
|
||||
return self._config_data.get(key, default)
|
||||
|
||||
def set(self, key: str, value: Any):
|
||||
"""设置配置值"""
|
||||
self._config_data[key] = value
|
||||
self._save_config()
|
||||
|
||||
def get_output_directory(self) -> str:
|
||||
"""获取输出目录"""
|
||||
path = self.get("output_directory", "")
|
||||
if path and os.path.isdir(path):
|
||||
return path
|
||||
return ""
|
||||
|
||||
def set_output_directory(self, path: str):
|
||||
"""设置输出目录"""
|
||||
self.set("output_directory", path)
|
||||
|
||||
|
||||
# 全局配置实例
|
||||
config = Config()
|
||||
|
||||
162
core/error_handler.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
全局错误处理模块
|
||||
- 重写 sys.excepthook 捕获主线程未处理异常
|
||||
- 使用 threading.excepthook 捕获子线程异常
|
||||
- 所有异常统一进入 ErrorHandler 处理
|
||||
- 弹框提示用户,不闪退
|
||||
"""
|
||||
import sys
|
||||
import logging
|
||||
import threading
|
||||
import traceback
|
||||
from PySide6.QtWidgets import QMessageBox, QTextEdit, QPushButton, QVBoxLayout, QDialog
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
|
||||
class ErrorSignal(QObject):
|
||||
"""用于跨线程传递错误信号"""
|
||||
error_occurred = Signal(str, str) # (error_message, error_detail)
|
||||
|
||||
|
||||
class ErrorDialog(QDialog):
|
||||
"""自定义错误对话框,支持显示详细堆栈"""
|
||||
|
||||
def __init__(self, title: str, message: str, detail: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(title)
|
||||
self.setMinimumSize(500, 200)
|
||||
self.setup_ui(message, detail)
|
||||
|
||||
def setup_ui(self, message: str, detail: str):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 错误消息
|
||||
from PySide6.QtWidgets import QLabel
|
||||
msg_label = QLabel(message)
|
||||
msg_label.setWordWrap(True)
|
||||
msg_label.setStyleSheet("font-size: 14px; color: #f87171;")
|
||||
layout.addWidget(msg_label)
|
||||
|
||||
# 详细信息(可展开)
|
||||
self.detail_edit = QTextEdit()
|
||||
self.detail_edit.setPlainText(detail)
|
||||
self.detail_edit.setReadOnly(True)
|
||||
self.detail_edit.setVisible(False)
|
||||
self.detail_edit.setMinimumHeight(200)
|
||||
layout.addWidget(self.detail_edit)
|
||||
|
||||
# 按钮区域
|
||||
from PySide6.QtWidgets import QHBoxLayout
|
||||
btn_layout = QHBoxLayout()
|
||||
|
||||
self.detail_btn = QPushButton("显示详情")
|
||||
self.detail_btn.setObjectName("secondary_btn")
|
||||
self.detail_btn.clicked.connect(self.toggle_detail)
|
||||
btn_layout.addWidget(self.detail_btn)
|
||||
|
||||
btn_layout.addStretch()
|
||||
|
||||
ok_btn = QPushButton("确定")
|
||||
ok_btn.setObjectName("primary_btn")
|
||||
ok_btn.clicked.connect(self.accept)
|
||||
btn_layout.addWidget(ok_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def toggle_detail(self):
|
||||
"""切换详情显示"""
|
||||
visible = not self.detail_edit.isVisible()
|
||||
self.detail_edit.setVisible(visible)
|
||||
self.detail_btn.setText("隐藏详情" if visible else "显示详情")
|
||||
|
||||
# 调整窗口大小
|
||||
if visible:
|
||||
self.resize(600, 450)
|
||||
else:
|
||||
self.resize(500, 200)
|
||||
|
||||
|
||||
class ErrorHandler:
|
||||
"""全局错误处理器"""
|
||||
|
||||
_instance = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, app=None):
|
||||
if hasattr(self, '_initialized') and self._initialized:
|
||||
return
|
||||
|
||||
self.app = app
|
||||
self.error_signal = ErrorSignal()
|
||||
self.error_signal.error_occurred.connect(self._show_error_dialog)
|
||||
|
||||
# 设置全局异常处理
|
||||
sys.excepthook = self.handle_exception
|
||||
threading.excepthook = self.handle_thread_exception
|
||||
|
||||
self._initialized = True
|
||||
logging.info("全局错误处理器已初始化")
|
||||
|
||||
def handle_exception(self, exc_type, exc_value, exc_tb):
|
||||
"""处理主线程未捕获的异常"""
|
||||
# 忽略键盘中断
|
||||
if issubclass(exc_type, KeyboardInterrupt):
|
||||
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
||||
return
|
||||
|
||||
# 格式化错误信息
|
||||
error_msg = str(exc_value)
|
||||
error_detail = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
||||
|
||||
# 记录日志
|
||||
logging.error(f"未捕获的异常:\n{error_detail}")
|
||||
|
||||
# 显示错误对话框
|
||||
self._show_error_dialog(error_msg, error_detail)
|
||||
|
||||
def handle_thread_exception(self, args):
|
||||
"""处理子线程未捕获的异常"""
|
||||
exc_type = args.exc_type
|
||||
exc_value = args.exc_value
|
||||
exc_tb = args.exc_traceback
|
||||
|
||||
# 格式化错误信息
|
||||
error_msg = str(exc_value) if exc_value else str(exc_type)
|
||||
error_detail = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
||||
|
||||
# 记录日志
|
||||
logging.error(f"线程异常 ({args.thread.name}):\n{error_detail}")
|
||||
|
||||
# 通过信号在主线程显示对话框
|
||||
self.error_signal.error_occurred.emit(error_msg, error_detail)
|
||||
|
||||
def _show_error_dialog(self, error_msg: str, error_detail: str):
|
||||
"""显示错误对话框"""
|
||||
try:
|
||||
dialog = ErrorDialog(
|
||||
title="发生错误",
|
||||
message=f"程序遇到了一个问题:\n\n{error_msg}\n\n详情已记录到日志文件。",
|
||||
detail=error_detail
|
||||
)
|
||||
dialog.exec()
|
||||
except Exception as e:
|
||||
# 如果对话框也出错,至少打印到控制台
|
||||
print(f"显示错误对话框失败: {e}")
|
||||
print(f"原始错误: {error_msg}")
|
||||
print(error_detail)
|
||||
|
||||
@staticmethod
|
||||
def safe_execute(func, *args, error_msg: str = "操作失败", **kwargs):
|
||||
"""安全执行函数,捕获异常并显示友好提示"""
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
logging.error(f"{error_msg}: {e}\n{traceback.format_exc()}")
|
||||
QMessageBox.warning(None, "警告", f"{error_msg}\n\n{str(e)}")
|
||||
return None
|
||||
|
||||
91
core/logger.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
日志配置模块
|
||||
- 日志文件: logs/app_YYYYMMDD.log
|
||||
- 日志格式: [时间] [级别] [模块] 消息
|
||||
- 自动清理30天前的旧日志
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_logs_dir() -> Path:
|
||||
"""获取日志目录路径"""
|
||||
# 获取应用根目录
|
||||
app_root = Path(__file__).parent.parent
|
||||
logs_dir = app_root / "logs"
|
||||
logs_dir.mkdir(exist_ok=True)
|
||||
return logs_dir
|
||||
|
||||
|
||||
def get_log_file_path() -> Path:
|
||||
"""获取当天的日志文件路径"""
|
||||
today = datetime.date.today().strftime("%Y%m%d")
|
||||
return get_logs_dir() / f"app_{today}.log"
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""配置日志系统"""
|
||||
log_file = get_log_file_path()
|
||||
|
||||
# 日志格式
|
||||
log_format = "[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s"
|
||||
date_format = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
# 配置根日志器
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format=log_format,
|
||||
datefmt=date_format,
|
||||
handlers=[
|
||||
# 文件处理器
|
||||
logging.FileHandler(log_file, encoding="utf-8"),
|
||||
# 控制台处理器
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
# 记录启动日志
|
||||
logging.info("=" * 50)
|
||||
logging.info("奶酪云工具箱 启动")
|
||||
logging.info("=" * 50)
|
||||
|
||||
# 清理旧日志
|
||||
cleanup_old_logs()
|
||||
|
||||
|
||||
def cleanup_old_logs(days: int = 30):
|
||||
"""清理指定天数前的旧日志"""
|
||||
logs_dir = get_logs_dir()
|
||||
cutoff_date = datetime.date.today() - datetime.timedelta(days=days)
|
||||
|
||||
for log_file in logs_dir.glob("app_*.log"):
|
||||
try:
|
||||
# 从文件名提取日期
|
||||
date_str = log_file.stem.replace("app_", "")
|
||||
file_date = datetime.datetime.strptime(date_str, "%Y%m%d").date()
|
||||
|
||||
if file_date < cutoff_date:
|
||||
log_file.unlink()
|
||||
logging.info(f"已清理旧日志文件: {log_file.name}")
|
||||
except (ValueError, OSError) as e:
|
||||
logging.warning(f"清理日志文件失败 {log_file}: {e}")
|
||||
|
||||
|
||||
def get_all_log_files() -> list:
|
||||
"""获取所有日志文件列表(按时间倒序)"""
|
||||
logs_dir = get_logs_dir()
|
||||
log_files = list(logs_dir.glob("app_*.log"))
|
||||
log_files.sort(key=lambda x: x.name, reverse=True)
|
||||
return log_files
|
||||
|
||||
|
||||
def read_log_file(file_path: Path) -> str:
|
||||
"""读取日志文件内容"""
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
return f"读取日志文件失败: {e}"
|
||||
|
||||
BIN
gen/30adcbef76094b3637c7e670b1cc7cd98c109dd2_watermarked.png
Normal file
|
After Width: | Height: | Size: 5.5 MiB |
BIN
gen/6683e880517b1098e0fbaa664ffa228d_compressed.ico
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
gen/【哲风壁纸】2024-10-31_22_58_08.pdf
Normal file
BIN
image/二维码.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
image/头像.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
image/生成奶酪商城官方店介绍.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
463
logs/app_20251210.log
Normal file
@@ -0,0 +1,463 @@
|
||||
[2025-12-10 23:45:39] [INFO] [root] ==================================================
|
||||
[2025-12-10 23:45:39] [INFO] [root] 奶酪云工具箱 启动
|
||||
[2025-12-10 23:45:39] [INFO] [root] ==================================================
|
||||
[2025-12-10 23:45:40] [INFO] [root] 全局错误处理器已初始化
|
||||
[2025-12-10 23:45:40] [INFO] [root] 加载了 1 个日志文件
|
||||
[2025-12-10 23:45:42] [DEBUG] [matplotlib] matplotlib data path: C:\Users\Lenovo\AppData\Local\Programs\Python\Python312\Lib\site-packages\matplotlib\mpl-data
|
||||
[2025-12-10 23:45:42] [DEBUG] [matplotlib] CONFIGDIR=C:\Users\Lenovo\.matplotlib
|
||||
[2025-12-10 23:45:42] [DEBUG] [matplotlib] interactive is False
|
||||
[2025-12-10 23:45:42] [DEBUG] [matplotlib] platform is win32
|
||||
[2025-12-10 23:45:42] [DEBUG] [matplotlib] CACHEDIR=C:\Users\Lenovo\.matplotlib
|
||||
[2025-12-10 23:45:42] [DEBUG] [matplotlib.font_manager] font search path [WindowsPath('C:/Users/Lenovo/AppData/Local/Programs/Python/Python312/Lib/site-packages/matplotlib/mpl-data/fonts/ttf'), WindowsPath('C:/Users/Lenovo/AppData/Local/Programs/Python/Python312/Lib/site-packages/matplotlib/mpl-data/fonts/afm'), WindowsPath('C:/Users/Lenovo/AppData/Local/Programs/Python/Python312/Lib/site-packages/matplotlib/mpl-data/fonts/pdfcorefonts')]
|
||||
[2025-12-10 23:45:42] [INFO] [matplotlib.font_manager] generated new fontManager
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: Matching sans\-serif:style=normal:variant=normal:weight=normal:stretch=normal:size=10.0.
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSans.ttf', name='DejaVu Sans', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 6.716666666666666
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSerif-Bold.ttf', name='DejaVu Serif', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSerifDisplay.ttf', name='DejaVu Serif Display', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXNonUni.ttf', name='STIXNonUnicode', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXSizThreeSymReg.ttf', name='STIXSizeThreeSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSerif-Italic.ttf', name='DejaVu Serif', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSansMono-BoldOblique.ttf', name='DejaVu Sans Mono', style='oblique', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSansMono-Oblique.ttf', name='DejaVu Sans Mono', style='oblique', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSansMono.ttf', name='DejaVu Sans Mono', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXSizOneSymReg.ttf', name='STIXSizeOneSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSans-BoldOblique.ttf', name='DejaVu Sans', style='oblique', variant='normal', weight=700, stretch='normal', size='scalable')) = 8.001666666666665
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXNonUniIta.ttf', name='STIXNonUnicode', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXGeneralBolIta.ttf', name='STIXGeneral', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXGeneral.ttf', name='STIXGeneral', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\cmr10.ttf', name='cmr10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXGeneralBol.ttf', name='STIXGeneral', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSansDisplay.ttf', name='DejaVu Sans Display', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXSizFourSymReg.ttf', name='STIXSizeFourSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSerif-BoldItalic.ttf', name='DejaVu Serif', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\cmsy10.ttf', name='cmsy10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSerif.ttf', name='DejaVu Serif', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\cmss10.ttf', name='cmss10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXGeneralItalic.ttf', name='STIXGeneral', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXSizFourSymBol.ttf', name='STIXSizeFourSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\cmex10.ttf', name='cmex10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXSizOneSymBol.ttf', name='STIXSizeOneSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\cmmi10.ttf', name='cmmi10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSans-Oblique.ttf', name='DejaVu Sans', style='oblique', variant='normal', weight=400, stretch='normal', size='scalable')) = 7.716666666666666
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXNonUniBolIta.ttf', name='STIXNonUnicode', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSans-Bold.ttf', name='DejaVu Sans', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 7.001666666666666
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\DejaVuSansMono-Bold.ttf', name='DejaVu Sans Mono', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\cmb10.ttf', name='cmb10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXNonUniBol.ttf', name='STIXNonUnicode', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXSizFiveSymReg.ttf', name='STIXSizeFiveSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXSizThreeSymBol.ttf', name='STIXSizeThreeSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\cmtt10.ttf', name='cmtt10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXSizTwoSymBol.ttf', name='STIXSizeTwoSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\matplotlib\\mpl-data\\fonts\\ttf\\STIXSizTwoSymReg.ttf', name='STIXSizeTwoSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ERASBD.TTF', name='Eras Bold ITC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LFAXD.TTF', name='Lucida Fax', style='normal', variant='normal', weight=600, stretch='normal', size='scalable')) = 10.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FRAHVIT.TTF', name='Franklin Gothic Heavy', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FORTE.TTF', name='Forte', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\NIAGSOL.TTF', name='Niagara Solid', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\verdana.ttf', name='Verdana', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Dengb.ttf', name='DengXian', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FREESCPT.TTF', name='Freestyle Script', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CALIST.TTF', name='Calisto MT', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\pala.ttf', name='Palatino Linotype', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CALISTI.TTF', name='Calisto MT', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LFAX.TTF', name='Lucida Fax', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOD_B.TTF', name='Bodoni MT', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\mingliub.ttc', name='MingLiU-ExtB', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\corbelli.ttf', name='Corbel', style='italic', variant='normal', weight=300, stretch='normal', size='scalable')) = 11.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\simhei.ttf', name='SimHei', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 3.383333333333333
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\simsun.ttc', name='SimSun', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\malgunsl.ttf', name='Malgun Gothic', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SNAP____.TTF', name='Snap ITC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\seguisym.ttf', name='Segoe UI Symbol', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SCHLBKI.TTF', name='Century Schoolbook', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\OCRAEXT.TTF', name='OCR A Extended', style='normal', variant='normal', weight=400, stretch='expanded', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\georgiaz.ttf', name='Georgia', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GIL_____.TTF', name='Gill Sans MT', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\segoeuib.ttf', name='Segoe UI', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\RAVIE.TTF', name='Ravie', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOD_BLAR.TTF', name='Bodoni MT', style='normal', variant='normal', weight=900, stretch='normal', size='scalable')) = 10.525
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\STLITI.TTF', name='STLiti', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CALIFI.TTF', name='Californian FB', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LSANSI.TTF', name='Lucida Sans', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ENGR.TTF', name='Engravers MT', style='normal', variant='normal', weight=500, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BAUHS93.TTF', name='Bauhaus 93', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SegUIVar.ttf', name='Segoe UI Variable', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOD_R.TTF', name='Bodoni MT', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\YuGothM.ttc', name='Yu Gothic', style='normal', variant='normal', weight=500, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\RAGE.TTF', name='Rage Italic', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\VINERITC.TTF', name='Viner Hand ITC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\MTEXTRA.TTF', name='MT Extra', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\corbel.ttf', name='Corbel', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SCHLBKB.TTF', name='Century Schoolbook', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BRLNSDB.TTF', name='Berlin Sans FB Demi', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FZSTK.TTF', name='FZShuTi', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\TCM_____.TTF', name='Tw Cen MT', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\HTOWERTI.TTF', name='High Tower Text', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\JOKERMAN.TTF', name='Jokerman', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\consolab.ttf', name='Consolas', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\taileb.ttf', name='Microsoft Tai Le', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SIMLI.TTF', name='LiSu', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\PRISTINA.TTF', name='Pristina', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BKANT.TTF', name='Book Antiqua', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BRITANIC.TTF', name='Britannic Bold', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Noto Sans SC (TrueType).otf', name='Noto Sans SC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CHILLER.TTF', name='Chiller', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\OUTLOOK.TTF', name='MS Outlook', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\mmrtext.ttf', name='Myanmar Text', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CASTELAR.TTF', name='Castellar', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\REFSPCL.TTF', name='MS Reference Specialty', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\arial.ttf', name='Arial', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\cambriai.ttf', name='Cambria', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ROCK.TTF', name='Rockwell', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\WINGDNG3.TTF', name='Wingdings 3', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\segoeuiz.ttf', name='Segoe UI', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\palab.ttf', name='Palatino Linotype', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\PERI____.TTF', name='Perpetua', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\POORICH.TTF', name='Poor Richard', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\comicz.ttf', name='Comic Sans MS', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CENSCBK.TTF', name='Century Schoolbook', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FRABK.TTF', name='Franklin Gothic Book', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SCHLBKBI.TTF', name='Century Schoolbook', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\times.ttf', name='Times New Roman', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SitkaVF.ttf', name='Sitka', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ONYX.TTF', name='Onyx', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FZYTK.TTF', name='FZYaoTi', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ITCEDSCR.TTF', name='Edwardian Script ITC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LSANSDI.TTF', name='Lucida Sans', style='italic', variant='normal', weight=600, stretch='normal', size='scalable')) = 11.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FELIXTI.TTF', name='Felix Titling', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\DUBAI-REGULAR.TTF', name='Dubai', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GOTHIC.TTF', name='Century Gothic', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\segmdl2.ttf', name='Segoe MDL2 Assets', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LCALLIG.TTF', name='Lucida Calligraphy', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\seguisbi.ttf', name='Segoe UI', style='italic', variant='normal', weight=600, stretch='normal', size='scalable')) = 11.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\PERB____.TTF', name='Perpetua', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ERASLGHT.TTF', name='Eras Light ITC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BELLB.TTF', name='Bell MT', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\NIAGENG.TTF', name='Niagara Engraved', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\constanz.ttf', name='Constantia', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\msyhl.ttc', name='Microsoft YaHei', style='normal', variant='normal', weight=290, stretch='normal', size='scalable')) = 0.1545
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOD_CI.TTF', name='Bodoni MT', style='italic', variant='normal', weight=400, stretch='condensed', size='scalable')) = 11.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\INFROMAN.TTF', name='Informal Roman', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GARABD.TTF', name='Garamond', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\WINGDNG2.TTF', name='Wingdings 2', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FRADMIT.TTF', name='Franklin Gothic Demi', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\consolai.ttf', name='Consolas', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CALISTB.TTF', name='Calisto MT', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ALGER.TTF', name='Algerian', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\COLONNA.TTF', name='Colonna MT', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Dengl.ttf', name='DengXian', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\STSONG.TTF', name='STSong', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\corbelb.ttf', name='Corbel', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\timesbd.ttf', name='Times New Roman', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\tahoma.ttf', name='Tahoma', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LHANDW.TTF', name='Lucida Handwriting', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GLSNECB.TTF', name='Gill Sans MT Ext Condensed Bold', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\segoescb.ttf', name='Segoe Script', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Candarai.ttf', name='Candara', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ebrima.ttf', name='Ebrima', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LSANS.TTF', name='Lucida Sans', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\phagspa.ttf', name='Microsoft PhagsPa', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\msgothic.ttc', name='MS Gothic', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ANTQUAI.TTF', name='Book Antiqua', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BERNHC.TTF', name='Bernard MT Condensed', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\PERTILI.TTF', name='Perpetua Titling MT', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SimsunExtG.ttf', name='SimSun-ExtG', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BSSYM7.TTF', name='Bookshelf Symbol 7', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\timesbi.ttf', name='Times New Roman', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\STXINWEI.TTF', name='STXinwei', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LTYPEB.TTF', name='Lucida Sans Typewriter', style='normal', variant='normal', weight=600, stretch='normal', size='scalable')) = 10.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ARIALN.TTF', name='Arial', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\palabi.ttf', name='Palatino Linotype', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOD_CB.TTF', name='Bodoni MT', style='normal', variant='normal', weight=700, stretch='condensed', size='scalable')) = 10.535
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\MAGNETOB.TTF', name='Magneto', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SansSerifCollection.ttf', name='Sans Serif Collection', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\monbaiti.ttf', name='Mongolian Baiti', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LBRITEDI.TTF', name='Lucida Bright', style='italic', variant='normal', weight=600, stretch='normal', size='scalable')) = 11.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\PLAYBILL.TTF', name='Playbill', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\taile.ttf', name='Microsoft Tai Le', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\NotoSansSC-VF.ttf', name='Noto Sans SC', style='normal', variant='normal', weight=100, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GILC____.TTF', name='Gill Sans MT Condensed', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\arialbi.ttf', name='Arial', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LFAXDI.TTF', name='Lucida Fax', style='italic', variant='normal', weight=600, stretch='normal', size='scalable')) = 11.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FRAMDCN.TTF', name='Franklin Gothic Medium Cond', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\sylfaen.ttf', name='Sylfaen', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ariblk.ttf', name='Arial', style='normal', variant='normal', weight=900, stretch='normal', size='scalable')) = 10.525
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GILSANUB.TTF', name='Gill Sans Ultra Bold', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BELLI.TTF', name='Bell MT', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\seguibl.ttf', name='Segoe UI', style='normal', variant='normal', weight=900, stretch='normal', size='scalable')) = 10.525
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\STXINGKA.TTF', name='STXingkai', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\malgunbd.ttf', name='Malgun Gothic', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\lucon.ttf', name='Lucida Console', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LTYPEO.TTF', name='Lucida Sans Typewriter', style='oblique', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\YuGothB.ttc', name='Yu Gothic', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\TCCEB.TTF', name='Tw Cen MT Condensed Extra Bold', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\TCCB____.TTF', name='Tw Cen MT Condensed', style='normal', variant='normal', weight=700, stretch='condensed', size='scalable')) = 10.535
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LeelUIsl.ttf', name='Leelawadee UI', style='normal', variant='normal', weight=350, stretch='normal', size='scalable')) = 10.0975
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\comici.ttf', name='Comic Sans MS', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\georgia.ttf', name='Georgia', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\MISTRAL.TTF', name='Mistral', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ROCKI.TTF', name='Rockwell', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\AGENCYB.TTF', name='Agency FB', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\comicbd.ttf', name='Comic Sans MS', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\segoeui.ttf', name='Segoe UI', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FRAHV.TTF', name='Franklin Gothic Heavy', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\comic.ttf', name='Comic Sans MS', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ntailub.ttf', name='Microsoft New Tai Lue', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LBRITED.TTF', name='Lucida Bright', style='normal', variant='normal', weight=600, stretch='normal', size='scalable')) = 10.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\trebucit.ttf', name='Trebuchet MS', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Candarali.ttf', name='Candara', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\corbeli.ttf', name='Corbel', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\cour.ttf', name='Courier New', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\HTOWERT.TTF', name='High Tower Text', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Noto Sans SC Medium (TrueType).otf', name='Noto Sans SC', style='normal', variant='normal', weight=500, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\seguisli.ttf', name='Segoe UI', style='italic', variant='normal', weight=350, stretch='normal', size='scalable')) = 11.0975
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\simfang.ttf', name='FangSong', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ROCKBI.TTF', name='Rockwell', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\PARCHM.TTF', name='Parchment', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOD_CBI.TTF', name='Bodoni MT', style='italic', variant='normal', weight=700, stretch='condensed', size='scalable')) = 11.535
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOOKOSB.TTF', name='Bookman Old Style', style='normal', variant='normal', weight=600, stretch='normal', size='scalable')) = 10.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\PERTIBD.TTF', name='Perpetua Titling MT', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ITCBLKAD.TTF', name='Blackadder ITC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ariali.ttf', name='Arial', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\georgiai.ttf', name='Georgia', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\consolaz.ttf', name='Consolas', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Gabriola.ttf', name='Gabriola', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SitkaVF-Italic.ttf', name='Sitka', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\PERBI___.TTF', name='Perpetua', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\gadugi.ttf', name='Gadugi', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ebrimabd.ttf', name='Ebrima', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\symbol.ttf', name='Symbol', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\OLDENGL.TTF', name='Old English Text MT', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\YuGothR.ttc', name='Yu Gothic', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\malgun.ttf', name='Malgun Gothic', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\wingding.ttf', name='Wingdings', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LBRITE.TTF', name='Lucida Bright', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ERASMD.TTF', name='Eras Medium ITC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GILB____.TTF', name='Gill Sans MT', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\consola.ttf', name='Consolas', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\mvboli.ttf', name='MV Boli', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GOTHICBI.TTF', name='Century Gothic', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BRLNSB.TTF', name='Berlin Sans FB', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOOKOS.TTF', name='Bookman Old Style', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GILLUBCD.TTF', name='Gill Sans Ultra Bold Condensed', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\simsunb.ttf', name='SimSun-ExtB', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\impact.ttf', name='Impact', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ROCCB___.TTF', name='Rockwell Condensed', style='normal', variant='normal', weight=700, stretch='condensed', size='scalable')) = 10.535
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BRADHITC.TTF', name='Bradley Hand ITC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\palai.ttf', name='Palatino Linotype', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\bahnschrift.ttf', name='Bahnschrift', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\trebucbd.ttf', name='Trebuchet MS', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ITCKRIST.TTF', name='Kristen ITC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FRABKIT.TTF', name='Franklin Gothic Book', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\couri.ttf', name='Courier New', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\IMPRISHA.TTF', name='Imprint MT Shadow', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\msjh.ttc', name='Microsoft JhengHei', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GILBI___.TTF', name='Gill Sans MT', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOOKOSI.TTF', name='Bookman Old Style', style='italic', variant='normal', weight=300, stretch='normal', size='scalable')) = 11.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOD_I.TTF', name='Bodoni MT', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\constan.ttf', name='Constantia', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\MTCORSVA.TTF', name='Monotype Corsiva', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\PAPYRUS.TTF', name='Papyrus', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Candara.ttf', name='Candara', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FTLTLT.TTF', name='Footlight MT Light', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FRSCRIPT.TTF', name='French Script MT', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\seguili.ttf', name='Segoe UI', style='italic', variant='normal', weight=300, stretch='normal', size='scalable')) = 11.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\calibri.ttf', name='Calibri', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\segoepr.ttf', name='Segoe Print', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\segoeuisl.ttf', name='Segoe UI', style='normal', variant='normal', weight=350, stretch='normal', size='scalable')) = 10.0975
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\msyhbd.ttc', name='Microsoft YaHei', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 0.33499999999999996
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\himalaya.ttf', name='Microsoft Himalaya', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\MOD20.TTF', name='Modern No. 20', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Deng.ttf', name='DengXian', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CURLZ___.TTF', name='Curlz MT', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ARIALNB.TTF', name='Arial', style='normal', variant='normal', weight=700, stretch='condensed', size='scalable')) = 10.535
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\verdanaz.ttf', name='Verdana', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LeelawUI.ttf', name='Leelawadee UI', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOD_BI.TTF', name='Bodoni MT', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\verdanai.ttf', name='Verdana', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\JUICE___.TTF', name='Juice ITC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\segoeuil.ttf', name='Segoe UI', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\VLADIMIR.TTF', name='Vladimir Script', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\webdings.ttf', name='Webdings', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LFAXI.TTF', name='Lucida Fax', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ARIALNBI.TTF', name='Arial', style='italic', variant='normal', weight=700, stretch='condensed', size='scalable')) = 11.535
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\COOPBL.TTF', name='Cooper Black', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GARA.TTF', name='Garamond', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\COPRGTB.TTF', name='Copperplate Gothic Bold', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GOUDYSTO.TTF', name='Goudy Stout', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\constanb.ttf', name='Constantia', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\trebuc.ttf', name='Trebuchet MS', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\corbell.ttf', name='Corbel', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOD_PSTC.TTF', name='Bodoni MT', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CALIFB.TTF', name='Californian FB', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\PALSCRI.TTF', name='Palace Script MT', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Nirmala.ttc', name='Nirmala UI', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\STENCIL.TTF', name='Stencil', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\georgiab.ttf', name='Georgia', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LeelaUIb.ttf', name='Leelawadee UI', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\STFANGSO.TTF', name='STFangsong', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BRLNSR.TTF', name='Berlin Sans FB', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\TEMPSITC.TTF', name='Tempus Sans ITC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\seguisb.ttf', name='Segoe UI', style='normal', variant='normal', weight=600, stretch='normal', size='scalable')) = 10.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Candarab.ttf', name='Candara', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\DUBAI-LIGHT.TTF', name='Dubai', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LBRITEI.TTF', name='Lucida Bright', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\HARLOWSI.TTF', name='Harlow Solid Italic', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\NotoSerifSC-VF.ttf', name='Noto Serif SC', style='normal', variant='normal', weight=200, stretch='normal', size='scalable')) = 10.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\cambria.ttc', name='Cambria', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\VIVALDII.TTF', name='Vivaldi', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\YuGothL.ttc', name='Yu Gothic', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\verdanab.ttf', name='Verdana', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GOTHICB.TTF', name='Century Gothic', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\javatext.ttf', name='Javanese Text', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\TCBI____.TTF', name='Tw Cen MT', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\framd.ttf', name='Franklin Gothic Medium', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Source Han Serif SC Heavy (TrueType).ttf', name='Source Han Serif SC', style='normal', variant='normal', weight=900, stretch='normal', size='scalable')) = 10.525
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\MATURASC.TTF', name='Matura MT Script Capitals', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\seguiemj.ttf', name='Segoe UI Emoji', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\simkai.ttf', name='KaiTi', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FRADM.TTF', name='Franklin Gothic Demi', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ELEPHNT.TTF', name='Elephant', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\courbd.ttf', name='Courier New', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\corbelz.ttf', name='Corbel', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Candaraz.ttf', name='Candara', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\l_10646.ttf', name='Lucida Sans Unicode', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\TCCM____.TTF', name='Tw Cen MT Condensed', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\calibril.ttf', name='Calibri', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\mmrtextb.ttf', name='Myanmar Text', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\calibrili.ttf', name='Calibri', style='italic', variant='normal', weight=300, stretch='normal', size='scalable')) = 11.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\msyi.ttf', name='Microsoft Yi Baiti', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ROCKEB.TTF', name='Rockwell Extra Bold', style='normal', variant='normal', weight=800, stretch='normal', size='scalable')) = 10.43
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\calibrib.ttf', name='Calibri', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOOKOSBI.TTF', name='Bookman Old Style', style='italic', variant='normal', weight=600, stretch='normal', size='scalable')) = 11.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Candaral.ttf', name='Candara', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\COPRGTL.TTF', name='Copperplate Gothic Light', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\HARNGTON.TTF', name='Harrington', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\TCB_____.TTF', name='Tw Cen MT', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\tahomabd.ttf', name='Tahoma', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SCRIPTBL.TTF', name='Script MT Bold', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GOUDOSI.TTF', name='Goudy Old Style', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Users\\Lenovo\\AppData\\Local\\Microsoft\\Windows\\Fonts\\UbuntuMono[wght].ttf', name='Ubuntu Mono', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SHOWG.TTF', name='Showcard Gothic', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\TCMI____.TTF', name='Tw Cen MT', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\gadugib.ttf', name='Gadugi', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOD_CR.TTF', name='Bodoni MT', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GARAIT.TTF', name='Garamond', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GOTHICI.TTF', name='Century Gothic', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\STHUPO.TTF', name='STHupo', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\constani.ttf', name='Constantia', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ARIALNI.TTF', name='Arial', style='italic', variant='normal', weight=400, stretch='condensed', size='scalable')) = 11.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ARLRDBD.TTF', name='Arial Rounded MT Bold', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\AGENCYR.TTF', name='Agency FB', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LTYPEBO.TTF', name='Lucida Sans Typewriter', style='oblique', variant='normal', weight=600, stretch='normal', size='scalable')) = 11.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\framdit.ttf', name='Franklin Gothic Medium', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\micross.ttf', name='Microsoft Sans Serif', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CALIFR.TTF', name='Californian FB', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BROADW.TTF', name='Broadway', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\phagspab.ttf', name='Microsoft PhagsPa', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ANTQUABI.TTF', name='Book Antiqua', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\courbi.ttf', name='Courier New', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\PER_____.TTF', name='Perpetua', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\HATTEN.TTF', name='Haettenschweiler', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\cambriaz.ttf', name='Cambria', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ANTQUAB.TTF', name='Book Antiqua', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\STKAITI.TTF', name='STKaiti', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\seguibli.ttf', name='Segoe UI', style='italic', variant='normal', weight=900, stretch='normal', size='scalable')) = 11.525
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\cambriab.ttf', name='Cambria', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\msjhl.ttc', name='Microsoft JhengHei', style='normal', variant='normal', weight=290, stretch='normal', size='scalable')) = 10.1545
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\MAIAN.TTF', name='Maiandra GD', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\DUBAI-BOLD.TTF', name='Dubai', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LTYPE.TTF', name='Lucida Sans Typewriter', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GIGI.TTF', name='Gigi', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BELL.TTF', name='Bell MT', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\FRADMCN.TTF', name='Franklin Gothic Demi Cond', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Noto Sans SC Bold (TrueType).otf', name='Noto Sans SC', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GOUDOSB.TTF', name='Goudy Old Style', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ROCKB.TTF', name='Rockwell', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GILI____.TTF', name='Gill Sans MT', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\msjhbd.ttc', name='Microsoft JhengHei', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ntailu.ttf', name='Microsoft New Tai Lue', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GLECB.TTF', name='Gloucester MT Extra Condensed', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\DUBAI-MEDIUM.TTF', name='Dubai', style='normal', variant='normal', weight=500, stretch='normal', size='scalable')) = 10.145
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ROCC____.TTF', name='Rockwell Condensed', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CENTURY.TTF', name='Century', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\arialbd.ttf', name='Arial', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LATINWD.TTF', name='Wide Latin', style='normal', variant='normal', weight=400, stretch='expanded', size='scalable')) = 10.25
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\GOUDOS.TTF', name='Goudy Old Style', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\STXIHEI.TTF', name='STXihei', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SIMYOU.TTF', name='YouYuan', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\calibrii.ttf', name='Calibri', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\seguihis.ttf', name='Segoe UI Historic', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\SegoeIcons.ttf', name='Segoe Fluent Icons', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\segoesc.ttf', name='Segoe Script', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\STCAIYUN.TTF', name='STCaiyun', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CALISTBI.TTF', name='Calisto MT', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\calibriz.ttf', name='Calibri', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BRUSHSCI.TTF', name='Brush Script MT', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\segoeuii.ttf', name='Segoe UI', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\msyh.ttc', name='Microsoft YaHei', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 0.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\STZHONGS.TTF', name='STZhongsong', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\LSANSD.TTF', name='Lucida Sans', style='normal', variant='normal', weight=600, stretch='normal', size='scalable')) = 10.24
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\segoeprb.ttf', name='Segoe Print', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\CENTAUR.TTF', name='Centaur', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\Inkfree.ttf', name='Ink Free', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\REFSAN.TTF', name='MS Reference Sans Serif', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BASKVILL.TTF', name='Baskerville Old Face', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\BOD_BLAI.TTF', name='Bodoni MT', style='italic', variant='normal', weight=900, stretch='normal', size='scalable')) = 11.525
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\KUNSTLER.TTF', name='Kunstler Script', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ERASDEMI.TTF', name='Eras Demi ITC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\trebucbi.ttf', name='Trebuchet MS', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\timesi.ttf', name='Times New Roman', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: score(FontEntry(fname='C:\\Windows\\Fonts\\ELEPHNTI.TTF', name='Elephant', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05
|
||||
[2025-12-10 23:45:43] [DEBUG] [matplotlib.font_manager] findfont: Matching sans\-serif:style=normal:variant=normal:weight=normal:stretch=normal:size=10.0 to Microsoft YaHei ('C:\\Windows\\Fonts\\msyh.ttc') with score of 0.050000.
|
||||
[2025-12-10 23:45:43] [INFO] [root] 主窗口初始化完成
|
||||
[2025-12-10 23:45:45] [INFO] [root] 选择工具: 图片压缩 (img-compress)
|
||||
[2025-12-10 23:45:48] [DEBUG] [root] 切换分类: pdf
|
||||
[2025-12-10 23:45:50] [DEBUG] [root] 切换分类: image
|
||||
[2025-12-10 23:45:51] [INFO] [root] 选择工具: 图片压缩 (img-compress)
|
||||
[2025-12-10 23:46:15] [INFO] [root] 添加了 1 个图片文件
|
||||
[2025-12-10 23:46:44] [INFO] [root] 开始压缩 1 个文件, 质量: 10%
|
||||
[2025-12-10 23:46:45] [INFO] [root] 压缩完成: 成功 1/1
|
||||
[2025-12-10 23:47:52] [DEBUG] [root] 打开日志查看器
|
||||
[2025-12-10 23:47:57] [DEBUG] [root] 切换分类: pdf
|
||||
[2025-12-10 23:47:58] [DEBUG] [root] 切换分类: excel
|
||||
[2025-12-10 23:48:00] [DEBUG] [root] 切换分类: image
|
||||
[2025-12-10 23:48:02] [DEBUG] [root] 快捷入口: pdf-split, pdf
|
||||
[2025-12-10 23:48:02] [INFO] [root] 选择工具: PDF 拆分 (pdf-split)
|
||||
[2025-12-10 23:48:04] [INFO] [root] 选择工具: PDF 合并 (pdf-merge)
|
||||
[2025-12-10 23:48:05] [INFO] [root] 选择工具: PDF 转 Word (pdf-word)
|
||||
[2025-12-10 23:48:06] [DEBUG] [root] 切换分类: excel
|
||||
[2025-12-10 23:48:07] [DEBUG] [root] 切换分类: image
|
||||
[2025-12-10 23:53:19] [DEBUG] [root] 切换分类: pdf
|
||||
[2025-12-10 23:53:19] [DEBUG] [root] 切换分类: image
|
||||
[2025-12-10 23:53:24] [INFO] [root] ==================================================
|
||||
[2025-12-10 23:53:24] [INFO] [root] 奶酪云工具箱 启动
|
||||
[2025-12-10 23:53:24] [INFO] [root] ==================================================
|
||||
[2025-12-10 23:53:24] [INFO] [root] 全局错误处理器已初始化
|
||||
[2025-12-10 23:53:24] [ERROR] [root] 未捕获的异常:
|
||||
Traceback (most recent call last):
|
||||
File "D:\workers\project\ai\nly-tool\main.py", line 60, in <module>
|
||||
main()
|
||||
File "D:\workers\project\ai\nly-tool\main.py", line 52, in main
|
||||
window = MainWindow()
|
||||
^^^^^^^^^^^^
|
||||
File "D:\workers\project\ai\nly-tool\ui\main_window.py", line 32, in __init__
|
||||
self.setup_ui()
|
||||
File "D:\workers\project\ai\nly-tool\ui\main_window.py", line 49, in setup_ui
|
||||
self.primary_sidebar = PrimarySidebar()
|
||||
^^^^^^^^^^^^^^^^
|
||||
File "D:\workers\project\ai\nly-tool\ui\sidebar.py", line 189, in __init__
|
||||
self.setup_ui()
|
||||
File "D:\workers\project\ai\nly-tool\ui\sidebar.py", line 204, in setup_ui
|
||||
self.image_btn = CategoryButton("image", "图片工具")
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\workers\project\ai\nly-tool\ui\sidebar.py", line 72, in __init__
|
||||
self.icon_widget = IconWidget(icon_name, 24, "#94a3b8", self)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\workers\project\ai\nly-tool\ui\sidebar.py", line 37, in __init__
|
||||
self.update_icon()
|
||||
File "D:\workers\project\ai\nly-tool\ui\sidebar.py", line 49, in update_icon
|
||||
pixmap = QPixmap(self.icon_size, self.icon_size)
|
||||
^^^^^^^
|
||||
NameError: name 'QPixmap' is not defined
|
||||
|
||||
2301
logs/app_20251211.log
Normal file
61
main.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
奶酪云工具箱 - 主入口
|
||||
Cheese Cloud Tools - Main Entry
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到路径
|
||||
PROJECT_ROOT = Path(__file__).parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtGui import QFont
|
||||
|
||||
from core.logger import setup_logging
|
||||
from core.error_handler import ErrorHandler
|
||||
from ui.main_window import MainWindow
|
||||
|
||||
|
||||
def load_stylesheet() -> str:
|
||||
"""加载样式表"""
|
||||
style_path = PROJECT_ROOT / "resources" / "style.qss"
|
||||
if style_path.exists():
|
||||
with open(style_path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 初始化日志
|
||||
setup_logging()
|
||||
|
||||
# 创建应用
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("奶酪云工具箱")
|
||||
app.setApplicationVersion("1.0.0")
|
||||
|
||||
# 设置默认字体
|
||||
font = QFont("Microsoft YaHei", 10)
|
||||
app.setFont(font)
|
||||
|
||||
# 初始化全局错误处理
|
||||
error_handler = ErrorHandler(app)
|
||||
|
||||
# 加载样式表
|
||||
stylesheet = load_stylesheet()
|
||||
if stylesheet:
|
||||
app.setStyleSheet(stylesheet)
|
||||
|
||||
# 创建主窗口
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
|
||||
# 运行应用
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
21
requirements.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
# GUI Framework (LGPL, 商用免费)
|
||||
PySide6>=6.6.0
|
||||
|
||||
# Image Processing
|
||||
Pillow>=10.0.0
|
||||
|
||||
# PDF Processing
|
||||
PyMuPDF>=1.23.0
|
||||
|
||||
# PDF to Word
|
||||
pdf2docx>=0.5.6
|
||||
|
||||
# Excel Processing
|
||||
openpyxl>=3.1.0
|
||||
pandas>=2.0.0
|
||||
|
||||
# Chart Generation
|
||||
matplotlib>=3.7.0
|
||||
|
||||
# Build Tools (打包工具)
|
||||
pyinstaller>=6.0.0
|
||||
689
resources/style.qss
Normal file
@@ -0,0 +1,689 @@
|
||||
/* 奶酪云工具箱 - 暗色主题样式表 */
|
||||
/* 精确还原 UI设计.html 配色 */
|
||||
|
||||
/* ==================== 配色变量参考 ==================== */
|
||||
/*
|
||||
cheese-400: #fbbf24 (主强调色)
|
||||
cheese-500: #f59e0b (深强调色)
|
||||
cheese-600: #d97706 (更深)
|
||||
darkbg-900: #0f172a (主背景)
|
||||
darkbg-800: #1e293b (侧边栏)
|
||||
darkbg-700: #334155 (边框/悬浮)
|
||||
slate-300: #cbd5e1 (主文字)
|
||||
slate-400: #94a3b8 (次要文字)
|
||||
slate-500: #64748b (弱文字)
|
||||
slate-600: #475569 (placeholder)
|
||||
*/
|
||||
|
||||
/* ==================== 全局样式 ==================== */
|
||||
* {
|
||||
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
QWidget {
|
||||
background-color: #0f172a;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* ==================== 主窗口 ==================== */
|
||||
QMainWindow {
|
||||
background-color: #0f172a;
|
||||
}
|
||||
|
||||
/* ==================== 滚动条 ==================== */
|
||||
QScrollBar:vertical {
|
||||
background: transparent;
|
||||
width: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical {
|
||||
background: #334155;
|
||||
border-radius: 3px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:vertical,
|
||||
QScrollBar::sub-line:vertical,
|
||||
QScrollBar::add-page:vertical,
|
||||
QScrollBar::sub-page:vertical {
|
||||
background: transparent;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
QScrollBar:horizontal {
|
||||
background: transparent;
|
||||
height: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal {
|
||||
background: #334155;
|
||||
border-radius: 3px;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:horizontal,
|
||||
QScrollBar::sub-line:horizontal,
|
||||
QScrollBar::add-page:horizontal,
|
||||
QScrollBar::sub-page:horizontal {
|
||||
background: transparent;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* ==================== 一级侧边栏 ==================== */
|
||||
#primary_sidebar {
|
||||
background-color: #1e293b;
|
||||
border-right: 1px solid #334155;
|
||||
}
|
||||
|
||||
/* ==================== 二级侧边栏 ==================== */
|
||||
#secondary_sidebar {
|
||||
background-color: rgba(30, 41, 59, 0.5);
|
||||
border-right: 1px solid #334155;
|
||||
}
|
||||
|
||||
/* ==================== 工作区 ==================== */
|
||||
#workspace {
|
||||
background-color: #0f172a;
|
||||
}
|
||||
|
||||
/* ==================== 按钮样式 ==================== */
|
||||
QPushButton {
|
||||
background-color: #334155;
|
||||
border: 1px solid #475569;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #475569;
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background-color: #1e293b;
|
||||
}
|
||||
|
||||
QPushButton:disabled {
|
||||
background-color: #1e293b;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* 主要按钮 */
|
||||
#primary_btn {
|
||||
background-color: #f59e0b;
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#primary_btn:hover {
|
||||
background-color: #fbbf24;
|
||||
}
|
||||
|
||||
#primary_btn:pressed {
|
||||
background-color: #d97706;
|
||||
}
|
||||
|
||||
/* 次要按钮 */
|
||||
#secondary_btn {
|
||||
background-color: transparent;
|
||||
border: 1px solid #334155;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
#secondary_btn:hover {
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
/* ==================== 输入框 ==================== */
|
||||
QLineEdit {
|
||||
background-color: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
selection-background-color: #fbbf24;
|
||||
selection-color: white;
|
||||
}
|
||||
|
||||
QLineEdit:focus {
|
||||
border-color: rgba(251, 191, 36, 0.5);
|
||||
}
|
||||
|
||||
QLineEdit::placeholder {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
/* ==================== 滑块 ==================== */
|
||||
QSlider::groove:horizontal {
|
||||
background: #0f172a;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QSlider::handle:horizontal {
|
||||
background: #fbbf24;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: -5px 0;
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
QSlider::handle:horizontal:hover {
|
||||
background: #fcd34d;
|
||||
}
|
||||
|
||||
QSlider::sub-page:horizontal {
|
||||
background: #fbbf24;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ==================== 进度条 ==================== */
|
||||
QProgressBar {
|
||||
background-color: #0f172a;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 8px;
|
||||
text-align: center;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #fbbf24;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ==================== 表格 ==================== */
|
||||
QTableWidget {
|
||||
background-color: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
gridline-color: #334155;
|
||||
alternate-background-color: #0f172a;
|
||||
}
|
||||
|
||||
QTableWidget::item {
|
||||
padding: 8px;
|
||||
color: #e2e8f0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
QTableWidget::item:selected {
|
||||
background-color: rgba(251, 191, 36, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
QHeaderView::section {
|
||||
background-color: #0f172a;
|
||||
color: #94a3b8;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-bottom: 1px solid #334155;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ==================== 列表 ==================== */
|
||||
QListWidget {
|
||||
background-color: rgba(15, 23, 42, 0.5);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
QListWidget::item {
|
||||
background-color: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
margin: 4px;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
QListWidget::item:hover {
|
||||
border-color: rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
|
||||
QListWidget::item:selected {
|
||||
background-color: rgba(251, 191, 36, 0.1);
|
||||
border-color: #fbbf24;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* ==================== 标签页 ==================== */
|
||||
QTabWidget::pane {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
QTabBar::tab {
|
||||
background-color: transparent;
|
||||
color: #94a3b8;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
QTabBar::tab:hover {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
QTabBar::tab:selected {
|
||||
color: #fbbf24;
|
||||
border-bottom: 2px solid #fbbf24;
|
||||
}
|
||||
|
||||
/* ==================== 单选按钮 ==================== */
|
||||
QRadioButton {
|
||||
color: #e2e8f0;
|
||||
spacing: 8px;
|
||||
}
|
||||
|
||||
QRadioButton::indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
border: 2px solid #475569;
|
||||
background-color: #0f172a;
|
||||
}
|
||||
|
||||
QRadioButton::indicator:hover {
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
|
||||
QRadioButton::indicator:checked {
|
||||
background-color: #fbbf24;
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
|
||||
/* ==================== 复选框 ==================== */
|
||||
QCheckBox {
|
||||
color: #e2e8f0;
|
||||
spacing: 8px;
|
||||
}
|
||||
|
||||
QCheckBox::indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #475569;
|
||||
background-color: #0f172a;
|
||||
}
|
||||
|
||||
QCheckBox::indicator:hover {
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
|
||||
QCheckBox::indicator:checked {
|
||||
background-color: #fbbf24;
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
|
||||
/* ==================== 下拉框 ==================== */
|
||||
QComboBox {
|
||||
background-color: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
color: #e2e8f0;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
QComboBox:hover {
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
QComboBox:focus {
|
||||
border-color: rgba(251, 191, 36, 0.5);
|
||||
}
|
||||
|
||||
QComboBox::drop-down {
|
||||
border: none;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
QComboBox::down-arrow {
|
||||
image: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
selection-background-color: rgba(251, 191, 36, 0.2);
|
||||
selection-color: #fbbf24;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* ==================== SpinBox ==================== */
|
||||
QSpinBox {
|
||||
background-color: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
QSpinBox:focus {
|
||||
border-color: rgba(251, 191, 36, 0.5);
|
||||
}
|
||||
|
||||
QSpinBox::up-button, QSpinBox::down-button {
|
||||
background: #334155;
|
||||
border: none;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
/* ==================== 分组框 ==================== */
|
||||
QGroupBox {
|
||||
background-color: transparent;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
QGroupBox::title {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
subcontrol-origin: margin;
|
||||
left: 16px;
|
||||
top: 4px;
|
||||
padding: 0 8px;
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
/* ==================== 文本编辑器 ==================== */
|
||||
QTextEdit {
|
||||
background-color: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
color: #e2e8f0;
|
||||
font-family: "Consolas", "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
selection-background-color: #fbbf24;
|
||||
selection-color: #0f172a;
|
||||
}
|
||||
|
||||
QTextEdit:focus {
|
||||
border-color: rgba(251, 191, 36, 0.5);
|
||||
}
|
||||
|
||||
/* ==================== 消息框 ==================== */
|
||||
QMessageBox {
|
||||
background-color: #1e293b;
|
||||
}
|
||||
|
||||
QMessageBox QLabel {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
QMessageBox QPushButton {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
/* ==================== 对话框 ==================== */
|
||||
QDialog {
|
||||
background-color: #1e293b;
|
||||
}
|
||||
|
||||
/* ==================== 工具提示 ==================== */
|
||||
QToolTip {
|
||||
background-color: #0f172a;
|
||||
color: #e2e8f0;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
/* ==================== 菜单 ==================== */
|
||||
QMenu {
|
||||
background-color: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
QMenu::item {
|
||||
padding: 8px 24px;
|
||||
border-radius: 4px;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
QMenu::item:selected {
|
||||
background-color: rgba(251, 191, 36, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* ==================== 分割器 ==================== */
|
||||
QSplitter::handle {
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
QSplitter::handle:horizontal {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
QSplitter::handle:vertical {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
/* ==================== 文件对话框 ==================== */
|
||||
QFileDialog {
|
||||
background-color: #1e293b;
|
||||
}
|
||||
|
||||
QFileDialog QListView,
|
||||
QFileDialog QTreeView {
|
||||
background-color: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
QFileDialog QListView::item,
|
||||
QFileDialog QTreeView::item {
|
||||
padding: 4px;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
QFileDialog QListView::item:selected,
|
||||
QFileDialog QTreeView::item:selected {
|
||||
background-color: rgba(251, 191, 36, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* ==================== 卡片样式 ==================== */
|
||||
#card {
|
||||
background-color: rgba(30, 41, 59, 0.5);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
#card:hover {
|
||||
border-color: rgba(251, 191, 36, 0.5);
|
||||
}
|
||||
|
||||
/* ==================== 标签样式 ==================== */
|
||||
QLabel {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ==================== 框架样式 ==================== */
|
||||
QFrame {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ==================== 动画相关样式 ==================== */
|
||||
/* 注意: Qt不支持CSS动画,但我们可以通过QPropertyAnimation实现 */
|
||||
|
||||
/* ==================== 预览面板样式 ==================== */
|
||||
ImagePreviewWidget {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
ImagePreviewWidget:hover {
|
||||
border-color: rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
|
||||
/* ==================== 双栏预览样式 ==================== */
|
||||
DualPreviewWidget {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ==================== 设置页面样式 ==================== */
|
||||
SettingsPage QGroupBox {
|
||||
background: rgba(30, 41, 59, 0.3);
|
||||
border: 1px solid rgba(51, 65, 85, 0.5);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
SettingsPage QGroupBox::title {
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ==================== 滑块增强样式 ==================== */
|
||||
QSlider::groove:horizontal {
|
||||
background: #1e293b;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
QSlider::handle:horizontal {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 #fbbf24, stop:1 #f59e0b);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: -5px 0;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #fbbf24;
|
||||
}
|
||||
|
||||
QSlider::handle:horizontal:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 #fcd34d, stop:1 #fbbf24);
|
||||
border-color: #fcd34d;
|
||||
}
|
||||
|
||||
QSlider::sub-page:horizontal {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop:0 #f59e0b, stop:1 #fbbf24);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ==================== 按钮过渡效果 ==================== */
|
||||
QPushButton {
|
||||
/* transition通过QPropertyAnimation实现 */
|
||||
}
|
||||
|
||||
#primary_btn {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
||||
stop:0 #fbbf24, stop:1 #f59e0b);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
#primary_btn:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
||||
stop:0 #fcd34d, stop:1 #fbbf24);
|
||||
}
|
||||
|
||||
#primary_btn:pressed {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
||||
stop:0 #f59e0b, stop:1 #d97706);
|
||||
}
|
||||
|
||||
/* ==================== 输入框焦点效果 ==================== */
|
||||
QLineEdit:focus {
|
||||
border-color: #fbbf24;
|
||||
background: rgba(251, 191, 36, 0.05);
|
||||
}
|
||||
|
||||
/* ==================== 进度条渐变 ==================== */
|
||||
QProgressBar::chunk {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop:0 #f59e0b, stop:1 #fbbf24);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ==================== 卡片悬停效果 ==================== */
|
||||
#card {
|
||||
background: rgba(30, 41, 59, 0.5);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
#card:hover {
|
||||
border-color: rgba(251, 191, 36, 0.4);
|
||||
background: rgba(30, 41, 59, 0.7);
|
||||
}
|
||||
|
||||
/* ==================== 列表项选中高亮 ==================== */
|
||||
QListWidget::item:selected {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop:0 rgba(251, 191, 36, 0.15), stop:1 rgba(251, 191, 36, 0.05));
|
||||
border-left: 3px solid #fbbf24;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* ==================== 标签页增强 ==================== */
|
||||
QTabBar::tab {
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
QTabBar::tab:hover {
|
||||
color: #cbd5e1;
|
||||
background: rgba(51, 65, 85, 0.3);
|
||||
}
|
||||
|
||||
QTabBar::tab:selected {
|
||||
color: #fbbf24;
|
||||
border-bottom: 2px solid #fbbf24;
|
||||
background: rgba(251, 191, 36, 0.05);
|
||||
}
|
||||
|
||||
/* ==================== 复选框增强 ==================== */
|
||||
QCheckBox::indicator:checked {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
||||
stop:0 #fbbf24, stop:1 #f59e0b);
|
||||
border-color: #fbbf24;
|
||||
image: url(none);
|
||||
}
|
||||
|
||||
QCheckBox::indicator:checked::after {
|
||||
color: white;
|
||||
}
|
||||
6
tools/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
工具模块
|
||||
- 图片工具
|
||||
- PDF工具
|
||||
- Excel工具
|
||||
"""
|
||||
2
tools/excel/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# Excel tools module
|
||||
|
||||
375
tools/excel/chart.py
Normal file
@@ -0,0 +1,375 @@
|
||||
"""
|
||||
Excel图表生成工具
|
||||
- 选择数据列
|
||||
- 图表类型选择(柱状图/折线图/饼图)
|
||||
- matplotlib图表嵌入Qt窗口
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QFileDialog, QMessageBox, QComboBox, QListWidget,
|
||||
QListWidgetItem, QAbstractItemView, QSplitter, QGroupBox
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont
|
||||
|
||||
from ui.workspace import BaseWorkspace, UploadArea
|
||||
|
||||
try:
|
||||
import pandas as pd
|
||||
HAS_PANDAS = True
|
||||
except ImportError:
|
||||
HAS_PANDAS = False
|
||||
|
||||
try:
|
||||
import matplotlib
|
||||
matplotlib.use('QtAgg')
|
||||
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
||||
from matplotlib.figure import Figure
|
||||
import matplotlib.pyplot as plt
|
||||
HAS_MATPLOTLIB = True
|
||||
|
||||
# 设置中文字体
|
||||
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
except ImportError:
|
||||
HAS_MATPLOTLIB = False
|
||||
logging.warning("matplotlib未安装, 图表生成功能不可用")
|
||||
|
||||
|
||||
class ChartCanvas(FigureCanvas if HAS_MATPLOTLIB else QWidget):
|
||||
"""图表画布"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
if HAS_MATPLOTLIB:
|
||||
self.figure = Figure(figsize=(8, 6), facecolor='#1e293b')
|
||||
super().__init__(self.figure)
|
||||
self.axes = self.figure.add_subplot(111)
|
||||
self.configure_axes()
|
||||
else:
|
||||
super().__init__(parent)
|
||||
|
||||
def configure_axes(self):
|
||||
"""配置坐标轴样式"""
|
||||
self.axes.set_facecolor('#0f172a')
|
||||
self.axes.tick_params(colors='#94a3b8')
|
||||
self.axes.xaxis.label.set_color('#e2e8f0')
|
||||
self.axes.yaxis.label.set_color('#e2e8f0')
|
||||
self.axes.title.set_color('white')
|
||||
|
||||
for spine in self.axes.spines.values():
|
||||
spine.set_color('#334155')
|
||||
|
||||
def clear_chart(self):
|
||||
"""清空图表"""
|
||||
if HAS_MATPLOTLIB:
|
||||
self.axes.clear()
|
||||
self.configure_axes()
|
||||
self.draw()
|
||||
|
||||
def draw_bar_chart(self, x_data, y_data, x_label: str, y_label: str, title: str):
|
||||
"""绘制柱状图"""
|
||||
self.clear_chart()
|
||||
|
||||
colors = ['#fbbf24', '#3b82f6', '#22c55e', '#ef4444', '#8b5cf6', '#ec4899']
|
||||
bars = self.axes.bar(x_data, y_data, color=colors[:len(x_data)])
|
||||
|
||||
self.axes.set_xlabel(x_label)
|
||||
self.axes.set_ylabel(y_label)
|
||||
self.axes.set_title(title, fontsize=14, fontweight='bold')
|
||||
|
||||
# 旋转x轴标签
|
||||
self.axes.tick_params(axis='x', rotation=45)
|
||||
self.figure.tight_layout()
|
||||
self.draw()
|
||||
|
||||
def draw_line_chart(self, x_data, y_data, x_label: str, y_label: str, title: str):
|
||||
"""绘制折线图"""
|
||||
self.clear_chart()
|
||||
|
||||
self.axes.plot(x_data, y_data, color='#fbbf24', linewidth=2, marker='o', markersize=6)
|
||||
self.axes.fill_between(x_data, y_data, alpha=0.2, color='#fbbf24')
|
||||
|
||||
self.axes.set_xlabel(x_label)
|
||||
self.axes.set_ylabel(y_label)
|
||||
self.axes.set_title(title, fontsize=14, fontweight='bold')
|
||||
self.axes.grid(True, alpha=0.3, color='#334155')
|
||||
|
||||
self.axes.tick_params(axis='x', rotation=45)
|
||||
self.figure.tight_layout()
|
||||
self.draw()
|
||||
|
||||
def draw_pie_chart(self, labels, values, title: str):
|
||||
"""绘制饼图"""
|
||||
self.clear_chart()
|
||||
|
||||
colors = ['#fbbf24', '#3b82f6', '#22c55e', '#ef4444', '#8b5cf6',
|
||||
'#ec4899', '#14b8a6', '#f97316', '#6366f1', '#84cc16']
|
||||
|
||||
wedges, texts, autotexts = self.axes.pie(
|
||||
values,
|
||||
labels=labels,
|
||||
autopct='%1.1f%%',
|
||||
colors=colors[:len(values)],
|
||||
textprops={'color': '#e2e8f0'}
|
||||
)
|
||||
|
||||
for autotext in autotexts:
|
||||
autotext.set_color('#0f172a')
|
||||
autotext.set_fontweight('bold')
|
||||
|
||||
self.axes.set_title(title, fontsize=14, fontweight='bold')
|
||||
self.figure.tight_layout()
|
||||
self.draw()
|
||||
|
||||
def save_chart(self, file_path: str):
|
||||
"""保存图表"""
|
||||
if HAS_MATPLOTLIB:
|
||||
self.figure.savefig(file_path, dpi=150, facecolor='#1e293b', edgecolor='none')
|
||||
|
||||
|
||||
class ExcelChartPage(BaseWorkspace):
|
||||
"""Excel图表生成页面"""
|
||||
|
||||
CHART_TYPES = [
|
||||
("📊 柱状图", "bar"),
|
||||
("📈 折线图", "line"),
|
||||
("🥧 饼图", "pie")
|
||||
]
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.excel_path = None
|
||||
self.df = None
|
||||
self.setup_chart_ui()
|
||||
|
||||
def setup_chart_ui(self):
|
||||
"""设置图表UI"""
|
||||
self.history_btn.hide()
|
||||
self.export_btn.clicked.connect(self.export_chart)
|
||||
|
||||
# 上传区域
|
||||
self.upload_area = UploadArea("Excel文件 (*.xlsx *.xls)")
|
||||
self.upload_area.files_dropped.connect(self.on_file_added)
|
||||
self.content_layout.addWidget(self.upload_area)
|
||||
|
||||
# 主工作区
|
||||
self.work_area = QSplitter(Qt.Orientation.Horizontal)
|
||||
self.work_area.setVisible(False)
|
||||
|
||||
# 左侧设置面板
|
||||
settings_frame = QFrame()
|
||||
settings_frame.setObjectName("card")
|
||||
settings_frame.setFixedWidth(280)
|
||||
settings_layout = QVBoxLayout(settings_frame)
|
||||
settings_layout.setContentsMargins(16, 16, 16, 16)
|
||||
settings_layout.setSpacing(16)
|
||||
|
||||
# 文件信息
|
||||
self.file_label = QLabel("")
|
||||
self.file_label.setStyleSheet("color: #fbbf24; font-size: 12px;")
|
||||
self.file_label.setWordWrap(True)
|
||||
settings_layout.addWidget(self.file_label)
|
||||
|
||||
# Sheet选择
|
||||
sheet_group = QGroupBox("📋 工作表")
|
||||
sheet_layout = QVBoxLayout(sheet_group)
|
||||
self.sheet_combo = QComboBox()
|
||||
self.sheet_combo.currentTextChanged.connect(self.on_sheet_changed)
|
||||
sheet_layout.addWidget(self.sheet_combo)
|
||||
settings_layout.addWidget(sheet_group)
|
||||
|
||||
# 图表类型
|
||||
type_group = QGroupBox("📊 图表类型")
|
||||
type_layout = QVBoxLayout(type_group)
|
||||
self.type_combo = QComboBox()
|
||||
for text, value in self.CHART_TYPES:
|
||||
self.type_combo.addItem(text, value)
|
||||
self.type_combo.currentIndexChanged.connect(self.update_chart)
|
||||
type_layout.addWidget(self.type_combo)
|
||||
settings_layout.addWidget(type_group)
|
||||
|
||||
# X轴数据(标签列)
|
||||
x_group = QGroupBox("📌 X轴 / 标签列")
|
||||
x_layout = QVBoxLayout(x_group)
|
||||
self.x_combo = QComboBox()
|
||||
self.x_combo.currentIndexChanged.connect(self.update_chart)
|
||||
x_layout.addWidget(self.x_combo)
|
||||
settings_layout.addWidget(x_group)
|
||||
|
||||
# Y轴数据(数值列)
|
||||
y_group = QGroupBox("📈 Y轴 / 数值列")
|
||||
y_layout = QVBoxLayout(y_group)
|
||||
self.y_combo = QComboBox()
|
||||
self.y_combo.currentIndexChanged.connect(self.update_chart)
|
||||
y_layout.addWidget(self.y_combo)
|
||||
settings_layout.addWidget(y_group)
|
||||
|
||||
settings_layout.addStretch()
|
||||
|
||||
# 更换文件按钮
|
||||
change_btn = QPushButton("📂 更换文件")
|
||||
change_btn.setObjectName("secondary_btn")
|
||||
change_btn.clicked.connect(self.change_file)
|
||||
settings_layout.addWidget(change_btn)
|
||||
|
||||
# 生成图表按钮
|
||||
self.generate_btn = QPushButton("⚡ 生成图表")
|
||||
self.generate_btn.setObjectName("primary_btn")
|
||||
self.generate_btn.setMinimumHeight(40)
|
||||
self.generate_btn.clicked.connect(self.update_chart)
|
||||
settings_layout.addWidget(self.generate_btn)
|
||||
|
||||
self.work_area.addWidget(settings_frame)
|
||||
|
||||
# 右侧图表区
|
||||
chart_frame = QFrame()
|
||||
chart_frame.setObjectName("card")
|
||||
chart_layout = QVBoxLayout(chart_frame)
|
||||
chart_layout.setContentsMargins(16, 16, 16, 16)
|
||||
|
||||
if HAS_MATPLOTLIB:
|
||||
self.chart_canvas = ChartCanvas()
|
||||
chart_layout.addWidget(self.chart_canvas)
|
||||
else:
|
||||
no_chart_label = QLabel("matplotlib未安装,无法显示图表")
|
||||
no_chart_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
no_chart_label.setStyleSheet("color: #ef4444;")
|
||||
chart_layout.addWidget(no_chart_label)
|
||||
|
||||
self.work_area.addWidget(chart_frame)
|
||||
self.work_area.setSizes([280, 700])
|
||||
|
||||
self.content_layout.addWidget(self.work_area, 1)
|
||||
|
||||
def on_file_added(self, files: list):
|
||||
"""文件添加"""
|
||||
if not files:
|
||||
return
|
||||
|
||||
excel_file = None
|
||||
for f in files:
|
||||
if f.lower().endswith(('.xlsx', '.xls')):
|
||||
excel_file = f
|
||||
break
|
||||
|
||||
if not excel_file:
|
||||
QMessageBox.warning(self, "提示", "请选择Excel文件")
|
||||
return
|
||||
|
||||
self.load_excel(excel_file)
|
||||
|
||||
def load_excel(self, file_path: str):
|
||||
"""加载Excel"""
|
||||
if not HAS_PANDAS:
|
||||
QMessageBox.critical(self, "错误", "pandas未安装,无法读取Excel")
|
||||
return
|
||||
|
||||
try:
|
||||
self.excel_path = file_path
|
||||
self.excel_file = pd.ExcelFile(file_path)
|
||||
|
||||
# 更新sheet下拉框
|
||||
self.sheet_combo.clear()
|
||||
self.sheet_combo.addItems(self.excel_file.sheet_names)
|
||||
|
||||
self.file_label.setText(f"📁 {Path(file_path).name}")
|
||||
|
||||
self.upload_area.setVisible(False)
|
||||
self.work_area.setVisible(True)
|
||||
|
||||
logging.info(f"加载Excel: {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"加载Excel失败:\n{e}")
|
||||
logging.error(f"加载Excel失败: {e}")
|
||||
|
||||
def on_sheet_changed(self, sheet_name: str):
|
||||
"""Sheet变化"""
|
||||
if not sheet_name:
|
||||
return
|
||||
|
||||
try:
|
||||
self.df = pd.read_excel(self.excel_file, sheet_name=sheet_name)
|
||||
|
||||
# 更新列下拉框
|
||||
columns = list(self.df.columns)
|
||||
|
||||
self.x_combo.clear()
|
||||
self.x_combo.addItems([str(c) for c in columns])
|
||||
|
||||
self.y_combo.clear()
|
||||
# 尝试只添加数值列
|
||||
numeric_cols = self.df.select_dtypes(include=['number']).columns.tolist()
|
||||
if numeric_cols:
|
||||
self.y_combo.addItems([str(c) for c in numeric_cols])
|
||||
else:
|
||||
self.y_combo.addItems([str(c) for c in columns])
|
||||
|
||||
self.update_chart()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"读取工作表失败: {e}")
|
||||
|
||||
def update_chart(self):
|
||||
"""更新图表"""
|
||||
if not HAS_MATPLOTLIB or self.df is None:
|
||||
return
|
||||
|
||||
x_col = self.x_combo.currentText()
|
||||
y_col = self.y_combo.currentText()
|
||||
chart_type = self.type_combo.currentData()
|
||||
|
||||
if not x_col or not y_col:
|
||||
return
|
||||
|
||||
try:
|
||||
# 获取数据
|
||||
x_data = self.df[x_col].astype(str).tolist()
|
||||
y_data = pd.to_numeric(self.df[y_col], errors='coerce').fillna(0).tolist()
|
||||
|
||||
# 限制数据量
|
||||
max_items = 20
|
||||
if len(x_data) > max_items:
|
||||
x_data = x_data[:max_items]
|
||||
y_data = y_data[:max_items]
|
||||
|
||||
title = f"{y_col} by {x_col}"
|
||||
|
||||
if chart_type == "bar":
|
||||
self.chart_canvas.draw_bar_chart(x_data, y_data, x_col, y_col, title)
|
||||
elif chart_type == "line":
|
||||
self.chart_canvas.draw_line_chart(x_data, y_data, x_col, y_col, title)
|
||||
elif chart_type == "pie":
|
||||
self.chart_canvas.draw_pie_chart(x_data, y_data, title)
|
||||
|
||||
logging.debug(f"生成图表: {chart_type}, X={x_col}, Y={y_col}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"生成图表失败: {e}")
|
||||
QMessageBox.warning(self, "警告", f"生成图表失败:\n{e}")
|
||||
|
||||
def export_chart(self):
|
||||
"""导出图表"""
|
||||
if not HAS_MATPLOTLIB:
|
||||
return
|
||||
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
self, "导出图表", "chart.png", "PNG图片 (*.png);;JPEG图片 (*.jpg);;PDF文档 (*.pdf)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
try:
|
||||
self.chart_canvas.save_chart(file_path)
|
||||
QMessageBox.information(self, "成功", f"图表已导出到:\n{file_path}")
|
||||
logging.info(f"图表导出: {file_path}")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"导出失败:\n{e}")
|
||||
|
||||
def change_file(self):
|
||||
"""更换文件"""
|
||||
self.work_area.setVisible(False)
|
||||
self.upload_area.setVisible(True)
|
||||
self.upload_area.open_file_dialog()
|
||||
|
||||
248
tools/excel/preview.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
Excel预览工具
|
||||
- 读取.xlsx/.xls文件
|
||||
- QTableWidget显示表格数据
|
||||
- 多Sheet标签页切换
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QFileDialog, QMessageBox, QTableWidget, QTableWidgetItem,
|
||||
QTabWidget, QHeaderView, QAbstractItemView
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtGui import QFont, QColor
|
||||
|
||||
from ui.workspace import BaseWorkspace, UploadArea
|
||||
|
||||
try:
|
||||
import pandas as pd
|
||||
HAS_PANDAS = True
|
||||
except ImportError:
|
||||
HAS_PANDAS = False
|
||||
logging.warning("pandas未安装, Excel预览功能不可用")
|
||||
|
||||
try:
|
||||
import openpyxl
|
||||
HAS_OPENPYXL = True
|
||||
except ImportError:
|
||||
HAS_OPENPYXL = False
|
||||
logging.warning("openpyxl未安装, Excel预览功能不可用")
|
||||
|
||||
|
||||
class ExcelLoadWorker(QThread):
|
||||
"""Excel加载线程"""
|
||||
finished = Signal(dict) # {sheet_name: DataFrame}
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, file_path: str):
|
||||
super().__init__()
|
||||
self.file_path = file_path
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
# 使用pandas读取所有sheet
|
||||
excel_file = pd.ExcelFile(self.file_path)
|
||||
sheets_data = {}
|
||||
|
||||
for sheet_name in excel_file.sheet_names:
|
||||
df = pd.read_excel(excel_file, sheet_name=sheet_name)
|
||||
sheets_data[sheet_name] = df
|
||||
|
||||
self.finished.emit(sheets_data)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"加载Excel失败: {e}")
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
class SheetTable(QTableWidget):
|
||||
"""Sheet表格组件"""
|
||||
|
||||
def __init__(self, df: 'pd.DataFrame', parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_table(df)
|
||||
|
||||
def setup_table(self, df: 'pd.DataFrame'):
|
||||
"""设置表格数据"""
|
||||
# 设置行列数
|
||||
self.setRowCount(len(df))
|
||||
self.setColumnCount(len(df.columns))
|
||||
|
||||
# 设置表头
|
||||
headers = [str(col) for col in df.columns]
|
||||
self.setHorizontalHeaderLabels(headers)
|
||||
|
||||
# 填充数据
|
||||
for row_idx, (_, row) in enumerate(df.iterrows()):
|
||||
for col_idx, value in enumerate(row):
|
||||
item = QTableWidgetItem(str(value) if pd.notna(value) else "")
|
||||
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) # 只读
|
||||
self.setItem(row_idx, col_idx, item)
|
||||
|
||||
# 设置样式
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.horizontalHeader().setStretchLastSection(True)
|
||||
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||
self.verticalHeader().setDefaultSectionSize(35)
|
||||
|
||||
# 自动调整列宽
|
||||
self.resizeColumnsToContents()
|
||||
|
||||
# 限制最大列宽
|
||||
for col in range(self.columnCount()):
|
||||
if self.columnWidth(col) > 300:
|
||||
self.setColumnWidth(col, 300)
|
||||
|
||||
|
||||
class ExcelPreviewPage(BaseWorkspace):
|
||||
"""Excel预览页面"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.excel_path = None
|
||||
self.sheets_data = {}
|
||||
self.setup_preview_ui()
|
||||
|
||||
def setup_preview_ui(self):
|
||||
"""设置预览UI"""
|
||||
self.history_btn.hide()
|
||||
|
||||
# 上传区域
|
||||
self.upload_area = UploadArea("Excel文件 (*.xlsx *.xls)")
|
||||
self.upload_area.files_dropped.connect(self.on_file_added)
|
||||
self.content_layout.addWidget(self.upload_area)
|
||||
|
||||
# 预览区域
|
||||
self.preview_frame = QFrame()
|
||||
self.preview_frame.setObjectName("card")
|
||||
self.preview_frame.setVisible(False)
|
||||
preview_layout = QVBoxLayout(self.preview_frame)
|
||||
preview_layout.setContentsMargins(0, 0, 0, 0)
|
||||
preview_layout.setSpacing(0)
|
||||
|
||||
# 工具栏
|
||||
toolbar = QWidget()
|
||||
toolbar.setStyleSheet("background: rgba(15, 23, 42, 0.5); border-bottom: 1px solid #334155;")
|
||||
toolbar_layout = QHBoxLayout(toolbar)
|
||||
toolbar_layout.setContentsMargins(16, 12, 16, 12)
|
||||
|
||||
# 文件信息
|
||||
self.file_label = QLabel("")
|
||||
self.file_label.setStyleSheet("color: white; font-weight: 500;")
|
||||
toolbar_layout.addWidget(self.file_label)
|
||||
|
||||
toolbar_layout.addStretch()
|
||||
|
||||
# 统计信息
|
||||
self.stats_label = QLabel("")
|
||||
self.stats_label.setStyleSheet("color: #64748b; font-size: 12px;")
|
||||
toolbar_layout.addWidget(self.stats_label)
|
||||
|
||||
# 重新选择按钮
|
||||
change_btn = QPushButton("📂 更换文件")
|
||||
change_btn.setObjectName("secondary_btn")
|
||||
change_btn.clicked.connect(self.change_file)
|
||||
toolbar_layout.addWidget(change_btn)
|
||||
|
||||
preview_layout.addWidget(toolbar)
|
||||
|
||||
# Sheet标签页
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget.setStyleSheet("""
|
||||
QTabWidget::pane {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
QTabBar::tab {
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
QTabBar::tab:hover {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
QTabBar::tab:selected {
|
||||
color: #fbbf24;
|
||||
border-bottom: 2px solid #fbbf24;
|
||||
}
|
||||
""")
|
||||
preview_layout.addWidget(self.tab_widget, 1)
|
||||
|
||||
self.content_layout.addWidget(self.preview_frame, 1)
|
||||
|
||||
def on_file_added(self, files: list):
|
||||
"""文件添加"""
|
||||
if not files:
|
||||
return
|
||||
|
||||
excel_file = None
|
||||
for f in files:
|
||||
if f.lower().endswith(('.xlsx', '.xls')):
|
||||
excel_file = f
|
||||
break
|
||||
|
||||
if not excel_file:
|
||||
QMessageBox.warning(self, "提示", "请选择Excel文件")
|
||||
return
|
||||
|
||||
self.load_excel(excel_file)
|
||||
|
||||
def load_excel(self, file_path: str):
|
||||
"""加载Excel文件"""
|
||||
if not HAS_PANDAS or not HAS_OPENPYXL:
|
||||
QMessageBox.critical(self, "错误", "pandas或openpyxl未安装,无法预览Excel文件")
|
||||
return
|
||||
|
||||
self.excel_path = file_path
|
||||
self.file_label.setText(f"📊 {Path(file_path).name}")
|
||||
|
||||
# 清空现有标签页
|
||||
self.tab_widget.clear()
|
||||
self.sheets_data.clear()
|
||||
|
||||
# 启动加载线程
|
||||
self.worker = ExcelLoadWorker(file_path)
|
||||
self.worker.finished.connect(self.on_load_finished)
|
||||
self.worker.error.connect(self.on_load_error)
|
||||
self.worker.start()
|
||||
|
||||
logging.info(f"开始加载Excel: {file_path}")
|
||||
|
||||
def on_load_finished(self, sheets_data: dict):
|
||||
"""加载完成"""
|
||||
self.sheets_data = sheets_data
|
||||
self.preview_frame.setVisible(True)
|
||||
self.upload_area.setVisible(False)
|
||||
|
||||
total_rows = 0
|
||||
total_cols = 0
|
||||
|
||||
# 创建标签页
|
||||
for sheet_name, df in sheets_data.items():
|
||||
table = SheetTable(df)
|
||||
self.tab_widget.addTab(table, f"📋 {sheet_name}")
|
||||
total_rows += len(df)
|
||||
total_cols = max(total_cols, len(df.columns))
|
||||
|
||||
self.stats_label.setText(
|
||||
f"{len(sheets_data)} 个工作表 | 共 {total_rows} 行 | {total_cols} 列"
|
||||
)
|
||||
|
||||
logging.info(f"Excel加载完成: {len(sheets_data)} 个工作表")
|
||||
|
||||
def on_load_error(self, error: str):
|
||||
"""加载错误"""
|
||||
QMessageBox.critical(self, "错误", f"加载Excel失败:\n{error}")
|
||||
logging.error(f"加载Excel失败: {error}")
|
||||
|
||||
def change_file(self):
|
||||
"""更换文件"""
|
||||
self.preview_frame.setVisible(False)
|
||||
self.upload_area.setVisible(True)
|
||||
self.upload_area.open_file_dialog()
|
||||
|
||||
15
tools/image/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
图片工具模块
|
||||
- 压缩
|
||||
- 格式转换
|
||||
- 水印
|
||||
"""
|
||||
from .compress import ImageCompressPage
|
||||
from .convert import ImageConvertPage
|
||||
from .watermark import ImageWatermarkPage
|
||||
|
||||
__all__ = [
|
||||
'ImageCompressPage',
|
||||
'ImageConvertPage',
|
||||
'ImageWatermarkPage'
|
||||
]
|
||||
630
tools/image/compress.py
Normal file
@@ -0,0 +1,630 @@
|
||||
"""
|
||||
图片压缩工具 - 极致优化版
|
||||
专注于:在保证视觉效果不降低的情况下,极致压缩文件大小
|
||||
- 保持原有格式(不转换格式)
|
||||
- 多种压缩模式
|
||||
- 智能参数优化
|
||||
"""
|
||||
import os
|
||||
import io
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QSlider, QFrame, QFileDialog, QMessageBox,
|
||||
QProgressBar, QListWidget, QListWidgetItem, QCheckBox,
|
||||
QGroupBox, QRadioButton, QButtonGroup, QComboBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtGui import QFont
|
||||
|
||||
from ui.workspace import BaseWorkspace, UploadArea
|
||||
from ui.image_preview import DualPreviewWidget
|
||||
from core.config import config
|
||||
|
||||
|
||||
class SmartCompressor:
|
||||
"""智能图片压缩器 - 保持原格式,极致压缩"""
|
||||
|
||||
# 压缩模式
|
||||
MODE_VISUALLY_LOSSLESS = "visually" # 视觉无损(推荐)
|
||||
MODE_BALANCED = "balanced" # 均衡模式
|
||||
MODE_MAXIMUM = "maximum" # 极致压缩
|
||||
MODE_LOSSLESS = "lossless" # 完全无损
|
||||
|
||||
@classmethod
|
||||
def compress(cls, img: Image.Image, original_format: str, mode: str,
|
||||
quality_override: int = None) -> tuple:
|
||||
"""
|
||||
压缩图片(保持原格式)
|
||||
|
||||
Args:
|
||||
img: PIL Image对象
|
||||
original_format: 原始格式 (jpeg/png/webp)
|
||||
mode: 压缩模式
|
||||
quality_override: 手动覆盖质量值
|
||||
|
||||
Returns:
|
||||
(compressed_data, output_extension)
|
||||
"""
|
||||
# 标准化格式名
|
||||
fmt = original_format.lower()
|
||||
if fmt in ['jpg', 'jpeg']:
|
||||
return cls._compress_jpeg(img, mode, quality_override)
|
||||
elif fmt == 'png':
|
||||
return cls._compress_png(img, mode)
|
||||
elif fmt == 'webp':
|
||||
return cls._compress_webp(img, mode, quality_override)
|
||||
elif fmt == 'gif':
|
||||
return cls._compress_gif(img)
|
||||
else:
|
||||
# 未知格式,转为JPEG压缩
|
||||
if img.mode in ('RGBA', 'LA', 'P'):
|
||||
background = Image.new('RGB', img.size, (255, 255, 255))
|
||||
if img.mode == 'P':
|
||||
img = img.convert('RGBA')
|
||||
if img.mode in ('RGBA', 'LA'):
|
||||
background.paste(img, mask=img.split()[-1])
|
||||
else:
|
||||
background.paste(img)
|
||||
img = background
|
||||
elif img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
return cls._compress_jpeg(img, mode, quality_override)
|
||||
|
||||
@classmethod
|
||||
def _compress_jpeg(cls, img: Image.Image, mode: str, quality_override: int = None) -> tuple:
|
||||
"""JPEG极致压缩"""
|
||||
# 确保是RGB模式
|
||||
if img.mode in ('RGBA', 'LA', 'P'):
|
||||
background = Image.new('RGB', img.size, (255, 255, 255))
|
||||
if img.mode == 'P':
|
||||
img = img.convert('RGBA')
|
||||
if img.mode in ('RGBA', 'LA'):
|
||||
background.paste(img, mask=img.split()[-1])
|
||||
else:
|
||||
background.paste(img)
|
||||
img = background
|
||||
elif img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
|
||||
buffer = io.BytesIO()
|
||||
|
||||
# 根据模式选择参数
|
||||
if quality_override is not None:
|
||||
quality = quality_override
|
||||
else:
|
||||
quality = {
|
||||
cls.MODE_LOSSLESS: 100,
|
||||
cls.MODE_VISUALLY_LOSSLESS: 88, # 视觉无损的最佳质量
|
||||
cls.MODE_BALANCED: 80,
|
||||
cls.MODE_MAXIMUM: 70,
|
||||
}.get(mode, 85)
|
||||
|
||||
# 子采样设置:quality高时用4:4:4保持质量
|
||||
if quality >= 90:
|
||||
subsampling = 0 # 4:4:4
|
||||
elif quality >= 80:
|
||||
subsampling = 1 # 4:2:2
|
||||
else:
|
||||
subsampling = 2 # 4:2:0
|
||||
|
||||
img.save(
|
||||
buffer,
|
||||
"JPEG",
|
||||
quality=quality,
|
||||
optimize=True,
|
||||
subsampling=subsampling,
|
||||
progressive=True
|
||||
)
|
||||
|
||||
return buffer.getvalue(), ".jpg"
|
||||
|
||||
@classmethod
|
||||
def _compress_png(cls, img: Image.Image, mode: str) -> tuple:
|
||||
"""PNG压缩(无损,但优化)"""
|
||||
buffer = io.BytesIO()
|
||||
|
||||
# PNG是无损格式,只能通过优化来减小
|
||||
# 对于极致压缩模式,尝试减少颜色
|
||||
if mode == cls.MODE_MAXIMUM:
|
||||
# 检查是否可以用调色板模式
|
||||
if img.mode == 'RGBA':
|
||||
colors = img.getcolors(maxcolors=256)
|
||||
if colors:
|
||||
img = img.convert('P', palette=Image.Palette.ADAPTIVE, colors=len(colors))
|
||||
elif img.mode == 'RGB':
|
||||
colors = img.getcolors(maxcolors=256)
|
||||
if colors:
|
||||
img = img.convert('P', palette=Image.Palette.ADAPTIVE, colors=len(colors))
|
||||
|
||||
img.save(
|
||||
buffer,
|
||||
"PNG",
|
||||
optimize=True,
|
||||
compress_level=9 # 最大压缩级别
|
||||
)
|
||||
|
||||
return buffer.getvalue(), ".png"
|
||||
|
||||
@classmethod
|
||||
def _compress_webp(cls, img: Image.Image, mode: str, quality_override: int = None) -> tuple:
|
||||
"""WebP压缩"""
|
||||
buffer = io.BytesIO()
|
||||
|
||||
if mode == cls.MODE_LOSSLESS:
|
||||
img.save(buffer, "WEBP", lossless=True, quality=100)
|
||||
else:
|
||||
if quality_override is not None:
|
||||
quality = quality_override
|
||||
else:
|
||||
quality = {
|
||||
cls.MODE_VISUALLY_LOSSLESS: 88,
|
||||
cls.MODE_BALANCED: 80,
|
||||
cls.MODE_MAXIMUM: 70,
|
||||
}.get(mode, 85)
|
||||
|
||||
img.save(
|
||||
buffer,
|
||||
"WEBP",
|
||||
quality=quality,
|
||||
method=6 # 最慢但压缩率最高
|
||||
)
|
||||
|
||||
return buffer.getvalue(), ".webp"
|
||||
|
||||
@classmethod
|
||||
def _compress_gif(cls, img: Image.Image) -> tuple:
|
||||
"""GIF保持原样(GIF压缩会丢失动画)"""
|
||||
buffer = io.BytesIO()
|
||||
img.save(buffer, "GIF", optimize=True)
|
||||
return buffer.getvalue(), ".gif"
|
||||
|
||||
|
||||
class CompressWorker(QThread):
|
||||
"""压缩工作线程"""
|
||||
progress = Signal(int, int)
|
||||
file_processed = Signal(str, bytes, dict)
|
||||
finished = Signal(list)
|
||||
|
||||
def __init__(self, files: list, compress_mode: str, quality: int = None,
|
||||
resize_percent: int = 100):
|
||||
super().__init__()
|
||||
self.files = files
|
||||
self.compress_mode = compress_mode
|
||||
self.quality = quality
|
||||
self.resize_percent = resize_percent
|
||||
|
||||
def run(self):
|
||||
results = []
|
||||
total = len(self.files)
|
||||
|
||||
for i, file_path in enumerate(self.files):
|
||||
try:
|
||||
result = self.compress_image(file_path)
|
||||
results.append(result)
|
||||
|
||||
if result.get("success") and result.get("data"):
|
||||
self.file_processed.emit(
|
||||
file_path,
|
||||
result["data"],
|
||||
{
|
||||
"size": result["compressed_size"],
|
||||
"name": result.get("output_name", ""),
|
||||
"original_size": result["original_size"]
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"压缩失败 {file_path}: {e}")
|
||||
results.append({
|
||||
"file": file_path,
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
self.progress.emit(i + 1, total)
|
||||
|
||||
self.finished.emit(results)
|
||||
|
||||
def compress_image(self, file_path: str) -> dict:
|
||||
"""压缩单个图片"""
|
||||
original_size = os.path.getsize(file_path)
|
||||
original_ext = Path(file_path).suffix.lower()
|
||||
|
||||
# 获取原始格式
|
||||
original_format = original_ext.lstrip('.')
|
||||
|
||||
with Image.open(file_path) as img:
|
||||
original_width, original_height = img.size
|
||||
|
||||
# 调整尺寸(如果需要)
|
||||
if self.resize_percent < 100:
|
||||
new_width = int(original_width * self.resize_percent / 100)
|
||||
new_height = int(original_height * self.resize_percent / 100)
|
||||
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# 压缩(保持原格式)
|
||||
compressed_data, ext = SmartCompressor.compress(
|
||||
img,
|
||||
original_format,
|
||||
self.compress_mode,
|
||||
self.quality
|
||||
)
|
||||
|
||||
compressed_size = len(compressed_data)
|
||||
|
||||
# 如果压缩后反而变大,使用原文件
|
||||
if compressed_size >= original_size and self.resize_percent == 100:
|
||||
with open(file_path, 'rb') as f:
|
||||
compressed_data = f.read()
|
||||
compressed_size = original_size
|
||||
ext = original_ext
|
||||
|
||||
output_name = Path(file_path).stem + "_compressed" + ext
|
||||
|
||||
return {
|
||||
"file": file_path,
|
||||
"output_name": output_name,
|
||||
"original_size": original_size,
|
||||
"compressed_size": compressed_size,
|
||||
"ratio": (1 - compressed_size / original_size) * 100 if original_size > 0 else 0,
|
||||
"success": True,
|
||||
"data": compressed_data
|
||||
}
|
||||
|
||||
|
||||
class ImageCompressPage(BaseWorkspace):
|
||||
"""图片压缩页面"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.files = []
|
||||
self.current_file_index = 0
|
||||
self.processed_results = {}
|
||||
self.setup_compress_ui()
|
||||
|
||||
def setup_compress_ui(self):
|
||||
"""设置压缩UI"""
|
||||
self.history_btn.hide()
|
||||
self.export_btn.setText("💾 批量保存")
|
||||
self.export_btn.clicked.connect(self.batch_save)
|
||||
|
||||
# 上传区域
|
||||
self.upload_area = UploadArea("图片文件 (*.jpg *.jpeg *.png *.webp *.gif *.bmp)")
|
||||
self.upload_area.files_dropped.connect(self.on_files_added)
|
||||
self.content_layout.addWidget(self.upload_area)
|
||||
|
||||
# 主内容区
|
||||
content_widget = QWidget()
|
||||
content_layout = QHBoxLayout(content_widget)
|
||||
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
content_layout.setSpacing(24)
|
||||
|
||||
# 左侧 - 预览区
|
||||
self.preview_widget = DualPreviewWidget()
|
||||
self.preview_widget.save_requested.connect(self.on_file_saved)
|
||||
content_layout.addWidget(self.preview_widget, 2)
|
||||
|
||||
# 右侧设置区
|
||||
settings_frame = QFrame()
|
||||
settings_frame.setObjectName("card")
|
||||
settings_frame.setFixedWidth(300)
|
||||
settings_frame.setStyleSheet("""
|
||||
#card {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 16px;
|
||||
}
|
||||
""")
|
||||
settings_layout = QVBoxLayout(settings_frame)
|
||||
settings_layout.setContentsMargins(20, 20, 20, 20)
|
||||
settings_layout.setSpacing(16)
|
||||
|
||||
# ====== 压缩模式 ======
|
||||
mode_group = QGroupBox("🎯 压缩模式")
|
||||
mode_group.setStyleSheet("""
|
||||
QGroupBox {
|
||||
font-weight: bold;
|
||||
color: #e2e8f0;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 12px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
""")
|
||||
mode_layout = QVBoxLayout(mode_group)
|
||||
mode_layout.setSpacing(6)
|
||||
|
||||
self.mode_group = QButtonGroup(self)
|
||||
|
||||
modes = [
|
||||
("visually", "🔒 视觉无损(推荐)", "肉眼几乎看不出差异", True),
|
||||
("balanced", "⚖️ 均衡模式", "平衡质量与压缩率"),
|
||||
("maximum", "🚀 极致压缩", "最大压缩,可能有轻微损失"),
|
||||
("lossless", "💎 完全无损", "100%保留原质量"),
|
||||
]
|
||||
|
||||
for i, mode_data in enumerate(modes):
|
||||
mode_id, text, desc = mode_data[:3]
|
||||
is_default = len(mode_data) > 3 and mode_data[3]
|
||||
|
||||
radio = QRadioButton(text)
|
||||
radio.setProperty("mode_id", mode_id)
|
||||
radio.setStyleSheet("color: #e2e8f0; font-size: 12px;")
|
||||
if is_default:
|
||||
radio.setChecked(True)
|
||||
self.mode_group.addButton(radio, i)
|
||||
mode_layout.addWidget(radio)
|
||||
|
||||
desc_label = QLabel(f" {desc}")
|
||||
desc_label.setStyleSheet("color: #64748b; font-size: 10px;")
|
||||
mode_layout.addWidget(desc_label)
|
||||
|
||||
settings_layout.addWidget(mode_group)
|
||||
|
||||
# ====== 高级设置 ======
|
||||
advanced_group = QGroupBox("⚙️ 高级选项")
|
||||
advanced_group.setStyleSheet(mode_group.styleSheet())
|
||||
advanced_layout = QVBoxLayout(advanced_group)
|
||||
advanced_layout.setSpacing(10)
|
||||
|
||||
# 手动质量
|
||||
self.manual_quality_check = QCheckBox("手动指定质量")
|
||||
self.manual_quality_check.setStyleSheet("color: #cbd5e1; font-size: 12px;")
|
||||
self.manual_quality_check.stateChanged.connect(self.on_manual_quality_changed)
|
||||
advanced_layout.addWidget(self.manual_quality_check)
|
||||
|
||||
quality_row = QHBoxLayout()
|
||||
self.quality_slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self.quality_slider.setRange(50, 100)
|
||||
self.quality_slider.setValue(85)
|
||||
self.quality_slider.setEnabled(False)
|
||||
self.quality_slider.valueChanged.connect(self.on_quality_changed)
|
||||
quality_row.addWidget(self.quality_slider, 1)
|
||||
|
||||
self.quality_label = QLabel("85%")
|
||||
self.quality_label.setStyleSheet("color: #fbbf24; font-weight: bold; min-width: 35px;")
|
||||
quality_row.addWidget(self.quality_label)
|
||||
advanced_layout.addLayout(quality_row)
|
||||
|
||||
# 缩放
|
||||
resize_row = QHBoxLayout()
|
||||
resize_row.addWidget(QLabel("尺寸:"))
|
||||
self.resize_combo = QComboBox()
|
||||
self.resize_combo.addItem("100% 原尺寸", 100)
|
||||
self.resize_combo.addItem("75%", 75)
|
||||
self.resize_combo.addItem("50%", 50)
|
||||
resize_row.addWidget(self.resize_combo, 1)
|
||||
advanced_layout.addLayout(resize_row)
|
||||
|
||||
settings_layout.addWidget(advanced_group)
|
||||
|
||||
# ====== 文件列表 ======
|
||||
files_header = QHBoxLayout()
|
||||
files_label = QLabel("📁 待压缩文件")
|
||||
files_label.setStyleSheet("color: #e2e8f0; font-weight: bold; font-size: 12px;")
|
||||
files_header.addWidget(files_label)
|
||||
|
||||
self.files_count = QLabel("0")
|
||||
self.files_count.setStyleSheet("color: #fbbf24;")
|
||||
files_header.addWidget(self.files_count)
|
||||
files_header.addStretch()
|
||||
|
||||
clear_btn = QPushButton("清空")
|
||||
clear_btn.setObjectName("secondary_btn")
|
||||
clear_btn.setFixedWidth(50)
|
||||
clear_btn.clicked.connect(self.clear_files)
|
||||
files_header.addWidget(clear_btn)
|
||||
|
||||
settings_layout.addLayout(files_header)
|
||||
|
||||
self.files_list = QListWidget()
|
||||
self.files_list.setMaximumHeight(100)
|
||||
self.files_list.itemClicked.connect(self.on_file_clicked)
|
||||
settings_layout.addWidget(self.files_list)
|
||||
|
||||
settings_layout.addStretch()
|
||||
|
||||
# 进度条
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
settings_layout.addWidget(self.progress_bar)
|
||||
|
||||
# 预览按钮
|
||||
self.preview_btn = QPushButton("👁️ 预览效果")
|
||||
self.preview_btn.setObjectName("secondary_btn")
|
||||
self.preview_btn.setMinimumHeight(38)
|
||||
self.preview_btn.clicked.connect(self.preview_current)
|
||||
settings_layout.addWidget(self.preview_btn)
|
||||
|
||||
# 开始压缩按钮
|
||||
self.compress_btn = QPushButton("⚡ 开始压缩")
|
||||
self.compress_btn.setObjectName("primary_btn")
|
||||
self.compress_btn.setMinimumHeight(45)
|
||||
self.compress_btn.setFont(QFont("Microsoft YaHei", 12, QFont.Weight.Bold))
|
||||
self.compress_btn.clicked.connect(self.start_compress_all)
|
||||
settings_layout.addWidget(self.compress_btn)
|
||||
|
||||
content_layout.addWidget(settings_frame)
|
||||
|
||||
self.content_layout.addWidget(content_widget, 1)
|
||||
|
||||
def get_compress_settings(self) -> dict:
|
||||
"""获取压缩设置"""
|
||||
selected_btn = self.mode_group.checkedButton()
|
||||
mode = selected_btn.property("mode_id") if selected_btn else "visually"
|
||||
|
||||
quality = None
|
||||
if self.manual_quality_check.isChecked():
|
||||
quality = self.quality_slider.value()
|
||||
|
||||
resize_percent = self.resize_combo.currentData()
|
||||
|
||||
return {"mode": mode, "quality": quality, "resize": resize_percent}
|
||||
|
||||
def on_manual_quality_changed(self, state):
|
||||
self.quality_slider.setEnabled(state == Qt.CheckState.Checked.value)
|
||||
|
||||
def on_quality_changed(self, value: int):
|
||||
self.quality_label.setText(f"{value}%")
|
||||
|
||||
def on_files_added(self, files: list):
|
||||
valid_exts = ('.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp')
|
||||
for file_path in files:
|
||||
if file_path.lower().endswith(valid_exts):
|
||||
if file_path not in self.files:
|
||||
self.files.append(file_path)
|
||||
size = os.path.getsize(file_path)
|
||||
size_str = self.format_size(size)
|
||||
item = QListWidgetItem(f"📷 {Path(file_path).name} ({size_str})")
|
||||
item.setData(Qt.ItemDataRole.UserRole, file_path)
|
||||
self.files_list.addItem(item)
|
||||
|
||||
self.files_count.setText(str(len(self.files)))
|
||||
|
||||
if self.files:
|
||||
self.files_list.setCurrentRow(0)
|
||||
self.preview_widget.set_original(self.files[0])
|
||||
self.current_file_index = 0
|
||||
|
||||
def on_file_clicked(self, item: QListWidgetItem):
|
||||
file_path = item.data(Qt.ItemDataRole.UserRole)
|
||||
self.current_file_index = self.files.index(file_path)
|
||||
self.preview_widget.set_original(file_path)
|
||||
|
||||
if file_path in self.processed_results:
|
||||
result = self.processed_results[file_path]
|
||||
self.preview_widget.set_result(
|
||||
result["data"],
|
||||
{"size": result["compressed_size"], "name": result["output_name"]},
|
||||
result["output_name"]
|
||||
)
|
||||
|
||||
def clear_files(self):
|
||||
self.files.clear()
|
||||
self.files_list.clear()
|
||||
self.files_count.setText("0")
|
||||
self.processed_results.clear()
|
||||
self.preview_widget.clear()
|
||||
|
||||
def preview_current(self):
|
||||
if not self.files:
|
||||
QMessageBox.warning(self, "提示", "请先添加要压缩的图片")
|
||||
return
|
||||
|
||||
settings = self.get_compress_settings()
|
||||
file_path = self.files[self.current_file_index]
|
||||
|
||||
self.preview_btn.setEnabled(False)
|
||||
self.preview_btn.setText("处理中...")
|
||||
|
||||
self.worker = CompressWorker(
|
||||
[file_path], settings["mode"], settings["quality"], settings["resize"]
|
||||
)
|
||||
self.worker.file_processed.connect(self.on_preview_ready)
|
||||
self.worker.finished.connect(lambda: self.preview_btn.setEnabled(True))
|
||||
self.worker.finished.connect(lambda: self.preview_btn.setText("👁️ 预览效果"))
|
||||
self.worker.start()
|
||||
|
||||
def on_preview_ready(self, file_path: str, data: bytes, info: dict):
|
||||
output_name = info.get("name", Path(file_path).stem + "_compressed.jpg")
|
||||
self.preview_widget.set_result(data, info, output_name)
|
||||
self.processed_results[file_path] = {
|
||||
"data": data,
|
||||
"compressed_size": info.get("size", len(data)),
|
||||
"output_name": output_name
|
||||
}
|
||||
|
||||
def start_compress_all(self):
|
||||
if not self.files:
|
||||
QMessageBox.warning(self, "提示", "请先添加要压缩的图片")
|
||||
return
|
||||
|
||||
settings = self.get_compress_settings()
|
||||
|
||||
self.compress_btn.setEnabled(False)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
self.worker = CompressWorker(
|
||||
self.files, settings["mode"], settings["quality"], settings["resize"]
|
||||
)
|
||||
self.worker.progress.connect(self.on_progress)
|
||||
self.worker.file_processed.connect(self.on_file_processed)
|
||||
self.worker.finished.connect(self.on_compress_finished)
|
||||
self.worker.start()
|
||||
|
||||
def on_progress(self, current: int, total: int):
|
||||
self.progress_bar.setValue(int(current / total * 100))
|
||||
|
||||
def on_file_processed(self, file_path: str, data: bytes, info: dict):
|
||||
output_name = info.get("name", Path(file_path).stem + "_compressed.jpg")
|
||||
self.processed_results[file_path] = {
|
||||
"data": data,
|
||||
"compressed_size": info.get("size", len(data)),
|
||||
"output_name": output_name,
|
||||
"original_size": info.get("original_size", 0)
|
||||
}
|
||||
|
||||
if self.files.index(file_path) == self.current_file_index:
|
||||
self.preview_widget.set_result(data, info, output_name)
|
||||
|
||||
def on_compress_finished(self, results: list):
|
||||
self.compress_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
success = sum(1 for r in results if r.get("success"))
|
||||
total_orig = sum(r.get("original_size", 0) for r in results if r.get("success"))
|
||||
total_comp = sum(r.get("compressed_size", 0) for r in results if r.get("success"))
|
||||
saved = total_orig - total_comp
|
||||
|
||||
if total_orig > 0:
|
||||
pct = (saved / total_orig) * 100
|
||||
msg = (f"压缩完成!\n\n"
|
||||
f"✅ 成功: {success}/{len(results)}\n"
|
||||
f"📊 原始: {self.format_size(total_orig)}\n"
|
||||
f"📦 压缩后: {self.format_size(total_comp)}\n"
|
||||
f"💾 节省: {self.format_size(saved)} ({pct:.1f}%)")
|
||||
else:
|
||||
msg = f"压缩完成!\n✅ 成功: {success}/{len(results)}"
|
||||
|
||||
QMessageBox.information(self, "完成", msg)
|
||||
|
||||
def on_file_saved(self, path):
|
||||
logging.info(f"已保存: {path}")
|
||||
|
||||
def batch_save(self):
|
||||
if not self.processed_results:
|
||||
QMessageBox.warning(self, "提示", "没有可保存的结果,请先压缩")
|
||||
return
|
||||
|
||||
output_dir = QFileDialog.getExistingDirectory(
|
||||
self, "选择保存目录", config.get_output_directory()
|
||||
)
|
||||
if not output_dir:
|
||||
return
|
||||
|
||||
saved = 0
|
||||
for fp, result in self.processed_results.items():
|
||||
try:
|
||||
with open(os.path.join(output_dir, result["output_name"]), 'wb') as f:
|
||||
f.write(result["data"])
|
||||
saved += 1
|
||||
except Exception as e:
|
||||
logging.error(f"保存失败: {e}")
|
||||
|
||||
QMessageBox.information(self, "完成", f"已保存 {saved} 个文件到:\n{output_dir}")
|
||||
|
||||
@staticmethod
|
||||
def format_size(size: int) -> str:
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} TB"
|
||||
412
tools/image/convert.py
Normal file
@@ -0,0 +1,412 @@
|
||||
"""
|
||||
图片格式转换工具
|
||||
- 支持 JPG/PNG/WEBP/ICO/PDF 互转
|
||||
- 预览转换效果
|
||||
- 批量转换
|
||||
- 进度显示
|
||||
"""
|
||||
import os
|
||||
import io
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QFileDialog, QMessageBox, QProgressBar,
|
||||
QListWidget, QListWidgetItem, QButtonGroup
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtGui import QFont
|
||||
|
||||
from ui.workspace import BaseWorkspace, UploadArea
|
||||
from ui.image_preview import DualPreviewWidget
|
||||
from core.config import config
|
||||
|
||||
|
||||
class ConvertWorker(QThread):
|
||||
"""转换工作线程"""
|
||||
progress = Signal(int, int)
|
||||
file_processed = Signal(str, bytes, dict, str) # file_path, data, info, output_name
|
||||
finished = Signal(list)
|
||||
|
||||
def __init__(self, files: list, target_format: str, output_dir: str = None):
|
||||
super().__init__()
|
||||
self.files = files
|
||||
self.target_format = target_format.lower()
|
||||
self.output_dir = output_dir
|
||||
self.save_files = output_dir is not None
|
||||
|
||||
def run(self):
|
||||
results = []
|
||||
total = len(self.files)
|
||||
|
||||
for i, file_path in enumerate(self.files):
|
||||
try:
|
||||
result = self.convert_image(file_path)
|
||||
results.append(result)
|
||||
|
||||
if result.get("success") and result.get("data"):
|
||||
self.file_processed.emit(
|
||||
file_path,
|
||||
result["data"],
|
||||
{"size": len(result["data"]), "name": result["output_name"]},
|
||||
result["output_name"]
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"转换失败 {file_path}: {e}")
|
||||
results.append({
|
||||
"file": file_path,
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
self.progress.emit(i + 1, total)
|
||||
|
||||
self.finished.emit(results)
|
||||
|
||||
def convert_image(self, file_path: str) -> dict:
|
||||
"""转换单个图片"""
|
||||
output_name = Path(file_path).stem + f".{self.target_format}"
|
||||
output_buffer = io.BytesIO()
|
||||
|
||||
with Image.open(file_path) as img:
|
||||
# 处理透明通道
|
||||
if self.target_format in ['jpg', 'jpeg', 'pdf']:
|
||||
if img.mode in ('RGBA', 'P', 'LA'):
|
||||
background = Image.new('RGB', img.size, (255, 255, 255))
|
||||
if img.mode == 'P':
|
||||
img = img.convert('RGBA')
|
||||
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
|
||||
img = background
|
||||
elif img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
|
||||
# 保存到缓冲区
|
||||
if self.target_format == 'ico':
|
||||
sizes = [(256, 256), (128, 128), (64, 64), (48, 48), (32, 32), (16, 16)]
|
||||
img.save(output_buffer, format='ICO', sizes=sizes)
|
||||
elif self.target_format == 'pdf':
|
||||
img.save(output_buffer, 'PDF', resolution=100.0)
|
||||
else:
|
||||
save_format = 'JPEG' if self.target_format in ['jpg', 'jpeg'] else self.target_format.upper()
|
||||
img.save(output_buffer, save_format, quality=95)
|
||||
|
||||
data = output_buffer.getvalue()
|
||||
|
||||
# 如果需要保存
|
||||
output_path = None
|
||||
if self.save_files and self.output_dir:
|
||||
output_path = os.path.join(self.output_dir, output_name)
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
return {
|
||||
"file": file_path,
|
||||
"output": output_path,
|
||||
"output_name": output_name,
|
||||
"success": True,
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
class ImageConvertPage(BaseWorkspace):
|
||||
"""图片格式转换页面"""
|
||||
|
||||
FORMATS = ['JPG', 'PNG', 'WEBP', 'ICO', 'PDF']
|
||||
FORMAT_COLORS = {
|
||||
'JPG': '#3b82f6',
|
||||
'PNG': '#22c55e',
|
||||
'WEBP': '#8b5cf6',
|
||||
'ICO': '#f59e0b',
|
||||
'PDF': '#ef4444'
|
||||
}
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.files = []
|
||||
self.current_file_index = 0
|
||||
self.processed_results = {}
|
||||
self.selected_format = 'WEBP'
|
||||
self.setup_convert_ui()
|
||||
|
||||
def setup_convert_ui(self):
|
||||
"""设置转换UI"""
|
||||
self.history_btn.hide()
|
||||
self.export_btn.setText("💾 批量保存")
|
||||
self.export_btn.clicked.connect(self.batch_save)
|
||||
|
||||
# 上传区域
|
||||
self.upload_area = UploadArea("图片文件 (*.jpg *.jpeg *.png *.webp *.bmp *.gif)")
|
||||
self.upload_area.files_dropped.connect(self.on_files_added)
|
||||
self.content_layout.addWidget(self.upload_area)
|
||||
|
||||
# 主内容区
|
||||
content_widget = QWidget()
|
||||
content_layout = QHBoxLayout(content_widget)
|
||||
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
content_layout.setSpacing(24)
|
||||
|
||||
# 左侧 - 预览区
|
||||
self.preview_widget = DualPreviewWidget()
|
||||
self.preview_widget.save_requested.connect(self.on_file_saved)
|
||||
content_layout.addWidget(self.preview_widget, 2)
|
||||
|
||||
# 右侧设置区
|
||||
settings_frame = QFrame()
|
||||
settings_frame.setObjectName("card")
|
||||
settings_frame.setFixedWidth(280)
|
||||
settings_frame.setStyleSheet("""
|
||||
#card {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 16px;
|
||||
}
|
||||
""")
|
||||
settings_layout = QVBoxLayout(settings_frame)
|
||||
settings_layout.setContentsMargins(24, 24, 24, 24)
|
||||
settings_layout.setSpacing(16)
|
||||
|
||||
# 标题
|
||||
title = QLabel("🔄 选择目标格式")
|
||||
title.setStyleSheet("color: white; font-weight: 600; font-size: 14px;")
|
||||
settings_layout.addWidget(title)
|
||||
|
||||
# 格式按钮
|
||||
formats_widget = QWidget()
|
||||
formats_layout = QVBoxLayout(formats_widget)
|
||||
formats_layout.setSpacing(8)
|
||||
|
||||
self.format_buttons = {}
|
||||
for fmt in self.FORMATS:
|
||||
btn = QPushButton(fmt)
|
||||
btn.setCheckable(True)
|
||||
btn.setMinimumHeight(40)
|
||||
color = self.FORMAT_COLORS.get(fmt, '#64748b')
|
||||
btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background: rgba({self._hex_to_rgb(color)}, 0.1);
|
||||
border: 2px solid {color};
|
||||
border-radius: 8px;
|
||||
color: {color};
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background: rgba({self._hex_to_rgb(color)}, 0.2);
|
||||
}}
|
||||
QPushButton:checked {{
|
||||
background: {color};
|
||||
color: white;
|
||||
}}
|
||||
""")
|
||||
btn.clicked.connect(lambda checked, f=fmt: self.on_format_selected(f))
|
||||
formats_layout.addWidget(btn)
|
||||
self.format_buttons[fmt] = btn
|
||||
|
||||
# 默认选中 WEBP
|
||||
self.format_buttons['WEBP'].setChecked(True)
|
||||
|
||||
settings_layout.addWidget(formats_widget)
|
||||
|
||||
# 文件列表
|
||||
files_header = QHBoxLayout()
|
||||
files_label = QLabel("待转换文件")
|
||||
files_label.setStyleSheet("color: #cbd5e1; font-size: 13px;")
|
||||
files_header.addWidget(files_label)
|
||||
|
||||
self.files_count = QLabel("0")
|
||||
self.files_count.setStyleSheet("color: #fbbf24; font-size: 12px;")
|
||||
files_header.addWidget(self.files_count)
|
||||
files_header.addStretch()
|
||||
|
||||
clear_btn = QPushButton("清空")
|
||||
clear_btn.setObjectName("secondary_btn")
|
||||
clear_btn.setFixedWidth(60)
|
||||
clear_btn.clicked.connect(self.clear_files)
|
||||
files_header.addWidget(clear_btn)
|
||||
|
||||
settings_layout.addLayout(files_header)
|
||||
|
||||
self.files_list = QListWidget()
|
||||
self.files_list.setMaximumHeight(120)
|
||||
self.files_list.itemClicked.connect(self.on_file_clicked)
|
||||
settings_layout.addWidget(self.files_list)
|
||||
|
||||
settings_layout.addStretch()
|
||||
|
||||
# 进度条
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
settings_layout.addWidget(self.progress_bar)
|
||||
|
||||
# 预览按钮
|
||||
self.preview_btn = QPushButton("👁️ 预览效果")
|
||||
self.preview_btn.setObjectName("secondary_btn")
|
||||
self.preview_btn.setMinimumHeight(40)
|
||||
self.preview_btn.clicked.connect(self.preview_current)
|
||||
settings_layout.addWidget(self.preview_btn)
|
||||
|
||||
# 转换按钮
|
||||
self.convert_btn = QPushButton("⚡ 转换全部")
|
||||
self.convert_btn.setObjectName("primary_btn")
|
||||
self.convert_btn.setMinimumSize(150, 45)
|
||||
self.convert_btn.setFont(QFont("Microsoft YaHei", 12, QFont.Weight.Bold))
|
||||
self.convert_btn.clicked.connect(self.start_convert_all)
|
||||
settings_layout.addWidget(self.convert_btn)
|
||||
|
||||
content_layout.addWidget(settings_frame)
|
||||
|
||||
self.content_layout.addWidget(content_widget, 1)
|
||||
|
||||
def on_format_selected(self, fmt: str):
|
||||
"""格式选择"""
|
||||
self.selected_format = fmt
|
||||
for f, btn in self.format_buttons.items():
|
||||
btn.setChecked(f == fmt)
|
||||
|
||||
def on_files_added(self, files: list):
|
||||
"""文件添加"""
|
||||
valid_extensions = ('.jpg', '.jpeg', '.png', '.webp', '.bmp', '.gif')
|
||||
for file_path in files:
|
||||
if file_path.lower().endswith(valid_extensions):
|
||||
if file_path not in self.files:
|
||||
self.files.append(file_path)
|
||||
item = QListWidgetItem(f"📷 {Path(file_path).name}")
|
||||
item.setData(Qt.ItemDataRole.UserRole, file_path)
|
||||
self.files_list.addItem(item)
|
||||
|
||||
self.files_count.setText(str(len(self.files)))
|
||||
|
||||
if self.files:
|
||||
self.files_list.setCurrentRow(0)
|
||||
self.preview_widget.set_original(self.files[0])
|
||||
self.current_file_index = 0
|
||||
|
||||
logging.info(f"添加了 {len(files)} 个文件用于转换")
|
||||
|
||||
def on_file_clicked(self, item: QListWidgetItem):
|
||||
"""文件点击"""
|
||||
file_path = item.data(Qt.ItemDataRole.UserRole)
|
||||
self.current_file_index = self.files.index(file_path)
|
||||
self.preview_widget.set_original(file_path)
|
||||
|
||||
if file_path in self.processed_results:
|
||||
result = self.processed_results[file_path]
|
||||
self.preview_widget.set_result(
|
||||
result["data"],
|
||||
{"size": len(result["data"]), "name": result["output_name"]},
|
||||
result["output_name"]
|
||||
)
|
||||
|
||||
def clear_files(self):
|
||||
"""清空文件"""
|
||||
self.files.clear()
|
||||
self.files_list.clear()
|
||||
self.files_count.setText("0")
|
||||
self.processed_results.clear()
|
||||
self.preview_widget.clear()
|
||||
|
||||
def preview_current(self):
|
||||
"""预览当前文件"""
|
||||
if not self.files:
|
||||
QMessageBox.warning(self, "提示", "请先添加要转换的图片文件")
|
||||
return
|
||||
|
||||
file_path = self.files[self.current_file_index]
|
||||
|
||||
self.preview_btn.setEnabled(False)
|
||||
self.preview_btn.setText("处理中...")
|
||||
|
||||
self.worker = ConvertWorker([file_path], self.selected_format, None)
|
||||
self.worker.file_processed.connect(self.on_preview_ready)
|
||||
self.worker.finished.connect(lambda: self.preview_btn.setEnabled(True))
|
||||
self.worker.finished.connect(lambda: self.preview_btn.setText("👁️ 预览效果"))
|
||||
self.worker.start()
|
||||
|
||||
def on_preview_ready(self, file_path: str, data: bytes, info: dict, output_name: str):
|
||||
"""预览完成"""
|
||||
self.preview_widget.set_result(data, info, output_name, show_size_compare=False)
|
||||
self.processed_results[file_path] = {
|
||||
"data": data,
|
||||
"output_name": output_name
|
||||
}
|
||||
|
||||
def start_convert_all(self):
|
||||
"""转换所有文件"""
|
||||
if not self.files:
|
||||
QMessageBox.warning(self, "提示", "请先添加要转换的图片文件")
|
||||
return
|
||||
|
||||
self.convert_btn.setEnabled(False)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
self.worker = ConvertWorker(self.files, self.selected_format, None)
|
||||
self.worker.progress.connect(self.on_progress)
|
||||
self.worker.file_processed.connect(self.on_file_processed)
|
||||
self.worker.finished.connect(self.on_convert_finished)
|
||||
self.worker.start()
|
||||
|
||||
logging.info(f"开始转换 {len(self.files)} 个文件为 {self.selected_format}")
|
||||
|
||||
def on_progress(self, current: int, total: int):
|
||||
"""进度更新"""
|
||||
self.progress_bar.setValue(int(current / total * 100))
|
||||
|
||||
def on_file_processed(self, file_path: str, data: bytes, info: dict, output_name: str):
|
||||
"""文件处理完成"""
|
||||
self.processed_results[file_path] = {
|
||||
"data": data,
|
||||
"output_name": output_name
|
||||
}
|
||||
|
||||
if self.files.index(file_path) == self.current_file_index:
|
||||
self.preview_widget.set_result(data, info, output_name, show_size_compare=False)
|
||||
|
||||
def on_convert_finished(self, results: list):
|
||||
"""转换完成"""
|
||||
self.convert_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
success_count = sum(1 for r in results if r.get("success"))
|
||||
|
||||
msg = f"转换完成!\n\n✅ 成功: {success_count}/{len(results)}\n\n请点击「批量保存」或在预览中单独保存"
|
||||
QMessageBox.information(self, "转换结果", msg)
|
||||
logging.info(f"转换完成: 成功 {success_count}/{len(results)}")
|
||||
|
||||
def on_file_saved(self, save_path):
|
||||
"""文件保存"""
|
||||
logging.info(f"文件已保存: {save_path}")
|
||||
|
||||
def batch_save(self):
|
||||
"""批量保存"""
|
||||
if not self.processed_results:
|
||||
QMessageBox.warning(self, "提示", "没有可保存的处理结果")
|
||||
return
|
||||
|
||||
default_dir = config.get_output_directory()
|
||||
output_dir = QFileDialog.getExistingDirectory(self, "选择保存目录", default_dir)
|
||||
|
||||
if not output_dir:
|
||||
return
|
||||
|
||||
saved_count = 0
|
||||
for file_path, result in self.processed_results.items():
|
||||
try:
|
||||
output_path = os.path.join(output_dir, result["output_name"])
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(result["data"])
|
||||
saved_count += 1
|
||||
except Exception as e:
|
||||
logging.error(f"保存失败 {file_path}: {e}")
|
||||
|
||||
QMessageBox.information(
|
||||
self, "保存完成",
|
||||
f"已保存 {saved_count}/{len(self.processed_results)} 个文件到:\n{output_dir}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _hex_to_rgb(hex_color: str) -> str:
|
||||
hex_color = hex_color.lstrip('#')
|
||||
r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
return f"{r}, {g}, {b}"
|
||||
579
tools/image/watermark.py
Normal file
@@ -0,0 +1,579 @@
|
||||
"""
|
||||
图片加水印工具
|
||||
- 支持文字水印和图片水印
|
||||
- 可调整位置、透明度、大小
|
||||
- 预览功能
|
||||
- 批量处理
|
||||
"""
|
||||
import os
|
||||
import io
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QSlider, QFrame, QFileDialog, QMessageBox, QProgressBar,
|
||||
QListWidget, QListWidgetItem, QLineEdit, QComboBox,
|
||||
QTabWidget, QSpinBox, QColorDialog
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtGui import QFont, QColor
|
||||
|
||||
from ui.workspace import BaseWorkspace, UploadArea
|
||||
from ui.image_preview import DualPreviewWidget
|
||||
from core.config import config
|
||||
|
||||
|
||||
class WatermarkWorker(QThread):
|
||||
"""水印工作线程"""
|
||||
progress = Signal(int, int)
|
||||
file_processed = Signal(str, bytes, dict, str) # file_path, data, info, output_name
|
||||
finished = Signal(list)
|
||||
|
||||
def __init__(self, files: list, watermark_config: dict, output_dir: str = None):
|
||||
super().__init__()
|
||||
self.files = files
|
||||
self.config = watermark_config
|
||||
self.output_dir = output_dir
|
||||
self.save_files = output_dir is not None
|
||||
|
||||
def run(self):
|
||||
results = []
|
||||
total = len(self.files)
|
||||
|
||||
for i, file_path in enumerate(self.files):
|
||||
try:
|
||||
result = self.add_watermark(file_path)
|
||||
results.append(result)
|
||||
|
||||
if result.get("success") and result.get("data"):
|
||||
self.file_processed.emit(
|
||||
file_path,
|
||||
result["data"],
|
||||
{"size": len(result["data"]), "name": result["output_name"]},
|
||||
result["output_name"]
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"添加水印失败 {file_path}: {e}")
|
||||
results.append({
|
||||
"file": file_path,
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
self.progress.emit(i + 1, total)
|
||||
|
||||
self.finished.emit(results)
|
||||
|
||||
def add_watermark(self, file_path: str) -> dict:
|
||||
"""添加水印"""
|
||||
ext = Path(file_path).suffix.lower()
|
||||
output_name = Path(file_path).stem + "_watermarked" + ext
|
||||
output_buffer = io.BytesIO()
|
||||
|
||||
with Image.open(file_path) as img:
|
||||
if img.mode != 'RGBA':
|
||||
img = img.convert('RGBA')
|
||||
|
||||
watermark_layer = Image.new('RGBA', img.size, (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(watermark_layer)
|
||||
|
||||
if self.config['type'] == 'text':
|
||||
self.add_text_watermark(draw, img.size)
|
||||
else:
|
||||
self.add_image_watermark(watermark_layer, img.size)
|
||||
|
||||
result = Image.alpha_composite(img, watermark_layer)
|
||||
|
||||
# 保存
|
||||
if ext in ['.jpg', '.jpeg']:
|
||||
result = result.convert('RGB')
|
||||
result.save(output_buffer, 'JPEG', quality=95)
|
||||
elif ext == '.png':
|
||||
result.save(output_buffer, 'PNG')
|
||||
else:
|
||||
result = result.convert('RGB')
|
||||
result.save(output_buffer, 'JPEG', quality=95)
|
||||
output_name = Path(file_path).stem + "_watermarked.jpg"
|
||||
|
||||
data = output_buffer.getvalue()
|
||||
|
||||
# 如果需要保存
|
||||
output_path = None
|
||||
if self.save_files and self.output_dir:
|
||||
output_path = os.path.join(self.output_dir, output_name)
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
return {
|
||||
"file": file_path,
|
||||
"output": output_path,
|
||||
"output_name": output_name,
|
||||
"success": True,
|
||||
"data": data
|
||||
}
|
||||
|
||||
def add_text_watermark(self, draw: ImageDraw, img_size: tuple):
|
||||
"""添加文字水印"""
|
||||
text = self.config.get('text', 'Watermark')
|
||||
opacity = int(self.config.get('opacity', 50) * 2.55)
|
||||
font_size = self.config.get('font_size', 48) # 默认更大的字体
|
||||
color = self.config.get('color', (255, 255, 255))
|
||||
position = self.config.get('position', 'center')
|
||||
|
||||
# 尝试使用支持中文的字体
|
||||
font = None
|
||||
# Windows 中文字体列表
|
||||
chinese_fonts = [
|
||||
"C:/Windows/Fonts/msyh.ttc", # 微软雅黑
|
||||
"C:/Windows/Fonts/simhei.ttf", # 黑体
|
||||
"C:/Windows/Fonts/simsun.ttc", # 宋体
|
||||
"C:/Windows/Fonts/simkai.ttf", # 楷体
|
||||
"msyh.ttc",
|
||||
"simhei.ttf",
|
||||
"arial.ttf",
|
||||
]
|
||||
|
||||
for font_path in chinese_fonts:
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if font is None:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
positions = {
|
||||
'top-left': (20, 20),
|
||||
'top-right': (img_size[0] - text_width - 20, 20),
|
||||
'bottom-left': (20, img_size[1] - text_height - 20),
|
||||
'bottom-right': (img_size[0] - text_width - 20, img_size[1] - text_height - 20),
|
||||
'center': ((img_size[0] - text_width) // 2, (img_size[1] - text_height) // 2)
|
||||
}
|
||||
|
||||
x, y = positions.get(position, positions['center'])
|
||||
draw.text((x, y), text, font=font, fill=(*color, opacity))
|
||||
|
||||
def add_image_watermark(self, layer: Image, img_size: tuple):
|
||||
"""添加图片水印"""
|
||||
watermark_path = self.config.get('image_path')
|
||||
if not watermark_path or not os.path.exists(watermark_path):
|
||||
return
|
||||
|
||||
opacity = self.config.get('opacity', 50) / 100
|
||||
scale = self.config.get('scale', 20) / 100
|
||||
position = self.config.get('position', 'center')
|
||||
|
||||
with Image.open(watermark_path) as watermark:
|
||||
if watermark.mode != 'RGBA':
|
||||
watermark = watermark.convert('RGBA')
|
||||
|
||||
new_width = int(img_size[0] * scale)
|
||||
ratio = new_width / watermark.width
|
||||
new_height = int(watermark.height * ratio)
|
||||
watermark = watermark.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
alpha = watermark.split()[3]
|
||||
alpha = alpha.point(lambda p: int(p * opacity))
|
||||
watermark.putalpha(alpha)
|
||||
|
||||
positions = {
|
||||
'top-left': (20, 20),
|
||||
'top-right': (img_size[0] - new_width - 20, 20),
|
||||
'bottom-left': (20, img_size[1] - new_height - 20),
|
||||
'bottom-right': (img_size[0] - new_width - 20, img_size[1] - new_height - 20),
|
||||
'center': ((img_size[0] - new_width) // 2, (img_size[1] - new_height) // 2)
|
||||
}
|
||||
|
||||
x, y = positions.get(position, positions['center'])
|
||||
layer.paste(watermark, (x, y), watermark)
|
||||
|
||||
|
||||
class ImageWatermarkPage(BaseWorkspace):
|
||||
"""图片加水印页面"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.files = []
|
||||
self.current_file_index = 0
|
||||
self.processed_results = {}
|
||||
self.watermark_color = (255, 255, 255)
|
||||
self.watermark_image_path = None
|
||||
self.setup_watermark_ui()
|
||||
|
||||
def setup_watermark_ui(self):
|
||||
"""设置水印UI"""
|
||||
self.history_btn.hide()
|
||||
self.export_btn.setText("💾 批量保存")
|
||||
self.export_btn.clicked.connect(self.batch_save)
|
||||
|
||||
# 上传区域
|
||||
self.upload_area = UploadArea("图片文件 (*.jpg *.jpeg *.png *.webp)")
|
||||
self.upload_area.files_dropped.connect(self.on_files_added)
|
||||
self.content_layout.addWidget(self.upload_area)
|
||||
|
||||
# 主内容区
|
||||
content_widget = QWidget()
|
||||
content_layout = QHBoxLayout(content_widget)
|
||||
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
content_layout.setSpacing(24)
|
||||
|
||||
# 左侧 - 预览区
|
||||
self.preview_widget = DualPreviewWidget()
|
||||
self.preview_widget.save_requested.connect(self.on_file_saved)
|
||||
content_layout.addWidget(self.preview_widget, 2)
|
||||
|
||||
# 右侧设置区
|
||||
settings_frame = QFrame()
|
||||
settings_frame.setObjectName("card")
|
||||
settings_frame.setFixedWidth(300)
|
||||
settings_frame.setStyleSheet("""
|
||||
#card {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 16px;
|
||||
}
|
||||
""")
|
||||
settings_layout = QVBoxLayout(settings_frame)
|
||||
settings_layout.setContentsMargins(20, 20, 20, 20)
|
||||
settings_layout.setSpacing(16)
|
||||
|
||||
# 标签页
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget.setStyleSheet("""
|
||||
QTabWidget::pane { border: none; background: transparent; }
|
||||
QTabBar::tab {
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
QTabBar::tab:selected { color: #fbbf24; border-bottom: 2px solid #fbbf24; }
|
||||
""")
|
||||
|
||||
# 文字水印
|
||||
text_tab = QWidget()
|
||||
text_layout = QVBoxLayout(text_tab)
|
||||
text_layout.setSpacing(12)
|
||||
|
||||
text_input_layout = QHBoxLayout()
|
||||
text_input_layout.addWidget(QLabel("水印文字:"))
|
||||
self.text_input = QLineEdit("© 奶酪云工具箱")
|
||||
text_input_layout.addWidget(self.text_input, 1)
|
||||
text_layout.addLayout(text_input_layout)
|
||||
|
||||
font_layout = QHBoxLayout()
|
||||
font_layout.addWidget(QLabel("字体大小:"))
|
||||
self.font_size_spin = QSpinBox()
|
||||
self.font_size_spin.setRange(24, 300)
|
||||
self.font_size_spin.setValue(72) # 默认更大的字体
|
||||
font_layout.addWidget(self.font_size_spin)
|
||||
font_layout.addStretch()
|
||||
|
||||
font_layout.addWidget(QLabel("颜色:"))
|
||||
self.color_btn = QPushButton()
|
||||
self.color_btn.setFixedSize(40, 30)
|
||||
self.color_btn.setStyleSheet("background: white; border-radius: 4px;")
|
||||
self.color_btn.clicked.connect(self.choose_color)
|
||||
font_layout.addWidget(self.color_btn)
|
||||
text_layout.addLayout(font_layout)
|
||||
|
||||
self.tab_widget.addTab(text_tab, "📝 文字水印")
|
||||
|
||||
# 图片水印
|
||||
image_tab = QWidget()
|
||||
image_layout = QVBoxLayout(image_tab)
|
||||
image_layout.setSpacing(12)
|
||||
|
||||
img_select_layout = QHBoxLayout()
|
||||
img_select_layout.addWidget(QLabel("水印图片:"))
|
||||
self.watermark_path_label = QLabel("未选择")
|
||||
self.watermark_path_label.setStyleSheet("color: #64748b;")
|
||||
img_select_layout.addWidget(self.watermark_path_label, 1)
|
||||
|
||||
select_img_btn = QPushButton("选择")
|
||||
select_img_btn.setObjectName("secondary_btn")
|
||||
select_img_btn.clicked.connect(self.select_watermark_image)
|
||||
img_select_layout.addWidget(select_img_btn)
|
||||
image_layout.addLayout(img_select_layout)
|
||||
|
||||
scale_layout = QHBoxLayout()
|
||||
scale_layout.addWidget(QLabel("缩放:"))
|
||||
self.scale_slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self.scale_slider.setRange(5, 50)
|
||||
self.scale_slider.setValue(20)
|
||||
scale_layout.addWidget(self.scale_slider, 1)
|
||||
self.scale_value = QLabel("20%")
|
||||
self.scale_slider.valueChanged.connect(lambda v: self.scale_value.setText(f"{v}%"))
|
||||
scale_layout.addWidget(self.scale_value)
|
||||
image_layout.addLayout(scale_layout)
|
||||
|
||||
self.tab_widget.addTab(image_tab, "🖼️ 图片水印")
|
||||
|
||||
settings_layout.addWidget(self.tab_widget)
|
||||
|
||||
# 通用设置
|
||||
common_frame = QFrame()
|
||||
common_frame.setStyleSheet("background: rgba(15, 23, 42, 0.5); border-radius: 8px; padding: 8px;")
|
||||
common_layout = QVBoxLayout(common_frame)
|
||||
common_layout.setSpacing(8)
|
||||
|
||||
opacity_layout = QHBoxLayout()
|
||||
opacity_layout.addWidget(QLabel("透明度:"))
|
||||
self.opacity_slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self.opacity_slider.setRange(10, 100)
|
||||
self.opacity_slider.setValue(50)
|
||||
opacity_layout.addWidget(self.opacity_slider, 1)
|
||||
self.opacity_value = QLabel("50%")
|
||||
self.opacity_slider.valueChanged.connect(lambda v: self.opacity_value.setText(f"{v}%"))
|
||||
opacity_layout.addWidget(self.opacity_value)
|
||||
common_layout.addLayout(opacity_layout)
|
||||
|
||||
pos_layout = QHBoxLayout()
|
||||
pos_layout.addWidget(QLabel("位置:"))
|
||||
self.position_combo = QComboBox()
|
||||
positions = [("左上角", "top-left"), ("右上角", "top-right"),
|
||||
("左下角", "bottom-left"), ("右下角", "bottom-right"), ("居中", "center")]
|
||||
for text, value in positions:
|
||||
self.position_combo.addItem(text, value)
|
||||
self.position_combo.setCurrentIndex(4)
|
||||
pos_layout.addWidget(self.position_combo)
|
||||
pos_layout.addStretch()
|
||||
common_layout.addLayout(pos_layout)
|
||||
|
||||
settings_layout.addWidget(common_frame)
|
||||
|
||||
# 文件列表
|
||||
files_header = QHBoxLayout()
|
||||
files_label = QLabel("📁 待处理:")
|
||||
files_header.addWidget(files_label)
|
||||
self.count_label = QLabel("0")
|
||||
self.count_label.setStyleSheet("color: #fbbf24;")
|
||||
files_header.addWidget(self.count_label)
|
||||
files_header.addStretch()
|
||||
|
||||
clear_btn = QPushButton("清空")
|
||||
clear_btn.setObjectName("secondary_btn")
|
||||
clear_btn.clicked.connect(self.clear_files)
|
||||
files_header.addWidget(clear_btn)
|
||||
settings_layout.addLayout(files_header)
|
||||
|
||||
self.files_list = QListWidget()
|
||||
self.files_list.setMaximumHeight(80)
|
||||
self.files_list.itemClicked.connect(self.on_file_clicked)
|
||||
settings_layout.addWidget(self.files_list)
|
||||
|
||||
settings_layout.addStretch()
|
||||
|
||||
# 进度条
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
settings_layout.addWidget(self.progress_bar)
|
||||
|
||||
# 预览按钮
|
||||
self.preview_btn = QPushButton("👁️ 预览效果")
|
||||
self.preview_btn.setObjectName("secondary_btn")
|
||||
self.preview_btn.setMinimumHeight(40)
|
||||
self.preview_btn.clicked.connect(self.preview_current)
|
||||
settings_layout.addWidget(self.preview_btn)
|
||||
|
||||
# 开始按钮
|
||||
self.start_btn = QPushButton("💧 添加水印")
|
||||
self.start_btn.setObjectName("primary_btn")
|
||||
self.start_btn.setMinimumSize(150, 45)
|
||||
self.start_btn.setFont(QFont("Microsoft YaHei", 12, QFont.Weight.Bold))
|
||||
self.start_btn.clicked.connect(self.start_watermark_all)
|
||||
settings_layout.addWidget(self.start_btn)
|
||||
|
||||
content_layout.addWidget(settings_frame)
|
||||
|
||||
self.content_layout.addWidget(content_widget, 1)
|
||||
|
||||
def on_files_added(self, files: list):
|
||||
"""文件添加"""
|
||||
for file_path in files:
|
||||
if file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.webp')):
|
||||
if file_path not in self.files:
|
||||
self.files.append(file_path)
|
||||
self.files_list.addItem(f"📷 {Path(file_path).name}")
|
||||
|
||||
self.count_label.setText(str(len(self.files)))
|
||||
|
||||
if self.files:
|
||||
self.files_list.setCurrentRow(0)
|
||||
self.preview_widget.set_original(self.files[0])
|
||||
self.current_file_index = 0
|
||||
|
||||
def on_file_clicked(self, item):
|
||||
"""文件点击"""
|
||||
row = self.files_list.currentRow()
|
||||
if row >= 0 and row < len(self.files):
|
||||
self.current_file_index = row
|
||||
file_path = self.files[row]
|
||||
self.preview_widget.set_original(file_path)
|
||||
|
||||
if file_path in self.processed_results:
|
||||
result = self.processed_results[file_path]
|
||||
self.preview_widget.set_result(
|
||||
result["data"],
|
||||
{"size": len(result["data"]), "name": result["output_name"]},
|
||||
result["output_name"]
|
||||
)
|
||||
|
||||
def clear_files(self):
|
||||
"""清空文件"""
|
||||
self.files.clear()
|
||||
self.files_list.clear()
|
||||
self.count_label.setText("0")
|
||||
self.processed_results.clear()
|
||||
self.preview_widget.clear()
|
||||
|
||||
def choose_color(self):
|
||||
"""选择颜色"""
|
||||
color = QColorDialog.getColor(QColor(*self.watermark_color), self)
|
||||
if color.isValid():
|
||||
self.watermark_color = (color.red(), color.green(), color.blue())
|
||||
self.color_btn.setStyleSheet(f"background: {color.name()}; border-radius: 4px;")
|
||||
|
||||
def select_watermark_image(self):
|
||||
"""选择水印图片"""
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self, "选择水印图片", "", "图片文件 (*.png *.jpg *.jpeg)"
|
||||
)
|
||||
if file_path:
|
||||
self.watermark_image_path = file_path
|
||||
self.watermark_path_label.setText(Path(file_path).name)
|
||||
|
||||
def get_watermark_config(self) -> dict:
|
||||
"""获取水印配置"""
|
||||
is_text = self.tab_widget.currentIndex() == 0
|
||||
config = {
|
||||
'type': 'text' if is_text else 'image',
|
||||
'opacity': self.opacity_slider.value(),
|
||||
'position': self.position_combo.currentData()
|
||||
}
|
||||
|
||||
if is_text:
|
||||
config['text'] = self.text_input.text() or 'Watermark'
|
||||
config['font_size'] = self.font_size_spin.value()
|
||||
config['color'] = self.watermark_color
|
||||
else:
|
||||
config['image_path'] = self.watermark_image_path
|
||||
config['scale'] = self.scale_slider.value()
|
||||
|
||||
return config
|
||||
|
||||
def preview_current(self):
|
||||
"""预览当前文件"""
|
||||
if not self.files:
|
||||
QMessageBox.warning(self, "提示", "请先添加要处理的图片文件")
|
||||
return
|
||||
|
||||
watermark_config = self.get_watermark_config()
|
||||
if watermark_config['type'] == 'image' and not self.watermark_image_path:
|
||||
QMessageBox.warning(self, "提示", "请先选择水印图片")
|
||||
return
|
||||
|
||||
file_path = self.files[self.current_file_index]
|
||||
|
||||
self.preview_btn.setEnabled(False)
|
||||
self.preview_btn.setText("处理中...")
|
||||
|
||||
self.worker = WatermarkWorker([file_path], watermark_config, None)
|
||||
self.worker.file_processed.connect(self.on_preview_ready)
|
||||
self.worker.finished.connect(lambda: self.preview_btn.setEnabled(True))
|
||||
self.worker.finished.connect(lambda: self.preview_btn.setText("👁️ 预览效果"))
|
||||
self.worker.start()
|
||||
|
||||
def on_preview_ready(self, file_path: str, data: bytes, info: dict, output_name: str):
|
||||
"""预览完成"""
|
||||
self.preview_widget.set_result(data, info, output_name, show_size_compare=False)
|
||||
self.processed_results[file_path] = {
|
||||
"data": data,
|
||||
"output_name": output_name
|
||||
}
|
||||
|
||||
def start_watermark_all(self):
|
||||
"""处理所有文件"""
|
||||
if not self.files:
|
||||
QMessageBox.warning(self, "提示", "请先添加要处理的图片文件")
|
||||
return
|
||||
|
||||
watermark_config = self.get_watermark_config()
|
||||
if watermark_config['type'] == 'image' and not self.watermark_image_path:
|
||||
QMessageBox.warning(self, "提示", "请先选择水印图片")
|
||||
return
|
||||
|
||||
self.start_btn.setEnabled(False)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
self.worker = WatermarkWorker(self.files, watermark_config, None)
|
||||
self.worker.progress.connect(self.on_progress)
|
||||
self.worker.file_processed.connect(self.on_file_processed)
|
||||
self.worker.finished.connect(self.on_finished)
|
||||
self.worker.start()
|
||||
|
||||
logging.info(f"开始添加水印, 文件数: {len(self.files)}")
|
||||
|
||||
def on_progress(self, current: int, total: int):
|
||||
"""进度更新"""
|
||||
self.progress_bar.setValue(int(current / total * 100))
|
||||
|
||||
def on_file_processed(self, file_path: str, data: bytes, info: dict, output_name: str):
|
||||
"""文件处理完成"""
|
||||
self.processed_results[file_path] = {
|
||||
"data": data,
|
||||
"output_name": output_name
|
||||
}
|
||||
|
||||
if self.files.index(file_path) == self.current_file_index:
|
||||
self.preview_widget.set_result(data, info, output_name, show_size_compare=False)
|
||||
|
||||
def on_finished(self, results: list):
|
||||
"""处理完成"""
|
||||
self.start_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
success_count = sum(1 for r in results if r.get("success"))
|
||||
QMessageBox.information(
|
||||
self, "完成",
|
||||
f"水印添加完成!\n\n✅ 成功: {success_count}/{len(results)}\n\n请点击「批量保存」或在预览中单独保存"
|
||||
)
|
||||
logging.info(f"水印添加完成: 成功 {success_count}/{len(results)}")
|
||||
|
||||
def on_file_saved(self, save_path):
|
||||
"""文件保存"""
|
||||
logging.info(f"文件已保存: {save_path}")
|
||||
|
||||
def batch_save(self):
|
||||
"""批量保存"""
|
||||
if not self.processed_results:
|
||||
QMessageBox.warning(self, "提示", "没有可保存的处理结果")
|
||||
return
|
||||
|
||||
default_dir = config.get_output_directory()
|
||||
output_dir = QFileDialog.getExistingDirectory(self, "选择保存目录", default_dir)
|
||||
|
||||
if not output_dir:
|
||||
return
|
||||
|
||||
saved_count = 0
|
||||
for file_path, result in self.processed_results.items():
|
||||
try:
|
||||
output_path = os.path.join(output_dir, result["output_name"])
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(result["data"])
|
||||
saved_count += 1
|
||||
except Exception as e:
|
||||
logging.error(f"保存失败 {file_path}: {e}")
|
||||
|
||||
QMessageBox.information(
|
||||
self, "保存完成",
|
||||
f"已保存 {saved_count}/{len(self.processed_results)} 个文件到:\n{output_dir}"
|
||||
)
|
||||
2
tools/pdf/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# PDF tools module
|
||||
|
||||
387
tools/pdf/merge.py
Normal file
@@ -0,0 +1,387 @@
|
||||
"""
|
||||
PDF合并工具
|
||||
- 文件列表 + 拖拽排序
|
||||
- 添加/删除/上下移动
|
||||
- 一键合并
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QFileDialog, QMessageBox, QProgressBar,
|
||||
QListWidget, QListWidgetItem, QAbstractItemView
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtGui import QFont, QIcon
|
||||
|
||||
from ui.workspace import BaseWorkspace, UploadArea
|
||||
|
||||
try:
|
||||
import fitz
|
||||
HAS_PYMUPDF = True
|
||||
except ImportError:
|
||||
HAS_PYMUPDF = False
|
||||
|
||||
|
||||
class MergeWorker(QThread):
|
||||
"""合并工作线程"""
|
||||
progress = Signal(int, int)
|
||||
finished = Signal(str)
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, files: list, output_path: str):
|
||||
super().__init__()
|
||||
self.files = files
|
||||
self.output_path = output_path
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
merged = fitz.open()
|
||||
total = len(self.files)
|
||||
|
||||
for i, file_path in enumerate(self.files):
|
||||
doc = fitz.open(file_path)
|
||||
merged.insert_pdf(doc)
|
||||
doc.close()
|
||||
self.progress.emit(i + 1, total)
|
||||
|
||||
merged.save(self.output_path)
|
||||
merged.close()
|
||||
|
||||
self.finished.emit(self.output_path)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"合并PDF失败: {e}")
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
class PDFFileItem(QWidget):
|
||||
"""PDF文件列表项"""
|
||||
|
||||
remove_clicked = Signal(str) # file_path
|
||||
|
||||
def __init__(self, file_path: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.file_path = file_path
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(12, 8, 12, 8)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# 拖拽手柄
|
||||
handle = QLabel("⋮⋮")
|
||||
handle.setStyleSheet("color: #64748b; font-size: 16px;")
|
||||
handle.setCursor(Qt.CursorShape.OpenHandCursor)
|
||||
layout.addWidget(handle)
|
||||
|
||||
# PDF图标
|
||||
icon = QLabel("📄")
|
||||
icon.setFont(QFont("Segoe UI Emoji", 16))
|
||||
layout.addWidget(icon)
|
||||
|
||||
# 文件信息
|
||||
info_layout = QVBoxLayout()
|
||||
info_layout.setSpacing(2)
|
||||
|
||||
name = QLabel(Path(self.file_path).name)
|
||||
name.setStyleSheet("color: #e2e8f0; font-size: 13px; font-weight: 500;")
|
||||
info_layout.addWidget(name)
|
||||
|
||||
# 文件大小
|
||||
size = os.path.getsize(self.file_path)
|
||||
size_str = self.format_size(size)
|
||||
size_label = QLabel(size_str)
|
||||
size_label.setStyleSheet("color: #64748b; font-size: 11px;")
|
||||
info_layout.addWidget(size_label)
|
||||
|
||||
layout.addLayout(info_layout, 1)
|
||||
|
||||
# 删除按钮
|
||||
remove_btn = QPushButton("🗑")
|
||||
remove_btn.setFixedSize(32, 32)
|
||||
remove_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
""")
|
||||
remove_btn.clicked.connect(lambda: self.remove_clicked.emit(self.file_path))
|
||||
layout.addWidget(remove_btn)
|
||||
|
||||
@staticmethod
|
||||
def format_size(size: int) -> str:
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
|
||||
class PDFMergePage(BaseWorkspace):
|
||||
"""PDF合并页面"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.files = []
|
||||
self.setup_merge_ui()
|
||||
|
||||
def setup_merge_ui(self):
|
||||
"""设置合并UI"""
|
||||
self.history_btn.hide()
|
||||
|
||||
# 上传区域
|
||||
self.upload_area = UploadArea("PDF文件 (*.pdf)")
|
||||
self.upload_area.files_dropped.connect(self.on_files_added)
|
||||
self.content_layout.addWidget(self.upload_area)
|
||||
|
||||
# 文件列表区域
|
||||
list_frame = QFrame()
|
||||
list_frame.setObjectName("card")
|
||||
list_layout = QVBoxLayout(list_frame)
|
||||
list_layout.setContentsMargins(20, 20, 20, 20)
|
||||
list_layout.setSpacing(16)
|
||||
|
||||
# 标题
|
||||
header = QHBoxLayout()
|
||||
|
||||
title = QLabel("📑 待合并文件列表")
|
||||
title.setStyleSheet("color: white; font-weight: 600; font-size: 16px;")
|
||||
header.addWidget(title)
|
||||
|
||||
header.addStretch()
|
||||
|
||||
hint = QLabel("(可拖拽排序)")
|
||||
hint.setStyleSheet("color: #64748b; font-size: 12px;")
|
||||
header.addWidget(hint)
|
||||
|
||||
list_layout.addLayout(header)
|
||||
|
||||
# 文件列表
|
||||
self.file_list = QListWidget()
|
||||
self.file_list.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
||||
self.file_list.setDefaultDropAction(Qt.DropAction.MoveAction)
|
||||
self.file_list.setMinimumHeight(250)
|
||||
self.file_list.setStyleSheet("""
|
||||
QListWidget {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
}
|
||||
QListWidget::item {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
QListWidget::item:hover {
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
QListWidget::item:selected {
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
""")
|
||||
self.file_list.model().rowsMoved.connect(self.on_rows_moved)
|
||||
list_layout.addWidget(self.file_list)
|
||||
|
||||
# 操作按钮
|
||||
btn_layout = QHBoxLayout()
|
||||
|
||||
add_btn = QPushButton("➕ 添加文件")
|
||||
add_btn.setObjectName("secondary_btn")
|
||||
add_btn.clicked.connect(self.add_files)
|
||||
btn_layout.addWidget(add_btn)
|
||||
|
||||
move_up_btn = QPushButton("⬆️ 上移")
|
||||
move_up_btn.setObjectName("secondary_btn")
|
||||
move_up_btn.clicked.connect(self.move_up)
|
||||
btn_layout.addWidget(move_up_btn)
|
||||
|
||||
move_down_btn = QPushButton("⬇️ 下移")
|
||||
move_down_btn.setObjectName("secondary_btn")
|
||||
move_down_btn.clicked.connect(self.move_down)
|
||||
btn_layout.addWidget(move_down_btn)
|
||||
|
||||
btn_layout.addStretch()
|
||||
|
||||
clear_btn = QPushButton("🗑️ 清空列表")
|
||||
clear_btn.setObjectName("secondary_btn")
|
||||
clear_btn.clicked.connect(self.clear_files)
|
||||
btn_layout.addWidget(clear_btn)
|
||||
|
||||
list_layout.addLayout(btn_layout)
|
||||
|
||||
# 进度条
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
list_layout.addWidget(self.progress_bar)
|
||||
|
||||
# 合并按钮
|
||||
self.merge_btn = QPushButton("📑 合并为单个 PDF")
|
||||
self.merge_btn.setObjectName("primary_btn")
|
||||
self.merge_btn.setMinimumHeight(50)
|
||||
self.merge_btn.setFont(QFont("Microsoft YaHei", 13, QFont.Weight.Bold))
|
||||
self.merge_btn.clicked.connect(self.do_merge)
|
||||
list_layout.addWidget(self.merge_btn)
|
||||
|
||||
self.content_layout.addWidget(list_frame, 1)
|
||||
|
||||
def on_files_added(self, files: list):
|
||||
"""文件添加"""
|
||||
for file_path in files:
|
||||
if file_path.lower().endswith('.pdf') and file_path not in self.files:
|
||||
self.files.append(file_path)
|
||||
self.add_file_item(file_path)
|
||||
|
||||
logging.info(f"添加了 {len(files)} 个PDF文件")
|
||||
|
||||
def add_file_item(self, file_path: str):
|
||||
"""添加文件列表项"""
|
||||
item = QListWidgetItem()
|
||||
item.setData(Qt.ItemDataRole.UserRole, file_path)
|
||||
item.setSizeHint(QListWidgetItem().sizeHint())
|
||||
item.setSizeHint(item.sizeHint().expandedTo(QListWidgetItem().sizeHint()))
|
||||
|
||||
widget = PDFFileItem(file_path)
|
||||
widget.remove_clicked.connect(self.remove_file)
|
||||
|
||||
item.setSizeHint(widget.sizeHint())
|
||||
self.file_list.addItem(item)
|
||||
self.file_list.setItemWidget(item, widget)
|
||||
|
||||
def remove_file(self, file_path: str):
|
||||
"""移除文件"""
|
||||
if file_path in self.files:
|
||||
self.files.remove(file_path)
|
||||
|
||||
for i in range(self.file_list.count()):
|
||||
item = self.file_list.item(i)
|
||||
if item.data(Qt.ItemDataRole.UserRole) == file_path:
|
||||
self.file_list.takeItem(i)
|
||||
break
|
||||
|
||||
def add_files(self):
|
||||
"""添加文件对话框"""
|
||||
files, _ = QFileDialog.getOpenFileNames(
|
||||
self, "选择PDF文件", "", "PDF文件 (*.pdf)"
|
||||
)
|
||||
if files:
|
||||
self.on_files_added(files)
|
||||
|
||||
def move_up(self):
|
||||
"""上移"""
|
||||
row = self.file_list.currentRow()
|
||||
if row > 0:
|
||||
item = self.file_list.takeItem(row)
|
||||
self.file_list.insertItem(row - 1, item)
|
||||
self.file_list.setCurrentRow(row - 1)
|
||||
|
||||
# 重新创建widget
|
||||
file_path = item.data(Qt.ItemDataRole.UserRole)
|
||||
widget = PDFFileItem(file_path)
|
||||
widget.remove_clicked.connect(self.remove_file)
|
||||
item.setSizeHint(widget.sizeHint())
|
||||
self.file_list.setItemWidget(item, widget)
|
||||
|
||||
self.sync_files_order()
|
||||
|
||||
def move_down(self):
|
||||
"""下移"""
|
||||
row = self.file_list.currentRow()
|
||||
if row < self.file_list.count() - 1:
|
||||
item = self.file_list.takeItem(row)
|
||||
self.file_list.insertItem(row + 1, item)
|
||||
self.file_list.setCurrentRow(row + 1)
|
||||
|
||||
# 重新创建widget
|
||||
file_path = item.data(Qt.ItemDataRole.UserRole)
|
||||
widget = PDFFileItem(file_path)
|
||||
widget.remove_clicked.connect(self.remove_file)
|
||||
item.setSizeHint(widget.sizeHint())
|
||||
self.file_list.setItemWidget(item, widget)
|
||||
|
||||
self.sync_files_order()
|
||||
|
||||
def on_rows_moved(self):
|
||||
"""行移动后同步文件顺序"""
|
||||
self.sync_files_order()
|
||||
|
||||
def sync_files_order(self):
|
||||
"""同步文件顺序"""
|
||||
self.files = []
|
||||
for i in range(self.file_list.count()):
|
||||
item = self.file_list.item(i)
|
||||
file_path = item.data(Qt.ItemDataRole.UserRole)
|
||||
if file_path:
|
||||
self.files.append(file_path)
|
||||
|
||||
def clear_files(self):
|
||||
"""清空文件"""
|
||||
self.files.clear()
|
||||
self.file_list.clear()
|
||||
|
||||
def do_merge(self):
|
||||
"""执行合并"""
|
||||
if not HAS_PYMUPDF:
|
||||
QMessageBox.critical(self, "错误", "PyMuPDF未安装,无法合并PDF")
|
||||
return
|
||||
|
||||
if len(self.files) < 2:
|
||||
QMessageBox.warning(self, "提示", "请至少添加2个PDF文件")
|
||||
return
|
||||
|
||||
# 同步顺序
|
||||
self.sync_files_order()
|
||||
|
||||
# 选择保存路径
|
||||
save_path, _ = QFileDialog.getSaveFileName(
|
||||
self, "保存合并后的PDF", "merged.pdf", "PDF文件 (*.pdf)"
|
||||
)
|
||||
|
||||
if not save_path:
|
||||
return
|
||||
|
||||
# 开始合并
|
||||
self.merge_btn.setEnabled(False)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
self.worker = MergeWorker(self.files, save_path)
|
||||
self.worker.progress.connect(self.on_progress)
|
||||
self.worker.finished.connect(self.on_merge_finished)
|
||||
self.worker.error.connect(self.on_merge_error)
|
||||
self.worker.start()
|
||||
|
||||
logging.info(f"开始合并 {len(self.files)} 个PDF文件")
|
||||
|
||||
def on_progress(self, current: int, total: int):
|
||||
"""进度更新"""
|
||||
self.progress_bar.setValue(int(current / total * 100))
|
||||
|
||||
def on_merge_finished(self, output_path: str):
|
||||
"""合并完成"""
|
||||
self.merge_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
QMessageBox.information(
|
||||
self, "成功",
|
||||
f"PDF合并完成!\n\n共合并 {len(self.files)} 个文件\n保存到: {output_path}"
|
||||
)
|
||||
logging.info(f"PDF合并完成: {output_path}")
|
||||
|
||||
def on_merge_error(self, error: str):
|
||||
"""合并错误"""
|
||||
self.merge_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
QMessageBox.critical(self, "错误", f"合并失败:\n{error}")
|
||||
logging.error(f"PDF合并失败: {error}")
|
||||
|
||||
400
tools/pdf/split.py
Normal file
@@ -0,0 +1,400 @@
|
||||
"""
|
||||
PDF拆分工具
|
||||
- 渲染PDF页面缩略图网格
|
||||
- 多选页面(复选框)
|
||||
- 导出选中页面为新PDF
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QFileDialog, QMessageBox, QProgressBar,
|
||||
QScrollArea, QGridLayout, QCheckBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal, QSize
|
||||
from PySide6.QtGui import QFont, QPixmap, QImage
|
||||
|
||||
from ui.workspace import BaseWorkspace, UploadArea
|
||||
|
||||
# PDF处理
|
||||
try:
|
||||
import fitz # PyMuPDF
|
||||
HAS_PYMUPDF = True
|
||||
except ImportError:
|
||||
HAS_PYMUPDF = False
|
||||
logging.warning("PyMuPDF未安装, PDF功能不可用")
|
||||
|
||||
|
||||
class PDFRenderWorker(QThread):
|
||||
"""PDF页面渲染线程"""
|
||||
page_rendered = Signal(int, QPixmap) # page_num, pixmap
|
||||
finished = Signal(int) # total_pages
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, pdf_path: str, dpi: int = 72):
|
||||
super().__init__()
|
||||
self.pdf_path = pdf_path
|
||||
self.dpi = dpi
|
||||
|
||||
def run(self):
|
||||
if not HAS_PYMUPDF:
|
||||
self.error.emit("PyMuPDF未安装")
|
||||
return
|
||||
|
||||
try:
|
||||
doc = fitz.open(self.pdf_path)
|
||||
total_pages = len(doc)
|
||||
|
||||
for page_num in range(total_pages):
|
||||
page = doc[page_num]
|
||||
# 渲染页面
|
||||
mat = fitz.Matrix(self.dpi / 72, self.dpi / 72)
|
||||
pix = page.get_pixmap(matrix=mat)
|
||||
|
||||
# 转换为 QPixmap
|
||||
img = QImage(
|
||||
pix.samples,
|
||||
pix.width,
|
||||
pix.height,
|
||||
pix.stride,
|
||||
QImage.Format.Format_RGB888 if pix.n == 3 else QImage.Format.Format_RGBA8888
|
||||
)
|
||||
pixmap = QPixmap.fromImage(img)
|
||||
|
||||
self.page_rendered.emit(page_num, pixmap)
|
||||
|
||||
doc.close()
|
||||
self.finished.emit(total_pages)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"渲染PDF失败: {e}")
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
class PageThumbnail(QFrame):
|
||||
"""页面缩略图组件"""
|
||||
|
||||
selection_changed = Signal(int, bool) # page_num, selected
|
||||
|
||||
def __init__(self, page_num: int, parent=None):
|
||||
super().__init__(parent)
|
||||
self.page_num = page_num
|
||||
self.setObjectName("page_thumbnail")
|
||||
self.setFixedSize(140, 200)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setStyleSheet("""
|
||||
#page_thumbnail {
|
||||
background: white;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
#page_thumbnail:hover {
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(4, 4, 4, 4)
|
||||
layout.setSpacing(4)
|
||||
|
||||
# 预览图
|
||||
self.preview_label = QLabel()
|
||||
self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.preview_label.setStyleSheet("background: #f1f5f9; border-radius: 4px;")
|
||||
self.preview_label.setMinimumHeight(150)
|
||||
layout.addWidget(self.preview_label, 1)
|
||||
|
||||
# 底部信息
|
||||
bottom = QHBoxLayout()
|
||||
|
||||
self.checkbox = QCheckBox()
|
||||
self.checkbox.stateChanged.connect(self.on_checkbox_changed)
|
||||
bottom.addWidget(self.checkbox)
|
||||
|
||||
page_label = QLabel(f"第 {self.page_num + 1} 页")
|
||||
page_label.setStyleSheet("color: #1e293b; font-size: 11px;")
|
||||
bottom.addWidget(page_label)
|
||||
bottom.addStretch()
|
||||
|
||||
layout.addLayout(bottom)
|
||||
|
||||
def set_pixmap(self, pixmap: QPixmap):
|
||||
"""设置预览图"""
|
||||
scaled = pixmap.scaled(
|
||||
QSize(130, 140),
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
self.preview_label.setPixmap(scaled)
|
||||
|
||||
def on_checkbox_changed(self, state):
|
||||
"""复选框状态变化"""
|
||||
selected = state == Qt.CheckState.Checked.value
|
||||
self.selection_changed.emit(self.page_num, selected)
|
||||
|
||||
# 更新样式
|
||||
if selected:
|
||||
self.setStyleSheet("""
|
||||
#page_thumbnail {
|
||||
background: white;
|
||||
border: 2px solid #fbbf24;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 0 2px rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet("""
|
||||
#page_thumbnail {
|
||||
background: white;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
#page_thumbnail:hover {
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
""")
|
||||
|
||||
def set_selected(self, selected: bool):
|
||||
"""设置选中状态"""
|
||||
self.checkbox.setChecked(selected)
|
||||
|
||||
def is_selected(self) -> bool:
|
||||
"""是否选中"""
|
||||
return self.checkbox.isChecked()
|
||||
|
||||
|
||||
class PDFSplitPage(BaseWorkspace):
|
||||
"""PDF拆分页面"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.pdf_path = None
|
||||
self.total_pages = 0
|
||||
self.page_thumbnails = []
|
||||
self.selected_pages = set()
|
||||
self.setup_split_ui()
|
||||
|
||||
def setup_split_ui(self):
|
||||
"""设置拆分UI"""
|
||||
self.history_btn.hide()
|
||||
|
||||
# 上传区域
|
||||
self.upload_area = UploadArea("PDF文件 (*.pdf)")
|
||||
self.upload_area.files_dropped.connect(self.on_file_added)
|
||||
self.content_layout.addWidget(self.upload_area)
|
||||
|
||||
# 页面选择区域
|
||||
self.pages_frame = QFrame()
|
||||
self.pages_frame.setObjectName("card")
|
||||
self.pages_frame.setVisible(False)
|
||||
pages_layout = QVBoxLayout(self.pages_frame)
|
||||
pages_layout.setContentsMargins(20, 20, 20, 20)
|
||||
pages_layout.setSpacing(16)
|
||||
|
||||
# 标题栏
|
||||
header = QHBoxLayout()
|
||||
|
||||
title = QLabel("📄 选择页面")
|
||||
title.setStyleSheet("color: white; font-weight: 600; font-size: 16px;")
|
||||
header.addWidget(title)
|
||||
|
||||
header.addStretch()
|
||||
|
||||
# 全选/清空
|
||||
select_all_btn = QPushButton("全选")
|
||||
select_all_btn.setObjectName("secondary_btn")
|
||||
select_all_btn.clicked.connect(self.select_all)
|
||||
header.addWidget(select_all_btn)
|
||||
|
||||
clear_btn = QPushButton("清空")
|
||||
clear_btn.setObjectName("secondary_btn")
|
||||
clear_btn.clicked.connect(self.clear_selection)
|
||||
header.addWidget(clear_btn)
|
||||
|
||||
pages_layout.addLayout(header)
|
||||
|
||||
# 文件信息
|
||||
self.file_info = QLabel("")
|
||||
self.file_info.setStyleSheet("color: #94a3b8; font-size: 12px;")
|
||||
pages_layout.addWidget(self.file_info)
|
||||
|
||||
# 页面网格(滚动区域)
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
scroll.setStyleSheet("background: rgba(15, 23, 42, 0.3); border-radius: 8px;")
|
||||
scroll.setMinimumHeight(350)
|
||||
|
||||
self.grid_container = QWidget()
|
||||
self.grid_layout = QGridLayout(self.grid_container)
|
||||
self.grid_layout.setSpacing(16)
|
||||
self.grid_layout.setContentsMargins(16, 16, 16, 16)
|
||||
|
||||
scroll.setWidget(self.grid_container)
|
||||
pages_layout.addWidget(scroll, 1)
|
||||
|
||||
# 进度条
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
pages_layout.addWidget(self.progress_bar)
|
||||
|
||||
# 底部操作
|
||||
bottom = QHBoxLayout()
|
||||
|
||||
self.selection_label = QLabel("已选择: 0 页")
|
||||
self.selection_label.setStyleSheet("color: #fbbf24; font-size: 13px;")
|
||||
bottom.addWidget(self.selection_label)
|
||||
|
||||
bottom.addStretch()
|
||||
|
||||
self.split_btn = QPushButton("✂️ 拆分选定页面")
|
||||
self.split_btn.setObjectName("primary_btn")
|
||||
self.split_btn.setMinimumSize(150, 40)
|
||||
self.split_btn.setFont(QFont("Microsoft YaHei", 11, QFont.Weight.Bold))
|
||||
self.split_btn.clicked.connect(self.do_split)
|
||||
bottom.addWidget(self.split_btn)
|
||||
|
||||
pages_layout.addLayout(bottom)
|
||||
|
||||
self.content_layout.addWidget(self.pages_frame, 1)
|
||||
|
||||
def on_file_added(self, files: list):
|
||||
"""PDF文件添加"""
|
||||
if not files:
|
||||
return
|
||||
|
||||
pdf_file = None
|
||||
for f in files:
|
||||
if f.lower().endswith('.pdf'):
|
||||
pdf_file = f
|
||||
break
|
||||
|
||||
if not pdf_file:
|
||||
QMessageBox.warning(self, "提示", "请选择PDF文件")
|
||||
return
|
||||
|
||||
self.pdf_path = pdf_file
|
||||
self.load_pdf()
|
||||
|
||||
def load_pdf(self):
|
||||
"""加载PDF"""
|
||||
if not HAS_PYMUPDF:
|
||||
QMessageBox.critical(self, "错误", "PyMuPDF未安装,无法处理PDF文件")
|
||||
return
|
||||
|
||||
# 清空现有内容
|
||||
self.clear_pages()
|
||||
|
||||
# 显示页面区域
|
||||
self.pages_frame.setVisible(True)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
self.file_info.setText(f"📁 {Path(self.pdf_path).name}")
|
||||
|
||||
# 启动渲染线程
|
||||
self.render_worker = PDFRenderWorker(self.pdf_path)
|
||||
self.render_worker.page_rendered.connect(self.on_page_rendered)
|
||||
self.render_worker.finished.connect(self.on_render_finished)
|
||||
self.render_worker.error.connect(self.on_render_error)
|
||||
self.render_worker.start()
|
||||
|
||||
logging.info(f"开始加载PDF: {self.pdf_path}")
|
||||
|
||||
def on_page_rendered(self, page_num: int, pixmap: QPixmap):
|
||||
"""页面渲染完成"""
|
||||
thumbnail = PageThumbnail(page_num)
|
||||
thumbnail.set_pixmap(pixmap)
|
||||
thumbnail.selection_changed.connect(self.on_page_selection_changed)
|
||||
|
||||
# 添加到网格
|
||||
row = page_num // 5
|
||||
col = page_num % 5
|
||||
self.grid_layout.addWidget(thumbnail, row, col)
|
||||
self.page_thumbnails.append(thumbnail)
|
||||
|
||||
# 更新进度
|
||||
if self.total_pages > 0:
|
||||
self.progress_bar.setValue(int((page_num + 1) / self.total_pages * 100))
|
||||
|
||||
def on_render_finished(self, total_pages: int):
|
||||
"""渲染完成"""
|
||||
self.total_pages = total_pages
|
||||
self.progress_bar.setVisible(False)
|
||||
self.file_info.setText(f"📁 {Path(self.pdf_path).name} | 共 {total_pages} 页")
|
||||
logging.info(f"PDF加载完成: {total_pages} 页")
|
||||
|
||||
def on_render_error(self, error: str):
|
||||
"""渲染错误"""
|
||||
self.progress_bar.setVisible(False)
|
||||
QMessageBox.critical(self, "错误", f"加载PDF失败:\n{error}")
|
||||
logging.error(f"加载PDF失败: {error}")
|
||||
|
||||
def on_page_selection_changed(self, page_num: int, selected: bool):
|
||||
"""页面选择变化"""
|
||||
if selected:
|
||||
self.selected_pages.add(page_num)
|
||||
else:
|
||||
self.selected_pages.discard(page_num)
|
||||
|
||||
self.selection_label.setText(f"已选择: {len(self.selected_pages)} 页")
|
||||
|
||||
def select_all(self):
|
||||
"""全选"""
|
||||
for thumb in self.page_thumbnails:
|
||||
thumb.set_selected(True)
|
||||
|
||||
def clear_selection(self):
|
||||
"""清空选择"""
|
||||
for thumb in self.page_thumbnails:
|
||||
thumb.set_selected(False)
|
||||
|
||||
def clear_pages(self):
|
||||
"""清空页面"""
|
||||
for thumb in self.page_thumbnails:
|
||||
thumb.deleteLater()
|
||||
self.page_thumbnails.clear()
|
||||
self.selected_pages.clear()
|
||||
self.selection_label.setText("已选择: 0 页")
|
||||
|
||||
def do_split(self):
|
||||
"""执行拆分"""
|
||||
if not self.selected_pages:
|
||||
QMessageBox.warning(self, "提示", "请先选择要提取的页面")
|
||||
return
|
||||
|
||||
# 选择保存路径
|
||||
save_path, _ = QFileDialog.getSaveFileName(
|
||||
self, "保存拆分后的PDF",
|
||||
f"{Path(self.pdf_path).stem}_split.pdf",
|
||||
"PDF文件 (*.pdf)"
|
||||
)
|
||||
|
||||
if not save_path:
|
||||
return
|
||||
|
||||
try:
|
||||
doc = fitz.open(self.pdf_path)
|
||||
new_doc = fitz.open()
|
||||
|
||||
# 按页码顺序添加
|
||||
for page_num in sorted(self.selected_pages):
|
||||
new_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
|
||||
|
||||
new_doc.save(save_path)
|
||||
new_doc.close()
|
||||
doc.close()
|
||||
|
||||
QMessageBox.information(
|
||||
self, "成功",
|
||||
f"已成功提取 {len(self.selected_pages)} 页!\n\n保存到: {save_path}"
|
||||
)
|
||||
logging.info(f"PDF拆分完成: {len(self.selected_pages)} 页 -> {save_path}")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"拆分失败:\n{e}")
|
||||
logging.error(f"PDF拆分失败: {e}")
|
||||
|
||||
246
tools/pdf/to_word.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
PDF转Word工具
|
||||
- 单文件上传
|
||||
- 进度条显示转换进度
|
||||
- 保持原始排版
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QFileDialog, QMessageBox, QProgressBar
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtGui import QFont, QPixmap
|
||||
|
||||
from ui.workspace import BaseWorkspace, UploadArea
|
||||
|
||||
try:
|
||||
from pdf2docx import Converter
|
||||
HAS_PDF2DOCX = True
|
||||
except ImportError:
|
||||
HAS_PDF2DOCX = False
|
||||
logging.warning("pdf2docx未安装, PDF转Word功能不可用")
|
||||
|
||||
|
||||
class ConvertWorker(QThread):
|
||||
"""转换工作线程"""
|
||||
progress = Signal(int) # 百分比
|
||||
finished = Signal(str) # 输出路径
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, pdf_path: str, output_path: str):
|
||||
super().__init__()
|
||||
self.pdf_path = pdf_path
|
||||
self.output_path = output_path
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
cv = Converter(self.pdf_path)
|
||||
|
||||
# pdf2docx 没有直接的进度回调,我们模拟进度
|
||||
self.progress.emit(10)
|
||||
|
||||
cv.convert(self.output_path)
|
||||
self.progress.emit(90)
|
||||
|
||||
cv.close()
|
||||
self.progress.emit(100)
|
||||
|
||||
self.finished.emit(self.output_path)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"PDF转Word失败: {e}")
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
class PDFToWordPage(BaseWorkspace):
|
||||
"""PDF转Word页面"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.pdf_path = None
|
||||
self.setup_convert_ui()
|
||||
|
||||
def setup_convert_ui(self):
|
||||
"""设置转换UI"""
|
||||
self.history_btn.hide()
|
||||
|
||||
# 上传区域
|
||||
self.upload_area = UploadArea("PDF文件 (*.pdf)")
|
||||
self.upload_area.files_dropped.connect(self.on_file_added)
|
||||
self.content_layout.addWidget(self.upload_area)
|
||||
|
||||
# 转换区域
|
||||
convert_frame = QFrame()
|
||||
convert_frame.setObjectName("card")
|
||||
convert_layout = QVBoxLayout(convert_frame)
|
||||
convert_layout.setContentsMargins(32, 32, 32, 32)
|
||||
convert_layout.setSpacing(24)
|
||||
convert_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 图标
|
||||
icon_layout = QHBoxLayout()
|
||||
icon_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
icon_layout.setSpacing(24)
|
||||
|
||||
pdf_icon = QLabel("📄")
|
||||
pdf_icon.setFont(QFont("Segoe UI Emoji", 48))
|
||||
pdf_icon.setStyleSheet("background: rgba(239, 68, 68, 0.1); border-radius: 16px; padding: 16px;")
|
||||
icon_layout.addWidget(pdf_icon)
|
||||
|
||||
arrow = QLabel("➡️")
|
||||
arrow.setFont(QFont("Segoe UI Emoji", 32))
|
||||
icon_layout.addWidget(arrow)
|
||||
|
||||
word_icon = QLabel("📝")
|
||||
word_icon.setFont(QFont("Segoe UI Emoji", 48))
|
||||
word_icon.setStyleSheet("background: rgba(59, 130, 246, 0.1); border-radius: 16px; padding: 16px;")
|
||||
icon_layout.addWidget(word_icon)
|
||||
|
||||
convert_layout.addLayout(icon_layout)
|
||||
|
||||
# 文件信息
|
||||
self.file_info = QLabel("选择PDF文件开始转换")
|
||||
self.file_info.setStyleSheet("color: #94a3b8; font-size: 14px;")
|
||||
self.file_info.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
convert_layout.addWidget(self.file_info)
|
||||
|
||||
# 特性说明
|
||||
features_layout = QHBoxLayout()
|
||||
features_layout.setSpacing(32)
|
||||
features_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
features = [
|
||||
("✅", "保持排版"),
|
||||
("✅", "保留图片"),
|
||||
("✅", "提取表格")
|
||||
]
|
||||
|
||||
for icon, text in features:
|
||||
feature = QLabel(f"{icon} {text}")
|
||||
feature.setStyleSheet("color: #22c55e; font-size: 13px;")
|
||||
features_layout.addWidget(feature)
|
||||
|
||||
convert_layout.addLayout(features_layout)
|
||||
|
||||
# 进度条
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
self.progress_bar.setMinimumWidth(400)
|
||||
convert_layout.addWidget(self.progress_bar, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 状态标签
|
||||
self.status_label = QLabel("")
|
||||
self.status_label.setStyleSheet("color: #fbbf24; font-size: 13px;")
|
||||
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.status_label.setVisible(False)
|
||||
convert_layout.addWidget(self.status_label)
|
||||
|
||||
# 转换按钮
|
||||
self.convert_btn = QPushButton("📝 开始转换")
|
||||
self.convert_btn.setObjectName("primary_btn")
|
||||
self.convert_btn.setMinimumSize(200, 50)
|
||||
self.convert_btn.setFont(QFont("Microsoft YaHei", 13, QFont.Weight.Bold))
|
||||
self.convert_btn.clicked.connect(self.do_convert)
|
||||
self.convert_btn.setEnabled(False)
|
||||
convert_layout.addWidget(self.convert_btn, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 提示
|
||||
hint = QLabel("提示: 转换复杂PDF可能需要较长时间,请耐心等待")
|
||||
hint.setStyleSheet("color: #64748b; font-size: 11px;")
|
||||
hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
convert_layout.addWidget(hint)
|
||||
|
||||
self.content_layout.addWidget(convert_frame)
|
||||
self.content_layout.addStretch()
|
||||
|
||||
def on_file_added(self, files: list):
|
||||
"""文件添加"""
|
||||
if not files:
|
||||
return
|
||||
|
||||
pdf_file = None
|
||||
for f in files:
|
||||
if f.lower().endswith('.pdf'):
|
||||
pdf_file = f
|
||||
break
|
||||
|
||||
if not pdf_file:
|
||||
QMessageBox.warning(self, "提示", "请选择PDF文件")
|
||||
return
|
||||
|
||||
self.pdf_path = pdf_file
|
||||
self.file_info.setText(f"📁 {Path(pdf_file).name}")
|
||||
self.file_info.setStyleSheet("color: white; font-size: 14px; font-weight: 500;")
|
||||
self.convert_btn.setEnabled(True)
|
||||
|
||||
logging.info(f"已选择PDF文件: {pdf_file}")
|
||||
|
||||
def do_convert(self):
|
||||
"""执行转换"""
|
||||
if not HAS_PDF2DOCX:
|
||||
QMessageBox.critical(self, "错误", "pdf2docx未安装,无法转换PDF")
|
||||
return
|
||||
|
||||
if not self.pdf_path:
|
||||
QMessageBox.warning(self, "提示", "请先选择PDF文件")
|
||||
return
|
||||
|
||||
# 选择保存路径
|
||||
default_name = Path(self.pdf_path).stem + ".docx"
|
||||
save_path, _ = QFileDialog.getSaveFileName(
|
||||
self, "保存Word文档", default_name, "Word文档 (*.docx)"
|
||||
)
|
||||
|
||||
if not save_path:
|
||||
return
|
||||
|
||||
# 开始转换
|
||||
self.convert_btn.setEnabled(False)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setValue(0)
|
||||
self.status_label.setVisible(True)
|
||||
self.status_label.setText("正在转换中,请稍候...")
|
||||
|
||||
self.worker = ConvertWorker(self.pdf_path, save_path)
|
||||
self.worker.progress.connect(self.on_progress)
|
||||
self.worker.finished.connect(self.on_convert_finished)
|
||||
self.worker.error.connect(self.on_convert_error)
|
||||
self.worker.start()
|
||||
|
||||
logging.info(f"开始转换PDF: {self.pdf_path}")
|
||||
|
||||
def on_progress(self, value: int):
|
||||
"""进度更新"""
|
||||
self.progress_bar.setValue(value)
|
||||
|
||||
if value < 30:
|
||||
self.status_label.setText("正在解析PDF结构...")
|
||||
elif value < 70:
|
||||
self.status_label.setText("正在转换内容...")
|
||||
else:
|
||||
self.status_label.setText("正在生成Word文档...")
|
||||
|
||||
def on_convert_finished(self, output_path: str):
|
||||
"""转换完成"""
|
||||
self.convert_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
self.status_label.setVisible(False)
|
||||
|
||||
QMessageBox.information(
|
||||
self, "成功",
|
||||
f"PDF转Word完成!\n\n保存到: {output_path}"
|
||||
)
|
||||
logging.info(f"PDF转Word完成: {output_path}")
|
||||
|
||||
def on_convert_error(self, error: str):
|
||||
"""转换错误"""
|
||||
self.convert_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
self.status_label.setVisible(False)
|
||||
|
||||
QMessageBox.critical(self, "错误", f"转换失败:\n{error}")
|
||||
logging.error(f"PDF转Word失败: {error}")
|
||||
|
||||
31
ui/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
UI 模块
|
||||
- 主窗口
|
||||
- 侧边栏
|
||||
- 工作区
|
||||
- 动画效果
|
||||
"""
|
||||
from .main_window import MainWindow
|
||||
from .sidebar import PrimarySidebar
|
||||
from .tool_list import SecondarySidebar
|
||||
from .workspace import BaseWorkspace, WelcomeWorkspace, UploadArea
|
||||
from .settings import SettingsPage
|
||||
from .log_viewer import LogViewer
|
||||
from .image_preview import ImagePreviewWidget, DualPreviewWidget
|
||||
from .animations import AnimationMixin, animate_widget_show, animate_widget_hide
|
||||
|
||||
__all__ = [
|
||||
'MainWindow',
|
||||
'PrimarySidebar',
|
||||
'SecondarySidebar',
|
||||
'BaseWorkspace',
|
||||
'WelcomeWorkspace',
|
||||
'UploadArea',
|
||||
'SettingsPage',
|
||||
'LogViewer',
|
||||
'ImagePreviewWidget',
|
||||
'DualPreviewWidget',
|
||||
'AnimationMixin',
|
||||
'animate_widget_show',
|
||||
'animate_widget_hide'
|
||||
]
|
||||
183
ui/animations.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
UI动画效果模块
|
||||
- 渐入渐出
|
||||
- 滑动效果
|
||||
- 缩放效果
|
||||
"""
|
||||
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
|
||||
from PySide6.QtCore import (
|
||||
QPropertyAnimation, QEasingCurve, QParallelAnimationGroup,
|
||||
QSequentialAnimationGroup, Property, QPoint, QSize
|
||||
)
|
||||
from PySide6.QtGui import QColor
|
||||
|
||||
from core.config import config
|
||||
|
||||
|
||||
class AnimationMixin:
|
||||
"""动画混入类,为QWidget添加动画功能"""
|
||||
|
||||
def setup_animation(self):
|
||||
"""初始化动画效果"""
|
||||
if not hasattr(self, '_opacity_effect'):
|
||||
self._opacity_effect = QGraphicsOpacityEffect(self)
|
||||
self._opacity_effect.setOpacity(1.0)
|
||||
self.setGraphicsEffect(self._opacity_effect)
|
||||
|
||||
def fade_in(self, duration: int = None, callback=None):
|
||||
"""渐入动画"""
|
||||
if not config.get("animation_enabled", True):
|
||||
if callback:
|
||||
callback()
|
||||
return
|
||||
|
||||
self.setup_animation()
|
||||
duration = duration or config.get("animation_duration", 300)
|
||||
|
||||
self._opacity_effect.setOpacity(0)
|
||||
self.show()
|
||||
|
||||
self._fade_anim = QPropertyAnimation(self._opacity_effect, b"opacity")
|
||||
self._fade_anim.setDuration(duration)
|
||||
self._fade_anim.setStartValue(0.0)
|
||||
self._fade_anim.setEndValue(1.0)
|
||||
self._fade_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
if callback:
|
||||
self._fade_anim.finished.connect(callback)
|
||||
|
||||
self._fade_anim.start()
|
||||
|
||||
def fade_out(self, duration: int = None, callback=None):
|
||||
"""渐出动画"""
|
||||
if not config.get("animation_enabled", True):
|
||||
self.hide()
|
||||
if callback:
|
||||
callback()
|
||||
return
|
||||
|
||||
self.setup_animation()
|
||||
duration = duration or config.get("animation_duration", 300)
|
||||
|
||||
self._fade_anim = QPropertyAnimation(self._opacity_effect, b"opacity")
|
||||
self._fade_anim.setDuration(duration)
|
||||
self._fade_anim.setStartValue(1.0)
|
||||
self._fade_anim.setEndValue(0.0)
|
||||
self._fade_anim.setEasingCurve(QEasingCurve.Type.InCubic)
|
||||
|
||||
def on_finished():
|
||||
self.hide()
|
||||
self._opacity_effect.setOpacity(1.0)
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
self._fade_anim.finished.connect(on_finished)
|
||||
self._fade_anim.start()
|
||||
|
||||
|
||||
def create_fade_animation(widget: QWidget, fade_in: bool = True, duration: int = None) -> QPropertyAnimation:
|
||||
"""创建渐变动画"""
|
||||
if not config.get("animation_enabled", True):
|
||||
return None
|
||||
|
||||
duration = duration or config.get("animation_duration", 300)
|
||||
|
||||
# 确保有透明度效果
|
||||
effect = widget.graphicsEffect()
|
||||
if not isinstance(effect, QGraphicsOpacityEffect):
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
widget.setGraphicsEffect(effect)
|
||||
|
||||
anim = QPropertyAnimation(effect, b"opacity")
|
||||
anim.setDuration(duration)
|
||||
|
||||
if fade_in:
|
||||
anim.setStartValue(0.0)
|
||||
anim.setEndValue(1.0)
|
||||
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
else:
|
||||
anim.setStartValue(1.0)
|
||||
anim.setEndValue(0.0)
|
||||
anim.setEasingCurve(QEasingCurve.Type.InCubic)
|
||||
|
||||
return anim
|
||||
|
||||
|
||||
def create_slide_animation(widget: QWidget, start_pos: QPoint, end_pos: QPoint,
|
||||
duration: int = None) -> QPropertyAnimation:
|
||||
"""创建滑动动画"""
|
||||
if not config.get("animation_enabled", True):
|
||||
widget.move(end_pos)
|
||||
return None
|
||||
|
||||
duration = duration or config.get("animation_duration", 300)
|
||||
|
||||
anim = QPropertyAnimation(widget, b"pos")
|
||||
anim.setDuration(duration)
|
||||
anim.setStartValue(start_pos)
|
||||
anim.setEndValue(end_pos)
|
||||
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
return anim
|
||||
|
||||
|
||||
def animate_widget_show(widget: QWidget, direction: str = "fade"):
|
||||
"""显示控件动画"""
|
||||
if not config.get("animation_enabled", True):
|
||||
widget.show()
|
||||
return
|
||||
|
||||
duration = config.get("animation_duration", 300)
|
||||
|
||||
if direction == "fade":
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
effect.setOpacity(0)
|
||||
widget.setGraphicsEffect(effect)
|
||||
widget.show()
|
||||
|
||||
anim = QPropertyAnimation(effect, b"opacity")
|
||||
anim.setDuration(duration)
|
||||
anim.setStartValue(0.0)
|
||||
anim.setEndValue(1.0)
|
||||
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
anim.start()
|
||||
|
||||
# 保持引用
|
||||
widget._show_anim = anim
|
||||
else:
|
||||
widget.show()
|
||||
|
||||
|
||||
def animate_widget_hide(widget: QWidget, callback=None):
|
||||
"""隐藏控件动画"""
|
||||
if not config.get("animation_enabled", True):
|
||||
widget.hide()
|
||||
if callback:
|
||||
callback()
|
||||
return
|
||||
|
||||
duration = config.get("animation_duration", 300)
|
||||
|
||||
effect = widget.graphicsEffect()
|
||||
if not isinstance(effect, QGraphicsOpacityEffect):
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
widget.setGraphicsEffect(effect)
|
||||
|
||||
anim = QPropertyAnimation(effect, b"opacity")
|
||||
anim.setDuration(duration)
|
||||
anim.setStartValue(1.0)
|
||||
anim.setEndValue(0.0)
|
||||
anim.setEasingCurve(QEasingCurve.Type.InCubic)
|
||||
|
||||
def on_finished():
|
||||
widget.hide()
|
||||
effect.setOpacity(1.0)
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
anim.finished.connect(on_finished)
|
||||
anim.start()
|
||||
|
||||
# 保持引用
|
||||
widget._hide_anim = anim
|
||||
|
||||
379
ui/image_preview.py
Normal file
@@ -0,0 +1,379 @@
|
||||
"""
|
||||
图片预览组件
|
||||
- 原图/处理后对比预览
|
||||
- 缩放/平移
|
||||
- 信息显示
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QScrollArea, QSplitter, QFileDialog, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, QSize
|
||||
from PySide6.QtGui import QPixmap, QImage, QFont
|
||||
|
||||
from core.config import config
|
||||
|
||||
|
||||
class ImagePreviewWidget(QFrame):
|
||||
"""单个图片预览组件"""
|
||||
|
||||
def __init__(self, title: str = "预览", parent=None):
|
||||
super().__init__(parent)
|
||||
self.title = title
|
||||
self._pixmap = None
|
||||
self._image_info = {}
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setStyleSheet("""
|
||||
ImagePreviewWidget {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(12, 12, 12, 12)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# 标题栏
|
||||
header = QHBoxLayout()
|
||||
|
||||
self.title_label = QLabel(self.title)
|
||||
self.title_label.setStyleSheet("""
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
""")
|
||||
header.addWidget(self.title_label)
|
||||
header.addStretch()
|
||||
|
||||
# 尺寸信息
|
||||
self.size_label = QLabel("")
|
||||
self.size_label.setStyleSheet("color: #64748b; font-size: 10px;")
|
||||
header.addWidget(self.size_label)
|
||||
|
||||
layout.addLayout(header)
|
||||
|
||||
# 图片显示区域
|
||||
self.image_container = QFrame()
|
||||
self.image_container.setStyleSheet("""
|
||||
background: #0f172a;
|
||||
border-radius: 8px;
|
||||
""")
|
||||
self.image_container.setMinimumHeight(250)
|
||||
|
||||
container_layout = QVBoxLayout(self.image_container)
|
||||
container_layout.setContentsMargins(8, 8, 8, 8)
|
||||
container_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 占位图标
|
||||
self.placeholder_icon = QLabel("🖼️")
|
||||
self.placeholder_icon.setFont(QFont("Segoe UI Emoji", 40))
|
||||
self.placeholder_icon.setStyleSheet("color: #334155;")
|
||||
self.placeholder_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
container_layout.addWidget(self.placeholder_icon)
|
||||
|
||||
# 占位文字
|
||||
self.placeholder_text = QLabel("暂无图片")
|
||||
self.placeholder_text.setStyleSheet("color: #64748b; font-size: 12px;")
|
||||
self.placeholder_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
container_layout.addWidget(self.placeholder_text)
|
||||
|
||||
# 图片标签
|
||||
self.image_label = QLabel()
|
||||
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.image_label.setVisible(False)
|
||||
container_layout.addWidget(self.image_label)
|
||||
|
||||
layout.addWidget(self.image_container, 1)
|
||||
|
||||
# 文件信息
|
||||
self.info_label = QLabel("")
|
||||
self.info_label.setStyleSheet("color: #94a3b8; font-size: 11px;")
|
||||
self.info_label.setWordWrap(True)
|
||||
layout.addWidget(self.info_label)
|
||||
|
||||
def set_image(self, pixmap: QPixmap = None, file_path: str = None,
|
||||
pil_image: Image.Image = None, image_bytes: bytes = None):
|
||||
"""设置预览图片"""
|
||||
if pixmap:
|
||||
self._pixmap = pixmap
|
||||
elif file_path and os.path.exists(file_path):
|
||||
self._pixmap = QPixmap(file_path)
|
||||
self._image_info = self._get_file_info(file_path)
|
||||
elif pil_image:
|
||||
self._pixmap = self._pil_to_pixmap(pil_image)
|
||||
elif image_bytes:
|
||||
self._pixmap = QPixmap()
|
||||
self._pixmap.loadFromData(image_bytes)
|
||||
else:
|
||||
self._pixmap = None
|
||||
|
||||
self._update_display()
|
||||
|
||||
def set_image_from_bytes(self, data: bytes, info: dict = None):
|
||||
"""从字节数据设置图片"""
|
||||
self._pixmap = QPixmap()
|
||||
self._pixmap.loadFromData(data)
|
||||
if info:
|
||||
self._image_info = info
|
||||
self._update_display()
|
||||
|
||||
def _pil_to_pixmap(self, pil_image: Image.Image) -> QPixmap:
|
||||
"""PIL Image转QPixmap"""
|
||||
if pil_image.mode == "RGB":
|
||||
data = pil_image.tobytes("raw", "RGB")
|
||||
qimage = QImage(data, pil_image.width, pil_image.height,
|
||||
pil_image.width * 3, QImage.Format.Format_RGB888)
|
||||
elif pil_image.mode == "RGBA":
|
||||
data = pil_image.tobytes("raw", "RGBA")
|
||||
qimage = QImage(data, pil_image.width, pil_image.height,
|
||||
pil_image.width * 4, QImage.Format.Format_RGBA8888)
|
||||
else:
|
||||
pil_image = pil_image.convert("RGB")
|
||||
data = pil_image.tobytes("raw", "RGB")
|
||||
qimage = QImage(data, pil_image.width, pil_image.height,
|
||||
pil_image.width * 3, QImage.Format.Format_RGB888)
|
||||
return QPixmap.fromImage(qimage)
|
||||
|
||||
def _get_file_info(self, file_path: str) -> dict:
|
||||
"""获取文件信息"""
|
||||
try:
|
||||
size = os.path.getsize(file_path)
|
||||
with Image.open(file_path) as img:
|
||||
return {
|
||||
"path": file_path,
|
||||
"name": Path(file_path).name,
|
||||
"width": img.width,
|
||||
"height": img.height,
|
||||
"size": size,
|
||||
"format": img.format
|
||||
}
|
||||
except:
|
||||
return {}
|
||||
|
||||
def _update_display(self):
|
||||
"""更新显示"""
|
||||
if self._pixmap and not self._pixmap.isNull():
|
||||
# 隐藏占位符
|
||||
self.placeholder_icon.setVisible(False)
|
||||
self.placeholder_text.setVisible(False)
|
||||
self.image_label.setVisible(True)
|
||||
|
||||
# 缩放图片适应容器
|
||||
container_size = self.image_container.size()
|
||||
max_width = container_size.width() - 20
|
||||
max_height = container_size.height() - 20
|
||||
|
||||
scaled = self._pixmap.scaled(
|
||||
QSize(max_width, max_height),
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
self.image_label.setPixmap(scaled)
|
||||
|
||||
# 更新尺寸信息
|
||||
self.size_label.setText(f"{self._pixmap.width()}×{self._pixmap.height()}")
|
||||
|
||||
# 更新文件信息
|
||||
if self._image_info:
|
||||
size_str = self._format_size(self._image_info.get("size", 0))
|
||||
self.info_label.setText(
|
||||
f"📁 {self._image_info.get('name', '')} | 💾 {size_str}"
|
||||
)
|
||||
else:
|
||||
# 显示占位符
|
||||
self.placeholder_icon.setVisible(True)
|
||||
self.placeholder_text.setVisible(True)
|
||||
self.image_label.setVisible(False)
|
||||
self.size_label.setText("")
|
||||
self.info_label.setText("")
|
||||
|
||||
def set_info(self, info: dict):
|
||||
"""设置图片信息"""
|
||||
self._image_info = info
|
||||
if info:
|
||||
size_str = self._format_size(info.get("size", 0))
|
||||
self.info_label.setText(
|
||||
f"📁 {info.get('name', '')} | 💾 {size_str}"
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
"""清空预览"""
|
||||
self._pixmap = None
|
||||
self._image_info = {}
|
||||
self._update_display()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""窗口大小变化时重新缩放图片"""
|
||||
super().resizeEvent(event)
|
||||
if self._pixmap and not self._pixmap.isNull():
|
||||
self._update_display()
|
||||
|
||||
@staticmethod
|
||||
def _format_size(size: int) -> str:
|
||||
"""格式化文件大小"""
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
|
||||
class DualPreviewWidget(QWidget):
|
||||
"""双栏预览组件 - 原图/处理后对比"""
|
||||
|
||||
save_requested = Signal(object) # 发出保存请求信号,携带处理后的数据
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._processed_data = None # 保存处理后的数据
|
||||
self._output_filename = ""
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 预览区 - 左右分栏
|
||||
preview_layout = QHBoxLayout()
|
||||
preview_layout.setSpacing(16)
|
||||
|
||||
# 原图预览
|
||||
self.original_preview = ImagePreviewWidget("📷 原图")
|
||||
preview_layout.addWidget(self.original_preview, 1)
|
||||
|
||||
# 处理后预览
|
||||
self.result_preview = ImagePreviewWidget("✨ 处理结果")
|
||||
preview_layout.addWidget(self.result_preview, 1)
|
||||
|
||||
layout.addLayout(preview_layout, 1)
|
||||
|
||||
# 对比信息
|
||||
self.compare_label = QLabel("")
|
||||
self.compare_label.setStyleSheet("""
|
||||
color: #22c55e;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-radius: 8px;
|
||||
""")
|
||||
self.compare_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.compare_label.setVisible(False)
|
||||
layout.addWidget(self.compare_label)
|
||||
|
||||
# 操作按钮
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.addStretch()
|
||||
|
||||
self.save_btn = QPushButton("💾 保存结果")
|
||||
self.save_btn.setObjectName("primary_btn")
|
||||
self.save_btn.setMinimumSize(150, 40)
|
||||
self.save_btn.clicked.connect(self._on_save_clicked)
|
||||
self.save_btn.setEnabled(False)
|
||||
btn_layout.addWidget(self.save_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def set_original(self, file_path: str):
|
||||
"""设置原图"""
|
||||
self.original_preview.set_image(file_path=file_path)
|
||||
|
||||
def set_result(self, data: bytes, info: dict = None, filename: str = "",
|
||||
show_size_compare: bool = True):
|
||||
"""设置处理结果"""
|
||||
self._processed_data = data
|
||||
self._output_filename = filename
|
||||
self.result_preview.set_image_from_bytes(data, info)
|
||||
self.save_btn.setEnabled(True)
|
||||
|
||||
# 显示对比信息(仅在压缩场景下显示大小对比)
|
||||
if info and show_size_compare:
|
||||
original_size = self.original_preview._image_info.get("size", 0)
|
||||
result_size = info.get("size", len(data))
|
||||
if original_size > 0:
|
||||
saved = original_size - result_size
|
||||
percent = (saved / original_size) * 100
|
||||
if saved > 0:
|
||||
self.compare_label.setText(
|
||||
f"✅ 处理完成! 节省 {self._format_size(saved)} ({percent:.1f}%)"
|
||||
)
|
||||
self.compare_label.setStyleSheet("""
|
||||
color: #22c55e;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-radius: 8px;
|
||||
""")
|
||||
self.compare_label.setVisible(True)
|
||||
else:
|
||||
# 文件变大或相同,不显示对比(可能是添加水印等操作)
|
||||
self.compare_label.setVisible(False)
|
||||
elif not show_size_compare:
|
||||
# 不需要显示大小对比(如水印、格式转换)
|
||||
self.compare_label.setText("✅ 处理完成!")
|
||||
self.compare_label.setStyleSheet("""
|
||||
color: #22c55e;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-radius: 8px;
|
||||
""")
|
||||
self.compare_label.setVisible(True)
|
||||
|
||||
def _on_save_clicked(self):
|
||||
"""保存按钮点击"""
|
||||
if not self._processed_data:
|
||||
return
|
||||
|
||||
# 检查是否有默认保存路径
|
||||
default_dir = config.get_output_directory()
|
||||
auto_save = config.get("auto_save_to_default", False)
|
||||
|
||||
if default_dir and auto_save:
|
||||
# 自动保存
|
||||
save_path = os.path.join(default_dir, self._output_filename)
|
||||
else:
|
||||
# 询问保存位置
|
||||
save_path, _ = QFileDialog.getSaveFileName(
|
||||
self, "保存文件",
|
||||
os.path.join(default_dir, self._output_filename) if default_dir else self._output_filename,
|
||||
"图片文件 (*.jpg *.png *.webp)"
|
||||
)
|
||||
|
||||
if save_path:
|
||||
try:
|
||||
with open(save_path, 'wb') as f:
|
||||
f.write(self._processed_data)
|
||||
QMessageBox.information(self, "成功", f"文件已保存到:\n{save_path}")
|
||||
self.save_requested.emit(save_path)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"保存失败:\n{e}")
|
||||
|
||||
def clear(self):
|
||||
"""清空预览"""
|
||||
self.original_preview.clear()
|
||||
self.result_preview.clear()
|
||||
self._processed_data = None
|
||||
self._output_filename = ""
|
||||
self.save_btn.setEnabled(False)
|
||||
self.compare_label.setVisible(False)
|
||||
|
||||
@staticmethod
|
||||
def _format_size(size: int) -> str:
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
242
ui/log_viewer.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
日志查看器界面
|
||||
- 日志文件列表
|
||||
- 日志内容显示
|
||||
- 搜索过滤
|
||||
- 导出功能
|
||||
"""
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QListWidget, QListWidgetItem, QTextEdit,
|
||||
QSplitter, QFrame, QFileDialog, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
from core.logger import get_all_log_files, read_log_file
|
||||
|
||||
|
||||
class LogViewer(QWidget):
|
||||
"""日志查看器 - 可嵌入设置页面"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.current_log_content = ""
|
||||
self.setup_ui()
|
||||
self.load_log_files()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# 主内容区 - 分割器
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
splitter.setStyleSheet("QSplitter::handle { background: #334155; }")
|
||||
|
||||
# 左侧 - 日志文件列表
|
||||
left_panel = QFrame()
|
||||
left_panel.setStyleSheet("background: rgba(30, 41, 59, 0.5); border-radius: 12px;")
|
||||
left_layout = QVBoxLayout(left_panel)
|
||||
left_layout.setContentsMargins(12, 12, 12, 12)
|
||||
left_layout.setSpacing(8)
|
||||
|
||||
list_title = QLabel("日志文件")
|
||||
list_title.setStyleSheet("color: #94a3b8; font-size: 12px; font-weight: 600;")
|
||||
left_layout.addWidget(list_title)
|
||||
|
||||
self.file_list = QListWidget()
|
||||
self.file_list.setMinimumWidth(180)
|
||||
self.file_list.currentItemChanged.connect(self.on_file_selected)
|
||||
left_layout.addWidget(self.file_list)
|
||||
|
||||
# 刷新按钮
|
||||
refresh_btn = QPushButton("🔄 刷新")
|
||||
refresh_btn.setObjectName("secondary_btn")
|
||||
refresh_btn.clicked.connect(self.load_log_files)
|
||||
left_layout.addWidget(refresh_btn)
|
||||
|
||||
splitter.addWidget(left_panel)
|
||||
|
||||
# 右侧 - 日志内容
|
||||
right_panel = QFrame()
|
||||
right_panel.setStyleSheet("background: rgba(30, 41, 59, 0.5); border-radius: 12px;")
|
||||
right_layout = QVBoxLayout(right_panel)
|
||||
right_layout.setContentsMargins(12, 12, 12, 12)
|
||||
right_layout.setSpacing(8)
|
||||
|
||||
# 搜索和操作栏
|
||||
toolbar = QHBoxLayout()
|
||||
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input.setPlaceholderText("🔍 搜索日志内容...")
|
||||
self.search_input.textChanged.connect(self.filter_log)
|
||||
toolbar.addWidget(self.search_input, 1)
|
||||
|
||||
# 级别过滤
|
||||
self.level_buttons = {}
|
||||
for level, color in [("ERROR", "#ef4444"), ("WARNING", "#f59e0b"), ("INFO", "#22c55e"), ("DEBUG", "#64748b")]:
|
||||
btn = QPushButton(level)
|
||||
btn.setCheckable(True)
|
||||
btn.setChecked(True)
|
||||
btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background: rgba({self.hex_to_rgb(color)}, 0.2);
|
||||
border: 1px solid {color};
|
||||
color: {color};
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
}}
|
||||
QPushButton:checked {{
|
||||
background: {color};
|
||||
color: white;
|
||||
}}
|
||||
""")
|
||||
btn.clicked.connect(self.filter_log)
|
||||
toolbar.addWidget(btn)
|
||||
self.level_buttons[level] = btn
|
||||
|
||||
right_layout.addLayout(toolbar)
|
||||
|
||||
# 日志内容显示
|
||||
self.log_text = QTextEdit()
|
||||
self.log_text.setReadOnly(True)
|
||||
self.log_text.setFont(QFont("Consolas", 11))
|
||||
self.log_text.setStyleSheet("""
|
||||
QTextEdit {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
""")
|
||||
right_layout.addWidget(self.log_text, 1)
|
||||
|
||||
# 底部操作
|
||||
bottom_bar = QHBoxLayout()
|
||||
|
||||
self.status_label = QLabel("选择左侧日志文件查看")
|
||||
self.status_label.setStyleSheet("color: #64748b; font-size: 12px;")
|
||||
bottom_bar.addWidget(self.status_label)
|
||||
|
||||
bottom_bar.addStretch()
|
||||
|
||||
export_btn = QPushButton("📤 导出日志")
|
||||
export_btn.setObjectName("primary_btn")
|
||||
export_btn.clicked.connect(self.export_log)
|
||||
bottom_bar.addWidget(export_btn)
|
||||
|
||||
right_layout.addLayout(bottom_bar)
|
||||
|
||||
splitter.addWidget(right_panel)
|
||||
splitter.setSizes([200, 600])
|
||||
|
||||
layout.addWidget(splitter, 1)
|
||||
|
||||
def load_log_files(self):
|
||||
"""加载日志文件列表"""
|
||||
self.file_list.clear()
|
||||
log_files = get_all_log_files()
|
||||
|
||||
for log_file in log_files:
|
||||
item = QListWidgetItem(log_file.name)
|
||||
item.setData(Qt.ItemDataRole.UserRole, log_file)
|
||||
self.file_list.addItem(item)
|
||||
|
||||
# 自动选择第一个(最新)
|
||||
if self.file_list.count() > 0:
|
||||
self.file_list.setCurrentRow(0)
|
||||
|
||||
logging.info(f"加载了 {len(log_files)} 个日志文件")
|
||||
|
||||
def on_file_selected(self, current: QListWidgetItem, previous: QListWidgetItem):
|
||||
"""日志文件选择变化"""
|
||||
if current:
|
||||
file_path = current.data(Qt.ItemDataRole.UserRole)
|
||||
self.current_log_content = read_log_file(file_path)
|
||||
self.filter_log()
|
||||
self.status_label.setText(f"文件: {file_path.name}")
|
||||
|
||||
def filter_log(self):
|
||||
"""过滤日志内容"""
|
||||
if not self.current_log_content:
|
||||
return
|
||||
|
||||
search_text = self.search_input.text().lower()
|
||||
active_levels = [level for level, btn in self.level_buttons.items() if btn.isChecked()]
|
||||
|
||||
lines = self.current_log_content.split('\n')
|
||||
filtered_lines = []
|
||||
|
||||
for line in lines:
|
||||
# 检查级别过滤
|
||||
level_match = any(f"[{level}]" in line for level in active_levels)
|
||||
if not level_match and line.strip():
|
||||
# 如果不是日志行(没有级别标记),检查是否是错误堆栈的一部分
|
||||
if filtered_lines and not line.startswith('['):
|
||||
level_match = True
|
||||
else:
|
||||
continue
|
||||
|
||||
# 检查搜索过滤
|
||||
if search_text and search_text not in line.lower():
|
||||
continue
|
||||
|
||||
filtered_lines.append(line)
|
||||
|
||||
# 语法高亮
|
||||
highlighted = self.highlight_log('\n'.join(filtered_lines))
|
||||
self.log_text.setHtml(highlighted)
|
||||
|
||||
def highlight_log(self, text: str) -> str:
|
||||
"""日志语法高亮"""
|
||||
import html
|
||||
text = html.escape(text)
|
||||
|
||||
# 替换颜色
|
||||
text = text.replace('[ERROR]', '<span style="color: #ef4444; font-weight: bold;">[ERROR]</span>')
|
||||
text = text.replace('[WARNING]', '<span style="color: #f59e0b; font-weight: bold;">[WARNING]</span>')
|
||||
text = text.replace('[INFO]', '<span style="color: #22c55e;">[INFO]</span>')
|
||||
text = text.replace('[DEBUG]', '<span style="color: #64748b;">[DEBUG]</span>')
|
||||
|
||||
# 时间戳颜色
|
||||
import re
|
||||
text = re.sub(
|
||||
r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]',
|
||||
r'<span style="color: #94a3b8;">[\1]</span>',
|
||||
text
|
||||
)
|
||||
|
||||
return f'<pre style="margin: 0; font-family: Consolas, monospace;">{text}</pre>'
|
||||
|
||||
def export_log(self):
|
||||
"""导出日志文件"""
|
||||
if not self.current_log_content:
|
||||
QMessageBox.warning(self, "提示", "请先选择一个日志文件")
|
||||
return
|
||||
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
self, "导出日志", "log_export.txt", "文本文件 (*.txt)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
try:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(self.log_text.toPlainText())
|
||||
QMessageBox.information(self, "成功", f"日志已导出到:\n{file_path}")
|
||||
logging.info(f"日志已导出到: {file_path}")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"导出失败: {e}")
|
||||
logging.error(f"导出日志失败: {e}")
|
||||
|
||||
@staticmethod
|
||||
def hex_to_rgb(hex_color: str) -> str:
|
||||
"""十六进制颜色转RGB"""
|
||||
hex_color = hex_color.lstrip('#')
|
||||
r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
return f"{r}, {g}, {b}"
|
||||
|
||||
189
ui/main_window.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
主窗口 - 三栏布局
|
||||
- 左侧: 一级菜单(80px)
|
||||
- 中间: 二级菜单(256px)
|
||||
- 右侧: 工作区(弹性宽度)
|
||||
"""
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow, QWidget, QHBoxLayout, QStackedWidget, QGraphicsOpacityEffect
|
||||
)
|
||||
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve
|
||||
from PySide6.QtGui import QFont
|
||||
import logging
|
||||
|
||||
from ui.sidebar import PrimarySidebar
|
||||
from ui.tool_list import SecondarySidebar, TOOLS_DATA
|
||||
from ui.workspace import WelcomeWorkspace
|
||||
from ui.settings import SettingsPage
|
||||
from ui.animations import animate_widget_show
|
||||
from ui.profile_card import ProfileCard
|
||||
from core.config import config
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""主窗口"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("奶酪云工具箱 | Cheese Cloud Tools")
|
||||
self.setMinimumSize(1200, 700)
|
||||
self.resize(1400, 800)
|
||||
|
||||
# 工具页面映射
|
||||
self.tool_pages = {}
|
||||
|
||||
self.setup_ui()
|
||||
self.setup_connections()
|
||||
|
||||
logging.info("主窗口初始化完成")
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI"""
|
||||
# 中央部件
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
# 主布局
|
||||
main_layout = QHBoxLayout(central_widget)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
|
||||
# 1. 左侧一级菜单
|
||||
self.primary_sidebar = PrimarySidebar()
|
||||
main_layout.addWidget(self.primary_sidebar)
|
||||
|
||||
# 2. 中间二级菜单
|
||||
self.secondary_sidebar = SecondarySidebar()
|
||||
main_layout.addWidget(self.secondary_sidebar)
|
||||
|
||||
# 3. 右侧工作区(使用堆叠窗口切换不同工具页面)
|
||||
self.workspace_stack = QStackedWidget()
|
||||
self.workspace_stack.setObjectName("workspace")
|
||||
main_layout.addWidget(self.workspace_stack, 1)
|
||||
|
||||
# 添加欢迎页
|
||||
self.welcome_page = WelcomeWorkspace()
|
||||
self.workspace_stack.addWidget(self.welcome_page)
|
||||
|
||||
# 添加设置页面
|
||||
self.settings_page = SettingsPage()
|
||||
self.workspace_stack.addWidget(self.settings_page)
|
||||
|
||||
# 延迟导入并添加工具页面
|
||||
self.init_tool_pages()
|
||||
|
||||
def init_tool_pages(self):
|
||||
"""初始化所有工具页面"""
|
||||
# 图片工具
|
||||
from tools.image.compress import ImageCompressPage
|
||||
from tools.image.convert import ImageConvertPage
|
||||
from tools.image.watermark import ImageWatermarkPage
|
||||
|
||||
self.tool_pages["img-compress"] = ImageCompressPage()
|
||||
self.tool_pages["img-convert"] = ImageConvertPage()
|
||||
self.tool_pages["img-watermark"] = ImageWatermarkPage()
|
||||
|
||||
# PDF工具
|
||||
from tools.pdf.split import PDFSplitPage
|
||||
from tools.pdf.merge import PDFMergePage
|
||||
from tools.pdf.to_word import PDFToWordPage
|
||||
|
||||
self.tool_pages["pdf-split"] = PDFSplitPage()
|
||||
self.tool_pages["pdf-merge"] = PDFMergePage()
|
||||
self.tool_pages["pdf-word"] = PDFToWordPage()
|
||||
|
||||
# Excel工具
|
||||
from tools.excel.preview import ExcelPreviewPage
|
||||
from tools.excel.chart import ExcelChartPage
|
||||
|
||||
self.tool_pages["xls-view"] = ExcelPreviewPage()
|
||||
self.tool_pages["xls-chart"] = ExcelChartPage()
|
||||
|
||||
# 添加到堆叠窗口
|
||||
for page in self.tool_pages.values():
|
||||
self.workspace_stack.addWidget(page)
|
||||
|
||||
def setup_connections(self):
|
||||
"""设置信号连接"""
|
||||
# 一级菜单切换
|
||||
self.primary_sidebar.category_changed.connect(self.on_category_changed)
|
||||
self.primary_sidebar.settings_clicked.connect(self.show_settings)
|
||||
self.primary_sidebar.logo_clicked.connect(self.show_profile_card)
|
||||
|
||||
# 二级菜单工具选择
|
||||
self.secondary_sidebar.tool_selected.connect(self.on_tool_selected)
|
||||
|
||||
# 欢迎页快捷入口
|
||||
self.welcome_page.tool_clicked.connect(self.on_shortcut_clicked)
|
||||
|
||||
def on_category_changed(self, category: str):
|
||||
"""分类切换"""
|
||||
logging.debug(f"切换分类: {category}")
|
||||
self.secondary_sidebar.load_tools(category)
|
||||
# 显示欢迎页
|
||||
self._switch_page(self.welcome_page)
|
||||
|
||||
def on_tool_selected(self, tool_data: dict):
|
||||
"""工具选择"""
|
||||
tool_id = tool_data.get("id")
|
||||
logging.info(f"选择工具: {tool_data.get('name')} ({tool_id})")
|
||||
|
||||
if tool_id in self.tool_pages:
|
||||
page = self.tool_pages[tool_id]
|
||||
# 设置面包屑
|
||||
category_title = TOOLS_DATA.get(self.secondary_sidebar.current_category, {}).get("title", "")
|
||||
page.set_breadcrumb(category_title, tool_data.get("name", ""))
|
||||
page.set_title(tool_data.get("name", ""))
|
||||
|
||||
self._switch_page(page)
|
||||
else:
|
||||
logging.warning(f"未找到工具页面: {tool_id}")
|
||||
|
||||
def on_shortcut_clicked(self, tool_id: str, category: str):
|
||||
"""快捷入口点击"""
|
||||
logging.debug(f"快捷入口: {tool_id}, {category}")
|
||||
self.primary_sidebar.set_category(category)
|
||||
self.secondary_sidebar.load_tools(category)
|
||||
self.secondary_sidebar.select_tool(tool_id)
|
||||
|
||||
def show_settings(self):
|
||||
"""显示设置页面"""
|
||||
logging.debug("打开设置页面")
|
||||
self._switch_page(self.settings_page)
|
||||
|
||||
def show_profile_card(self):
|
||||
"""显示个人信息卡片"""
|
||||
logging.debug("打开个人信息卡片")
|
||||
dialog = ProfileCard(self)
|
||||
dialog.exec()
|
||||
|
||||
def _switch_page(self, page: QWidget):
|
||||
"""切换页面(带动画)"""
|
||||
if not config.get("animation_enabled", True):
|
||||
self.workspace_stack.setCurrentWidget(page)
|
||||
return
|
||||
|
||||
# 获取当前页面
|
||||
current = self.workspace_stack.currentWidget()
|
||||
if current == page:
|
||||
return
|
||||
|
||||
# 设置新页面透明度效果
|
||||
effect = QGraphicsOpacityEffect(page)
|
||||
effect.setOpacity(0)
|
||||
page.setGraphicsEffect(effect)
|
||||
|
||||
# 切换页面
|
||||
self.workspace_stack.setCurrentWidget(page)
|
||||
|
||||
# 渐入动画
|
||||
duration = config.get("animation_duration", 300)
|
||||
anim = QPropertyAnimation(effect, b"opacity")
|
||||
anim.setDuration(duration)
|
||||
anim.setStartValue(0.0)
|
||||
anim.setEndValue(1.0)
|
||||
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
# 保持引用防止被垃圾回收
|
||||
page._fade_anim = anim
|
||||
anim.start()
|
||||
539
ui/profile_card.py
Normal file
@@ -0,0 +1,539 @@
|
||||
"""
|
||||
个人信息卡片模态框
|
||||
- 头像、微信二维码、邮箱
|
||||
- 高级UI设计,带装饰元素和动画
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QWidget, QGraphicsOpacityEffect, QApplication
|
||||
)
|
||||
from PySide6.QtCore import (
|
||||
Qt, QPropertyAnimation, QEasingCurve, QTimer,
|
||||
QPoint, QRect, QParallelAnimationGroup, Signal
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QPixmap, QPainter, QColor, QLinearGradient, QRadialGradient,
|
||||
QFont, QPen, QBrush, QPainterPath, QCursor
|
||||
)
|
||||
import math
|
||||
|
||||
|
||||
def get_resource_path(relative_path: str) -> str:
|
||||
"""获取资源文件的绝对路径"""
|
||||
base_path = Path(__file__).parent.parent
|
||||
return str(base_path / relative_path)
|
||||
|
||||
|
||||
# 资源路径
|
||||
AVATAR_PATH = get_resource_path("image/头像.jpg")
|
||||
QRCODE_PATH = get_resource_path("image/二维码.jpg")
|
||||
|
||||
|
||||
class FloatingOrb(QWidget):
|
||||
"""浮动装饰球"""
|
||||
|
||||
def __init__(self, size: int, color: QColor, parent=None):
|
||||
super().__init__(parent)
|
||||
self.orb_size = size
|
||||
self.orb_color = color
|
||||
self.setFixedSize(size, size)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||
|
||||
# 设置透明效果
|
||||
self.opacity_effect = QGraphicsOpacityEffect(self)
|
||||
self.opacity_effect.setOpacity(0.6)
|
||||
self.setGraphicsEffect(self.opacity_effect)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
# 径向渐变
|
||||
gradient = QRadialGradient(
|
||||
self.orb_size / 2, self.orb_size / 2, self.orb_size / 2
|
||||
)
|
||||
gradient.setColorAt(0, QColor(self.orb_color.red(), self.orb_color.green(), self.orb_color.blue(), 120))
|
||||
gradient.setColorAt(0.5, QColor(self.orb_color.red(), self.orb_color.green(), self.orb_color.blue(), 60))
|
||||
gradient.setColorAt(1, QColor(self.orb_color.red(), self.orb_color.green(), self.orb_color.blue(), 0))
|
||||
|
||||
painter.setBrush(gradient)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawEllipse(0, 0, self.orb_size, self.orb_size)
|
||||
|
||||
|
||||
class GlowButton(QPushButton):
|
||||
"""发光按钮"""
|
||||
|
||||
def __init__(self, text: str, primary: bool = True, parent=None):
|
||||
super().__init__(text, parent)
|
||||
self.primary = primary
|
||||
self._hovered = False
|
||||
self.setFixedHeight(44)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.setFont(QFont("Microsoft YaHei UI", 10, QFont.Weight.Medium))
|
||||
|
||||
self.setStyleSheet("""
|
||||
QPushButton {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 10px 24px;
|
||||
color: white;
|
||||
}
|
||||
""")
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self.update()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
rect = self.rect()
|
||||
|
||||
if self.primary:
|
||||
# 主按钮 - 奶酪色渐变
|
||||
gradient = QLinearGradient(0, 0, rect.width(), 0)
|
||||
if self._hovered:
|
||||
gradient.setColorAt(0, QColor("#fcd34d"))
|
||||
gradient.setColorAt(1, QColor("#f59e0b"))
|
||||
else:
|
||||
gradient.setColorAt(0, QColor("#fbbf24"))
|
||||
gradient.setColorAt(1, QColor("#d97706"))
|
||||
|
||||
painter.setBrush(gradient)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawRoundedRect(rect, 10, 10)
|
||||
|
||||
# 发光效果
|
||||
if self._hovered:
|
||||
glow = QRadialGradient(rect.center().x(), rect.center().y(), rect.width() / 2)
|
||||
glow.setColorAt(0, QColor(251, 191, 36, 60))
|
||||
glow.setColorAt(1, QColor(251, 191, 36, 0))
|
||||
painter.setBrush(glow)
|
||||
painter.drawRoundedRect(rect.adjusted(-5, -5, 5, 5), 15, 15)
|
||||
else:
|
||||
# 次按钮 - 透明边框
|
||||
if self._hovered:
|
||||
painter.setBrush(QColor(71, 85, 105, 80))
|
||||
else:
|
||||
painter.setBrush(QColor(71, 85, 105, 40))
|
||||
|
||||
painter.setPen(QPen(QColor("#475569"), 1))
|
||||
painter.drawRoundedRect(rect.adjusted(1, 1, -1, -1), 10, 10)
|
||||
|
||||
# 绘制文字
|
||||
painter.setPen(QColor("white") if self.primary else QColor("#94a3b8"))
|
||||
painter.setFont(self.font())
|
||||
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, self.text())
|
||||
|
||||
|
||||
class ProfileCard(QDialog):
|
||||
"""个人信息卡片模态框"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowFlags(
|
||||
Qt.WindowType.FramelessWindowHint |
|
||||
Qt.WindowType.Dialog
|
||||
)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.setModal(True)
|
||||
self.setFixedSize(480, 780)
|
||||
|
||||
self._drag_pos = None
|
||||
self.setup_ui()
|
||||
self.setup_animations()
|
||||
self.add_decorations()
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI"""
|
||||
# 主容器
|
||||
self.container = QWidget(self)
|
||||
self.container.setGeometry(0, 0, 480, 780)
|
||||
|
||||
main_layout = QVBoxLayout(self.container)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
|
||||
# 内容卡片
|
||||
self.card = QWidget()
|
||||
self.card.setObjectName("profile_card")
|
||||
card_layout = QVBoxLayout(self.card)
|
||||
card_layout.setContentsMargins(32, 24, 32, 24)
|
||||
card_layout.setSpacing(12)
|
||||
|
||||
# 关闭按钮
|
||||
close_btn = QPushButton("×")
|
||||
close_btn.setFixedSize(36, 36)
|
||||
close_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
close_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: rgba(71, 85, 105, 0.4);
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
color: #94a3b8;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(239, 68, 68, 0.6);
|
||||
color: white;
|
||||
}
|
||||
""")
|
||||
close_btn.clicked.connect(self.close_with_animation)
|
||||
|
||||
close_layout = QHBoxLayout()
|
||||
close_layout.addStretch()
|
||||
close_layout.addWidget(close_btn)
|
||||
card_layout.addLayout(close_layout)
|
||||
|
||||
# 头像区域
|
||||
avatar_container = QWidget()
|
||||
avatar_container.setFixedHeight(100)
|
||||
avatar_layout = QHBoxLayout(avatar_container)
|
||||
avatar_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.avatar_label = QLabel()
|
||||
self.avatar_label.setFixedSize(100, 100)
|
||||
self.avatar_label.setStyleSheet("""
|
||||
QLabel {
|
||||
border-radius: 50px;
|
||||
border: 3px solid #fbbf24;
|
||||
}
|
||||
""")
|
||||
self._load_avatar()
|
||||
avatar_layout.addWidget(self.avatar_label)
|
||||
card_layout.addWidget(avatar_container)
|
||||
|
||||
# 名称
|
||||
name_label = QLabel("奶酪云工具箱")
|
||||
name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
name_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #f8fafc;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
font-family: "Microsoft YaHei UI";
|
||||
}
|
||||
""")
|
||||
card_layout.addWidget(name_label)
|
||||
|
||||
# 副标题
|
||||
subtitle_label = QLabel("高效办公 · 精致生活")
|
||||
subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
subtitle_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-family: "Microsoft YaHei UI";
|
||||
}
|
||||
""")
|
||||
card_layout.addWidget(subtitle_label)
|
||||
|
||||
card_layout.addSpacing(8)
|
||||
|
||||
# 分割线
|
||||
divider = QWidget()
|
||||
divider.setFixedHeight(1)
|
||||
divider.setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 transparent, stop:0.5 #334155, stop:1 transparent);")
|
||||
card_layout.addWidget(divider)
|
||||
|
||||
card_layout.addSpacing(8)
|
||||
|
||||
# 二维码区域
|
||||
qr_container = QWidget()
|
||||
qr_layout = QVBoxLayout(qr_container)
|
||||
qr_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
qr_layout.setSpacing(8)
|
||||
|
||||
qr_title = QLabel("📱 微信扫码添加好友")
|
||||
qr_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
qr_title.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-family: "Microsoft YaHei UI";
|
||||
}
|
||||
""")
|
||||
qr_layout.addWidget(qr_title)
|
||||
|
||||
self.qr_label = QLabel()
|
||||
self.qr_label.setFixedSize(220, 220)
|
||||
self.qr_label.setStyleSheet("""
|
||||
QLabel {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
}
|
||||
""")
|
||||
self._load_qrcode()
|
||||
qr_layout.addWidget(self.qr_label, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
card_layout.addWidget(qr_container)
|
||||
|
||||
card_layout.addSpacing(10)
|
||||
|
||||
# 邮箱区域
|
||||
email_container = QWidget()
|
||||
email_container.setStyleSheet("""
|
||||
QWidget {
|
||||
background: rgba(30, 41, 59, 0.6);
|
||||
border-radius: 12px;
|
||||
}
|
||||
""")
|
||||
email_layout = QVBoxLayout(email_container)
|
||||
email_layout.setContentsMargins(20, 16, 20, 16)
|
||||
email_layout.setSpacing(8)
|
||||
|
||||
email_title = QLabel("📧 合作联系邮箱")
|
||||
email_title.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
}
|
||||
""")
|
||||
email_layout.addWidget(email_title)
|
||||
|
||||
email_row = QHBoxLayout()
|
||||
|
||||
self.email_label = QLabel("workerqi@163.com")
|
||||
self.email_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #fbbf24;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
""")
|
||||
email_row.addWidget(self.email_label)
|
||||
|
||||
email_row.addStretch()
|
||||
|
||||
copy_btn = GlowButton("复制", False)
|
||||
copy_btn.setFixedSize(70, 34)
|
||||
copy_btn.clicked.connect(self.copy_email)
|
||||
email_row.addWidget(copy_btn)
|
||||
|
||||
send_btn = GlowButton("发送邮件", True)
|
||||
send_btn.setFixedSize(90, 34)
|
||||
send_btn.clicked.connect(self.send_email)
|
||||
email_row.addWidget(send_btn)
|
||||
|
||||
email_layout.addLayout(email_row)
|
||||
card_layout.addWidget(email_container)
|
||||
|
||||
card_layout.addStretch()
|
||||
|
||||
# 底部提示
|
||||
tip_label = QLabel("点击空白处关闭")
|
||||
tip_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
tip_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #475569;
|
||||
font-size: 11px;
|
||||
}
|
||||
""")
|
||||
card_layout.addWidget(tip_label)
|
||||
|
||||
main_layout.addWidget(self.card)
|
||||
|
||||
def _load_avatar(self):
|
||||
"""加载头像"""
|
||||
if os.path.exists(AVATAR_PATH):
|
||||
pixmap = QPixmap(AVATAR_PATH)
|
||||
if not pixmap.isNull():
|
||||
# 创建圆形头像
|
||||
scaled = pixmap.scaled(94, 94, Qt.AspectRatioMode.KeepAspectRatioByExpanding,
|
||||
Qt.TransformationMode.SmoothTransformation)
|
||||
|
||||
# 裁剪为圆形
|
||||
rounded = QPixmap(94, 94)
|
||||
rounded.fill(Qt.GlobalColor.transparent)
|
||||
|
||||
painter = QPainter(rounded)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
path = QPainterPath()
|
||||
path.addEllipse(0, 0, 94, 94)
|
||||
painter.setClipPath(path)
|
||||
|
||||
# 居中裁剪
|
||||
x = (scaled.width() - 94) // 2
|
||||
y = (scaled.height() - 94) // 2
|
||||
painter.drawPixmap(0, 0, scaled, x, y, 94, 94)
|
||||
painter.end()
|
||||
|
||||
self.avatar_label.setPixmap(rounded)
|
||||
return
|
||||
|
||||
# 默认头像
|
||||
self.avatar_label.setText("🧀")
|
||||
self.avatar_label.setStyleSheet(self.avatar_label.styleSheet() + """
|
||||
font-size: 48px;
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #fbbf24, stop:1 #d97706);
|
||||
""")
|
||||
|
||||
def _load_qrcode(self):
|
||||
"""加载二维码"""
|
||||
if os.path.exists(QRCODE_PATH):
|
||||
pixmap = QPixmap(QRCODE_PATH)
|
||||
if not pixmap.isNull():
|
||||
# 缩放到完整显示,保持宽高比
|
||||
scaled = pixmap.scaled(210, 210, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation)
|
||||
self.qr_label.setPixmap(scaled)
|
||||
self.qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
return
|
||||
|
||||
self.qr_label.setText("二维码")
|
||||
self.qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
def add_decorations(self):
|
||||
"""添加装饰元素"""
|
||||
# 添加浮动装饰球
|
||||
orb1 = FloatingOrb(120, QColor("#fbbf24"), self.container)
|
||||
orb1.move(-30, -30)
|
||||
|
||||
orb2 = FloatingOrb(80, QColor("#3b82f6"), self.container)
|
||||
orb2.move(420, 50)
|
||||
|
||||
orb3 = FloatingOrb(60, QColor("#8b5cf6"), self.container)
|
||||
orb3.move(400, 500)
|
||||
|
||||
orb4 = FloatingOrb(100, QColor("#ec4899"), self.container)
|
||||
orb4.move(-20, 480)
|
||||
|
||||
# 确保装饰在底层
|
||||
orb1.lower()
|
||||
orb2.lower()
|
||||
orb3.lower()
|
||||
orb4.lower()
|
||||
|
||||
def setup_animations(self):
|
||||
"""设置动画"""
|
||||
# 卡片淡入效果
|
||||
self.card_opacity = QGraphicsOpacityEffect(self.card)
|
||||
self.card_opacity.setOpacity(0)
|
||||
self.card.setGraphicsEffect(self.card_opacity)
|
||||
|
||||
self.fade_anim = QPropertyAnimation(self.card_opacity, b"opacity")
|
||||
self.fade_anim.setDuration(300)
|
||||
self.fade_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
# 居中显示
|
||||
if self.parent():
|
||||
parent_rect = self.parent().rect()
|
||||
x = (parent_rect.width() - self.width()) // 2
|
||||
y = (parent_rect.height() - self.height()) // 2
|
||||
self.move(self.parent().mapToGlobal(QPoint(x, y)))
|
||||
|
||||
# 播放淡入动画
|
||||
self.fade_anim.setStartValue(0)
|
||||
self.fade_anim.setEndValue(1)
|
||||
self.fade_anim.start()
|
||||
|
||||
def close_with_animation(self):
|
||||
"""带动画关闭"""
|
||||
self.fade_anim.setStartValue(1)
|
||||
self.fade_anim.setEndValue(0)
|
||||
self.fade_anim.finished.connect(self.accept)
|
||||
self.fade_anim.start()
|
||||
|
||||
def copy_email(self):
|
||||
"""复制邮箱"""
|
||||
clipboard = QApplication.clipboard()
|
||||
clipboard.setText("workerqi@163.com")
|
||||
|
||||
# 临时显示"已复制"
|
||||
original_text = self.email_label.text()
|
||||
self.email_label.setText("✓ 已复制到剪贴板")
|
||||
self.email_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #22c55e;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
""")
|
||||
QTimer.singleShot(1500, lambda: self._reset_email_label(original_text))
|
||||
|
||||
def _reset_email_label(self, text):
|
||||
"""重置邮箱标签"""
|
||||
self.email_label.setText(text)
|
||||
self.email_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #fbbf24;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
""")
|
||||
|
||||
def send_email(self):
|
||||
"""发送邮件"""
|
||||
import webbrowser
|
||||
webbrowser.open("mailto:workerqi@163.com?subject=奶酪云工具箱-合作咨询")
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""绘制背景"""
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
# 半透明遮罩背景
|
||||
painter.fillRect(self.rect(), QColor(0, 0, 0, 120))
|
||||
|
||||
# 卡片背景
|
||||
card_rect = self.card.geometry().adjusted(20, 20, -20, -20)
|
||||
|
||||
# 卡片阴影
|
||||
shadow_color = QColor(0, 0, 0, 80)
|
||||
for i in range(10):
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.setBrush(QColor(0, 0, 0, int(80 - i * 8)))
|
||||
painter.drawRoundedRect(card_rect.adjusted(-i*2, -i*2, i*2, i*2), 24 + i, 24 + i)
|
||||
|
||||
# 卡片主体 - 玻璃质感
|
||||
gradient = QLinearGradient(card_rect.topLeft(), card_rect.bottomRight())
|
||||
gradient.setColorAt(0, QColor(30, 41, 59, 240))
|
||||
gradient.setColorAt(0.5, QColor(15, 23, 42, 250))
|
||||
gradient.setColorAt(1, QColor(30, 41, 59, 240))
|
||||
|
||||
painter.setBrush(gradient)
|
||||
painter.setPen(QPen(QColor(71, 85, 105, 100), 1))
|
||||
painter.drawRoundedRect(card_rect, 24, 24)
|
||||
|
||||
# 顶部高光
|
||||
highlight_rect = QRect(card_rect.x() + 40, card_rect.y(), card_rect.width() - 80, 2)
|
||||
highlight_gradient = QLinearGradient(highlight_rect.topLeft(), highlight_rect.topRight())
|
||||
highlight_gradient.setColorAt(0, QColor(251, 191, 36, 0))
|
||||
highlight_gradient.setColorAt(0.5, QColor(251, 191, 36, 150))
|
||||
highlight_gradient.setColorAt(1, QColor(251, 191, 36, 0))
|
||||
painter.setBrush(highlight_gradient)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawRoundedRect(highlight_rect, 1, 1)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""点击空白处关闭"""
|
||||
card_rect = self.card.geometry().adjusted(20, 20, -20, -20)
|
||||
if not card_rect.contains(event.pos()):
|
||||
self.close_with_animation()
|
||||
else:
|
||||
self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""拖动窗口"""
|
||||
if self._drag_pos:
|
||||
self.move(event.globalPosition().toPoint() - self._drag_pos)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
self._drag_pos = None
|
||||
|
||||
234
ui/settings.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
设置页面
|
||||
- 全局配置选项
|
||||
- 文件保存位置设置
|
||||
- 日志查看器
|
||||
"""
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QLineEdit, QFileDialog, QCheckBox, QSlider,
|
||||
QTabWidget, QSpinBox, QMessageBox, QScrollArea, QGroupBox
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont
|
||||
import logging
|
||||
|
||||
from core.config import config
|
||||
from ui.log_viewer import LogViewer
|
||||
|
||||
|
||||
class SettingsPage(QWidget):
|
||||
"""设置页面"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
self.load_settings()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(24, 24, 24, 24)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 标题
|
||||
title = QLabel("⚙️ 设置")
|
||||
title.setFont(QFont("Microsoft YaHei", 20, QFont.Weight.Bold))
|
||||
title.setStyleSheet("color: white;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# 标签页
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget.setStyleSheet("""
|
||||
QTabWidget::pane {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
QTabBar::tab {
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-size: 13px;
|
||||
}
|
||||
QTabBar::tab:hover {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
QTabBar::tab:selected {
|
||||
color: #fbbf24;
|
||||
border-bottom: 2px solid #fbbf24;
|
||||
}
|
||||
""")
|
||||
|
||||
# 通用设置标签页
|
||||
self.tab_widget.addTab(self.create_general_tab(), "📁 通用设置")
|
||||
|
||||
# 界面设置标签页
|
||||
self.tab_widget.addTab(self.create_ui_tab(), "🎨 界面设置")
|
||||
|
||||
# 日志查看标签页
|
||||
self.log_viewer = LogViewer()
|
||||
self.tab_widget.addTab(self.log_viewer, "📋 日志查看")
|
||||
|
||||
layout.addWidget(self.tab_widget, 1)
|
||||
|
||||
# 底部按钮
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.addStretch()
|
||||
|
||||
reset_btn = QPushButton("🔄 恢复默认")
|
||||
reset_btn.setObjectName("secondary_btn")
|
||||
reset_btn.clicked.connect(self.reset_settings)
|
||||
btn_layout.addWidget(reset_btn)
|
||||
|
||||
save_btn = QPushButton("💾 保存设置")
|
||||
save_btn.setObjectName("primary_btn")
|
||||
save_btn.clicked.connect(self.save_settings)
|
||||
btn_layout.addWidget(save_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def create_general_tab(self) -> QWidget:
|
||||
"""创建通用设置标签页"""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(20)
|
||||
|
||||
# 输出目录设置
|
||||
output_group = QGroupBox("📂 默认保存位置")
|
||||
output_layout = QVBoxLayout(output_group)
|
||||
output_layout.setSpacing(12)
|
||||
|
||||
# 路径选择
|
||||
path_layout = QHBoxLayout()
|
||||
|
||||
self.output_path_edit = QLineEdit()
|
||||
self.output_path_edit.setPlaceholderText("未设置 - 每次操作时询问保存位置")
|
||||
self.output_path_edit.setReadOnly(True)
|
||||
self.output_path_edit.setStyleSheet("""
|
||||
QLineEdit {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
}
|
||||
""")
|
||||
path_layout.addWidget(self.output_path_edit, 1)
|
||||
|
||||
browse_btn = QPushButton("📁 浏览")
|
||||
browse_btn.setObjectName("secondary_btn")
|
||||
browse_btn.clicked.connect(self.browse_output_dir)
|
||||
path_layout.addWidget(browse_btn)
|
||||
|
||||
clear_btn = QPushButton("✖ 清除")
|
||||
clear_btn.setObjectName("secondary_btn")
|
||||
clear_btn.clicked.connect(self.clear_output_dir)
|
||||
path_layout.addWidget(clear_btn)
|
||||
|
||||
output_layout.addLayout(path_layout)
|
||||
|
||||
# 自动保存选项
|
||||
self.auto_save_check = QCheckBox("自动保存到默认目录(不再询问)")
|
||||
self.auto_save_check.setStyleSheet("color: #cbd5e1; font-size: 13px;")
|
||||
output_layout.addWidget(self.auto_save_check)
|
||||
|
||||
hint = QLabel("提示: 设置默认保存位置后,处理完成的文件将自动保存到该目录")
|
||||
hint.setStyleSheet("color: #64748b; font-size: 11px;")
|
||||
hint.setWordWrap(True)
|
||||
output_layout.addWidget(hint)
|
||||
|
||||
layout.addWidget(output_group)
|
||||
|
||||
layout.addStretch()
|
||||
return widget
|
||||
|
||||
def create_ui_tab(self) -> QWidget:
|
||||
"""创建界面设置标签页"""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(20)
|
||||
|
||||
# 动画设置
|
||||
anim_group = QGroupBox("✨ 动画效果")
|
||||
anim_layout = QVBoxLayout(anim_group)
|
||||
anim_layout.setSpacing(12)
|
||||
|
||||
self.animation_check = QCheckBox("启用界面动画(渐入渐出效果)")
|
||||
self.animation_check.setStyleSheet("color: #cbd5e1; font-size: 13px;")
|
||||
self.animation_check.setChecked(True)
|
||||
self.animation_check.stateChanged.connect(self.on_animation_toggle)
|
||||
anim_layout.addWidget(self.animation_check)
|
||||
|
||||
# 动画时长
|
||||
duration_row = QHBoxLayout()
|
||||
duration_row.addWidget(QLabel("动画时长:"))
|
||||
duration_row.addStretch()
|
||||
|
||||
self.duration_spin = QSpinBox()
|
||||
self.duration_spin.setRange(100, 1000)
|
||||
self.duration_spin.setValue(300)
|
||||
self.duration_spin.setSuffix(" ms")
|
||||
self.duration_spin.setFixedWidth(100)
|
||||
duration_row.addWidget(self.duration_spin)
|
||||
|
||||
anim_layout.addLayout(duration_row)
|
||||
|
||||
layout.addWidget(anim_group)
|
||||
|
||||
layout.addStretch()
|
||||
return widget
|
||||
|
||||
def browse_output_dir(self):
|
||||
"""浏览输出目录"""
|
||||
current = self.output_path_edit.text() or ""
|
||||
path = QFileDialog.getExistingDirectory(self, "选择默认保存目录", current)
|
||||
if path:
|
||||
self.output_path_edit.setText(path)
|
||||
|
||||
def clear_output_dir(self):
|
||||
"""清除输出目录"""
|
||||
self.output_path_edit.setText("")
|
||||
self.auto_save_check.setChecked(False)
|
||||
|
||||
def on_animation_toggle(self, state):
|
||||
"""动画开关切换"""
|
||||
self.duration_spin.setEnabled(state == Qt.CheckState.Checked.value)
|
||||
|
||||
def load_settings(self):
|
||||
"""加载设置"""
|
||||
self.output_path_edit.setText(config.get("output_directory", ""))
|
||||
self.auto_save_check.setChecked(config.get("auto_save_to_default", False))
|
||||
|
||||
self.animation_check.setChecked(config.get("animation_enabled", True))
|
||||
self.duration_spin.setValue(config.get("animation_duration", 300))
|
||||
self.duration_spin.setEnabled(config.get("animation_enabled", True))
|
||||
|
||||
def save_settings(self):
|
||||
"""保存设置"""
|
||||
config.set("output_directory", self.output_path_edit.text())
|
||||
config.set("auto_save_to_default", self.auto_save_check.isChecked())
|
||||
|
||||
config.set("animation_enabled", self.animation_check.isChecked())
|
||||
config.set("animation_duration", self.duration_spin.value())
|
||||
|
||||
QMessageBox.information(self, "成功", "设置已保存!")
|
||||
logging.info("用户设置已保存")
|
||||
|
||||
def reset_settings(self):
|
||||
"""恢复默认设置"""
|
||||
reply = QMessageBox.question(
|
||||
self, "确认", "确定要恢复默认设置吗?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
for key, value in config.DEFAULT_CONFIG.items():
|
||||
config.set(key, value)
|
||||
self.load_settings()
|
||||
QMessageBox.information(self, "成功", "已恢复默认设置!")
|
||||
logging.info("用户设置已恢复默认")
|
||||
|
||||
471
ui/sidebar.py
Normal file
@@ -0,0 +1,471 @@
|
||||
"""
|
||||
左侧一级菜单组件
|
||||
- Logo (使用图片,点击弹出个人信息卡片)
|
||||
- 图片工具、PDF工具箱、Excel表格 三个分类
|
||||
- 设置按钮
|
||||
- 选中状态有左侧高亮条
|
||||
- 水波纹点击效果
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QButtonGroup,
|
||||
QSpacerItem, QSizePolicy, QLabel, QFrame, QGraphicsOpacityEffect
|
||||
)
|
||||
from PySide6.QtCore import (
|
||||
Signal, Qt, QSize, QPropertyAnimation, QEasingCurve,
|
||||
QPoint, QTimer, QParallelAnimationGroup, QSequentialAnimationGroup,
|
||||
Property
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QFont, QPainter, QColor, QLinearGradient, QPen, QBrush, QPixmap,
|
||||
QRadialGradient, QPainterPath
|
||||
)
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
from PySide6.QtCore import QByteArray
|
||||
|
||||
|
||||
# 获取资源目录
|
||||
def get_resource_path(relative_path: str) -> str:
|
||||
"""获取资源文件的绝对路径"""
|
||||
base_path = Path(__file__).parent.parent
|
||||
return str(base_path / relative_path)
|
||||
|
||||
|
||||
# Logo图片路径
|
||||
LOGO_IMAGE_PATH = get_resource_path("image/生成奶酪商城官方店介绍.png")
|
||||
|
||||
|
||||
# SVG 图标定义 (Phosphor Icons 风格)
|
||||
ICONS = {
|
||||
"cheese": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M184 32a8 8 0 0 0-8 8v16h-16a8 8 0 0 0 0 16h16v16a8 8 0 0 0 16 0V72h16a8 8 0 0 0 0-16h-16V40a8 8 0 0 0-8-8m48 88h-64a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h64a8 8 0 0 0 8-8v-64a8 8 0 0 0-8-8m-8 64h-48v-48h48ZM80 40a8 8 0 0 0-8 8v16H56a8 8 0 0 0 0 16h16v16a8 8 0 0 0 16 0V80h16a8 8 0 0 0 0-16H88V48a8 8 0 0 0-8-8m48 88H64a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h64a8 8 0 0 0 8-8v-64a8 8 0 0 0-8-8m-8 64H72v-48h48Z"/></svg>""",
|
||||
"image": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M216 40H40a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16m0 16v102.75l-26.07-26.06a16 16 0 0 0-22.63 0l-20 20l-44-44a16 16 0 0 0-22.62 0L40 149.37V56ZM40 172l52-52l80 80H40Zm176 28h-21.37l-36-36l20-20L216 181.38V200m-72-100a12 12 0 1 1 12 12a12 12 0 0 1-12-12"/></svg>""",
|
||||
"file-pdf": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M224 152a8 8 0 0 1-8 8h-24v16h16a8 8 0 0 1 0 16h-16v16a8 8 0 0 1-16 0v-56a8 8 0 0 1 8-8h32a8 8 0 0 1 8 8M92 172a28 28 0 0 1-28 28h-8v8a8 8 0 0 1-16 0v-56a8 8 0 0 1 8-8h16a28 28 0 0 1 28 28m-16 0a12 12 0 0 0-12-12h-8v24h8a12 12 0 0 0 12-12m88 0c0 21.78-14.36 36-36 36h-16a8 8 0 0 1-8-8v-56a8 8 0 0 1 8-8h16c21.64 0 36 14.22 36 36m-16 0c0-13.56-8-20-20-20h-8v40h8c12 0 20-6.44 20-20M40 112V40a16 16 0 0 1 16-16h96a8 8 0 0 1 5.66 2.34l56 56A8 8 0 0 1 216 88v24a8 8 0 0 1-16 0V96h-48a8 8 0 0 1-8-8V40H56v72a8 8 0 0 1-16 0m120-32h28.69L160 51.31Z"/></svg>""",
|
||||
"file-xls": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M156 208a8 8 0 0 1-8 8h-24a8 8 0 0 1-8-8v-56a8 8 0 0 1 16 0v48h16a8 8 0 0 1 8 8m-62.37-55.58a8 8 0 0 0-11.26 1.21L68 172.32l-14.37-18.69a8 8 0 0 0-12.47 10.05L58.59 184l-17.43 20.32a8 8 0 0 0 12.47 10.05L68 195.68l14.37 18.69A8 8 0 0 0 88 216a8 8 0 0 0 5.05-1.79a8 8 0 0 0 1.21-11.26L77.41 184l16.85-19.63a8 8 0 0 0-1.63-11.95m72 5.58c-14.06 0-24.63 8.88-24.63 20.67S151.57 200 166.63 200a22.76 22.76 0 0 0 7.47-1.27a8 8 0 0 0-5.2-15.13a6.89 6.89 0 0 1-2.27.4c-4.39 0-8.63-2.72-8.63-4.67s4.24-4.66 8.63-4.66c3.36 0 5.15 1 5.79 1.55a8 8 0 0 0 10.63-12a22.6 22.6 0 0 0-16.42-5.89M40 112V40a16 16 0 0 1 16-16h96a8 8 0 0 1 5.66 2.34l56 56A8 8 0 0 1 216 88v24a8 8 0 0 1-16 0V96h-48a8 8 0 0 1-8-8V40H56v72a8 8 0 0 1-16 0m120-32h28.69L160 51.31Z"/></svg>""",
|
||||
"gear": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M128 80a48 48 0 1 0 48 48a48.05 48.05 0 0 0-48-48m0 80a32 32 0 1 1 32-32a32 32 0 0 1-32 32m88-29.84q.06-2.16 0-4.32l14.92-18.64a8 8 0 0 0 1.48-7.06a107.6 107.6 0 0 0-10.88-26.25a8 8 0 0 0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186 40.54a8 8 0 0 0-3.94-6a107.29 107.29 0 0 0-26.25-10.86a8 8 0 0 0-7.06 1.48L130.16 40h-4.32L107.2 25.11a8 8 0 0 0-7.06-1.48a107.6 107.6 0 0 0-26.25 10.88a8 8 0 0 0-3.93 6l-2.64 23.76q-1.56 1.49-3 3L40.54 70a8 8 0 0 0-6 3.94a107.71 107.71 0 0 0-10.87 26.25a8 8 0 0 0 1.49 7.06L40 125.84v4.32L25.11 148.8a8 8 0 0 0-1.48 7.06a107.6 107.6 0 0 0 10.88 26.25a8 8 0 0 0 6 3.93l23.72 2.64q1.49 1.56 3 3L70 215.46a8 8 0 0 0 3.94 6a107.71 107.71 0 0 0 26.25 10.87a8 8 0 0 0 7.06-1.49L125.84 216q2.16.06 4.32 0l18.64 14.92a8 8 0 0 0 7.06 1.48a107.21 107.21 0 0 0 26.25-10.88a8 8 0 0 0 3.93-6l2.64-23.72q1.56-1.48 3-3l23.78-2.8a8 8 0 0 0 6-3.94a107.71 107.71 0 0 0 10.87-26.25a8 8 0 0 0-1.49-7.06Zm-16.1-6.5a73.93 73.93 0 0 1 0 8.68a8 8 0 0 0 1.74 5.48l14.19 17.73a91.57 91.57 0 0 1-6.23 15l-22.6 2.56a8 8 0 0 0-5.1 2.64a74.11 74.11 0 0 1-6.14 6.14a8 8 0 0 0-2.64 5.1l-2.51 22.58a91.32 91.32 0 0 1-15 6.23l-17.74-14.19a8 8 0 0 0-5-1.75h-.48a73.93 73.93 0 0 1-8.68 0a8 8 0 0 0-5.48 1.74l-17.78 14.2a91.57 91.57 0 0 1-15-6.23L82.89 168a8 8 0 0 0-2.64-5.1a74.11 74.11 0 0 1-6.14-6.14a8 8 0 0 0-5.1-2.64l-22.58-2.52a91.32 91.32 0 0 1-6.23-15l14.19-17.74a8 8 0 0 0 1.74-5.48a73.93 73.93 0 0 1 0-8.68a8 8 0 0 0-1.74-5.48L40.2 81.76a91.57 91.57 0 0 1 6.23-15L69 64.2a8 8 0 0 0 5.1-2.64a74.11 74.11 0 0 1 6.14-6.14A8 8 0 0 0 82.89 50l2.51-22.57a91.32 91.32 0 0 1 15-6.23l17.74 14.19a8 8 0 0 0 5.48 1.74a73.93 73.93 0 0 1 8.68 0a8 8 0 0 0 5.48-1.74l17.77-14.19a91.57 91.57 0 0 1 15 6.23L168 50a8 8 0 0 0 2.64 5.1a74.11 74.11 0 0 1 6.14 6.14a8 8 0 0 0 5.1 2.64l22.58 2.51a91.32 91.32 0 0 1 6.23 15l-14.19 17.74a8 8 0 0 0-1.6 5.53Z"/></svg>""",
|
||||
}
|
||||
|
||||
|
||||
class IconWidget(QLabel):
|
||||
"""SVG图标组件"""
|
||||
|
||||
def __init__(self, icon_name: str, size: int = 24, color: str = "#94a3b8", parent=None):
|
||||
super().__init__(parent)
|
||||
self.icon_name = icon_name
|
||||
self.icon_size = size
|
||||
self.icon_color = color
|
||||
self.setFixedSize(size, size)
|
||||
self.update_icon()
|
||||
|
||||
def set_color(self, color: str):
|
||||
self.icon_color = color
|
||||
self.update_icon()
|
||||
|
||||
def update_icon(self):
|
||||
svg_data = ICONS.get(self.icon_name, "")
|
||||
if svg_data:
|
||||
# 替换颜色
|
||||
svg_data = svg_data.replace('currentColor', self.icon_color)
|
||||
renderer = QSvgRenderer(QByteArray(svg_data.encode()))
|
||||
pixmap = QPixmap(self.icon_size, self.icon_size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
self.setPixmap(pixmap)
|
||||
|
||||
|
||||
class RippleEffect:
|
||||
"""水波纹效果数据"""
|
||||
def __init__(self, center: QPoint, max_radius: float):
|
||||
self.center = center
|
||||
self.max_radius = max_radius
|
||||
self.current_radius = 0.0
|
||||
self.opacity = 0.4
|
||||
|
||||
|
||||
class CategoryButton(QWidget):
|
||||
"""分类按钮 - 带左侧高亮条和水波纹效果"""
|
||||
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, icon_name: str, tooltip: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.icon_name = icon_name
|
||||
self._checked = False
|
||||
self._hovered = False
|
||||
self._pressed = False
|
||||
self.setFixedSize(80, 56) # 放大按钮
|
||||
self.setToolTip(tooltip)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
# 水波纹效果
|
||||
self._ripples = []
|
||||
self._ripple_timer = QTimer(self)
|
||||
self._ripple_timer.timeout.connect(self._update_ripples)
|
||||
|
||||
# 图标 - 放大
|
||||
self.icon_widget = IconWidget(icon_name, 28, "#94a3b8", self)
|
||||
self.icon_widget.move(26, 14)
|
||||
|
||||
def setChecked(self, checked: bool):
|
||||
self._checked = checked
|
||||
self.update_style()
|
||||
self.update()
|
||||
|
||||
def isChecked(self) -> bool:
|
||||
return self._checked
|
||||
|
||||
def update_style(self):
|
||||
if self._checked:
|
||||
self.icon_widget.set_color("#fbbf24")
|
||||
elif self._hovered:
|
||||
self.icon_widget.set_color("#fbbf24")
|
||||
else:
|
||||
self.icon_widget.set_color("#94a3b8")
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self.update_style()
|
||||
self.update()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.update_style()
|
||||
self.update()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._pressed = True
|
||||
# 创建水波纹
|
||||
self._create_ripple(event.pos())
|
||||
self.update()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._pressed = False
|
||||
if self.rect().contains(event.pos()):
|
||||
self.clicked.emit()
|
||||
self.update()
|
||||
|
||||
def _create_ripple(self, pos: QPoint):
|
||||
"""创建水波纹效果"""
|
||||
# 计算最大半径
|
||||
distances = [
|
||||
(pos - QPoint(0, 0)).manhattanLength(),
|
||||
(pos - QPoint(self.width(), 0)).manhattanLength(),
|
||||
(pos - QPoint(0, self.height())).manhattanLength(),
|
||||
(pos - QPoint(self.width(), self.height())).manhattanLength()
|
||||
]
|
||||
max_radius = max(distances) * 0.8
|
||||
|
||||
ripple = RippleEffect(pos, max_radius)
|
||||
self._ripples.append(ripple)
|
||||
|
||||
if not self._ripple_timer.isActive():
|
||||
self._ripple_timer.start(16) # ~60fps
|
||||
|
||||
def _update_ripples(self):
|
||||
"""更新水波纹动画"""
|
||||
to_remove = []
|
||||
for ripple in self._ripples:
|
||||
ripple.current_radius += ripple.max_radius * 0.08
|
||||
ripple.opacity -= 0.025
|
||||
|
||||
if ripple.opacity <= 0:
|
||||
to_remove.append(ripple)
|
||||
|
||||
for ripple in to_remove:
|
||||
self._ripples.remove(ripple)
|
||||
|
||||
if not self._ripples:
|
||||
self._ripple_timer.stop()
|
||||
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
# 绘制按钮背景
|
||||
btn_rect = self.rect().adjusted(12, 4, -12, -4)
|
||||
|
||||
if self._checked:
|
||||
# 选中背景
|
||||
painter.setBrush(QColor(251, 191, 36, 30)) # cheese-400/20
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawRoundedRect(btn_rect, 14, 14)
|
||||
|
||||
# 左侧高亮条
|
||||
indicator_height = int(self.height() * 0.6)
|
||||
indicator_y = (self.height() - indicator_height) // 2
|
||||
|
||||
# 发光效果
|
||||
glow_color = QColor(251, 191, 36, 80)
|
||||
painter.setBrush(glow_color)
|
||||
painter.drawRoundedRect(0, indicator_y - 2, 6, indicator_height + 4, 3, 3)
|
||||
|
||||
# 实体条
|
||||
painter.setBrush(QColor("#fbbf24"))
|
||||
painter.drawRoundedRect(0, indicator_y, 4, indicator_height, 2, 2)
|
||||
|
||||
elif self._hovered:
|
||||
painter.setBrush(QColor(51, 65, 85, 128)) # darkbg-700/50
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawRoundedRect(btn_rect, 14, 14)
|
||||
|
||||
# 绘制水波纹
|
||||
if self._ripples:
|
||||
painter.save()
|
||||
# 创建裁剪区域
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(btn_rect.x(), btn_rect.y(), btn_rect.width(), btn_rect.height(), 14, 14)
|
||||
painter.setClipPath(path)
|
||||
|
||||
for ripple in self._ripples:
|
||||
color = QColor(251, 191, 36, int(ripple.opacity * 255))
|
||||
gradient = QRadialGradient(ripple.center.x(), ripple.center.y(), ripple.current_radius)
|
||||
gradient.setColorAt(0, QColor(251, 191, 36, 0))
|
||||
gradient.setColorAt(0.5, color)
|
||||
gradient.setColorAt(1, QColor(251, 191, 36, 0))
|
||||
|
||||
painter.setBrush(gradient)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawEllipse(
|
||||
ripple.center,
|
||||
int(ripple.current_radius),
|
||||
int(ripple.current_radius)
|
||||
)
|
||||
painter.restore()
|
||||
|
||||
|
||||
class LogoButton(QWidget):
|
||||
"""Logo按钮 - 使用图片,无背景,带水波纹效果"""
|
||||
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFixedSize(60, 60) # 放大
|
||||
self.setToolTip("点击查看个人信息")
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self._hovered = False
|
||||
self._pressed = False
|
||||
self._logo_pixmap = None
|
||||
self._scale = 1.0
|
||||
|
||||
# 水波纹效果
|
||||
self._ripples = []
|
||||
self._ripple_timer = QTimer(self)
|
||||
self._ripple_timer.timeout.connect(self._update_ripples)
|
||||
|
||||
# 悬停缩放动画
|
||||
self._scale_anim = QPropertyAnimation(self, b"scale_factor")
|
||||
self._scale_anim.setDuration(150)
|
||||
self._scale_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
self._load_logo()
|
||||
|
||||
def get_scale_factor(self):
|
||||
return self._scale
|
||||
|
||||
def set_scale_factor(self, value):
|
||||
self._scale = value
|
||||
self.update()
|
||||
|
||||
# 使用 PySide6 Property 以支持动画
|
||||
scale_factor = Property(float, get_scale_factor, set_scale_factor)
|
||||
|
||||
def _load_logo(self):
|
||||
"""加载Logo图片"""
|
||||
if os.path.exists(LOGO_IMAGE_PATH):
|
||||
self._logo_pixmap = QPixmap(LOGO_IMAGE_PATH)
|
||||
if not self._logo_pixmap.isNull():
|
||||
# 缩放到合适大小
|
||||
self._logo_pixmap = self._logo_pixmap.scaled(
|
||||
56, 56,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self._scale_anim.stop()
|
||||
self._scale_anim.setStartValue(self._scale)
|
||||
self._scale_anim.setEndValue(1.1)
|
||||
self._scale_anim.start()
|
||||
self.update()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self._scale_anim.stop()
|
||||
self._scale_anim.setStartValue(self._scale)
|
||||
self._scale_anim.setEndValue(1.0)
|
||||
self._scale_anim.start()
|
||||
self.update()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._pressed = True
|
||||
self._create_ripple(event.pos())
|
||||
self.update()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._pressed = False
|
||||
if self.rect().contains(event.pos()):
|
||||
self.clicked.emit()
|
||||
self.update()
|
||||
|
||||
def _create_ripple(self, pos: QPoint):
|
||||
"""创建水波纹效果"""
|
||||
max_radius = max(self.width(), self.height())
|
||||
ripple = RippleEffect(pos, max_radius)
|
||||
self._ripples.append(ripple)
|
||||
|
||||
if not self._ripple_timer.isActive():
|
||||
self._ripple_timer.start(16)
|
||||
|
||||
def _update_ripples(self):
|
||||
"""更新水波纹动画"""
|
||||
to_remove = []
|
||||
for ripple in self._ripples:
|
||||
ripple.current_radius += ripple.max_radius * 0.1
|
||||
ripple.opacity -= 0.03
|
||||
|
||||
if ripple.opacity <= 0:
|
||||
to_remove.append(ripple)
|
||||
|
||||
for ripple in to_remove:
|
||||
self._ripples.remove(ripple)
|
||||
|
||||
if not self._ripples:
|
||||
self._ripple_timer.stop()
|
||||
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
|
||||
|
||||
center = self.rect().center()
|
||||
|
||||
# 绘制Logo图片(无背景)
|
||||
if self._logo_pixmap and not self._logo_pixmap.isNull():
|
||||
# 应用缩放
|
||||
scaled_size = int(self._logo_pixmap.width() * self._scale)
|
||||
scaled_pixmap = self._logo_pixmap.scaled(
|
||||
scaled_size, scaled_size,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
|
||||
x = center.x() - scaled_pixmap.width() // 2
|
||||
y = center.y() - scaled_pixmap.height() // 2
|
||||
|
||||
# 悬停时添加发光效果
|
||||
if self._hovered:
|
||||
glow_rect = self.rect().adjusted(-4, -4, 4, 4)
|
||||
glow = QRadialGradient(center.x(), center.y(), 40)
|
||||
glow.setColorAt(0, QColor(251, 191, 36, 60))
|
||||
glow.setColorAt(0.7, QColor(251, 191, 36, 30))
|
||||
glow.setColorAt(1, QColor(251, 191, 36, 0))
|
||||
painter.setBrush(glow)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawEllipse(center, 36, 36)
|
||||
|
||||
painter.drawPixmap(x, y, scaled_pixmap)
|
||||
|
||||
# 绘制水波纹
|
||||
if self._ripples:
|
||||
painter.save()
|
||||
# 圆形裁剪
|
||||
path = QPainterPath()
|
||||
path.addEllipse(center.x() - 28, center.y() - 28, 56, 56)
|
||||
painter.setClipPath(path)
|
||||
|
||||
for ripple in self._ripples:
|
||||
color = QColor(251, 191, 36, int(ripple.opacity * 200))
|
||||
gradient = QRadialGradient(ripple.center.x(), ripple.center.y(), ripple.current_radius)
|
||||
gradient.setColorAt(0, QColor(251, 191, 36, 0))
|
||||
gradient.setColorAt(0.6, color)
|
||||
gradient.setColorAt(1, QColor(251, 191, 36, 0))
|
||||
|
||||
painter.setBrush(gradient)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawEllipse(
|
||||
ripple.center,
|
||||
int(ripple.current_radius),
|
||||
int(ripple.current_radius)
|
||||
)
|
||||
painter.restore()
|
||||
else:
|
||||
# 如果图片加载失败,使用emoji作为备选
|
||||
painter.setPen(QColor("#fbbf24"))
|
||||
font = QFont("Segoe UI Emoji", 28)
|
||||
painter.setFont(font)
|
||||
painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, "🧀")
|
||||
|
||||
|
||||
class PrimarySidebar(QWidget):
|
||||
"""左侧一级菜单"""
|
||||
|
||||
category_changed = Signal(str)
|
||||
settings_clicked = Signal()
|
||||
logo_clicked = Signal() # 新增:Logo点击信号
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("primary_sidebar")
|
||||
self.setFixedWidth(80)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 20, 0, 24)
|
||||
layout.setSpacing(8)
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
||||
|
||||
# Logo - 点击弹出个人信息卡片
|
||||
self.logo_btn = LogoButton()
|
||||
self.logo_btn.clicked.connect(self.logo_clicked.emit)
|
||||
layout.addWidget(self.logo_btn, 0, Qt.AlignmentFlag.AlignHCenter)
|
||||
|
||||
layout.addSpacing(28)
|
||||
|
||||
# 分类按钮
|
||||
self.image_btn = CategoryButton("image", "图片工具")
|
||||
self.image_btn.setChecked(True)
|
||||
self.image_btn.clicked.connect(lambda: self._on_category_click("image"))
|
||||
layout.addWidget(self.image_btn)
|
||||
|
||||
self.pdf_btn = CategoryButton("file-pdf", "PDF 工具箱")
|
||||
self.pdf_btn.clicked.connect(lambda: self._on_category_click("pdf"))
|
||||
layout.addWidget(self.pdf_btn)
|
||||
|
||||
self.excel_btn = CategoryButton("file-xls", "Excel 表格")
|
||||
self.excel_btn.clicked.connect(lambda: self._on_category_click("excel"))
|
||||
layout.addWidget(self.excel_btn)
|
||||
|
||||
self.buttons = {
|
||||
"image": self.image_btn,
|
||||
"pdf": self.pdf_btn,
|
||||
"excel": self.excel_btn
|
||||
}
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
# 设置按钮
|
||||
self.settings_btn = CategoryButton("gear", "设置 / 日志查看")
|
||||
self.settings_btn.clicked.connect(self.settings_clicked.emit)
|
||||
layout.addWidget(self.settings_btn)
|
||||
|
||||
def _on_category_click(self, category: str):
|
||||
for cat, btn in self.buttons.items():
|
||||
btn.setChecked(cat == category)
|
||||
self.category_changed.emit(category)
|
||||
|
||||
def set_category(self, category: str):
|
||||
for cat, btn in self.buttons.items():
|
||||
btn.setChecked(cat == category)
|
||||
425
ui/tool_list.py
Normal file
@@ -0,0 +1,425 @@
|
||||
"""
|
||||
中间二级菜单组件
|
||||
- 分类标题
|
||||
- 搜索框(带图标)
|
||||
- 工具列表(图标+名称+描述)
|
||||
- 用户信息
|
||||
"""
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QButtonGroup, QScrollArea, QFrame, QGraphicsDropShadowEffect
|
||||
)
|
||||
from PySide6.QtCore import Signal, Qt
|
||||
from PySide6.QtGui import QFont, QColor, QPainter, QLinearGradient
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
from PySide6.QtCore import QByteArray
|
||||
|
||||
|
||||
# 工具图标 SVG
|
||||
TOOL_ICONS = {
|
||||
"ph-arrows-in-line-horizontal": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M136 40v176a8 8 0 0 1-16 0V40a8 8 0 0 1 16 0M96 120H35.31l18.35-18.34a8 8 0 0 0-11.32-11.32l-32 32a8 8 0 0 0 0 11.32l32 32a8 8 0 0 0 11.32-11.32L35.31 136H96a8 8 0 0 0 0-16m149.66 2.34l-32-32a8 8 0 0 0-11.32 11.32L220.69 120H160a8 8 0 0 0 0 16h60.69l-18.35 18.34a8 8 0 0 0 11.32 11.32l32-32a8 8 0 0 0 0-11.32"/></svg>""",
|
||||
"ph-arrows-left-right": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="m213.66 181.66l-32 32a8 8 0 0 1-11.32-11.32L188.69 184H48a8 8 0 0 1 0-16h140.69l-18.35-18.34a8 8 0 0 1 11.32-11.32l32 32a8 8 0 0 1 0 11.32m-139.32-96a8 8 0 0 0 11.32-11.32L67.31 56H208a8 8 0 0 0 0-16H67.31l18.35-18.34a8 8 0 0 0-11.32-11.32l-32 32a8 8 0 0 0 0 11.32Z"/></svg>""",
|
||||
"ph-stamp": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M224 224a8 8 0 0 1-8 8H40a8 8 0 0 1 0-16h176a8 8 0 0 1 8 8m0-80v40a16 16 0 0 1-16 16H48a16 16 0 0 1-16-16v-40a16 16 0 0 1 16-16h60.43l-15.52-46.55A40 40 0 0 1 128 32a40 40 0 0 1 35.09 49.45L147.57 128H208a16 16 0 0 1 16 16m-16 0H48v40h160Z"/></svg>""",
|
||||
"ph-scissors": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M157.73 113.13A8 8 0 0 1 159.82 102L227.48 55.7a8 8 0 0 1 9 13.21l-67.67 46.3a7.92 7.92 0 0 1-4.51 1.4a8 8 0 0 1-6.57-3.48m80.87 85.09a8 8 0 0 1-11.12 2.08L136 137.7l-31.11 21.29a36 36 0 1 1-8.87-12.94l27.46-18.79l-27.46-18.79a36 36 0 1 1 8.87-12.94l123.6 84.59a8 8 0 0 1 2.11 11.1M80 180a20 20 0 1 0-20 20a20 20 0 0 0 20-20m0-104a20 20 0 1 0-20 20a20 20 0 0 0 20-20"/></svg>""",
|
||||
"ph-files": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M213.66 66.34l-40-40A8 8 0 0 0 168 24H88a16 16 0 0 0-16 16v16H56a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h112a16 16 0 0 0 16-16v-16h16a16 16 0 0 0 16-16V72a8 8 0 0 0-2.34-5.66M168 216H56V72h76.69L168 107.31Zm32-32h-16v-80a8 8 0 0 0-2.34-5.66l-40-40A8 8 0 0 0 136 56H88V40h76.69L200 75.31Zm-56-32a8 8 0 0 1-8 8H88a8 8 0 0 1 0-16h48a8 8 0 0 1 8 8m0 32a8 8 0 0 1-8 8H88a8 8 0 0 1 0-16h48a8 8 0 0 1 8 8"/></svg>""",
|
||||
"ph-microsoft-word-logo": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M200 24H72a16 16 0 0 0-16 16v24H40a16 16 0 0 0-16 16v96a16 16 0 0 0 16 16h16v24a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16V40a16 16 0 0 0-16-16m-40 176H72v-24h88a16 16 0 0 0 16-16v-23h24v63Zm0-79h-32V80h32Zm0-57h-32V40h32ZM40 80h88v96H40Zm160 136h-24v-24a16 16 0 0 0-16-16h-32v-55h72Zm0-111h-32V80a16 16 0 0 0-16-16h-12V40h60Zm-104.5 47.8l-9.36 34.14a6 6 0 0 1-11.56.12L68 162.18l-6.58 24.88a6 6 0 1 1-11.58-3.12l12-45a6 6 0 0 1 11.48-.16l7.18 27.14l9.86-36a6 6 0 0 1 11.56 0l9.86 36l7.18-27.14a6 6 0 0 1 11.48.16l12 45a6 6 0 1 1-11.58 3.12L124 162.18l-6.54 24.88a6 6 0 0 1-11.56-.12l-9.36-34.14Z"/></svg>""",
|
||||
"ph-eye": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M247.31 124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57 61.26 162.88 48 128 48S61.43 61.26 36.34 86.35C17.51 105.18 9 124 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208s66.57-13.26 91.66-38.34c18.83-18.83 27.3-37.61 27.65-38.4a8 8 0 0 0 0-6.5M128 192c-30.78 0-57.67-11.19-79.93-33.25A133.47 133.47 0 0 1 25 128a133.33 133.33 0 0 1 23.07-30.75C70.33 75.19 97.22 64 128 64s57.67 11.19 79.93 33.25A133.46 133.46 0 0 1 231.05 128c-7.21 13.46-38.62 64-103.05 64m0-112a48 48 0 1 0 48 48a48.05 48.05 0 0 0-48-48m0 80a32 32 0 1 1 32-32a32 32 0 0 1-32 32"/></svg>""",
|
||||
"ph-chart-bar": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M224 200h-8V40a8 8 0 0 0-8-8h-56a8 8 0 0 0-8 8v40H96a8 8 0 0 0-8 8v40H48a8 8 0 0 0-8 8v64h-8a8 8 0 0 0 0 16h192a8 8 0 0 0 0-16M160 48h40v152h-40Zm-56 48h40v104h-40Zm-48 48h32v56H56Z"/></svg>""",
|
||||
"ph-magnifying-glass": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="m229.66 218.34l-50.07-50.06a88.11 88.11 0 1 0-11.31 11.31l50.06 50.07a8 8 0 0 0 11.32-11.32M40 112a72 72 0 1 1 72 72a72.08 72.08 0 0 1-72-72"/></svg>""",
|
||||
}
|
||||
|
||||
|
||||
# 工具数据定义
|
||||
TOOLS_DATA = {
|
||||
"image": {
|
||||
"title": "图片工具",
|
||||
"items": [
|
||||
{"id": "img-compress", "name": "图片压缩", "icon": "ph-arrows-in-line-horizontal", "desc": "智能无损压缩"},
|
||||
{"id": "img-convert", "name": "格式转换", "icon": "ph-arrows-left-right", "desc": "JPG/PNG/WEBP"},
|
||||
{"id": "img-watermark", "name": "图片加水印", "icon": "ph-stamp", "desc": "批量添加水印"},
|
||||
]
|
||||
},
|
||||
"pdf": {
|
||||
"title": "PDF 工具箱",
|
||||
"items": [
|
||||
{"id": "pdf-split", "name": "PDF 拆分", "icon": "ph-scissors", "desc": "提取指定页面"},
|
||||
{"id": "pdf-merge", "name": "PDF 合并", "icon": "ph-files", "desc": "多文件合并"},
|
||||
{"id": "pdf-word", "name": "PDF 转 Word", "icon": "ph-microsoft-word-logo", "desc": "保持排版转换"},
|
||||
]
|
||||
},
|
||||
"excel": {
|
||||
"title": "Excel 表格",
|
||||
"items": [
|
||||
{"id": "xls-view", "name": "Excel 预览", "icon": "ph-eye", "desc": "在线查看表格"},
|
||||
{"id": "xls-chart", "name": "图表生成", "icon": "ph-chart-bar", "desc": "数据可视化"},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def render_svg_icon(svg_data: str, size: int = 18, color: str = "#94a3b8") -> 'QPixmap':
|
||||
"""渲染SVG图标"""
|
||||
from PySide6.QtGui import QPixmap
|
||||
svg_data = svg_data.replace('currentColor', color)
|
||||
renderer = QSvgRenderer(QByteArray(svg_data.encode()))
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
return pixmap
|
||||
|
||||
|
||||
class ToolButton(QFrame):
|
||||
"""工具按钮 - 还原HTML设计样式"""
|
||||
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, tool_data: dict, parent=None):
|
||||
super().__init__(parent)
|
||||
self.tool_data = tool_data
|
||||
self._checked = False
|
||||
self._hovered = False
|
||||
self.setFixedHeight(56)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(12, 8, 12, 8)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# 图标容器
|
||||
self.icon_frame = QFrame()
|
||||
self.icon_frame.setFixedSize(32, 32)
|
||||
self.icon_frame.setStyleSheet("""
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
""")
|
||||
icon_layout = QVBoxLayout(self.icon_frame)
|
||||
icon_layout.setContentsMargins(0, 0, 0, 0)
|
||||
icon_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 图标
|
||||
self.icon_label = QLabel()
|
||||
self.icon_label.setFixedSize(18, 18)
|
||||
self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
svg_data = TOOL_ICONS.get(self.tool_data.get('icon', ''), '')
|
||||
if svg_data:
|
||||
self.icon_label.setPixmap(render_svg_icon(svg_data, 18, "#94a3b8"))
|
||||
icon_layout.addWidget(self.icon_label)
|
||||
|
||||
layout.addWidget(self.icon_frame)
|
||||
|
||||
# 文字区域
|
||||
text_layout = QVBoxLayout()
|
||||
text_layout.setSpacing(2)
|
||||
|
||||
self.name_label = QLabel(self.tool_data.get('name', ''))
|
||||
self.name_label.setStyleSheet("color: #e2e8f0; font-size: 13px; font-weight: 500;")
|
||||
text_layout.addWidget(self.name_label)
|
||||
|
||||
self.desc_label = QLabel(self.tool_data.get('desc', ''))
|
||||
self.desc_label.setStyleSheet("color: #64748b; font-size: 10px;")
|
||||
text_layout.addWidget(self.desc_label)
|
||||
|
||||
layout.addLayout(text_layout, 1)
|
||||
|
||||
self.update_style()
|
||||
|
||||
def setChecked(self, checked: bool):
|
||||
self._checked = checked
|
||||
self.update_style()
|
||||
|
||||
def isChecked(self) -> bool:
|
||||
return self._checked
|
||||
|
||||
def update_style(self):
|
||||
if self._checked:
|
||||
self.setStyleSheet("""
|
||||
ToolButton {
|
||||
background: #334155;
|
||||
border-radius: 8px;
|
||||
border-left: 2px solid #fbbf24;
|
||||
}
|
||||
""")
|
||||
self.icon_frame.setStyleSheet("""
|
||||
background: #0f172a;
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
border-radius: 6px;
|
||||
""")
|
||||
svg_data = TOOL_ICONS.get(self.tool_data.get('icon', ''), '')
|
||||
if svg_data:
|
||||
self.icon_label.setPixmap(render_svg_icon(svg_data, 18, "#fbbf24"))
|
||||
self.name_label.setStyleSheet("color: white; font-size: 13px; font-weight: 500;")
|
||||
elif self._hovered:
|
||||
self.setStyleSheet("""
|
||||
ToolButton {
|
||||
background: rgba(51, 65, 85, 0.5);
|
||||
border-radius: 8px;
|
||||
}
|
||||
""")
|
||||
self.icon_frame.setStyleSheet("""
|
||||
background: #0f172a;
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
border-radius: 6px;
|
||||
""")
|
||||
svg_data = TOOL_ICONS.get(self.tool_data.get('icon', ''), '')
|
||||
if svg_data:
|
||||
self.icon_label.setPixmap(render_svg_icon(svg_data, 18, "#fbbf24"))
|
||||
self.name_label.setStyleSheet("color: white; font-size: 13px; font-weight: 500;")
|
||||
else:
|
||||
self.setStyleSheet("""
|
||||
ToolButton {
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
""")
|
||||
self.icon_frame.setStyleSheet("""
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
""")
|
||||
svg_data = TOOL_ICONS.get(self.tool_data.get('icon', ''), '')
|
||||
if svg_data:
|
||||
self.icon_label.setPixmap(render_svg_icon(svg_data, 18, "#94a3b8"))
|
||||
self.name_label.setStyleSheet("color: #e2e8f0; font-size: 13px; font-weight: 500;")
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self.update_style()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.update_style()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.clicked.emit()
|
||||
|
||||
|
||||
class SearchInput(QWidget):
|
||||
"""搜索框 - 带前置图标"""
|
||||
|
||||
textChanged = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# 容器
|
||||
container = QFrame()
|
||||
container.setStyleSheet("""
|
||||
QFrame {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
}
|
||||
QFrame:focus-within {
|
||||
border-color: rgba(251, 191, 36, 0.5);
|
||||
}
|
||||
""")
|
||||
container_layout = QHBoxLayout(container)
|
||||
container_layout.setContentsMargins(12, 8, 12, 8)
|
||||
container_layout.setSpacing(8)
|
||||
|
||||
# 搜索图标
|
||||
icon_label = QLabel()
|
||||
icon_label.setFixedSize(16, 16)
|
||||
svg_data = TOOL_ICONS.get("ph-magnifying-glass", "")
|
||||
if svg_data:
|
||||
icon_label.setPixmap(render_svg_icon(svg_data, 16, "#64748b"))
|
||||
container_layout.addWidget(icon_label)
|
||||
|
||||
# 输入框
|
||||
self.input = QLineEdit()
|
||||
self.input.setPlaceholderText("搜索功能...")
|
||||
self.input.setStyleSheet("""
|
||||
QLineEdit {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
}
|
||||
QLineEdit::placeholder {
|
||||
color: #475569;
|
||||
}
|
||||
""")
|
||||
self.input.textChanged.connect(self.textChanged.emit)
|
||||
container_layout.addWidget(self.input, 1)
|
||||
|
||||
layout.addWidget(container)
|
||||
|
||||
def text(self) -> str:
|
||||
return self.input.text()
|
||||
|
||||
|
||||
class UserInfoWidget(QFrame):
|
||||
"""用户信息区 - 渐变头像"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFixedHeight(70)
|
||||
self.setStyleSheet("""
|
||||
UserInfoWidget {
|
||||
background: rgba(30, 41, 59, 0.3);
|
||||
border-top: 1px solid rgba(51, 65, 85, 0.5);
|
||||
}
|
||||
""")
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(16, 12, 16, 12)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# 头像 - 渐变背景
|
||||
avatar = QFrame()
|
||||
avatar.setFixedSize(36, 36)
|
||||
avatar.setStyleSheet("""
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #8b5cf6, stop:1 #6366f1);
|
||||
border-radius: 18px;
|
||||
""")
|
||||
avatar_layout = QVBoxLayout(avatar)
|
||||
avatar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
avatar_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
avatar_text = QLabel("SV")
|
||||
avatar_text.setStyleSheet("color: white; font-size: 12px; font-weight: bold; background: transparent;")
|
||||
avatar_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
avatar_layout.addWidget(avatar_text)
|
||||
|
||||
layout.addWidget(avatar)
|
||||
|
||||
# 信息
|
||||
info_layout = QVBoxLayout()
|
||||
info_layout.setSpacing(2)
|
||||
|
||||
name_label = QLabel("超级会员")
|
||||
name_label.setStyleSheet("color: white; font-size: 12px; font-weight: 500;")
|
||||
info_layout.addWidget(name_label)
|
||||
|
||||
expire_label = QLabel("有效期至 2026-10")
|
||||
expire_label.setStyleSheet("color: #64748b; font-size: 10px;")
|
||||
info_layout.addWidget(expire_label)
|
||||
|
||||
layout.addLayout(info_layout, 1)
|
||||
|
||||
|
||||
class SecondarySidebar(QWidget):
|
||||
"""中间二级菜单"""
|
||||
|
||||
tool_selected = Signal(dict)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("secondary_sidebar")
|
||||
self.setFixedWidth(256)
|
||||
self.current_category = "image"
|
||||
self.tool_buttons = []
|
||||
self._stretch_item = None # 保存stretch引用
|
||||
self.setup_ui()
|
||||
self.load_tools("image")
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# 标题区域
|
||||
title_widget = QWidget()
|
||||
title_widget.setFixedHeight(80)
|
||||
title_widget.setStyleSheet("border-bottom: 1px solid rgba(51, 65, 85, 0.5);")
|
||||
title_layout = QVBoxLayout(title_widget)
|
||||
title_layout.setContentsMargins(24, 0, 24, 0)
|
||||
title_layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
self.category_title = QLabel("图片工具")
|
||||
self.category_title.setStyleSheet("color: white; font-size: 18px; font-weight: 600; letter-spacing: 1px;")
|
||||
title_layout.addWidget(self.category_title)
|
||||
|
||||
layout.addWidget(title_widget)
|
||||
|
||||
# 搜索区域
|
||||
search_widget = QWidget()
|
||||
search_layout = QVBoxLayout(search_widget)
|
||||
search_layout.setContentsMargins(16, 16, 16, 8)
|
||||
|
||||
self.search_input = SearchInput()
|
||||
self.search_input.textChanged.connect(self.filter_tools)
|
||||
search_layout.addWidget(self.search_input)
|
||||
|
||||
layout.addWidget(search_widget)
|
||||
|
||||
# 工具列表区域
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
scroll_area.setStyleSheet("background: transparent;")
|
||||
|
||||
self.tools_container = QWidget()
|
||||
self.tools_layout = QVBoxLayout(self.tools_container)
|
||||
self.tools_layout.setContentsMargins(12, 4, 12, 16)
|
||||
self.tools_layout.setSpacing(4)
|
||||
self.tools_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
scroll_area.setWidget(self.tools_container)
|
||||
layout.addWidget(scroll_area, 1)
|
||||
|
||||
# 用户信息
|
||||
layout.addWidget(UserInfoWidget())
|
||||
|
||||
def load_tools(self, category: str):
|
||||
self.current_category = category
|
||||
data = TOOLS_DATA.get(category, {})
|
||||
|
||||
self.category_title.setText(data.get("title", ""))
|
||||
|
||||
# 清空所有内容(包括stretch)
|
||||
for btn in self.tool_buttons:
|
||||
self.tools_layout.removeWidget(btn)
|
||||
btn.deleteLater()
|
||||
self.tool_buttons.clear()
|
||||
|
||||
# 移除旧的stretch
|
||||
while self.tools_layout.count() > 0:
|
||||
item = self.tools_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
# 添加工具按钮
|
||||
for tool in data.get("items", []):
|
||||
btn = ToolButton(tool)
|
||||
btn.clicked.connect(lambda t=tool: self.on_tool_clicked(t))
|
||||
self.tools_layout.addWidget(btn)
|
||||
self.tool_buttons.append(btn)
|
||||
|
||||
# 添加新的stretch
|
||||
self.tools_layout.addStretch()
|
||||
|
||||
def on_tool_clicked(self, tool_data: dict):
|
||||
# 更新选中状态
|
||||
for btn in self.tool_buttons:
|
||||
btn.setChecked(btn.tool_data.get('id') == tool_data.get('id'))
|
||||
self.tool_selected.emit(tool_data)
|
||||
|
||||
def filter_tools(self, text: str):
|
||||
text = text.lower()
|
||||
for btn in self.tool_buttons:
|
||||
name = btn.tool_data.get("name", "").lower()
|
||||
desc = btn.tool_data.get("desc", "").lower()
|
||||
btn.setVisible(text in name or text in desc or not text)
|
||||
|
||||
def select_tool(self, tool_id: str):
|
||||
for btn in self.tool_buttons:
|
||||
if btn.tool_data.get("id") == tool_id:
|
||||
btn.setChecked(True)
|
||||
self.tool_selected.emit(btn.tool_data)
|
||||
break
|
||||
495
ui/workspace.py
Normal file
@@ -0,0 +1,495 @@
|
||||
"""
|
||||
右侧工作区基类
|
||||
- 装饰性背景光晕
|
||||
- 毛玻璃效果头部
|
||||
- 面包屑导航 + 版本标签
|
||||
- 上传区域(虚线边框+图标)
|
||||
"""
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QScrollArea, QFileDialog, QGraphicsDropShadowEffect,
|
||||
QGraphicsBlurEffect
|
||||
)
|
||||
from PySide6.QtCore import Signal, Qt, QSize
|
||||
from PySide6.QtGui import (
|
||||
QFont, QDragEnterEvent, QDropEvent, QPainter,
|
||||
QColor, QRadialGradient, QLinearGradient, QPen, QBrush
|
||||
)
|
||||
|
||||
|
||||
class BackgroundDecoration(QWidget):
|
||||
"""装饰性背景 - 渐变光晕"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
# 右上角奶酪色光晕
|
||||
gradient1 = QRadialGradient(
|
||||
self.width() + 50, -50, # 圆心
|
||||
500 # 半径
|
||||
)
|
||||
gradient1.setColorAt(0, QColor(251, 191, 36, 25)) # cheese-500/10
|
||||
gradient1.setColorAt(1, QColor(251, 191, 36, 0))
|
||||
painter.setBrush(gradient1)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawEllipse(
|
||||
int(self.width() * 0.5), int(-self.height() * 0.2),
|
||||
int(self.width() * 0.7), int(self.height() * 0.7)
|
||||
)
|
||||
|
||||
# 左侧紫色光晕
|
||||
gradient2 = QRadialGradient(
|
||||
self.width() * 0.1, self.height() * 0.3,
|
||||
350
|
||||
)
|
||||
gradient2.setColorAt(0, QColor(139, 92, 246, 12)) # purple-500/5
|
||||
gradient2.setColorAt(1, QColor(139, 92, 246, 0))
|
||||
painter.setBrush(gradient2)
|
||||
painter.drawEllipse(
|
||||
int(-self.width() * 0.1), int(self.height() * 0.1),
|
||||
int(self.width() * 0.5), int(self.height() * 0.5)
|
||||
)
|
||||
|
||||
|
||||
class UploadArea(QFrame):
|
||||
"""上传区域 - 还原HTML设计样式"""
|
||||
|
||||
files_dropped = Signal(list)
|
||||
|
||||
def __init__(self, accept_types: str = "所有文件", parent=None):
|
||||
super().__init__(parent)
|
||||
self.accept_types = accept_types
|
||||
self.setAcceptDrops(True)
|
||||
self.setMinimumHeight(208) # h-52 = 13rem = 208px
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self._hovered = False
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# 图标容器 (圆形背景)
|
||||
self.icon_container = QFrame()
|
||||
self.icon_container.setFixedSize(56, 56)
|
||||
self.icon_container.setStyleSheet("""
|
||||
background: #334155;
|
||||
border-radius: 28px;
|
||||
""")
|
||||
icon_layout = QVBoxLayout(self.icon_container)
|
||||
icon_layout.setContentsMargins(0, 0, 0, 0)
|
||||
icon_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 云上传图标
|
||||
icon_label = QLabel("☁️")
|
||||
icon_label.setFont(QFont("Segoe UI Emoji", 24))
|
||||
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
icon_label.setStyleSheet("background: transparent;")
|
||||
icon_layout.addWidget(icon_label)
|
||||
|
||||
layout.addWidget(self.icon_container, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 主文字
|
||||
self.title_label = QLabel("点击或拖拽文件到此处")
|
||||
self.title_label.setStyleSheet("color: white; font-size: 16px; font-weight: 500; background: transparent;")
|
||||
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.title_label)
|
||||
|
||||
# 副文字
|
||||
self.desc_label = QLabel(f"支持批量上传 ({self.accept_types})")
|
||||
self.desc_label.setStyleSheet("color: #64748b; font-size: 13px; background: transparent;")
|
||||
self.desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.desc_label)
|
||||
|
||||
self.update_style()
|
||||
|
||||
def update_style(self):
|
||||
if self._hovered:
|
||||
self.setStyleSheet("""
|
||||
UploadArea {
|
||||
background: rgba(30, 41, 59, 0.5);
|
||||
border: 2px dashed rgba(251, 191, 36, 0.5);
|
||||
border-radius: 16px;
|
||||
}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet("""
|
||||
UploadArea {
|
||||
background: rgba(30, 41, 59, 0.3);
|
||||
border: 2px dashed #334155;
|
||||
border-radius: 16px;
|
||||
}
|
||||
""")
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self.update_style()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.update_style()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.open_file_dialog()
|
||||
|
||||
def open_file_dialog(self):
|
||||
files, _ = QFileDialog.getOpenFileNames(
|
||||
self, "选择文件", "", f"{self.accept_types}"
|
||||
)
|
||||
if files:
|
||||
self.files_dropped.emit(files)
|
||||
|
||||
def dragEnterEvent(self, event: QDragEnterEvent):
|
||||
if event.mimeData().hasUrls():
|
||||
event.acceptProposedAction()
|
||||
self._hovered = True
|
||||
self.update_style()
|
||||
|
||||
def dragLeaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.update_style()
|
||||
|
||||
def dropEvent(self, event: QDropEvent):
|
||||
self._hovered = False
|
||||
self.update_style()
|
||||
files = []
|
||||
for url in event.mimeData().urls():
|
||||
if url.isLocalFile():
|
||||
files.append(url.toLocalFile())
|
||||
if files:
|
||||
self.files_dropped.emit(files)
|
||||
|
||||
|
||||
class GlassHeader(QFrame):
|
||||
"""毛玻璃效果头部"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFixedHeight(80)
|
||||
self.setStyleSheet("""
|
||||
GlassHeader {
|
||||
background: rgba(30, 41, 59, 0.7);
|
||||
border-bottom: 1px solid rgba(51, 65, 85, 0.3);
|
||||
}
|
||||
""")
|
||||
|
||||
|
||||
class BaseWorkspace(QWidget):
|
||||
"""工作区基类"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("workspace")
|
||||
self.setup_base_ui()
|
||||
|
||||
def setup_base_ui(self):
|
||||
# 主布局
|
||||
self.main_layout = QVBoxLayout(self)
|
||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_layout.setSpacing(0)
|
||||
|
||||
# 背景装饰层
|
||||
self.bg_decoration = BackgroundDecoration(self)
|
||||
|
||||
# 头部区域
|
||||
self.header = GlassHeader()
|
||||
header_layout = QVBoxLayout(self.header)
|
||||
header_layout.setContentsMargins(32, 16, 32, 16)
|
||||
header_layout.setSpacing(4)
|
||||
|
||||
# 面包屑
|
||||
breadcrumb_layout = QHBoxLayout()
|
||||
breadcrumb_layout.setSpacing(8)
|
||||
|
||||
self.breadcrumb_category = QLabel("首页")
|
||||
self.breadcrumb_category.setStyleSheet("color: #64748b; font-size: 12px;")
|
||||
breadcrumb_layout.addWidget(self.breadcrumb_category)
|
||||
|
||||
arrow = QLabel("›")
|
||||
arrow.setStyleSheet("color: #64748b; font-size: 12px;")
|
||||
breadcrumb_layout.addWidget(arrow)
|
||||
|
||||
self.breadcrumb_tool = QLabel("控制台")
|
||||
self.breadcrumb_tool.setStyleSheet("color: #fbbf24; font-size: 12px;")
|
||||
breadcrumb_layout.addWidget(self.breadcrumb_tool)
|
||||
|
||||
breadcrumb_layout.addStretch()
|
||||
header_layout.addLayout(breadcrumb_layout)
|
||||
|
||||
# 标题行
|
||||
title_layout = QHBoxLayout()
|
||||
|
||||
# 标题 + 版本标签
|
||||
title_container = QHBoxLayout()
|
||||
title_container.setSpacing(8)
|
||||
|
||||
self.title_label = QLabel("欢迎回来")
|
||||
self.title_label.setStyleSheet("color: white; font-size: 22px; font-weight: 700;")
|
||||
title_container.addWidget(self.title_label)
|
||||
|
||||
self.version_label = QLabel("v2.1")
|
||||
self.version_label.setStyleSheet("""
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
""")
|
||||
self.version_label.setVisible(False)
|
||||
title_container.addWidget(self.version_label)
|
||||
title_container.addStretch()
|
||||
|
||||
title_layout.addLayout(title_container, 1)
|
||||
|
||||
# 按钮区域
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.setSpacing(12)
|
||||
|
||||
self.history_btn = QPushButton("🕐 历史")
|
||||
self.history_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: transparent;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
color: #cbd5e1;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #334155;
|
||||
}
|
||||
""")
|
||||
btn_layout.addWidget(self.history_btn)
|
||||
|
||||
self.export_btn = QPushButton("📤 导出")
|
||||
self.export_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #f59e0b;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #fbbf24;
|
||||
}
|
||||
""")
|
||||
btn_layout.addWidget(self.export_btn)
|
||||
|
||||
title_layout.addLayout(btn_layout)
|
||||
header_layout.addLayout(title_layout)
|
||||
|
||||
self.main_layout.addWidget(self.header)
|
||||
|
||||
# 内容区域
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
scroll_area.setStyleSheet("background: transparent;")
|
||||
|
||||
self.content_widget = QWidget()
|
||||
self.content_widget.setStyleSheet("background: transparent;")
|
||||
self.content_layout = QVBoxLayout(self.content_widget)
|
||||
self.content_layout.setContentsMargins(32, 24, 32, 24)
|
||||
self.content_layout.setSpacing(24)
|
||||
|
||||
scroll_area.setWidget(self.content_widget)
|
||||
self.main_layout.addWidget(scroll_area, 1)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
# 调整背景装饰大小
|
||||
self.bg_decoration.setGeometry(0, 0, self.width(), self.height())
|
||||
self.bg_decoration.lower()
|
||||
|
||||
def set_breadcrumb(self, category: str, tool: str):
|
||||
self.breadcrumb_category.setText(category)
|
||||
self.breadcrumb_tool.setText(tool)
|
||||
|
||||
def set_title(self, title: str):
|
||||
self.title_label.setText(title)
|
||||
self.version_label.setVisible(bool(title and title != "欢迎回来"))
|
||||
|
||||
def clear_content(self):
|
||||
while self.content_layout.count():
|
||||
item = self.content_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
|
||||
class ShortcutCard(QFrame):
|
||||
"""快捷入口卡片"""
|
||||
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, data: dict, parent=None):
|
||||
super().__init__(parent)
|
||||
self.data = data
|
||||
self.setFixedSize(200, 140)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self._hovered = False
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# 图标
|
||||
color = self.data.get("color", "#64748b")
|
||||
self.icon_frame = QFrame()
|
||||
self.icon_frame.setFixedSize(40, 40)
|
||||
r, g, b = self.hex_to_rgb(color)
|
||||
self.icon_frame.setStyleSheet(f"""
|
||||
background: rgba({r}, {g}, {b}, 0.1);
|
||||
border-radius: 8px;
|
||||
""")
|
||||
icon_layout = QVBoxLayout(self.icon_frame)
|
||||
icon_layout.setContentsMargins(0, 0, 0, 0)
|
||||
icon_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
icon = QLabel(self.data.get("icon", ""))
|
||||
icon.setFont(QFont("Segoe UI Emoji", 16))
|
||||
icon.setStyleSheet(f"color: {color}; background: transparent;")
|
||||
icon_layout.addWidget(icon)
|
||||
|
||||
layout.addWidget(self.icon_frame)
|
||||
|
||||
# 名称
|
||||
name = QLabel(self.data.get("name", ""))
|
||||
name.setStyleSheet("color: white; font-size: 14px; font-weight: 500;")
|
||||
layout.addWidget(name)
|
||||
|
||||
# 描述
|
||||
desc = QLabel(self.data.get("desc", ""))
|
||||
desc.setStyleSheet("color: #64748b; font-size: 11px;")
|
||||
layout.addWidget(desc)
|
||||
|
||||
layout.addStretch()
|
||||
self.update_style()
|
||||
|
||||
def update_style(self):
|
||||
if self._hovered:
|
||||
self.setStyleSheet("""
|
||||
ShortcutCard {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border: 1px solid rgba(251, 191, 36, 0.5);
|
||||
border-radius: 16px;
|
||||
}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet("""
|
||||
ShortcutCard {
|
||||
background: rgba(30, 41, 59, 0.5);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 16px;
|
||||
}
|
||||
""")
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self.update_style()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.update_style()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.clicked.emit()
|
||||
|
||||
@staticmethod
|
||||
def hex_to_rgb(hex_color: str) -> tuple:
|
||||
hex_color = hex_color.lstrip('#')
|
||||
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
|
||||
|
||||
class WelcomeWorkspace(BaseWorkspace):
|
||||
"""欢迎页面工作区"""
|
||||
|
||||
tool_clicked = Signal(str, str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_welcome_ui()
|
||||
|
||||
def setup_welcome_ui(self):
|
||||
self.set_breadcrumb("首页", "控制台")
|
||||
self.set_title("欢迎回来")
|
||||
self.version_label.setVisible(False)
|
||||
|
||||
self.history_btn.hide()
|
||||
self.export_btn.hide()
|
||||
|
||||
# 中心内容
|
||||
center_widget = QWidget()
|
||||
center_layout = QVBoxLayout(center_widget)
|
||||
center_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
center_layout.setSpacing(24)
|
||||
|
||||
# Logo区域
|
||||
logo_container = QFrame()
|
||||
logo_container.setFixedSize(128, 128)
|
||||
logo_container.setStyleSheet("""
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 64px;
|
||||
""")
|
||||
logo_layout = QVBoxLayout(logo_container)
|
||||
logo_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
logo = QLabel("🧀")
|
||||
logo.setFont(QFont("Segoe UI Emoji", 48))
|
||||
logo.setStyleSheet("color: #fbbf24; background: transparent;")
|
||||
logo_layout.addWidget(logo)
|
||||
|
||||
center_layout.addWidget(logo_container, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 标题
|
||||
title = QLabel("开始你的创作")
|
||||
title.setFont(QFont("Microsoft YaHei", 28, QFont.Weight.Bold))
|
||||
title.setStyleSheet("color: white;")
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
center_layout.addWidget(title)
|
||||
|
||||
# 描述
|
||||
desc = QLabel("从左侧选择一个工具。无论是压缩图片、拆分PDF还是处理数据,我们都能搞定。")
|
||||
desc.setStyleSheet("color: #94a3b8; font-size: 14px;")
|
||||
desc.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
desc.setWordWrap(True)
|
||||
desc.setMaximumWidth(500)
|
||||
center_layout.addWidget(desc)
|
||||
|
||||
center_layout.addSpacing(20)
|
||||
|
||||
# 快捷入口
|
||||
shortcuts_layout = QHBoxLayout()
|
||||
shortcuts_layout.setSpacing(16)
|
||||
shortcuts_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
shortcuts = [
|
||||
{"id": "img-compress", "category": "image", "name": "图片压缩", "desc": "智能无损压缩", "icon": "🖼", "color": "#3b82f6"},
|
||||
{"id": "pdf-split", "category": "pdf", "name": "PDF 拆分", "desc": "提取特定页面", "icon": "✂️", "color": "#ef4444"},
|
||||
{"id": "img-convert", "category": "image", "name": "格式转换", "desc": "格式互转工具", "icon": "🔄", "color": "#22c55e"},
|
||||
]
|
||||
|
||||
for item in shortcuts:
|
||||
card = ShortcutCard(item)
|
||||
card.clicked.connect(lambda i=item: self.tool_clicked.emit(i["id"], i["category"]))
|
||||
shortcuts_layout.addWidget(card)
|
||||
|
||||
center_layout.addLayout(shortcuts_layout)
|
||||
|
||||
self.content_layout.addStretch()
|
||||
self.content_layout.addWidget(center_widget)
|
||||
self.content_layout.addStretch()
|
||||
33
version_info.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
# UTF-8
|
||||
VSVersionInfo(
|
||||
ffi=FixedFileInfo(
|
||||
filevers=(1, 0, 0, 0),
|
||||
prodvers=(1, 0, 0, 0),
|
||||
mask=0x3f,
|
||||
flags=0x0,
|
||||
OS=0x40004,
|
||||
fileType=0x1,
|
||||
subtype=0x0,
|
||||
date=(0, 0)
|
||||
),
|
||||
kids=[
|
||||
StringFileInfo(
|
||||
[
|
||||
StringTable(
|
||||
u'080404b0',
|
||||
[
|
||||
StringStruct(u'CompanyName', u'奶酪源码'),
|
||||
StringStruct(u'FileDescription', u'多功能图片/PDF/Excel处理工具'),
|
||||
StringStruct(u'FileVersion', u'1.0.0'),
|
||||
StringStruct(u'InternalName', u'CheeseCloudTools'),
|
||||
StringStruct(u'LegalCopyright', u'Copyright (C) 2024 奶酪源码'),
|
||||
StringStruct(u'OriginalFilename', u'CheeseCloudTools.exe'),
|
||||
StringStruct(u'ProductName', u'奶酪云工具箱'),
|
||||
StringStruct(u'ProductVersion', u'1.0.0'),
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
VarFileInfo([VarStruct(u'Translation', [2052, 1200])])
|
||||
]
|
||||
)
|
||||