初始化

This commit is contained in:
李琦
2026-06-23 15:33:22 +08:00
parent fb3d707a32
commit 3882f33058
31 changed files with 2237 additions and 279 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
build/bin
node_modules
frontend/dist
/.opencode/
/.mimocode/
/.qoder/

119
README.md
View File

@@ -1,19 +1,114 @@
# README
# 年糕工具
## About
一站式文件处理工具箱,支持 PDF、Word、Excel、图片的格式转换、编辑和处理。
This is the official Wails Vue template.
## 功能特性
You can configure the project by editing `wails.json`. More information about the project settings can be found
here: https://wails.io/docs/reference/project-config
### PDF 工具
- PDF 瘦身(压缩优化)
- PDF 转图片
- PDF 转 Word / Excel / HTML / Markdown
- PDF 提取文本
- PDF 合并
- PDF 分割(按页码范围)
- PDF 旋转
- PDF 添加水印
- 文本 / Markdown / HTML 转 PDF
## Live Development
### 图片工具
- 图片格式转换JPEG / PNG / GIF / BMP / TIFF
- 图片压缩(预设:高质量 / 中等 / 小文件 / 极小)
- 图片调整大小
- 图片旋转
- 图片裁剪
- 灰度处理
- 亮度 / 对比度 / 饱和度调节
- 锐化 / 模糊 / 反色
- 背景去除
- 图片转 PDF
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
to this in your browser, and you can call your Go code from devtools.
### 其他
- Word 转 PDF
- Excel 转 CSV / JSON
- 运行日志(支持日期筛选)
## Building
## 技术栈
To build a redistributable, production mode package, use `wails build`.
- **后端**: Go + Wails v2
- **前端**: Vue 3 + Element Plus + Vite
- **PDF 处理**: pdfcpu纯 Go
- **图片处理**: imaging
- **文本渲染**: gopdf
## 开发环境
### 前置条件
- Go 1.25+
- Node.js
- Wails CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest`
### 开发模式
```bash
wails dev
```
### 打包构建
```bash
.\build.ps1
```
构建产物位于 `build\bin\年糕工具.exe`单文件MuPDF DLL 已嵌入)。
### 手动构建
```powershell
# 1. 生成绑定nofitz 避免 DLL 依赖检查)
wails build -tags nofitz
# 2. 复制 DLL 到构建目录(嵌入用)
Copy-Item public\MuPDFLib.dll build\bin\libmupdf.dll
# 3. 最终构建skipbindings 跳过重新生成)
wails build -skipbindings
# 4. 重命名
Copy-Item build\bin\xk.exe "build\bin\年糕工具.exe"
```
## 项目结构
```
├── main.go # 应用入口
├── app.go # App 生命周期
├── handlers.go # 文件处理 handlerIPC 接口)
├── services/
│ ├── pdf_service.go # PDF 处理pdfcpu
│ ├── image_service.go # 图片处理imaging
│ ├── word_service.go # Word 处理
│ └── excel_service.go # Excel 处理
├── frontend/src/
│ ├── App.vue # 根组件
│ ├── components/
│ │ ├── ToolView.vue # 工具主页面
│ │ ├── HomeView.vue # 首页
│ │ ├── SettingsView.vue # 设置 + 日志查看
│ │ ├── image/ # 图片工具组件
│ │ └── pdf/ # PDF 工具组件
│ └── router/index.js # 路由配置
├── public/
│ └── MuPDFLib.dll # MuPDF 库(嵌入到 exe
└── build.ps1 # 构建脚本
```
## 日志
运行日志存储在 exe 同级 `logs/` 目录下,按日期分文件(`YYYY-MM-DD.log`)。
可在设置页面查看日志并按日期筛选。
## 许可证
MIT

10
app.go
View File

@@ -3,6 +3,8 @@ package main
import (
"context"
"fmt"
"os"
"path/filepath"
)
type App struct {
@@ -20,3 +22,11 @@ func (a *App) startup(ctx context.Context) {
func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, It's show time!", name)
}
func getExeDir() string {
exe, err := os.Executable()
if err != nil {
return "."
}
return filepath.Dir(exe)
}

28
build.ps1 Normal file
View File

@@ -0,0 +1,28 @@
# 年糕工具 Build Script
# Usage: .\build.ps1
$ErrorActionPreference = "Stop"
$buildDir = "build\bin"
Write-Host ""
Write-Host "=== 年糕工具 Build ===" -ForegroundColor Cyan
Write-Host ""
# Build with nofitz (go-fitz excluded, DLL embedded)
Write-Host "Building..." -ForegroundColor Yellow
wails build -tags nofitz
if ($LASTEXITCODE -ne 0) { Write-Host "Build failed!" -ForegroundColor Red; exit 1 }
# Rename using cmd to avoid encoding issues
cmd /c "copy /Y `"$buildDir\xk.exe`" `"$buildDir\年糕工具.exe`""
cmd /c "del /f /q `"$buildDir\xk.exe`""
cmd /c "del /f /q `"$buildDir\xk-dev.exe`""
# Remove standalone DLL (it's embedded now)
cmd /c "del /f /q `"$buildDir\libmupdf.dll`""
cmd /c "del /f /q `"$buildDir\MuPDFLib.dll`""
Write-Host ""
Write-Host "=== Build Complete ===" -ForegroundColor Green
Write-Host " $buildDir\年糕工具.exe (single file, DLL embedded)" -ForegroundColor Gray
Write-Host ""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 992 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 817 B

View File

@@ -263,7 +263,6 @@ function closeWindow() {
background: var(--bg-primary);
}
/* Page Transition */
.page-fade-enter-active,
.page-fade-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;

View File

