Files
TinyLabel/src/components/ExportDialog/ExportDialog.tsx
T
steven 08cfcb50b0 🎉 初始提交:TinyLabel 图像文字编辑桌面工具
- 基于 Electron + React + TypeScript + Vite
- Fabric.js 画布编辑引擎
- Ant Design UI 组件 + Zustand 状态管理
- 支持文字添加、拖拽、字体/颜色/特效编辑
- 支持 PNG / JPEG / WebP 导出
2026-07-15 11:49:12 +08:00

208 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from 'react'
import {
Modal,
Select,
Slider,
InputNumber,
Button,
message,
Typography,
Space,
QRCode,
Divider,
} from 'antd'
import { DownloadOutlined, SettingOutlined } from '@ant-design/icons'
import { useEditorStore } from '@/stores/editorStore'
import { exportAndSave, formatFileSize } from '@/utils/imageExporter'
const { Text } = Typography
interface ExportDialogProps {
open: boolean
onClose: () => void
}
export default function ExportDialog({ open, onClose }: ExportDialogProps) {
const canvas = useEditorStore((s) => s.canvas)
const exportConfig = useEditorStore((s) => s.exportConfig)
const setExportConfig = useEditorStore((s) => s.setExportConfig)
const [estimatedSize, setEstimatedSize] = useState<string>('')
const [exporting, setExporting] = useState(false)
// Update size estimate when config changes
useEffect(() => {
if (!canvas || !open) return
// Estimate from canvas dimensions
const w = canvas.width || 800
const h = canvas.height || 600
const exportW = Math.round(w * exportConfig.scaleFactor)
const exportH = Math.round(h * exportConfig.scaleFactor)
// Rough size estimation
let bytes = exportW * exportH * 4 // RGBA
if (exportConfig.format === 'jpeg' || exportConfig.format === 'webp') {
bytes = Math.round(bytes * (exportConfig.quality / 100) * 0.3)
} else {
bytes = Math.round(bytes * 0.5)
}
setEstimatedSize(formatFileSize(bytes))
}, [canvas, open, exportConfig])
const handleExport = useCallback(async () => {
if (!canvas) return
setExporting(true)
try {
const success = await exportAndSave(canvas, exportConfig)
if (success) {
message.success('图片导出成功!')
onClose()
}
} catch (err) {
message.error('导出失败,请重试')
console.error(err)
} finally {
setExporting(false)
}
}, [canvas, exportConfig, onClose])
const canvasW = canvas?.width || 800
const canvasH = canvas?.height || 600
const exportW = Math.round(canvasW * exportConfig.scaleFactor)
const exportH = Math.round(canvasH * exportConfig.scaleFactor)
return (
<Modal
title={
<Space>
<DownloadOutlined />
<span>导出图片</span>
</Space>
}
open={open}
onCancel={onClose}
width={520}
footer={null}
destroyOnClose
centered
>
<div style={{ padding: '8px 0', display: 'flex', flexDirection: 'column', gap: 20 }}>
{/* Format */}
<div>
<Text style={{ color: 'var(--text-secondary)', fontSize: 12, display: 'block', marginBottom: 8 }}>
图片格式
</Text>
<Select
value={exportConfig.format}
onChange={(v) => setExportConfig({ format: v })}
style={{ width: '100%' }}
options={[
{ value: 'png', label: 'PNG(无损,支持透明)' },
{ value: 'jpeg', label: 'JPEG(有损,体积小)' },
{ value: 'webp', label: 'WebP(现代格式,高效)' },
]}
/>
</div>
{/* Quality (for JPEG/WebP) */}
{(exportConfig.format === 'jpeg' || exportConfig.format === 'webp') && (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<Text style={{ color: 'var(--text-secondary)', fontSize: 12 }}>图片质量</Text>
<Text style={{ color: 'var(--accent)', fontSize: 12, fontWeight: 600 }}>
{exportConfig.quality}%
</Text>
</div>
<Slider
value={exportConfig.quality}
onChange={(v) => setExportConfig({ quality: v })}
min={10}
max={100}
marks={{ 10: '低', 50: '中', 80: '高', 100: '最佳' }}
/>
</div>
)}
{/* Scale / Size */}
<div>
<Text style={{ color: 'var(--text-secondary)', fontSize: 12, display: 'block', marginBottom: 8 }}>
导出尺寸
</Text>
<div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 8 }}>
<InputNumber
value={exportW}
disabled
size="small"
addonBefore="宽"
style={{ flex: 1 }}
/>
<span style={{ color: 'var(--text-secondary)' }}>×</span>
<InputNumber
value={exportH}
disabled
size="small"
addonBefore="高"
style={{ flex: 1 }}
/>
</div>
<Slider
value={exportConfig.scaleFactor}
onChange={(v) => setExportConfig({ scaleFactor: v })}
min={0.25}
max={4}
step={0.25}
marks={{
0.25: '0.25×',
0.5: '0.5×',
1: '1×',
2: '2×',
4: '4×',
}}
tooltip={{ formatter: (v) => `${v}×` }}
/>
</div>
{/* Info */}
<div style={{
background: 'var(--bg-canvas)',
borderRadius: 6,
padding: '12px 16px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}>
<Space direction="vertical" size={2}>
<Text style={{ color: 'var(--text-secondary)', fontSize: 12 }}>
导出分辨率
</Text>
<Text style={{ color: 'var(--text-primary)', fontSize: 14, fontWeight: 500 }}>
{exportW} × {exportH}
</Text>
</Space>
<Space direction="vertical" size={2} style={{ textAlign: 'right' }}>
<Text style={{ color: 'var(--text-secondary)', fontSize: 12 }}>
预估文件大小
</Text>
<Text style={{ color: 'var(--accent)', fontSize: 14, fontWeight: 500 }}>
{estimatedSize}
</Text>
</Space>
</div>
{/* Actions */}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, marginTop: 8 }}>
<Button onClick={onClose}>取消</Button>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={handleExport}
loading={exporting}
size="large"
>
导出图片
</Button>
</div>
</div>
</Modal>
)
}