🎉 初始提交:TinyLabel 图像文字编辑桌面工具
- 基于 Electron + React + TypeScript + Vite - Fabric.js 画布编辑引擎 - Ant Design UI 组件 + Zustand 状态管理 - 支持文字添加、拖拽、字体/颜色/特效编辑 - 支持 PNG / JPEG / WebP 导出
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user