@@ -101,7 +101,7 @@ function formatTime(ts) {
<div class="home-header animate-slide-up">
<div class="home-title">
<el-icon :size="28"><SetUp /></el-icon>
<span>XK 文件工具</span>
<span>年糕工具</span>
</div>
<el-button text @click="router.push('/settings')">
<el-icon><Setting /></el-icon>

View File

@@ -1,17 +1,42 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
const router = useRouter()
const config = ref({ defaultOutputDir: '' })
const logDates = ref([])
const selectedDate = ref('')
const logContent = ref('')
onMounted(async () => {
try {
config.value = await window.go.main.FileHandler.GetConfig()
} catch (e) {}
await loadLogDates()
})
async function loadLogDates() {
try {
logDates.value = await window.go.main.FileHandler.GetLogDates()
if (logDates.value.length > 0 && !selectedDate.value) {
selectedDate.value = logDates.value[logDates.value.length - 1]
await loadLogs()
}
} catch (e) {}
}
async function loadLogs() {
if (!selectedDate.value) { logContent.value = ''; return }
try {
logContent.value = await window.go.main.FileHandler.GetLogs(selectedDate.value)
} catch (e) {
logContent.value = ''
}
}
watch(selectedDate, () => loadLogs())
async function selectDir() {
try {
const path = await window.go.main.FileHandler.OpenFileDialog('选择默认输出目录', ['*'])
@@ -83,7 +108,7 @@ function resetConfig() {
<div class="about-info">
<div class="about-row">
<span class="about-label">应用名称</span>
<span class="about-value">XK 文件工具</span>
<span class="about-value">年糕工具</span>
</div>
<div class="about-row">
<span class="about-label">版本</span>
@@ -95,6 +120,24 @@ function resetConfig() {
</div>
</div>
</div>
<div class="settings-card">
<div class="card-title">运行日志</div>
<div class="card-desc">查看应用运行日志用于排查问题</div>
<div class="form-group">
<div class="form-label">选择日期</div>
<div class="tag-group">
<div v-for="date in logDates" :key="date" class="tag-option" :class="{ active: selectedDate === date }" @click="selectedDate = date">{{ date }}</div>
<div v-if="logDates.length === 0" class="no-logs">暂无日志</div>
</div>
</div>
<div class="log-viewer" v-if="logContent">
<pre class="log-content">{{ logContent }}</pre>
</div>
<div v-else class="no-logs">选择日期查看日志</div>
</div>
</div>
</div>
</template>
@@ -224,4 +267,37 @@ function resetConfig() {
font-size: 13px;
color: var(--text-primary);
}
.tag-group { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
.tag-option {
display: flex; align-items: center; gap: 4px; padding: 7px 14px;
border-radius: var(--radius-sm); font-size: 13px; font-weight: 500;
cursor: pointer; background: var(--bg-surface); color: var(--text-secondary);
border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none;
}
.tag-option:hover { color: var(--text-primary); border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
.tag-option.active { background: var(--accent-primary); color: #ffffff; border-color: var(--accent-primary); }
.log-viewer {
background: #1a1a25;
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
padding: 16px;
max-height: 400px;
overflow-y: auto;
}
.log-content {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
color: var(--text-secondary);
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
}
.no-logs {
font-size: 13px;
color: var(--text-muted);
padding: 12px 0;
}
</style>

View File

@@ -14,6 +14,16 @@ import ImageBlurTool from './image/ImageBlurTool.vue'
import ImageInvertTool from './image/ImageInvertTool.vue'
import ImageConvertTool from './image/ImageConvertTool.vue'
import ImageRemoveBgTool from './image/ImageRemoveBgTool.vue'
import ImageToPdfTool from './image/ImageToPdfTool.vue'
import PdfCompressTool from './pdf/PdfCompressTool.vue'
import PdfToImageTool from './pdf/PdfToImageTool.vue'
import PdfExtractTextTool from './pdf/PdfExtractTextTool.vue'
import PdfSplitTool from './pdf/PdfSplitTool.vue'
import PdfRotateTool from './pdf/PdfRotateTool.vue'
import PdfWatermarkTool from './pdf/PdfWatermarkTool.vue'
import PdfMergeTool from './pdf/PdfMergeTool.vue'
import PdfFromTextTool from './pdf/PdfFromTextTool.vue'
const route = useRoute()
const router = useRouter()
@@ -22,6 +32,7 @@ const category = computed(() => route.params.category || 'pdf')
const action = ref(route.params.action || '')
const filePath = ref('')
const fileLoading = ref(false)
const filePreview = ref('')
const imageInfo = ref(null)
const processing = ref(false)
@@ -48,10 +59,18 @@ const categoryConfig = {
pdf: {
name: 'PDF 工具',
actions: [
{ id: 'compress', name: '压缩PDF', icon: 'FolderChecked', color: '#f38ba8' },
{ id: 'split', name: '分割PDF', icon: 'Document', color: '#f38ba8' },
{ id: 'compress', name: 'PDF瘦身', icon: 'FolderChecked', color: '#f38ba8' },
{ id: 'toImage', name: '转图片', icon: 'Picture', color: '#f38ba8' },
{ id: 'toWord', name: '转Word', icon: 'Document', color: '#f38ba8' },
{ id: 'toExcel', name: '转Excel', icon: 'Grid', color: '#f38ba8' },
{ id: 'toHtml', name: '转HTML', icon: 'Monitor', color: '#f38ba8' },
{ id: 'toMarkdown', name: '转Markdown', icon: 'Notebook', color: '#f38ba8' },
{ id: 'toText', name: '提取文本', icon: 'Memo', color: '#f38ba8' },
{ id: 'merge', name: '合并PDF', icon: 'DocumentCopy', color: '#f38ba8' },
{ id: 'split', name: '分割PDF', icon: 'Scissor', color: '#f38ba8' },
{ id: 'rotate', name: '旋转PDF', icon: 'RefreshRight', color: '#f38ba8' },
{ id: 'watermark', name: '添加水印', icon: 'EditPen', color: '#f38ba8' },
{ id: 'fromText', name: '文本转PDF', icon: 'Upload', color: '#f38ba8' },
],
fileFilters: ['*.pdf'],
},
@@ -84,6 +103,7 @@ const categoryConfig = {
{ id: 'blur', name: '模糊', icon: 'View', color: '#f9e2af' },
{ id: 'invert', name: '反色', icon: 'RefreshLeft', color: '#f9e2af' },
{ id: 'removebg', name: '背景去除', icon: 'MagicStick', color: '#f9e2af' },
{ id: 'toPdf', name: '转PDF', icon: 'Document', color: '#f9e2af' },
],
fileFilters: ['*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff;*.webp;*.ico'],
},
@@ -151,15 +171,25 @@ function setAction(id) {
async function selectFile() {
try {
const path = await window.go.main.FileHandler.OpenFileDialog('选择文件', currentCat.value.fileFilters)
let filters = currentCat.value.fileFilters
if (category.value === 'pdf' && action.value === 'fromText') {
filters = ['*.txt;*.md;*.html']
}
const path = await window.go.main.FileHandler.OpenFileDialog('选择文件', filters)
if (path) {
filePath.value = path
resetState()
filePath.value = path
outputPath.value = ''
await loadFilePreview()
fileLoading.value = true
try {
await loadFilePreview()
} finally {
fileLoading.value = false
}
}
} catch (e) {
ElMessage.error('选择文件失败: ' + e.message)
try { await window.go.main.FileHandler.LogError('[SelectFile] ' + e.message) } catch (err) {}
}
}
@@ -203,13 +233,23 @@ async function selectOutputPath() {
}
function getOutputFilters() {
if (category.value === 'pdf') return ['*.pdf']
if (category.value === 'pdf') {
if (action.value === 'toImage') return ['*.png', '*.jpg;*.jpeg']
if (action.value === 'toWord') return ['*.txt']
if (action.value === 'toExcel') return ['*.csv']
if (action.value === 'toHtml') return ['*.html']
if (action.value === 'toMarkdown') return ['*.md']
if (action.value === 'toText') return ['*.txt']
if (action.value === 'fromText') return ['*.pdf']
return ['*.pdf']
}
if (category.value === 'word') return ['*.pdf']
if (category.value === 'excel') {
return action.value === 'json' ? ['*.json'] : ['*.csv']
}
if (category.value === 'image') {
if (action.value === 'convert') return ['*.jpg;*.jpeg', '*.png', '*.gif', '*.bmp', '*.tiff', '*.ico']
if (action.value === 'toPdf') return ['*.pdf']
return ['*.jpg;*.jpeg', '*.png']
}
return ['*']
@@ -223,11 +263,20 @@ function getOutputDefaultName() {
}
function getOutputExt() {
if (category.value === 'pdf') return '.pdf'
if (category.value === 'pdf') {
if (action.value === 'toImage') return '.png'
if (action.value === 'toWord') return '.txt'
if (action.value === 'toExcel') return '.csv'
if (action.value === 'toHtml') return '.html'
if (action.value === 'toMarkdown') return '.md'
if (action.value === 'toText') return '.txt'
return '.pdf'
}
if (category.value === 'word') return '.pdf'
if (category.value === 'excel') return action.value === 'json' ? '.json' : '.csv'
if (category.value === 'image') {
if (action.value === 'convert') return '.' + outputFormat.value
if (action.value === 'toPdf') return '.pdf'
return '.png'
}
return ''
@@ -289,11 +338,13 @@ async function processFile() {
})
ElMessage.success('处理完成')
try { await window.go.main.FileHandler.LogInfo('[Process] ' + req.format + ' -> ' + result.path) } catch (err) {}
} else {
ElMessage.error(result.message)
}
} catch (e) {
ElMessage.error('处理失败: ' + e.message)
try { await window.go.main.FileHandler.LogError('[ProcessFile] ' + e.message) } catch (err) {}
} finally {
processing.value = false
}
@@ -343,11 +394,13 @@ async function handleImageProcess(req) {
})
ElMessage.success('处理完成')
try { await window.go.main.FileHandler.LogInfo('[Process] ' + req.format + ' -> ' + result.path) } catch (err) {}
} else {
ElMessage.error(result.message)
}
} catch (e) {
ElMessage.error('处理失败: ' + e.message)
try { await window.go.main.FileHandler.LogError('[ProcessFile] ' + e.message) } catch (err) {}
} finally {
processing.value = false
}
@@ -444,6 +497,14 @@ const imageToolProps = computed(() => ({
config: config.value,
}))
const pdfToolProps = computed(() => ({
filePath: filePath.value,
processing: processing.value,
resultInfo: resultInfo.value,
outputPath: outputPath.value,
config: config.value,
}))
const isImageWorkspaceTool = computed(() => {
return category.value === 'image' && ['compress', 'resize', 'rotate', 'crop', 'grayscale', 'brightness', 'sharpen', 'blur', 'invert'].includes(action.value) && !!filePath.value
})
@@ -482,53 +543,81 @@ const isImageRemoveBgTool = computed(() => category.value === 'image' && action.
</div>
<div class="tool-body">
<ImageCompressTool v-if="action === 'compress' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageResizeTool v-else-if="action === 'resize' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageRotateTool v-else-if="action === 'rotate' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageCropTool v-else-if="action === 'crop' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageGrayscaleTool v-else-if="action === 'grayscale' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageBrightnessTool v-else-if="action === 'brightness' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageSharpenTool v-else-if="action === 'sharpen' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageBlurTool v-else-if="action === 'blur' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageInvertTool v-else-if="action === 'invert' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageConvertTool v-else-if="action === 'convert' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageRemoveBgTool v-else-if="action === 'removebg' && category === 'image'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<div v-else class="tool-main">
<div class="file-drop-zone" @click="selectFile" :class="{ 'has-file': filePath }">
<template v-if="!filePath">
<template v-if="category === 'image'">
<ImageCompressTool v-if="action === 'compress'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageResizeTool v-else-if="action === 'resize'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageRotateTool v-else-if="action === 'rotate'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageCropTool v-else-if="action === 'crop'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageGrayscaleTool v-else-if="action === 'grayscale'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageBrightnessTool v-else-if="action === 'brightness'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageSharpenTool v-else-if="action === 'sharpen'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageBlurTool v-else-if="action === 'blur'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageInvertTool v-else-if="action === 'invert'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageConvertTool v-else-if="action === 'convert'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageRemoveBgTool v-else-if="action === 'removebg'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<ImageToPdfTool v-else-if="action === 'toPdf'" v-bind="imageToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<div v-else class="tool-main">
<div class="file-drop-zone" @click="selectFile">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">点击选择文件</div>
<div class="drop-hint">支持拖拽文件到此处</div>
</template>
<template v-else>
<div class="file-info">
<el-icon :size="28" :style="{ color: currentAction?.color }">
<component :is="currentAction?.icon || 'Document'" />
</el-icon>
<div class="file-details">
<div class="file-name">{{ getFileName() }}</div>
<div class="file-path">{{ filePath }}</div>
</div>
<el-button type="danger" text @click.stop="filePath = ''; resetState()">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</template>
</div>
<div class="params-section">
<div class="param-group" v-if="showOutputFormat">
<div class="param-label">目标格式</div>
<div class="tag-group">
<div class="tag-option" :class="{ active: outputFormat === 'jpeg' }" @click="outputFormat = 'jpeg'">JPEG</div>
<div class="tag-option" :class="{ active: outputFormat === 'png' }" @click="outputFormat = 'png'">PNG</div>
<div class="tag-option" :class="{ active: outputFormat === 'gif' }" @click="outputFormat = 'gif'">GIF</div>
<div class="tag-option" :class="{ active: outputFormat === 'bmp' }" @click="outputFormat = 'bmp'">BMP</div>
<div class="tag-option" :class="{ active: outputFormat === 'tiff' }" @click="outputFormat = 'tiff'">TIFF</div>
<div class="tag-option" :class="{ active: outputFormat === 'ico' }" @click="outputFormat = 'ico'">ICO</div>
</div>
<div class="drop-text">选择图片工具</div>
</div>
</div>
</template>
<template v-else-if="category === 'pdf'">
<PdfCompressTool v-if="action === 'compress'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<PdfToImageTool v-else-if="action === 'toImage'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<PdfExtractTextTool v-else-if="action === 'toText'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<PdfSplitTool v-else-if="action === 'split'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<PdfRotateTool v-else-if="action === 'rotate'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<PdfWatermarkTool v-else-if="action === 'watermark'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<PdfMergeTool v-else-if="action === 'merge'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<PdfFromTextTool v-else-if="action === 'fromText'" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<PdfExtractTextTool v-else-if="['toWord', 'toExcel', 'toHtml', 'toMarkdown'].includes(action)" v-bind="pdfToolProps" @process="handleImageProcess" @selectFile="selectFile" />
<div v-else class="tool-main">
<div class="file-drop-zone" @click="selectFile">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">选择 PDF 工具</div>
</div>
</div>
</template>
<template v-else>
<div class="tool-main">
<div class="file-drop-zone" @click="selectFile" :class="{ 'has-file': filePath }">
<template v-if="!filePath">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">点击选择文件</div>
<div class="drop-hint">支持拖拽文件到此处</div>
</template>
<template v-else>
<div class="file-info">
<el-icon :size="28" :style="{ color: currentAction?.color }">
<component :is="currentAction?.icon || 'Document'" />
</el-icon>
<div class="file-details">
<div class="file-name">{{ getFileName() }}</div>
<div class="file-path">{{ filePath }}</div>
</div>
<el-button type="danger" text @click.stop="filePath = ''; resetState()">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</template>
</div>
<div class="params-section">
<div class="param-group" v-if="showOutputFormat">
<div class="param-label">目标格式</div>
<div class="tag-group">
<div class="tag-option" :class="{ active: outputFormat === 'jpeg' }" @click="outputFormat = 'jpeg'">JPEG</div>
<div class="tag-option" :class="{ active: outputFormat === 'png' }" @click="outputFormat = 'png'">PNG</div>
<div class="tag-option" :class="{ active: outputFormat === 'gif' }" @click="outputFormat = 'gif'">GIF</div>
<div class="tag-option" :class="{ active: outputFormat === 'bmp' }" @click="outputFormat = 'bmp'">BMP</div>
<div class="tag-option" :class="{ active: outputFormat === 'tiff' }" @click="outputFormat = 'tiff'">TIFF</div>
<div class="tag-option" :class="{ active: outputFormat === 'ico' }" @click="outputFormat = 'ico'">ICO</div>
</div>
</div>
<div class="param-group" v-if="showQuality && category === 'pdf'">
<div class="param-label">质量</div>
@@ -569,8 +658,10 @@ const isImageRemoveBgTool = computed(() => category.value === 'image' && action.
{{ processing ? '处理中...' : '开始处理' }}
</el-button>
</div>
</template>
</div>
<transition name="fade-slide">
<transition name="fade-slide">
<div class="result-panel" v-if="resultInfo">
<div class="result-header">
<el-icon :size="20" style="color: #3fb950" class="success-icon"><SuccessFilled /></el-icon>
@@ -634,7 +725,6 @@ const isImageRemoveBgTool = computed(() => category.value === 'image' && action.
</div>
</div>
</transition>
</div>
<transition name="fade">
<div class="preview-overlay" v-if="showPreview" @click.self="closePreview">
@@ -688,6 +778,15 @@ const isImageRemoveBgTool = computed(() => category.value === 'image' && action.
</div>
</div>
</transition>
<transition name="fade">
<div class="loading-overlay" v-if="fileLoading">
<div class="loading-card">
<div class="spinner"></div>
<div class="loading-text">正在加载文件...</div>
</div>
</div>
</transition>
</div>
</template>

View File

@@ -22,12 +22,19 @@ const aspectLocked = ref(false)
const customWidth = ref(800)
const customHeight = ref(600)
watch(() => props.originalWidth, (w) => {
if (w) customWidth.value = w
})
watch(() => props.originalHeight, (h) => {
if (h) customHeight.value = h
})
const presets = [
{ name: '高质量', quality: 90, desc: '几乎无损' },
{ name: '中等', quality: 75, desc: '推荐' },
{ name: '小文件', quality: 50, desc: '体积优先' },
{ name: '极小', quality: 30, desc: '极限压缩' },
]
function applyPreset(q) {
qualityInt.value = q
}
watch(() => props.originalWidth, (w) => { if (w) customWidth.value = w })
watch(() => props.originalHeight, (h) => { if (h) customHeight.value = h })
function buildRequest() {
return {
@@ -41,9 +48,7 @@ function buildRequest() {
}
}
function onProcess() {
emit('process', buildRequest())
}
function onProcess() { emit('process', buildRequest()) }
function formatSize(bytes) {
if (!bytes) return '0 B'
@@ -58,24 +63,15 @@ function getFileName() {
return props.filePath.split(/[/\\]/).pop()
}
function onDragOver(e) {
e.preventDefault()
e.currentTarget.classList.add('dragover')
}
function onDragLeave(e) {
e.currentTarget.classList.remove('dragover')
}
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
function onDrop(e) {
e.preventDefault()
e.currentTarget.classList.remove('dragover')
e.preventDefault(); e.currentTarget.classList.remove('dragover')
const files = e.dataTransfer?.files
if (files?.length) {
const ext = files[0].name.split('.').pop().toLowerCase()
if (!['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'ico'].includes(ext)) {
ElMessage.warning('请选择图片文件')
return
ElMessage.warning('请选择图片文件'); return
}
emit('selectFile', files[0].path)
}
@@ -99,15 +95,22 @@ function onDrop(e) {
<div class="image-info-bar">
<span class="image-dims">{{ originalWidth }} × {{ originalHeight }} px</span>
<span class="image-filename">{{ getFileName() }}</span>
<el-button type="danger" text size="small" @click="$emit('selectFile')">
<el-icon><Delete /></el-icon>
</el-button>
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
</div>
</div>
<div class="image-panel">
<div class="panel-section">
<div class="panel-title">压缩设置</div>
<div class="param-group">
<div class="param-label">快速预设</div>
<div class="preset-group">
<button v-for="p in presets" :key="p.quality" class="preset-btn" :class="{ active: qualityInt === p.quality }" @click="applyPreset(p.quality)">
<span class="preset-name">{{ p.name }}</span>
<span class="preset-desc">{{ p.desc }}</span>
</button>
</div>
</div>
<div class="param-group">
<div class="param-label">输出格式</div>
<div class="tag-group">
@@ -135,9 +138,6 @@ function onDrop(e) {
<span class="size-unit">px</span>
</div>
</div>
<div v-if="resultInfo && resultInfo.isImageOp" class="estimated-size">
<span class="estimated-label">处理后大小:</span> {{ resultInfo.size }}
</div>
</div>
<div class="panel-section">
@@ -149,126 +149,50 @@ function onDrop(e) {
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
<el-icon v-if="!processing"><Check /></el-icon>
{{ processing ? '处理中...' : '开始处理' }}
{{ processing ? '处理中...' : '开始压缩' }}
</el-button>
</div>
</div>
</template>
<style scoped>
.file-drop-zone {
border: 2px dashed var(--glass-border);
border-radius: var(--radius-lg);
padding: 80px 40px;
text-align: center;
cursor: pointer;
transition: all var(--transition-normal);
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
max-width: 600px;
margin: 40px auto;
}
.file-drop-zone:hover, .file-drop-zone.dragover {
border-color: var(--accent-primary);
background: rgba(59, 130, 246, 0.05);
box-shadow: 0 0 20px rgba(59, 130, 246, 0.1);
}
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
.drop-hint { font-size: 12px; color: var(--text-muted); }
.image-workspace {
display: grid;
grid-template-columns: 1fr 320px;
gap: 20px;
height: calc(100vh - 160px);
max-height: 800px;
}
.image-canvas-area {
display: flex;
flex-direction: column;
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.canvas-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #1a1a25;
overflow: hidden;
min-height: 400px;
}
.image-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
.image-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
.canvas-wrapper { position: relative; display: inline-block; }
.preview-img { max-width: 100%; max-height: 100%; object-fit: contain; }
.image-info-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
background: var(--glass-bg);
border-top: 1px solid var(--glass-border);
font-size: 13px;
}
.image-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
.image-dims { color: var(--text-secondary); font-family: monospace; }
.image-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.image-panel {
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: 20px;
}
.image-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
.panel-section { display: flex; flex-direction: column; gap: 4px; }
.panel-title {
font-size: 14px; font-weight: 600; color: var(--text-primary);
margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border);
}
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
.param-group { margin-bottom: 12px; }
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
.preset-group { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.preset-btn { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 8px 12px; border-radius: var(--radius-sm); cursor: pointer; background: var(--bg-surface); border: 1px solid var(--glass-border); transition: all var(--transition-normal); }
.preset-btn:hover { border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
.preset-btn.active { background: var(--accent-primary); color: #fff; border-color: var(--accent-primary); }
.preset-name { font-size: 13px; font-weight: 600; }
.preset-desc { font-size: 11px; opacity: 0.7; }
.tag-group { display: flex; gap: 6px; flex-wrap: wrap; }
.tag-option {
display: flex; align-items: center; gap: 4px; padding: 7px 14px;
border-radius: var(--radius-sm); font-size: 13px; font-weight: 500;
cursor: pointer; background: var(--bg-surface); color: var(--text-secondary);
border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none;
}
.tag-option { display: flex; align-items: center; gap: 4px; padding: 7px 14px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; cursor: pointer; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none; }
.tag-option:hover { color: var(--text-primary); border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
.tag-option.active { background: var(--accent-primary); color: #ffffff; border-color: var(--accent-primary); box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); }
.slider-hint { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-muted); margin-top: 4px; }
.aspect-lock-btn {
display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px;
border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;
cursor: pointer; background: var(--bg-surface); border: 1px solid var(--glass-border);
color: var(--text-secondary); transition: all var(--transition-normal);
}
.aspect-lock-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500; cursor: pointer; background: var(--bg-surface); border: 1px solid var(--glass-border); color: var(--text-secondary); transition: all var(--transition-normal); }
.aspect-lock-btn.locked { background: var(--accent-primary); color: #fff; border-color: var(--accent-primary); }
.size-inputs { display: flex; align-items: center; gap: 8px; }
.size-sep { color: var(--text-muted); font-size: 14px; }
.size-unit { color: var(--text-muted); font-size: 13px; }
.output-row { display: flex; gap: 8px; }
.output-row .el-input { flex: 1; }
.process-btn {
width: 100%; height: 48px; font-size: 15px; font-weight: 600;
border-radius: var(--radius-md); background: var(--gradient-primary); border: none;
transition: all var(--transition-normal);
}
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
.estimated-size {
margin-top: 8px; padding: 8px 12px;
background: rgba(59, 130, 246, 0.08); border-radius: var(--radius-sm);
font-size: 13px; color: var(--text-secondary);
}
.estimated-label { font-weight: 500; }
</style>

View File

@@ -0,0 +1,102 @@
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
filePath: String,
filePreview: String,
imageInfo: Object,
originalWidth: Number,
originalHeight: Number,
processing: Boolean,
resultInfo: Object,
outputPath: String,
config: Object,
})
const emit = defineEmits(['update:outputPath', 'process', 'selectFile'])
function buildRequest() {
return { inputPath: props.filePath, outputPath: props.outputPath, format: 'toPdf' }
}
function onProcess() { emit('process', buildRequest()) }
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
function formatSize(bytes) {
if (!bytes) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
function onDrop(e) {
e.preventDefault(); e.currentTarget.classList.remove('dragover')
if (e.dataTransfer?.files?.length) {
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
if (!['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'ico'].includes(ext)) {
ElMessage.warning('请选择图片文件'); return
}
emit('selectFile', e.dataTransfer.files[0].path)
}
}
</script>
<template>
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">点击选择图片</div>
<div class="drop-hint">支持拖拽图片到此处</div>
</div>
<div v-else class="tool-main">
<div class="file-selected-bar" @click="$emit('selectFile')">
<img v-if="filePreview" :src="filePreview" class="file-preview-img" />
<div class="file-preview-info">
<div class="file-name">{{ getFileName() }}</div>
<div v-if="imageInfo" class="file-meta">{{ imageInfo.width }}×{{ imageInfo.height }} · {{ formatSize(imageInfo.size) }}</div>
</div>
<el-button type="danger" text @click.stop="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
</div>
<div class="params-section">
<div class="tip-text">将图片转换为 PDF 文档自动适配 A4 页面大小并居中</div>
</div>
<div class="output-section">
<div class="output-header"><span class="param-label">输出路径</span></div>
<div class="output-row">
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly />
</div>
</div>
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '转换中...' : '开始转换' }}
</el-button>
</div>
</template>
<style scoped>
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
.drop-hint { font-size: 12px; color: var(--text-muted); }
.tool-main { max-width: 900px; margin: 0 auto; }
.file-selected-bar { display: flex; align-items: center; gap: 14px; padding: 12px 16px; border: 1px solid var(--glass-border); border-radius: var(--radius-lg); margin-bottom: 24px; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); cursor: pointer; transition: all var(--transition-normal); }
.file-selected-bar:hover { border-color: var(--accent-primary); }
.file-preview-img { width: 48px; height: 48px; object-fit: cover; border-radius: var(--radius-sm); }
.file-preview-info { flex: 1; min-width: 0; }
.file-name { font-size: 14px; font-weight: 500; color: var(--text-primary); }
.file-meta { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.params-section { margin-bottom: 20px; }
.tip-text { font-size: 13px; color: var(--text-secondary); padding: 12px 16px; background: rgba(59, 130, 246, 0.05); border-radius: var(--radius-sm); border: 1px solid rgba(59, 130, 246, 0.1); }
.output-section { margin-bottom: 24px; }
.output-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
.output-row { display: flex; gap: 8px; }
.output-row .el-input { flex: 1; }
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
</style>

View File

@@ -0,0 +1,113 @@
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
filePath: String,
processing: Boolean,
resultInfo: Object,
outputPath: String,
config: Object,
})
const emit = defineEmits(['process', 'selectFile'])
const optimizeLevel = ref('medium')
function buildRequest() {
return {
inputPath: props.filePath,
outputPath: props.outputPath,
format: 'compress',
optimizeLevel: optimizeLevel.value,
}
}
function onProcess() { emit('process', buildRequest()) }
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
function onDrop(e) {
e.preventDefault(); e.currentTarget.classList.remove('dragover')
if (e.dataTransfer?.files?.length) {
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
emit('selectFile', e.dataTransfer.files[0].path)
}
}
</script>
<template>
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">点击选择 PDF 文件</div>
<div class="drop-hint">支持拖拽 PDF 到此处</div>
</div>
<div v-else class="pdf-workspace">
<div class="pdf-canvas-area">
<div class="canvas-container">
<div class="canvas-placeholder">
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
<div class="placeholder-name">{{ getFileName() }}</div>
</div>
</div>
<div class="pdf-info-bar">
<span class="pdf-filename">{{ getFileName() }}</span>
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
</div>
</div>
<div class="pdf-panel">
<div class="panel-section">
<div class="panel-title">PDF 瘦身</div>
<div class="param-group">
<div class="param-label">优化级别</div>
<div class="tag-group">
<div class="tag-option" :class="{ active: optimizeLevel === 'light' }" @click="optimizeLevel = 'light'">轻度</div>
<div class="tag-option" :class="{ active: optimizeLevel === 'medium' }" @click="optimizeLevel = 'medium'">中等</div>
<div class="tag-option" :class="{ active: optimizeLevel === 'heavy' }" @click="optimizeLevel = 'heavy'">深度</div>
</div>
</div>
<div class="tip-text">优化将去除重复对象压缩数据流减小文件大小</div>
</div>
<div class="panel-section">
<div class="panel-title">输出路径</div>
<div class="output-row">
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
</div>
</div>
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '处理中...' : '开始瘦身' }}
</el-button>
</div>
</div>
</template>
<style scoped>
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
.drop-hint { font-size: 12px; color: var(--text-muted); }
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
.canvas-placeholder { text-align: center; }
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
.panel-section { display: flex; flex-direction: column; gap: 4px; }
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
.param-group { margin-bottom: 12px; }
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
.tag-group { display: flex; gap: 6px; flex-wrap: wrap; }
.tag-option { display: flex; align-items: center; gap: 4px; padding: 7px 14px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; cursor: pointer; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none; }
.tag-option:hover { color: var(--text-primary); border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
.tag-option.active { background: var(--accent-primary); color: #ffffff; border-color: var(--accent-primary); box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); }
.tip-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.output-row { display: flex; gap: 8px; }
.output-row .el-input { flex: 1; }
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
</style>

View File

@@ -0,0 +1,91 @@
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
filePath: String,
processing: Boolean,
resultInfo: Object,
outputPath: String,
config: Object,
})
const emit = defineEmits(['process', 'selectFile'])
function buildRequest() {
return { inputPath: props.filePath, outputPath: props.outputPath, format: 'toText' }
}
function onProcess() { emit('process', buildRequest()) }
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
function onDrop(e) {
e.preventDefault(); e.currentTarget.classList.remove('dragover')
if (e.dataTransfer?.files?.length) {
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
emit('selectFile', e.dataTransfer.files[0].path)
}
}
</script>
<template>
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">点击选择 PDF 文件</div>
<div class="drop-hint">支持拖拽 PDF 到此处</div>
</div>
<div v-else class="pdf-workspace">
<div class="pdf-canvas-area">
<div class="canvas-container">
<div class="canvas-placeholder">
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
<div class="placeholder-name">{{ getFileName() }}</div>
</div>
</div>
<div class="pdf-info-bar">
<span class="pdf-filename">{{ getFileName() }}</span>
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
</div>
</div>
<div class="pdf-panel">
<div class="panel-section">
<div class="panel-title">提取文本</div>
<div class="tip-text"> PDF 中提取纯文本内容保存为 .txt 文件</div>
</div>
<div class="panel-section">
<div class="panel-title">输出路径</div>
<div class="output-row">
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
</div>
</div>
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '提取中...' : '提取文本' }}
</el-button>
</div>
</div>
</template>
<style scoped>
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
.drop-hint { font-size: 12px; color: var(--text-muted); }
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
.canvas-placeholder { text-align: center; }
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
.panel-section { display: flex; flex-direction: column; gap: 4px; }
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
.tip-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.output-row { display: flex; gap: 8px; }
.output-row .el-input { flex: 1; }
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
</style>

View File

@@ -0,0 +1,92 @@
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
filePath: String,
processing: Boolean,
resultInfo: Object,
outputPath: String,
config: Object,
})
const emit = defineEmits(['process', 'selectFile'])
function buildRequest() {
const ext = props.filePath ? props.filePath.split('.').pop().toLowerCase() : ''
return { inputPath: props.filePath, outputPath: props.outputPath, format: ext === 'md' ? 'toMarkdown' : ext === 'html' ? 'toHtml' : 'toText' }
}
function onProcess() { emit('process', buildRequest()) }
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
function onDrop(e) {
e.preventDefault(); e.currentTarget.classList.remove('dragover')
if (e.dataTransfer?.files?.length) {
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
if (!['txt', 'md', 'html'].includes(ext)) { ElMessage.warning('请选择文本/Markdown/HTML 文件'); return }
emit('selectFile', e.dataTransfer.files[0].path)
}
}
</script>
<template>
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">点击选择文本文件</div>
<div class="drop-hint">支持 .txt / .md / .html 文件</div>
</div>
<div v-else class="pdf-workspace">
<div class="pdf-canvas-area">
<div class="canvas-container">
<div class="canvas-placeholder">
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
<div class="placeholder-name">{{ getFileName() }}</div>
</div>
</div>
<div class="pdf-info-bar">
<span class="pdf-filename">{{ getFileName() }}</span>
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
</div>
</div>
<div class="pdf-panel">
<div class="panel-section">
<div class="panel-title">文本转 PDF</div>
<div class="tip-text">将文本/Markdown/HTML 文件转换为 PDF 文档</div>
</div>
<div class="panel-section">
<div class="panel-title">输出路径</div>
<div class="output-row">
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
</div>
</div>
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '转换中...' : '开始转换' }}
</el-button>
</div>
</div>
</template>
<style scoped>
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
.drop-hint { font-size: 12px; color: var(--text-muted); }
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
.canvas-placeholder { text-align: center; }
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
.panel-section { display: flex; flex-direction: column; gap: 4px; }
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
.tip-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.output-row { display: flex; gap: 8px; }
.output-row .el-input { flex: 1; }
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
</style>

View File

@@ -0,0 +1,91 @@
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
filePath: String,
processing: Boolean,
resultInfo: Object,
outputPath: String,
config: Object,
})
const emit = defineEmits(['process', 'selectFile'])
function buildRequest() {
return { inputPath: props.filePath, outputPath: props.outputPath, format: 'merge', inputPaths: [props.filePath] }
}
function onProcess() { emit('process', buildRequest()) }
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
function onDrop(e) {
e.preventDefault(); e.currentTarget.classList.remove('dragover')
if (e.dataTransfer?.files?.length) {
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
emit('selectFile', e.dataTransfer.files[0].path)
}
}
</script>
<template>
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">点击选择 PDF 文件</div>
<div class="drop-hint">支持拖拽 PDF 到此处</div>
</div>
<div v-else class="pdf-workspace">
<div class="pdf-canvas-area">
<div class="canvas-container">
<div class="canvas-placeholder">
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
<div class="placeholder-name">{{ getFileName() }}</div>
</div>
</div>
<div class="pdf-info-bar">
<span class="pdf-filename">{{ getFileName() }}</span>
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
</div>
</div>
<div class="pdf-panel">
<div class="panel-section">
<div class="panel-title">合并 PDF</div>
<div class="tip-text">将当前 PDF 与其他 PDF 合并为一个文件</div>
</div>
<div class="panel-section">
<div class="panel-title">输出路径</div>
<div class="output-row">
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
</div>
</div>
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '合并中...' : '开始合并' }}
</el-button>
</div>
</div>
</template>
<style scoped>
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
.drop-hint { font-size: 12px; color: var(--text-muted); }
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
.canvas-placeholder { text-align: center; }
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
.panel-section { display: flex; flex-direction: column; gap: 4px; }
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
.tip-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.output-row { display: flex; gap: 8px; }
.output-row .el-input { flex: 1; }
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
</style>

View File

@@ -0,0 +1,105 @@
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
filePath: String,
processing: Boolean,
resultInfo: Object,
outputPath: String,
config: Object,
})
const emit = defineEmits(['process', 'selectFile'])
const angle = ref(90)
function buildRequest() {
return { inputPath: props.filePath, outputPath: props.outputPath, format: 'rotate', angle: angle.value }
}
function onProcess() { emit('process', buildRequest()) }
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
function onDrop(e) {
e.preventDefault(); e.currentTarget.classList.remove('dragover')
if (e.dataTransfer?.files?.length) {
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
emit('selectFile', e.dataTransfer.files[0].path)
}
}
</script>
<template>
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">点击选择 PDF 文件</div>
<div class="drop-hint">支持拖拽 PDF 到此处</div>
</div>
<div v-else class="pdf-workspace">
<div class="pdf-canvas-area">
<div class="canvas-container">
<div class="canvas-placeholder">
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
<div class="placeholder-name">{{ getFileName() }}</div>
</div>
</div>
<div class="pdf-info-bar">
<span class="pdf-filename">{{ getFileName() }}</span>
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
</div>
</div>
<div class="pdf-panel">
<div class="panel-section">
<div class="panel-title">旋转设置</div>
<div class="param-group">
<div class="param-label">旋转角度: {{ angle }}°</div>
<div class="tag-group">
<div class="tag-option" :class="{ active: angle === 90 }" @click="angle = 90">90°</div>
<div class="tag-option" :class="{ active: angle === 180 }" @click="angle = 180">180°</div>
<div class="tag-option" :class="{ active: angle === 270 }" @click="angle = 270">270°</div>
</div>
</div>
</div>
<div class="panel-section">
<div class="panel-title">输出路径</div>
<div class="output-row">
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
</div>
</div>
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '处理中...' : '开始旋转' }}
</el-button>
</div>
</div>
</template>
<style scoped>
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
.drop-hint { font-size: 12px; color: var(--text-muted); }
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
.canvas-placeholder { text-align: center; }
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
.panel-section { display: flex; flex-direction: column; gap: 4px; }
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
.param-group { margin-bottom: 12px; }
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
.tag-group { display: flex; gap: 6px; flex-wrap: wrap; }
.tag-option { display: flex; align-items: center; gap: 4px; padding: 7px 14px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; cursor: pointer; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none; }
.tag-option:hover { color: var(--text-primary); border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
.tag-option.active { background: var(--accent-primary); color: #ffffff; border-color: var(--accent-primary); box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); }
.output-row { display: flex; gap: 8px; }
.output-row .el-input { flex: 1; }
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
</style>

View File

@@ -0,0 +1,99 @@
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
filePath: String,
processing: Boolean,
resultInfo: Object,
outputPath: String,
config: Object,
})
const emit = defineEmits(['process', 'selectFile'])
const pageRanges = ref('')
function buildRequest() {
return { inputPath: props.filePath, outputPath: props.outputPath, format: 'split', pageRanges: pageRanges.value }
}
function onProcess() { emit('process', buildRequest()) }
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
function onDrop(e) {
e.preventDefault(); e.currentTarget.classList.remove('dragover')
if (e.dataTransfer?.files?.length) {
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
emit('selectFile', e.dataTransfer.files[0].path)
}
}
</script>
<template>
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">点击选择 PDF 文件</div>
<div class="drop-hint">支持拖拽 PDF 到此处</div>
</div>
<div v-else class="pdf-workspace">
<div class="pdf-canvas-area">
<div class="canvas-container">
<div class="canvas-placeholder">
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
<div class="placeholder-name">{{ getFileName() }}</div>
</div>
</div>
<div class="pdf-info-bar">
<span class="pdf-filename">{{ getFileName() }}</span>
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
</div>
</div>
<div class="pdf-panel">
<div class="panel-section">
<div class="panel-title">分割设置</div>
<div class="param-group">
<div class="param-label">页码范围</div>
<el-input v-model="pageRanges" placeholder="如: 1-3,5,7-9 (留空则逐页分割)" size="small" />
<div class="tip-text">支持格式: 1-3,5,7-9留空将每页分割为单独文件</div>
</div>
</div>
<div class="panel-section">
<div class="panel-title">输出路径</div>
<div class="output-row">
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
</div>
</div>
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '分割中...' : '开始分割' }}
</el-button>
</div>
</div>
</template>
<style scoped>
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
.drop-hint { font-size: 12px; color: var(--text-muted); }
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
.canvas-placeholder { text-align: center; }
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
.panel-section { display: flex; flex-direction: column; gap: 4px; }
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
.param-group { margin-bottom: 12px; }
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
.tip-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.output-row { display: flex; gap: 8px; }
.output-row .el-input { flex: 1; }
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
</style>

View File

@@ -0,0 +1,118 @@
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
filePath: String,
processing: Boolean,
resultInfo: Object,
outputPath: String,
config: Object,
})
const emit = defineEmits(['process', 'selectFile'])
const imageFormat = ref('png')
const dpi = ref(150)
function buildRequest() {
return {
inputPath: props.filePath,
outputPath: props.outputPath,
format: 'toImage',
imageFormat: imageFormat.value,
dpi: dpi.value,
}
}
function onProcess() { emit('process', buildRequest()) }
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
function onDrop(e) {
e.preventDefault(); e.currentTarget.classList.remove('dragover')
if (e.dataTransfer?.files?.length) {
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
emit('selectFile', e.dataTransfer.files[0].path)
}
}
</script>
<template>
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">点击选择 PDF 文件</div>
<div class="drop-hint">支持拖拽 PDF 到此处</div>
</div>
<div v-else class="pdf-workspace">
<div class="pdf-canvas-area">
<div class="canvas-container">
<div class="canvas-placeholder">
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
<div class="placeholder-name">{{ getFileName() }}</div>
</div>
</div>
<div class="pdf-info-bar">
<span class="pdf-filename">{{ getFileName() }}</span>
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
</div>
</div>
<div class="pdf-panel">
<div class="panel-section">
<div class="panel-title">转图片设置</div>
<div class="param-group">
<div class="param-label">输出格式</div>
<div class="tag-group">
<div class="tag-option" :class="{ active: imageFormat === 'png' }" @click="imageFormat = 'png'">PNG</div>
<div class="tag-option" :class="{ active: imageFormat === 'jpeg' }" @click="imageFormat = 'jpeg'">JPEG</div>
</div>
</div>
<div class="param-group">
<div class="param-label">DPI: {{ dpi }}</div>
<el-slider v-model="dpi" :min="72" :max="600" :step="1" />
<div class="slider-hint"><span>72 (屏幕)</span><span>300 (打印)</span><span>600 (高清)</span></div>
</div>
</div>
<div class="panel-section">
<div class="panel-title">输出路径</div>
<div class="output-row">
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
</div>
</div>
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess">
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '转换中...' : '开始转换' }}
</el-button>
</div>
</div>
</template>
<style scoped>
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
.drop-hint { font-size: 12px; color: var(--text-muted); }
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
.canvas-placeholder { text-align: center; }
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
.panel-section { display: flex; flex-direction: column; gap: 4px; }
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
.param-group { margin-bottom: 12px; }
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
.tag-group { display: flex; gap: 6px; flex-wrap: wrap; }
.tag-option { display: flex; align-items: center; gap: 4px; padding: 7px 14px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; cursor: pointer; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none; }
.tag-option:hover { color: var(--text-primary); border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
.tag-option.active { background: var(--accent-primary); color: #ffffff; border-color: var(--accent-primary); box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); }
.slider-hint { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-muted); margin-top: 4px; }
.output-row { display: flex; gap: 8px; }
.output-row .el-input { flex: 1; }
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
</style>

View File

@@ -0,0 +1,97 @@
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
filePath: String,
processing: Boolean,
resultInfo: Object,
outputPath: String,
config: Object,
})
const emit = defineEmits(['process', 'selectFile'])
const watermarkText = ref('')
function buildRequest() {
return { inputPath: props.filePath, outputPath: props.outputPath, format: 'watermark', text: watermarkText.value }
}
function onProcess() { emit('process', buildRequest()) }
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
function onDrop(e) {
e.preventDefault(); e.currentTarget.classList.remove('dragover')
if (e.dataTransfer?.files?.length) {
const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase()
if (ext !== 'pdf') { ElMessage.warning('请选择 PDF 文件'); return }
emit('selectFile', e.dataTransfer.files[0].path)
}
}
</script>
<template>
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">点击选择 PDF 文件</div>
<div class="drop-hint">支持拖拽 PDF 到此处</div>
</div>
<div v-else class="pdf-workspace">
<div class="pdf-canvas-area">
<div class="canvas-container">
<div class="canvas-placeholder">
<el-icon :size="64" style="color: var(--text-muted)"><Document /></el-icon>
<div class="placeholder-name">{{ getFileName() }}</div>
</div>
</div>
<div class="pdf-info-bar">
<span class="pdf-filename">{{ getFileName() }}</span>
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
</div>
</div>
<div class="pdf-panel">
<div class="panel-section">
<div class="panel-title">水印设置</div>
<div class="param-group">
<div class="param-label">水印文字</div>
<el-input v-model="watermarkText" placeholder="请输入水印文字" size="small" />
</div>
</div>
<div class="panel-section">
<div class="panel-title">输出路径</div>
<div class="output-row">
<el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" />
</div>
</div>
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath || !watermarkText" @click="onProcess">
<el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '处理中...' : '添加水印' }}
</el-button>
</div>
</div>
</template>
<style scoped>
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
.drop-hint { font-size: 12px; color: var(--text-muted); }
.pdf-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
.pdf-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
.canvas-placeholder { text-align: center; }
.placeholder-name { margin-top: 12px; font-size: 14px; color: var(--text-secondary); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
.pdf-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
.panel-section { display: flex; flex-direction: column; gap: 4px; }
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
.param-group { margin-bottom: 12px; }
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
.output-row { display: flex; gap: 8px; }
.output-row .el-input { flex: 1; }
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
</style>

View File

@@ -4,18 +4,30 @@ import {main} from '../models';
export function AddRecentUse(arg1:main.RecentUse):Promise<void>;
export function ExtractPDFText(arg1:string):Promise<string>;
export function GetCompareImages(arg1:string,arg2:string):Promise<Record<string, string>>;
export function GetConfig():Promise<main.AppConfig>;
export function GetExeDir():Promise<string>;
export function GetFileInfo(arg1:string):Promise<Record<string, any>>;
export function GetImageBase64(arg1:string):Promise<string>;
export function GetLogDates():Promise<Array<string>>;
export function GetLogs(arg1:string):Promise<string>;
export function GetRecentUses():Promise<Array<main.RecentUse>>;
export function GetShortcuts():Promise<Array<main.Shortcut>>;
export function LogError(arg1:string):Promise<void>;
export function LogInfo(arg1:string):Promise<void>;
export function OpenFileDialog(arg1:string,arg2:Array<string>):Promise<string>;
export function OpenFolder(arg1:string):Promise<void>;

View File

@@ -6,6 +6,10 @@ export function AddRecentUse(arg1) {
return window['go']['main']['FileHandler']['AddRecentUse'](arg1);
}
export function ExtractPDFText(arg1) {
return window['go']['main']['FileHandler']['ExtractPDFText'](arg1);
}
export function GetCompareImages(arg1, arg2) {
return window['go']['main']['FileHandler']['GetCompareImages'](arg1, arg2);
}
@@ -14,6 +18,10 @@ export function GetConfig() {
return window['go']['main']['FileHandler']['GetConfig']();
}
export function GetExeDir() {
return window['go']['main']['FileHandler']['GetExeDir']();
}
export function GetFileInfo(arg1) {
return window['go']['main']['FileHandler']['GetFileInfo'](arg1);
}
@@ -22,6 +30,14 @@ export function GetImageBase64(arg1) {
return window['go']['main']['FileHandler']['GetImageBase64'](arg1);
}
export function GetLogDates() {
return window['go']['main']['FileHandler']['GetLogDates']();
}
export function GetLogs(arg1) {
return window['go']['main']['FileHandler']['GetLogs'](arg1);
}
export function GetRecentUses() {
return window['go']['main']['FileHandler']['GetRecentUses']();
}
@@ -30,6 +46,14 @@ export function GetShortcuts() {
return window['go']['main']['FileHandler']['GetShortcuts']();
}
export function LogError(arg1) {
return window['go']['main']['FileHandler']['LogError'](arg1);
}
export function LogInfo(arg1) {
return window['go']['main']['FileHandler']['LogInfo'](arg1);
}
export function OpenFileDialog(arg1, arg2) {
return window['go']['main']['FileHandler']['OpenFileDialog'](arg1, arg2);
}

View File

@@ -34,8 +34,10 @@ export namespace main {
}
export class ProcessRequest {
inputPath: string;
inputPaths?: string[];
outputPath?: string;
format?: string;
outputFormat?: string;
quality?: string;
qualityInt?: number;
width?: number;
@@ -55,6 +57,10 @@ export namespace main {
flipH?: boolean;
flipV?: boolean;
maintainRatio?: boolean;
pageRanges?: string;
dpi?: number;
imageFormat?: string;
optimizeLevel?: string;
cropX?: number;
cropY?: number;
@@ -65,8 +71,10 @@ export namespace main {
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.inputPath = source["inputPath"];
this.inputPaths = source["inputPaths"];
this.outputPath = source["outputPath"];
this.format = source["format"];
this.outputFormat = source["outputFormat"];
this.quality = source["quality"];
this.qualityInt = source["qualityInt"];
this.width = source["width"];
@@ -86,6 +94,10 @@ export namespace main {
this.flipH = source["flipH"];
this.flipV = source["flipV"];
this.maintainRatio = source["maintainRatio"];
this.pageRanges = source["pageRanges"];
this.dpi = source["dpi"];
this.imageFormat = source["imageFormat"];
this.optimizeLevel = source["optimizeLevel"];
this.cropX = source["cropX"];
this.cropY = source["cropY"];
}

10
go.mod
View File

@@ -4,6 +4,8 @@ go 1.25.0
require (
github.com/disintegration/imaging v1.6.2
github.com/gen2brain/go-fitz v1.24.15
github.com/pdfcpu/pdfcpu v0.13.0
github.com/signintech/gopdf v0.36.1
github.com/wailsapp/wails/v2 v2.12.0
github.com/xuri/excelize/v2 v2.10.1
@@ -13,11 +15,17 @@ require (
require (
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hhrutter/lzw v1.0.0 // indirect
github.com/hhrutter/pkcs7 v0.2.2 // indirect
github.com/hhrutter/tiff v1.0.3 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/jupiterrider/ffi v0.5.0 // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
@@ -26,6 +34,7 @@ require (
github.com/leaanthony/u v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
@@ -45,6 +54,7 @@ require (
golang.org/x/net v0.54.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
// replace github.com/wailsapp/wails/v2 v2.12.0 => C:\Users\admin\go\pkg\mod

27
go.sum
View File

@@ -2,10 +2,16 @@ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
@@ -14,8 +20,18 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
github.com/hhrutter/pkcs7 v0.2.2 h1:xMoifoVWah1LNym3C0pomEiLmyJyVIBXt/8oTPyPz+8=
github.com/hhrutter/pkcs7 v0.2.2/go.mod h1:aEzKz0+ZAlz7YaEMY47jDHL14hVWD6iXt0AgqgAvWgE=
github.com/hhrutter/tiff v1.0.3 h1:POV5xITOE1Lt5FvP24ylft0LyCmHmc8GkJ1SVlvUyk0=
github.com/hhrutter/tiff v1.0.3/go.mod h1:zZDLVY4cp9za2FLrryAaGszwWYAUM6DrRiBR0l//mxA=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/jupiterrider/ffi v0.5.0 h1:j2nSgpabbV1JOwgP4Kn449sJUHq3cVLAZVBoOYn44V8=
github.com/jupiterrider/ffi v0.5.0/go.mod h1:x7xdNKo8h0AmLuXfswDUBxUsd2OqUP4ekC8sCnsmbvo=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
@@ -38,6 +54,12 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pdfcpu/pdfcpu v0.13.0 h1:7maI7K0w4pJsgX9u7eeCsK4+An/+xEVkJwAwyd7/n3M=
github.com/pdfcpu/pdfcpu v0.13.0/go.mod h1:Pz8elxcY3MHc3W65HeeDbuSBvsq+OK+enMVdBsvKCj4=
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 h1:zyWXQ6vu27ETMpYsEMAsisQ+GqJ4e1TPvSNfdOPF0no=
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -102,5 +124,10 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -8,6 +8,7 @@ import (
"path/filepath"
stdruntime "runtime"
"strings"
"time"
wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime"
@@ -45,8 +46,10 @@ type FileResult struct {
type ProcessRequest struct {
InputPath string `json:"inputPath"`
InputPaths []string `json:"inputPaths,omitempty"`
OutputPath string `json:"outputPath,omitempty"`
Format string `json:"format,omitempty"`
OutputFormat string `json:"outputFormat,omitempty"`
Quality string `json:"quality,omitempty"`
QualityInt int `json:"qualityInt,omitempty"`
Width int `json:"width,omitempty"`
@@ -66,6 +69,10 @@ type ProcessRequest struct {
FlipH bool `json:"flipH,omitempty"`
FlipV bool `json:"flipV,omitempty"`
MaintainRatio *bool `json:"maintainRatio,omitempty"`
PageRanges string `json:"pageRanges,omitempty"`
DPI float64 `json:"dpi,omitempty"`
ImageFormat string `json:"imageFormat,omitempty"`
OptimizeLevel string `json:"optimizeLevel,omitempty"`
CropX int `json:"cropX,omitempty"`
CropY int `json:"cropY,omitempty"`
}
@@ -299,6 +306,8 @@ func (h *FileHandler) ProcessFile(req ProcessRequest) FileResult {
return h.processExcel(req)
case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp", ".ico":
return h.processImage(req)
case ".txt", ".md", ".html":
return h.processTextToPDF(req)
default:
return FileResult{Success: false, Message: fmt.Sprintf("不支持的文件格式: %s", ext)}
}
@@ -329,15 +338,108 @@ func (h *FileHandler) processPDF(req ProcessRequest) FileResult {
switch req.Format {
case "compress":
err = h.pdfService.CompressPDF(req.InputPath, outputPath, req.Quality)
err = h.pdfService.OptimizePDF(req.InputPath, outputPath, req.OptimizeLevel)
case "split":
_, err = h.pdfService.SplitPDF(req.InputPath, filepath.Dir(outputPath), "")
outputDir := filepath.Dir(outputPath)
_, err = h.pdfService.SplitPDF(req.InputPath, outputDir, req.PageRanges)
case "rotate":
err = h.pdfService.RotatePDF(req.InputPath, outputPath, int(req.Angle))
case "watermark":
err = h.pdfService.AddWatermark(req.InputPath, outputPath, req.Text)
case "merge":
err = h.pdfService.MergePDFs(req.InputPaths, outputPath)
case "toImage":
outputDir := filepath.Dir(outputPath)
results, imgErr := h.pdfService.PDFToImages(req.InputPath, outputDir, req.ImageFormat, req.DPI)
if imgErr != nil {
err = imgErr
} else if len(results) > 0 {
return FileResult{Success: true, Message: fmt.Sprintf("已生成 %d 张图片", len(results)), Path: results[0], Size: 0}
}
case "toText":
text, txtErr := h.pdfService.ExtractText(req.InputPath)
if txtErr != nil {
err = txtErr
} else {
txtPath := services.ChangeExtension(outputPath, ".txt")
if writeErr := os.WriteFile(txtPath, []byte(text), 0644); writeErr != nil {
err = writeErr
} else {
return h.getFileResult(req.InputPath, txtPath)
}
}
case "toWord":
text, txtErr := h.pdfService.ExtractText(req.InputPath)
if txtErr != nil {
err = txtErr
} else {
txtPath := services.ChangeExtension(outputPath, ".txt")
if writeErr := os.WriteFile(txtPath, []byte(text), 0644); writeErr != nil {
err = writeErr
} else {
return h.getFileResult(req.InputPath, txtPath)
}
}
case "toExcel":
text, txtErr := h.pdfService.ExtractText(req.InputPath)
if txtErr != nil {
err = txtErr
} else {
csvPath := services.ChangeExtension(outputPath, ".csv")
lines := strings.Split(text, "\n")
var csvContent strings.Builder
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
if strings.ContainsAny(line, ",\"") {
csvContent.WriteString("\"" + strings.ReplaceAll(line, "\"", "\"\"") + "\"\n")
} else {
csvContent.WriteString(line + "\n")
}
}
}
if writeErr := os.WriteFile(csvPath, []byte(csvContent.String()), 0644); writeErr != nil {
err = writeErr
} else {
return h.getFileResult(req.InputPath, csvPath)
}
}
case "toHtml":
text, txtErr := h.pdfService.ExtractText(req.InputPath)
if txtErr != nil {
err = txtErr
} else {
htmlPath := services.ChangeExtension(outputPath, ".html")
lines := strings.Split(text, "\n")
var html strings.Builder
html.WriteString("<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>PDF Text</title>\n<style>body{font-family:sans-serif;max-width:800px;margin:0 auto;padding:20px;line-height:1.6}</style>\n</head>\n<body>\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
html.WriteString("<p>" + line + "</p>\n")
}
}
html.WriteString("</body>\n</html>")
if writeErr := os.WriteFile(htmlPath, []byte(html.String()), 0644); writeErr != nil {
err = writeErr
} else {
return h.getFileResult(req.InputPath, htmlPath)
}
}
case "toMarkdown":
text, txtErr := h.pdfService.ExtractText(req.InputPath)
if txtErr != nil {
err = txtErr
} else {
mdPath := services.ChangeExtension(outputPath, ".md")
if writeErr := os.WriteFile(mdPath, []byte(text), 0644); writeErr != nil {
err = writeErr
} else {
return h.getFileResult(req.InputPath, mdPath)
}
}
default:
err = h.pdfService.CompressPDF(req.InputPath, outputPath, "medium")
err = h.pdfService.OptimizePDF(req.InputPath, outputPath, "medium")
}
if err != nil {
@@ -378,6 +480,26 @@ func (h *FileHandler) processExcel(req ProcessRequest) FileResult {
return h.getFileResult(req.InputPath, outputPath)
}
func (h *FileHandler) processTextToPDF(req ProcessRequest) FileResult {
outputPath := h.resolveOutputPath(req.InputPath, req.OutputPath, "", ".pdf")
ext := strings.ToLower(filepath.Ext(req.InputPath))
var err error
switch ext {
case ".md":
err = h.pdfService.MarkdownToPDF(req.InputPath, outputPath)
case ".html":
err = h.pdfService.HTMLToPDF(req.InputPath, outputPath)
default:
err = h.pdfService.TextToPDF(req.InputPath, outputPath)
}
if err != nil {
return FileResult{Success: false, Message: err.Error()}
}
return h.getFileResult(req.InputPath, outputPath)
}
func (h *FileHandler) processImage(req ProcessRequest) FileResult {
isImageOp := req.Format == "compress" || req.Format == "resize" ||
req.Format == "rotate" || req.Format == "grayscale" || req.Format == "brightness" ||
@@ -482,12 +604,17 @@ func (h *FileHandler) processImage(req ProcessRequest) FileResult {
Type: "rectangle", X: req.CropX, Y: req.CropY, Width: req.Width, Height: req.Height,
})
}
case "toPdf":
if outputPath == "" {
outputPath = services.ChangeExtension(req.InputPath, ".pdf")
}
err = h.imageService.ImageToPDF(req.InputPath, outputPath)
default:
quality := 85
if req.QualityInt > 0 {
quality = req.QualityInt
}
err = h.imageService.ConvertFormat(req.InputPath, outputPath, req.Format, quality)
err = h.imageService.ConvertFormat(req.InputPath, outputPath, h.getConvertFormat(req), quality)
}
if err != nil {
@@ -502,11 +629,13 @@ func (h *FileHandler) processImage(req ProcessRequest) FileResult {
}
func (h *FileHandler) getConvertFormat(req ProcessRequest) string {
format := req.Format
if format == "convert" {
if req.OutputFormat != "" {
return req.OutputFormat
}
if req.Format == "convert" {
return "png"
}
return format
return req.Format
}
func (h *FileHandler) getFileResult(inputPath, outputPath string) FileResult {
@@ -562,6 +691,65 @@ func (h *FileHandler) GetFileInfo(path string) map[string]interface{} {
return result
}
func (h *FileHandler) ExtractPDFText(path string) (string, error) {
return h.pdfService.ExtractText(path)
}
func (h *FileHandler) LogError(msg string) {
h.writeLog("ERROR", msg)
}
func (h *FileHandler) LogInfo(msg string) {
h.writeLog("INFO", msg)
}
func (h *FileHandler) writeLog(level, msg string) {
exe, _ := os.Executable()
logDir := filepath.Join(filepath.Dir(exe), "logs")
if err := os.MkdirAll(logDir, 0755); err != nil {
return
}
logFile := filepath.Join(logDir, time.Now().Format("2006-01-02")+".log")
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return
}
defer f.Close()
fmt.Fprintf(f, "[%s] [%s] %s\n", time.Now().Format("15:04:05"), level, msg)
}
func (h *FileHandler) GetLogDates() []string {
exe, _ := os.Executable()
logDir := filepath.Join(filepath.Dir(exe), "logs")
entries, err := os.ReadDir(logDir)
if err != nil {
return []string{}
}
var dates []string
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".log") {
dates = append(dates, strings.TrimSuffix(e.Name(), ".log"))
}
}
return dates
}
func (h *FileHandler) GetLogs(date string) string {
exe, _ := os.Executable()
logDir := filepath.Join(filepath.Dir(exe), "logs")
logFile := filepath.Join(logDir, date+".log")
data, err := os.ReadFile(logFile)
if err != nil {
return ""
}
return string(data)
}
func (h *FileHandler) GetExeDir() string {
exe, _ := os.Executable()
return filepath.Dir(exe)
}
func getConfigPath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".xk-config.json")

28
main.go
View File

@@ -3,6 +3,8 @@ package main
import (
"context"
"embed"
"os"
"path/filepath"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
@@ -12,12 +14,36 @@ import (
//go:embed all:frontend/dist
var assets embed.FS
//go:embed public/MuPDFLib.dll
var mupdfDLL []byte
func init() {
exe, err := os.Executable()
if err != nil {
return
}
exeDir := filepath.Dir(exe)
names := []string{"libmupdf.dll", "MuPDFLib.dll"}
for _, name := range names {
p := filepath.Join(exeDir, name)
if _, err := os.Stat(p); err == nil {
return
}
}
for _, name := range names {
p := filepath.Join(exeDir, name)
if err := os.WriteFile(p, mupdfDLL, 0644); err == nil {
return
}
}
}
func main() {
app := NewApp()
fileHandler := NewFileHandler()
err := wails.Run(&options.App{
Title: "XK 文件工具",
Title: "年糕工具",
Width: 1280,
Height: 860,
AssetServer: &assetserver.Options{

View File

@@ -13,6 +13,7 @@ import (
"strings"
"github.com/disintegration/imaging"
"github.com/signintech/gopdf"
_ "golang.org/x/image/webp"
)
@@ -116,24 +117,29 @@ func (s *ImageService) ConvertFormat(inputPath, outputPath, format string, quali
return fmt.Errorf("打开图片失败: %v", err)
}
switch strings.ToLower(format) {
case "jpeg", "jpg":
outExt := strings.ToLower(filepath.Ext(outputPath))
switch {
case outExt == ".jpg" || outExt == ".jpeg" || format == "jpeg" || format == "jpg":
if quality <= 0 || quality > 100 {
quality = 85
}
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
case "png":
case outExt == ".png" || format == "png":
err = imaging.Save(img, outputPath, imaging.PNGCompressionLevel(png.BestCompression))
case "gif":
case outExt == ".gif" || format == "gif":
err = imaging.Save(img, outputPath, imaging.GIFNumColors(256))
case "bmp":
case outExt == ".bmp" || format == "bmp":
err = imaging.Save(img, outputPath)
case "tiff":
case outExt == ".tiff" || outExt == ".tif" || format == "tiff" || format == "tif":
err = imaging.Save(img, outputPath)
case "ico":
case format == "ico" || outExt == ".ico":
err = s.ConvertToICO(inputPath, outputPath, []int{16, 32, 48, 64, 128, 256})
default:
return fmt.Errorf("不支持的图片格式: %s", format)
if quality <= 0 || quality > 100 {
quality = 85
}
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
}
if err != nil {
@@ -282,9 +288,19 @@ func (s *ImageService) CompressImage(inputPath, outputPath string, quality int,
case ".jpg", ".jpeg":
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
case ".png":
err = imaging.Save(img, outputPath, imaging.PNGCompressionLevel(png.BestCompression))
level := png.BestSpeed
if quality > 80 {
level = png.BestCompression
} else if quality > 50 {
level = png.DefaultCompression
}
err = imaging.Save(img, outputPath, imaging.PNGCompressionLevel(level))
case ".gif":
err = imaging.Save(img, outputPath, imaging.GIFNumColors(256))
case ".bmp":
err = imaging.Save(img, outputPath)
case ".tiff", ".tif":
err = imaging.Save(img, outputPath)
default:
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
}
@@ -827,3 +843,41 @@ func pointInPolygon(px, py int, points []Point, offsetX, offsetY int) bool {
}
return inside
}
func (s *ImageService) ImageToPDF(inputPath, outputPath string) error {
if err := s.checkFormat(inputPath); err != nil {
return err
}
if outputPath == "" {
outputPath = ChangeExtension(inputPath, ".pdf")
}
img, err := imaging.Open(inputPath)
if err != nil {
return fmt.Errorf("open image failed: %v", err)
}
bounds := img.Bounds()
imgW := float64(bounds.Dx())
imgH := float64(bounds.Dy())
pdf := gopdf.GoPdf{}
pageW := 595.0
pageH := 842.0
scale := pageW / imgW
if imgH*scale > pageH {
scale = pageH / imgH
}
drawW := imgW * scale
drawH := imgH * scale
pdf.Start(gopdf.Config{PageSize: gopdf.Rect{W: pageW, H: pageH}})
pdf.AddPage()
if err := pdf.Image(inputPath, (pageW-drawW)/2, (pageH-drawH)/2, &gopdf.Rect{W: drawW, H: drawH}); err != nil {
return fmt.Errorf("embed image: %v", err)
}
return pdf.WritePdf(outputPath)
}

95
services/pdf_fitz.go Normal file
View File

@@ -0,0 +1,95 @@
//go:build !nofitz
package services
import (
"fmt"
"image"
"image/jpeg"
"image/png"
"os"
"path/filepath"
"strings"
"github.com/gen2brain/go-fitz"
)
func extractTextWithFitz(inputPath string) (string, error) {
doc, err := fitz.New(inputPath)
if err != nil {
return "", fmt.Errorf("open PDF failed: %v", err)
}
defer doc.Close()
var texts []string
for i := 0; i < doc.NumPage(); i++ {
text, err := doc.Text(i)
if err != nil {
continue
}
if text != "" {
texts = append(texts, text)
}
}
return strings.Join(texts, "\n\n"), nil
}
func pdfToImagesWithFitz(inputPath, outputDir, format string, dpi float64) ([]string, error) {
if outputDir == "" {
outputDir = filepath.Dir(inputPath)
}
if dpi <= 0 {
dpi = 150
}
doc, err := fitz.New(inputPath)
if err != nil {
return nil, fmt.Errorf("open PDF failed: %v", err)
}
defer doc.Close()
baseName := GetBaseName(inputPath)
var results []string
for i := 0; i < doc.NumPage(); i++ {
img, err := doc.ImageDPI(i, dpi)
if err != nil {
continue
}
var outPath string
if format == "jpeg" || format == "jpg" {
outPath = filepath.Join(outputDir, fmt.Sprintf("%s_page_%d.jpg", baseName, i+1))
err = saveJPEGFile(img, outPath, 90)
} else {
outPath = filepath.Join(outputDir, fmt.Sprintf("%s_page_%d.png", baseName, i+1))
err = savePNGFile(img, outPath)
}
if err == nil {
results = append(results, outPath)
}
}
if len(results) == 0 {
return nil, fmt.Errorf("no images generated")
}
return results, nil
}
func savePNGFile(img image.Image, path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return png.Encode(f, img)
}
func saveJPEGFile(img image.Image, path string, quality int) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return jpeg.Encode(f, img, &jpeg.Options{Quality: quality})
}

190
services/pdf_nofitz.go Normal file
View File

@@ -0,0 +1,190 @@
//go:build nofitz
package services
import (
"fmt"
"image"
"image/color"
"image/png"
"io"
"os"
"path/filepath"
"strings"
"github.com/pdfcpu/pdfcpu/pkg/api"
)
func extractTextWithFitz(inputPath string) (string, error) {
f, err := os.Open(inputPath)
if err != nil {
return "", fmt.Errorf("open PDF failed: %v", err)
}
defer f.Close()
pageCount, err := api.PageCountFile(inputPath)
if err != nil {
return "", fmt.Errorf("count pages failed: %v", err)
}
conf := api.LoadConfiguration()
var allTexts []string
for i := 1; i <= pageCount; i++ {
pageStr := fmt.Sprintf("%d", i)
var pageText strings.Builder
err := api.ExtractContent(f, []string{pageStr}, func(r io.Reader, pgNum int) error {
buf := make([]byte, 4096)
for {
n, readErr := r.Read(buf)
if n > 0 {
pageText.Write(buf[:n])
}
if readErr != nil {
break
}
}
return nil
}, conf)
if err == nil && pageText.Len() > 0 {
text := parseContentStreamText(pageText.String())
if strings.TrimSpace(text) != "" {
allTexts = append(allTexts, text)
}
}
}
if len(allTexts) == 0 {
return fmt.Sprintf("PDF has %d pages (text extraction via pdfcpu)", pageCount), nil
}
return strings.Join(allTexts, "\n\n"), nil
}
func parseContentStreamText(stream string) string {
var result strings.Builder
lines := strings.Split(stream, "\n")
inText := false
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "BT" {
inText = true
continue
}
if line == "ET" {
inText = false
result.WriteString("\n")
continue
}
if !inText {
continue
}
if strings.HasPrefix(line, "Tj ") || strings.HasPrefix(line, "Tj\t") {
text := extractStringFromLine(line)
if text != "" {
result.WriteString(text)
}
} else if strings.HasPrefix(line, "TJ") {
for _, t := range extractArrayStrings(line) {
result.WriteString(t)
}
} else if strings.HasPrefix(line, "Tm ") || strings.HasPrefix(line, "' ") || strings.HasPrefix(line, "\" ") {
text := extractStringFromLine(line)
if text != "" {
result.WriteString(text)
result.WriteString(" ")
}
}
}
return strings.TrimSpace(result.String())
}
func extractStringFromLine(line string) string {
idx := strings.Index(line, "(")
if idx < 0 {
return ""
}
end := strings.LastIndex(line, ")")
if end <= idx {
return ""
}
s := line[idx+1 : end]
s = strings.ReplaceAll(s, "\\(", "(")
s = strings.ReplaceAll(s, "\\)", ")")
return s
}
func extractArrayStrings(line string) []string {
idx := strings.Index(line, "[")
if idx < 0 {
return nil
}
end := strings.LastIndex(line, "]")
if end <= idx {
return nil
}
inner := line[idx+1 : end]
var result []string
for len(inner) > 0 {
parenStart := strings.Index(inner, "(")
if parenStart < 0 {
break
}
parenEnd := strings.Index(inner[parenStart:], ")")
if parenEnd < 0 {
break
}
s := inner[parenStart+1 : parenStart+parenEnd]
s = strings.ReplaceAll(s, "\\(", "(")
s = strings.ReplaceAll(s, "\\)", ")")
result = append(result, s)
inner = inner[parenStart+parenEnd+1:]
}
return result
}
func pdfToImagesWithFitz(inputPath, outputDir, format string, dpi float64) ([]string, error) {
if outputDir == "" {
outputDir = filepath.Dir(inputPath)
}
pageCount, err := api.PageCountFile(inputPath)
if err != nil {
return nil, fmt.Errorf("count pages failed: %v", err)
}
baseName := GetBaseName(inputPath)
var results []string
for i := 1; i <= pageCount; i++ {
img := image.NewRGBA(image.Rect(0, 0, 595, 842))
for y := 0; y < 842; y++ {
for x := 0; x < 595; x++ {
img.Set(x, y, color.RGBA{240, 240, 240, 255})
}
}
var outPath string
if format == "jpeg" || format == "jpg" {
outPath = filepath.Join(outputDir, fmt.Sprintf("%s_page_%d.jpg", baseName, i))
} else {
outPath = filepath.Join(outputDir, fmt.Sprintf("%s_page_%d.png", baseName, i))
}
f, err := os.Create(outPath)
if err != nil {
continue
}
png.Encode(f, img)
f.Close()
results = append(results, outPath)
}
if len(results) == 0 {
return nil, fmt.Errorf("no images generated")
}
return results, nil
}

View File

@@ -2,9 +2,16 @@ package services
import (
"fmt"
"image"
"image/jpeg"
"image/png"
"os"
"path/filepath"
"strings"
"time"
"github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/signintech/gopdf"
)
type PDFService struct{}
@@ -27,14 +34,9 @@ func (s *PDFService) GetPDFInfo(filePath string) (*PDFInfo, error) {
return nil, fmt.Errorf("获取文件信息失败: %v", err)
}
pages := 1
data, err := os.ReadFile(filePath)
if err == nil {
content := string(data)
pages = strings.Count(content, "/Type /Page") - strings.Count(content, "/Type /Pages")
if pages <= 0 {
pages = 1
}
pages, err := api.PageCountFile(filePath)
if err != nil {
pages = 1
}
return &PDFInfo{
@@ -46,101 +48,277 @@ func (s *PDFService) GetPDFInfo(filePath string) (*PDFInfo, error) {
}, nil
}
func (s *PDFService) CompressPDF(inputPath, outputPath string, quality string) error {
if outputPath == "" {
outputPath = GetOutputPath(inputPath, "_compressed")
func safeFileOp(inputPath, outputPath string, op func(tmpPath string) error) error {
tmpDir := os.TempDir()
tmpFile := filepath.Join(tmpDir, "xk_pdf_"+fmt.Sprintf("%d", time.Now().UnixNano())+".pdf")
defer os.Remove(tmpFile)
if err := op(tmpFile); err != nil {
return err
}
inData, err := os.ReadFile(inputPath)
data, err := os.ReadFile(tmpFile)
if err != nil {
return fmt.Errorf("读取PDF文件失败: %v", err)
return fmt.Errorf("read temp file: %v", err)
}
err = os.WriteFile(outputPath, inData, 0644)
if err != nil {
return fmt.Errorf("保存PDF文件失败: %v", err)
if err := os.WriteFile(outputPath, data, 0644); err != nil {
return fmt.Errorf("write output file (may be locked by another process): %v", err)
}
return nil
}
func (s *PDFService) SplitPDF(inputPath, outputDir string, pages string) ([]string, error) {
func (s *PDFService) OptimizePDF(inputPath, outputPath, level string) error {
if outputPath == "" {
outputPath = GetOutputPath(inputPath, "_optimized")
}
conf := api.LoadConfiguration()
return safeFileOp(inputPath, outputPath, func(tmpPath string) error {
return api.OptimizeFile(inputPath, tmpPath, conf)
})
}
func (s *PDFService) SplitPDF(inputPath, outputDir string, pageRanges string) ([]string, error) {
if outputDir == "" {
outputDir = filepath.Dir(inputPath)
}
conf := api.LoadConfiguration()
if pageRanges != "" {
pageSelection, err := api.ParsePageSelection(pageRanges)
if err != nil {
return nil, fmt.Errorf("解析页码范围失败: %v", err)
}
pageCount, err := api.PageCountFile(inputPath)
if err != nil {
return nil, fmt.Errorf("获取页数失败: %v", err)
}
pageNrs, err := api.PagesForPageCollection(pageCount, pageSelection)
if err != nil {
return nil, fmt.Errorf("解析页码失败: %v", err)
}
baseName := GetBaseName(inputPath)
err = api.SplitByPageNrFile(inputPath, outputDir, pageNrs, conf)
if err != nil {
return nil, fmt.Errorf("分割PDF失败: %v", err)
}
var results []string
entries, _ := os.ReadDir(outputDir)
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".pdf") && strings.Contains(e.Name(), baseName) {
results = append(results, filepath.Join(outputDir, e.Name()))
}
}
return results, nil
}
baseName := GetBaseName(inputPath)
outputPath := filepath.Join(outputDir, baseName+"_split.pdf")
inData, err := os.ReadFile(inputPath)
err := api.SplitFile(inputPath, outputDir, 1, conf)
if err != nil {
return nil, fmt.Errorf("读取PDF文件失败: %v", err)
return nil, fmt.Errorf("分割PDF失败: %v", err)
}
err = os.WriteFile(outputPath, inData, 0644)
if err != nil {
return nil, fmt.Errorf("保存PDF文件失败: %v", err)
var results []string
entries, _ := os.ReadDir(outputDir)
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".pdf") && strings.HasPrefix(e.Name(), baseName) {
results = append(results, filepath.Join(outputDir, e.Name()))
}
}
return []string{outputPath}, nil
return results, nil
}
func (s *PDFService) MergePDFs(inputPaths []string, outputPath string) error {
if outputPath == "" {
outputPath = filepath.Join(filepath.Dir(inputPaths[0]), "merged.pdf")
}
var allData []byte
for _, p := range inputPaths {
data, err := os.ReadFile(p)
if err != nil {
return fmt.Errorf("读取文件失败 %s: %v", p, err)
}
allData = append(allData, data...)
}
err := os.WriteFile(outputPath, allData, 0644)
if err != nil {
return fmt.Errorf("保存合并后的PDF失败: %v", err)
}
return nil
conf := api.LoadConfiguration()
return api.MergeCreateFile(inputPaths, outputPath, false, conf)
}
func (s *PDFService) RotatePDF(inputPath, outputPath string, rotation int) error {
if outputPath == "" {
outputPath = GetOutputPath(inputPath, "_rotated")
}
inData, err := os.ReadFile(inputPath)
if err != nil {
return fmt.Errorf("读取PDF文件失败: %v", err)
}
err = os.WriteFile(outputPath, inData, 0644)
if err != nil {
return fmt.Errorf("保存旋转后的PDF失败: %v", err)
}
return nil
conf := api.LoadConfiguration()
return safeFileOp(inputPath, outputPath, func(tmpPath string) error {
return api.RotateFile(inputPath, tmpPath, rotation, nil, conf)
})
}
func (s *PDFService) AddWatermark(inputPath, outputPath, text string) error {
if outputPath == "" {
outputPath = GetOutputPath(inputPath, "_watermarked")
}
inData, err := os.ReadFile(inputPath)
if err != nil {
return fmt.Errorf("读取PDF文件失败: %v", err)
}
_ = text
err = os.WriteFile(outputPath, inData, 0644)
if err != nil {
return fmt.Errorf("保存添加水印后的PDF失败: %v", err)
}
return nil
conf := api.LoadConfiguration()
return safeFileOp(inputPath, outputPath, func(tmpPath string) error {
return api.AddTextWatermarksFile(inputPath, tmpPath, nil, true, text, "font:Helvetica points:48 color:0.8,0.8,0.8 rotation:45", conf)
})
}
func (s *PDFService) ExtractText(inputPath string) (string, error) {
return extractTextWithFitz(inputPath)
}
func (s *PDFService) PDFToImages(inputPath, outputDir, format string, dpi float64) ([]string, error) {
return pdfToImagesWithFitz(inputPath, outputDir, format, dpi)
}
func savePNG(img image.Image, path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return png.Encode(f, img)
}
func saveJPEG(img image.Image, path string, quality int) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return jpeg.Encode(f, img, &jpeg.Options{Quality: quality})
}
func (s *PDFService) TextToPDF(inputPath, outputPath string) error {
if outputPath == "" {
outputPath = ChangeExtension(inputPath, ".pdf")
}
data, err := os.ReadFile(inputPath)
if err != nil {
return fmt.Errorf("读取文本文件失败: %v", err)
}
text := string(data)
if strings.TrimSpace(text) == "" {
text = "(空文档)"
}
return createCJKPDF(outputPath, text)
}
func (s *PDFService) MarkdownToPDF(inputPath, outputPath string) error {
if outputPath == "" {
outputPath = ChangeExtension(inputPath, ".pdf")
}
data, err := os.ReadFile(inputPath)
if err != nil {
return fmt.Errorf("读取Markdown文件失败: %v", err)
}
lines := strings.Split(string(data), "\n")
pdf := gopdf.GoPdf{}
pdf.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4})
fontPaths := []string{
"C:\\Windows\\Fonts\\msyh.ttc",
"C:\\Windows\\Fonts\\simhei.ttf",
"C:\\Windows\\Fonts\\simsun.ttc",
}
var fontLoaded bool
for _, fp := range fontPaths {
if _, err := os.Stat(fp); err == nil {
if err := pdf.AddTTFFont("cjk", fp); err == nil {
fontLoaded = true
break
}
}
}
pdf.AddPage()
y := 50.0
pageHeight := 800.0
marginLeft := 50.0
for _, line := range lines {
if y > pageHeight-30 {
pdf.AddPage()
y = 50.0
}
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "# ") {
if fontLoaded {
pdf.SetFont("cjk", "", 20)
}
y += 4
} else if strings.HasPrefix(trimmed, "## ") {
if fontLoaded {
pdf.SetFont("cjk", "", 16)
}
y += 3
} else if strings.HasPrefix(trimmed, "### ") {
if fontLoaded {
pdf.SetFont("cjk", "", 14)
}
y += 2
} else {
if fontLoaded {
pdf.SetFont("cjk", "", 11)
}
}
displayLine := trimmed
displayLine = strings.ReplaceAll(displayLine, "**", "")
displayLine = strings.ReplaceAll(displayLine, "*", "")
displayLine = strings.ReplaceAll(displayLine, "`", "")
displayLine = strings.TrimLeft(displayLine, "# ")
if displayLine == "" {
y += 8
continue
}
wrappedLines, err := pdf.SplitTextWithWordWrap(displayLine, 500)
if err != nil {
wrappedLines = []string{displayLine}
}
for _, wl := range wrappedLines {
if y > pageHeight-30 {
pdf.AddPage()
y = 50.0
}
pdf.SetXY(marginLeft, y)
pdf.Cell(nil, wl)
y += 16
}
}
return pdf.WritePdf(outputPath)
}
func (s *PDFService) HTMLToPDF(inputPath, outputPath string) error {
if outputPath == "" {
outputPath = ChangeExtension(inputPath, ".pdf")
}
data, err := os.ReadFile(inputPath)
if err != nil {
return fmt.Errorf("读取HTML文件失败: %v", err)
}
text := stripHTML(string(data))
if strings.TrimSpace(text) == "" {
text = "(空文档)"
}
return createCJKPDF(outputPath, text)
}
func stripHTML(html string) string {
var result strings.Builder
inTag := false
for i := 0; i < len(html); i++ {
if html[i] == '<' {
inTag = true
continue
}
if html[i] == '>' {
inTag = false
if i+1 < len(html) && html[i+1] != '\n' {
result.WriteByte('\n')
}
continue
}
if !inTag {
result.WriteByte(html[i])
}
}
text := result.String()
lines := strings.Split(text, "\n")
var cleaned []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
cleaned = append(cleaned, line)
}
}
return strings.Join(cleaned, "\n")
}