mjpic 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.trae/documents/mjpic-prd.md +111 -0
- package/.trae/documents/mjpic-technical-architecture.md +234 -0
- package/README.md +57 -0
- package/api/app.ts +60 -0
- package/api/cli.ts +61 -0
- package/api/index.ts +19 -0
- package/api/routes/auth.ts +33 -0
- package/api/routes/image.ts +27 -0
- package/api/server.ts +45 -0
- package/dist/cli/app.js +43 -0
- package/dist/cli/cli.js +49 -0
- package/dist/cli/index.js +13 -0
- package/dist/cli/routes/auth.js +28 -0
- package/dist/cli/routes/image.js +21 -0
- package/dist/cli/server.js +38 -0
- package/dist/client/assets/index-BUIYLOn-.js +197 -0
- package/dist/client/assets/index-BoiS81Ei.css +1 -0
- package/dist/client/favicon.svg +4 -0
- package/dist/client/index.html +354 -0
- package/eslint.config.js +28 -0
- package/index.html +24 -0
- package/nodemon.json +10 -0
- package/package.json +68 -0
- package/postcss.config.js +10 -0
- package/public/favicon.svg +4 -0
- package/src/App.tsx +13 -0
- package/src/assets/react.svg +1 -0
- package/src/components/Empty.tsx +8 -0
- package/src/components/dialogs/AspectRatioDialog.tsx +218 -0
- package/src/components/dialogs/SaveDialog.tsx +150 -0
- package/src/components/layout/CanvasArea.tsx +874 -0
- package/src/components/layout/Header.tsx +156 -0
- package/src/components/layout/RightPanel.tsx +886 -0
- package/src/components/layout/Sidebar.tsx +36 -0
- package/src/components/layout/StatusBar.tsx +44 -0
- package/src/hooks/useDebounce.ts +17 -0
- package/src/hooks/useTheme.ts +29 -0
- package/src/i18n/index.ts +26 -0
- package/src/i18n/locales/en.json +56 -0
- package/src/i18n/locales/zh.json +59 -0
- package/src/index.css +14 -0
- package/src/lib/utils.ts +73 -0
- package/src/main.tsx +11 -0
- package/src/pages/Home.tsx +72 -0
- package/src/store/useImageStore.ts +316 -0
- package/src/store/usePresetStore.ts +65 -0
- package/src/store/useUIStore.ts +17 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +13 -0
- package/tmp/guangxi.jpg +0 -0
- package/tsconfig.json +40 -0
- package/tsconfig.server.json +15 -0
- package/vercel.json +12 -0
- package/vite.config.ts +50 -0
- package//345/217/202/350/200/203/345/233/276/347/211/207//346/210/252/345/261/2172026-02-18 16.47.45_/345/233/276/347/211/207/345/260/272/345/257/270/350/260/203/350/212/202_/351/242/204/350/256/276/345/260/272/345/257/270.jpg +0 -0
- package//345/217/202/350/200/203/345/233/276/347/211/207//346/210/252/345/261/2172026-02-18 16.47.51_/345/233/276/347/211/207/345/260/272/345/257/270/350/260/203/350/212/202_/346/211/213/345/267/245/350/276/223/345/205/245/345/260/272/345/257/270.jpg +0 -0
- package//345/217/202/350/200/203/345/233/276/347/211/207//346/210/252/345/261/2172026-02-18 16.54.56_/345/233/276/347/211/207/345/260/272/345/257/270/350/260/203/350/212/202_/346/267/273/345/212/240/345/270/270/347/224/250/345/260/272/345/257/270.jpg +0 -0
- package//345/217/202/350/200/203/345/233/276/347/211/207//346/210/252/345/261/2172026-02-18 16.55.11_/345/233/276/347/211/207/345/260/272/345/257/270/350/260/203/350/212/202_/345/210/240/351/231/244/345/270/270/347/224/250/345/260/272/345/257/270.jpg +0 -0
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
import { useUIStore } from '@/store/useUIStore';
|
|
2
|
+
import { useImageStore } from '@/store/useImageStore';
|
|
3
|
+
import { usePresetStore } from '@/store/usePresetStore';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { useState, useEffect } from 'react';
|
|
6
|
+
import { AspectRatioDialog } from '@/components/dialogs/AspectRatioDialog';
|
|
7
|
+
import { Settings, Ruler } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
// Reusable slider component with local state and debounce
|
|
10
|
+
const SliderControl = ({
|
|
11
|
+
label,
|
|
12
|
+
value,
|
|
13
|
+
min,
|
|
14
|
+
max,
|
|
15
|
+
step = 1,
|
|
16
|
+
onChange,
|
|
17
|
+
unit = ''
|
|
18
|
+
}: {
|
|
19
|
+
label: string;
|
|
20
|
+
value: number;
|
|
21
|
+
min: number;
|
|
22
|
+
max: number;
|
|
23
|
+
step?: number;
|
|
24
|
+
onChange: (val: number) => void;
|
|
25
|
+
unit?: string;
|
|
26
|
+
}) => {
|
|
27
|
+
const [localValue, setLocalValue] = useState(value);
|
|
28
|
+
|
|
29
|
+
// Sync local value when prop value changes (e.g. undo/redo)
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setLocalValue(value);
|
|
32
|
+
}, [value]);
|
|
33
|
+
|
|
34
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
35
|
+
const newValue = parseFloat(e.target.value);
|
|
36
|
+
setLocalValue(newValue);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleCommit = () => {
|
|
40
|
+
if (localValue !== value) {
|
|
41
|
+
onChange(localValue);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleIncrement = () => {
|
|
46
|
+
let nextVal = localValue + step;
|
|
47
|
+
if (nextVal > max) nextVal = max;
|
|
48
|
+
// Fix floating point issues
|
|
49
|
+
if (step < 1) {
|
|
50
|
+
nextVal = parseFloat(nextVal.toFixed(1));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setLocalValue(nextVal);
|
|
54
|
+
onChange(nextVal);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleDecrement = () => {
|
|
58
|
+
let nextVal = localValue - step;
|
|
59
|
+
if (nextVal < min) nextVal = min;
|
|
60
|
+
// Fix floating point issues
|
|
61
|
+
if (step < 1) {
|
|
62
|
+
nextVal = parseFloat(nextVal.toFixed(1));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setLocalValue(nextVal);
|
|
66
|
+
onChange(nextVal);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="space-y-2">
|
|
71
|
+
<div className="flex justify-between text-xs text-zinc-400">
|
|
72
|
+
<span>{label}</span>
|
|
73
|
+
<span>{localValue.toFixed(step < 1 ? 1 : 0)}{unit}</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="flex items-center gap-2">
|
|
76
|
+
<button
|
|
77
|
+
onClick={handleDecrement}
|
|
78
|
+
className="w-6 h-6 flex items-center justify-center bg-zinc-800 text-zinc-300 rounded hover:bg-zinc-700 text-lg leading-none pb-1"
|
|
79
|
+
>
|
|
80
|
+
-
|
|
81
|
+
</button>
|
|
82
|
+
<input
|
|
83
|
+
type="range" min={min} max={max} step={step}
|
|
84
|
+
value={localValue}
|
|
85
|
+
onChange={handleChange}
|
|
86
|
+
onMouseUp={handleCommit} // Only update store on release
|
|
87
|
+
onTouchEnd={handleCommit} // For touch devices
|
|
88
|
+
className="flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
|
89
|
+
/>
|
|
90
|
+
<button
|
|
91
|
+
onClick={handleIncrement}
|
|
92
|
+
className="w-6 h-6 flex items-center justify-center bg-zinc-800 text-zinc-300 rounded hover:bg-zinc-700 text-lg leading-none pb-1"
|
|
93
|
+
>
|
|
94
|
+
+
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const RightPanel = () => {
|
|
102
|
+
const { activeTool, isStraightenToolActive, setStraightenToolActive } = useUIStore();
|
|
103
|
+
const { config, updateConfig, originalWidth, originalHeight, applyCrop, cropRect } = useImageStore();
|
|
104
|
+
const { aspectRatios } = usePresetStore();
|
|
105
|
+
const { t } = useTranslation();
|
|
106
|
+
|
|
107
|
+
// Dialog state for aspect ratio manager
|
|
108
|
+
const [isAspectRatioManagerOpen, setIsAspectRatioManagerOpen] = useState(false);
|
|
109
|
+
|
|
110
|
+
// 计算基于原始图片比例的尺寸
|
|
111
|
+
const calculateSize = (targetValue: number, mode: 'width' | 'height'): { width: number; height: number } => {
|
|
112
|
+
if (originalWidth === 0 || originalHeight === 0) {
|
|
113
|
+
// 如果没有原始尺寸信息,使用默认比例 4:3
|
|
114
|
+
return mode === 'width'
|
|
115
|
+
? { width: targetValue, height: Math.round(targetValue * 0.75) }
|
|
116
|
+
: { width: Math.round(targetValue * 1.333), height: targetValue };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
120
|
+
|
|
121
|
+
if (mode === 'width') {
|
|
122
|
+
return {
|
|
123
|
+
width: targetValue,
|
|
124
|
+
height: Math.round(targetValue / aspectRatio)
|
|
125
|
+
};
|
|
126
|
+
} else {
|
|
127
|
+
return {
|
|
128
|
+
width: Math.round(targetValue * aspectRatio),
|
|
129
|
+
height: targetValue
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// 格式化尺寸显示字符串
|
|
135
|
+
const formatSizeDisplay = (targetValue: number, mode: 'width' | 'height'): string => {
|
|
136
|
+
const calculated = calculateSize(targetValue, mode);
|
|
137
|
+
|
|
138
|
+
if (mode === 'width') {
|
|
139
|
+
return `${targetValue} × [${calculated.height}]`;
|
|
140
|
+
} else {
|
|
141
|
+
return `[${calculated.width}] × ${targetValue}`;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// 解析预设尺寸字符串为宽高对象
|
|
146
|
+
const parseSizeString = (sizeStr: string): { width: number; height: number } | null => {
|
|
147
|
+
const [width, height] = sizeStr.split('x').map(Number);
|
|
148
|
+
if (isNaN(width) || isNaN(height)) return null;
|
|
149
|
+
return { width, height };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// 对话框状态
|
|
153
|
+
const [isAddSizeDialogOpen, setIsAddSizeDialogOpen] = useState(false);
|
|
154
|
+
const [isDeleteSizeDialogOpen, setIsDeleteSizeDialogOpen] = useState(false);
|
|
155
|
+
|
|
156
|
+
// 添加尺寸对话框状态
|
|
157
|
+
const [addSizeMode, setAddSizeMode] = useState<'width' | 'height'>('width');
|
|
158
|
+
const [addSizeValue, setAddSizeValue] = useState('');
|
|
159
|
+
|
|
160
|
+
// 删除尺寸对话框状态
|
|
161
|
+
const [deleteSizeMode, setDeleteSizeMode] = useState<'width' | 'height'>('width');
|
|
162
|
+
const [selectedSizeToDelete, setSelectedSizeToDelete] = useState('');
|
|
163
|
+
|
|
164
|
+
// 预设尺寸列表
|
|
165
|
+
type SizeMode = 'width' | 'height';
|
|
166
|
+
type PresetSize = { width: number; height: number; mode: SizeMode };
|
|
167
|
+
|
|
168
|
+
const [presetSizes, setPresetSizes] = useState<PresetSize[]>([
|
|
169
|
+
{ width: 450, height: 338, mode: 'width' },
|
|
170
|
+
{ width: 600, height: 450, mode: 'width' },
|
|
171
|
+
{ width: 850, height: 638, mode: 'width' },
|
|
172
|
+
{ width: 1440, height: 1080, mode: 'width' },
|
|
173
|
+
{ width: 1920, height: 1440, mode: 'width' },
|
|
174
|
+
{ width: 2400, height: 1800, mode: 'width' },
|
|
175
|
+
{ width: 3840, height: 2880, mode: 'width' },
|
|
176
|
+
{ width: 5120, height: 3840, mode: 'width' }
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
// 获取当前选中的预设尺寸的显示文本
|
|
180
|
+
const getSelectedSizeDisplay = (): string => {
|
|
181
|
+
if (!config.resize?.width || !config.resize?.height) return '';
|
|
182
|
+
|
|
183
|
+
const selectedSize = presetSizes.find(
|
|
184
|
+
(size) => size.width === config.resize?.width && size.height === config.resize?.height
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (selectedSize) {
|
|
188
|
+
return formatSizeDisplay(selectedSize.mode === 'width' ? selectedSize.width : selectedSize.height, selectedSize.mode);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return `${config.resize.width} × ${config.resize.height}`;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleReset = () => {
|
|
195
|
+
switch (activeTool) {
|
|
196
|
+
case 'enhance':
|
|
197
|
+
updateConfig({
|
|
198
|
+
enhancements: {
|
|
199
|
+
autoEnhance: false,
|
|
200
|
+
fillLight: false,
|
|
201
|
+
autoWhiteBalance: false
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
break;
|
|
205
|
+
case 'adjust':
|
|
206
|
+
updateConfig({
|
|
207
|
+
brightness: 0,
|
|
208
|
+
contrast: 0,
|
|
209
|
+
sharpness: 0
|
|
210
|
+
});
|
|
211
|
+
break;
|
|
212
|
+
case 'resize':
|
|
213
|
+
updateConfig({
|
|
214
|
+
resize: {
|
|
215
|
+
width: 0,
|
|
216
|
+
height: 0,
|
|
217
|
+
maintainAspectRatio: true
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
break;
|
|
221
|
+
case 'crop':
|
|
222
|
+
updateConfig({
|
|
223
|
+
crop: undefined
|
|
224
|
+
});
|
|
225
|
+
break;
|
|
226
|
+
case 'rotate':
|
|
227
|
+
updateConfig({
|
|
228
|
+
rotation: 0
|
|
229
|
+
});
|
|
230
|
+
break;
|
|
231
|
+
case 'border':
|
|
232
|
+
updateConfig({
|
|
233
|
+
border: undefined
|
|
234
|
+
});
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
if (!activeTool) return null;
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div className="w-[280px] bg-zinc-900 border-l border-zinc-800 p-4 flex flex-col gap-4 overflow-y-auto shrink-0 z-10">
|
|
243
|
+
<div className="flex justify-between items-center mb-2">
|
|
244
|
+
<h2 className="text-zinc-100 font-semibold capitalize">{t(`sidebar.${activeTool}`)}</h2>
|
|
245
|
+
<button
|
|
246
|
+
onClick={handleReset}
|
|
247
|
+
className="text-xs text-zinc-400 hover:text-white bg-zinc-800 hover:bg-zinc-700 px-2 py-1 rounded transition-colors"
|
|
248
|
+
>
|
|
249
|
+
{t('common.reset')}
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{activeTool === 'enhance' && (
|
|
254
|
+
<div className="flex flex-col gap-4">
|
|
255
|
+
<label className="flex items-center justify-between text-zinc-300 text-sm cursor-pointer p-2 hover:bg-zinc-800 rounded">
|
|
256
|
+
<span>{t('panels.autoEnhance')}</span>
|
|
257
|
+
<input
|
|
258
|
+
type="checkbox"
|
|
259
|
+
checked={config.enhancements.autoEnhance}
|
|
260
|
+
onChange={(e) => updateConfig({ enhancements: { ...config.enhancements, autoEnhance: e.target.checked } })}
|
|
261
|
+
className="accent-blue-500"
|
|
262
|
+
/>
|
|
263
|
+
</label>
|
|
264
|
+
<label className="flex items-center justify-between text-zinc-300 text-sm cursor-pointer p-2 hover:bg-zinc-800 rounded">
|
|
265
|
+
<span>{t('panels.fillLight')}</span>
|
|
266
|
+
<input
|
|
267
|
+
type="checkbox"
|
|
268
|
+
checked={config.enhancements.fillLight}
|
|
269
|
+
onChange={(e) => updateConfig({ enhancements: { ...config.enhancements, fillLight: e.target.checked } })}
|
|
270
|
+
className="accent-blue-500"
|
|
271
|
+
/>
|
|
272
|
+
</label>
|
|
273
|
+
<label className="flex items-center justify-between text-zinc-300 text-sm cursor-pointer p-2 hover:bg-zinc-800 rounded">
|
|
274
|
+
<span>{t('panels.autoWhiteBalance')}</span>
|
|
275
|
+
<input
|
|
276
|
+
type="checkbox"
|
|
277
|
+
checked={config.enhancements.autoWhiteBalance}
|
|
278
|
+
onChange={(e) => updateConfig({ enhancements: { ...config.enhancements, autoWhiteBalance: e.target.checked } })}
|
|
279
|
+
className="accent-blue-500"
|
|
280
|
+
/>
|
|
281
|
+
</label>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{activeTool === 'adjust' && (
|
|
286
|
+
<div className="flex flex-col gap-6">
|
|
287
|
+
<SliderControl
|
|
288
|
+
label={t('panels.brightness')}
|
|
289
|
+
value={config.brightness}
|
|
290
|
+
min={-100} max={100}
|
|
291
|
+
onChange={(val) => updateConfig({ brightness: val })}
|
|
292
|
+
/>
|
|
293
|
+
|
|
294
|
+
<SliderControl
|
|
295
|
+
label={t('panels.contrast')}
|
|
296
|
+
value={config.contrast}
|
|
297
|
+
min={-100} max={100}
|
|
298
|
+
onChange={(val) => updateConfig({ contrast: val })}
|
|
299
|
+
/>
|
|
300
|
+
|
|
301
|
+
<SliderControl
|
|
302
|
+
label={t('panels.sharpness')}
|
|
303
|
+
value={config.sharpness}
|
|
304
|
+
min={0} max={100}
|
|
305
|
+
onChange={(val) => updateConfig({ sharpness: val })}
|
|
306
|
+
/>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
|
|
310
|
+
{activeTool === 'resize' && (
|
|
311
|
+
<div className="flex flex-col gap-4">
|
|
312
|
+
{/* 预设尺寸 */}
|
|
313
|
+
<div className="flex flex-col gap-2">
|
|
314
|
+
<label className="text-xs text-zinc-400">{t('common.presetSizes')}</label>
|
|
315
|
+
<div className="relative">
|
|
316
|
+
<select
|
|
317
|
+
className="w-full bg-zinc-800 text-zinc-100 p-2 rounded border border-zinc-700 focus:border-blue-500 outline-none text-sm appearance-none cursor-pointer"
|
|
318
|
+
value={`${config.resize?.width || ''}x${config.resize?.height || ''}`}
|
|
319
|
+
onChange={(e) => {
|
|
320
|
+
const selectedSize = presetSizes.find(
|
|
321
|
+
(size) => `${size.width}x${size.height}` === e.target.value
|
|
322
|
+
);
|
|
323
|
+
if (selectedSize) {
|
|
324
|
+
// 根据当前图片的比例重新计算尺寸
|
|
325
|
+
const calculated = calculateSize(
|
|
326
|
+
selectedSize.mode === 'width' ? selectedSize.width : selectedSize.height,
|
|
327
|
+
selectedSize.mode
|
|
328
|
+
);
|
|
329
|
+
updateConfig({ resize: { width: calculated.width, height: calculated.height, maintainAspectRatio: true } });
|
|
330
|
+
} else {
|
|
331
|
+
// 清空尺寸
|
|
332
|
+
updateConfig({ resize: { width: 0, height: 0, maintainAspectRatio: true } });
|
|
333
|
+
}
|
|
334
|
+
}}
|
|
335
|
+
>
|
|
336
|
+
<option value="">选择预设尺寸</option>
|
|
337
|
+
{presetSizes.map((size) => (
|
|
338
|
+
<option key={`${size.width}x${size.height}`} value={`${size.width}x${size.height}`}>
|
|
339
|
+
{formatSizeDisplay(size.mode === 'width' ? size.width : size.height, size.mode)}
|
|
340
|
+
</option>
|
|
341
|
+
))}
|
|
342
|
+
</select>
|
|
343
|
+
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 pointer-events-none">
|
|
344
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-zinc-400"><path d="m6 9 6 6 6-6"/></svg>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
<div className="flex flex-col gap-1 mt-1">
|
|
348
|
+
<button
|
|
349
|
+
className="w-full text-left text-xs text-zinc-400 hover:text-zinc-300 py-1 px-2 rounded hover:bg-zinc-800"
|
|
350
|
+
onClick={() => {
|
|
351
|
+
setAddSizeMode('width');
|
|
352
|
+
setAddSizeValue('');
|
|
353
|
+
setIsAddSizeDialogOpen(true);
|
|
354
|
+
}}
|
|
355
|
+
>
|
|
356
|
+
添加常用尺寸
|
|
357
|
+
</button>
|
|
358
|
+
<button
|
|
359
|
+
className="w-full text-left text-xs text-zinc-400 hover:text-zinc-300 py-1 px-2 rounded hover:bg-zinc-800"
|
|
360
|
+
onClick={() => {
|
|
361
|
+
setDeleteSizeMode('width');
|
|
362
|
+
setSelectedSizeToDelete('');
|
|
363
|
+
setIsDeleteSizeDialogOpen(true);
|
|
364
|
+
}}
|
|
365
|
+
>
|
|
366
|
+
删除常用尺寸
|
|
367
|
+
</button>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
{/* 手工输入尺寸 */}
|
|
372
|
+
<div className="flex flex-col gap-4 p-4 bg-zinc-850 rounded-lg">
|
|
373
|
+
<div className="flex flex-col gap-3">
|
|
374
|
+
<div className="flex justify-between items-center">
|
|
375
|
+
<div className="flex flex-col">
|
|
376
|
+
<label className="text-xs text-zinc-400">{t('common.width')}</label>
|
|
377
|
+
<input
|
|
378
|
+
type="number"
|
|
379
|
+
className="bg-zinc-800 text-zinc-100 p-2 rounded border border-zinc-700 focus:border-blue-500 outline-none text-sm w-32"
|
|
380
|
+
value={config.resize?.width || ''}
|
|
381
|
+
onChange={(e) => {
|
|
382
|
+
const width = parseInt(e.target.value) || 0;
|
|
383
|
+
let height = config.resize?.height || 0;
|
|
384
|
+
|
|
385
|
+
if (config.resize?.maintainAspectRatio && width > 0 && originalWidth > 0 && originalHeight > 0) {
|
|
386
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
387
|
+
height = Math.round(width / aspectRatio);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
updateConfig({ resize: { ...config.resize, width, height, maintainAspectRatio: config.resize?.maintainAspectRatio ?? true } });
|
|
391
|
+
}}
|
|
392
|
+
/>
|
|
393
|
+
</div>
|
|
394
|
+
<span className="text-zinc-400 self-end mb-6">像素</span>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
<div className="flex justify-between items-center">
|
|
398
|
+
<div className="flex flex-col">
|
|
399
|
+
<label className="text-xs text-zinc-400">{t('common.height')}</label>
|
|
400
|
+
<input
|
|
401
|
+
type="number"
|
|
402
|
+
className="bg-zinc-800 text-zinc-100 p-2 rounded border border-zinc-700 focus:border-blue-500 outline-none text-sm w-32"
|
|
403
|
+
value={config.resize?.height || ''}
|
|
404
|
+
onChange={(e) => {
|
|
405
|
+
const height = parseInt(e.target.value) || 0;
|
|
406
|
+
let width = config.resize?.width || 0;
|
|
407
|
+
|
|
408
|
+
if (config.resize?.maintainAspectRatio && height > 0 && originalWidth > 0 && originalHeight > 0) {
|
|
409
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
410
|
+
width = Math.round(height * aspectRatio);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
updateConfig({ resize: { ...config.resize, width, height, maintainAspectRatio: config.resize?.maintainAspectRatio ?? true } });
|
|
414
|
+
}}
|
|
415
|
+
/>
|
|
416
|
+
</div>
|
|
417
|
+
<span className="text-zinc-400 self-end mb-6">像素</span>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<div className="flex items-center gap-2">
|
|
421
|
+
<input
|
|
422
|
+
type="checkbox"
|
|
423
|
+
id="maintainAspectRatio"
|
|
424
|
+
checked={config.resize?.maintainAspectRatio ?? true}
|
|
425
|
+
onChange={(e) => updateConfig({ resize: { ...config.resize, maintainAspectRatio: e.target.checked } })}
|
|
426
|
+
className="accent-blue-500"
|
|
427
|
+
/>
|
|
428
|
+
<label htmlFor="maintainAspectRatio" className="text-xs text-zinc-400 cursor-pointer">{t('common.lockAspectRatio')}</label>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<div className="flex gap-3 mt-2">
|
|
433
|
+
<button
|
|
434
|
+
onClick={() => {
|
|
435
|
+
// 确定按钮的逻辑
|
|
436
|
+
// 这里可以添加额外的验证或处理
|
|
437
|
+
}}
|
|
438
|
+
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white py-2 rounded text-sm font-medium"
|
|
439
|
+
>
|
|
440
|
+
{t('common.confirm')}
|
|
441
|
+
</button>
|
|
442
|
+
<button
|
|
443
|
+
onClick={() => {
|
|
444
|
+
// 取消按钮的逻辑
|
|
445
|
+
// 可以重置为之前的尺寸或保持不变
|
|
446
|
+
}}
|
|
447
|
+
className="flex-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-200 py-2 rounded text-sm"
|
|
448
|
+
>
|
|
449
|
+
{t('common.cancel')}
|
|
450
|
+
</button>
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
{activeTool === 'border' && (
|
|
457
|
+
<div className="flex flex-col gap-6">
|
|
458
|
+
<SliderControl
|
|
459
|
+
label={t('panels.borderSize')}
|
|
460
|
+
value={config.border?.size ?? 0}
|
|
461
|
+
min={0} max={300}
|
|
462
|
+
onChange={(val) => updateConfig({
|
|
463
|
+
border: {
|
|
464
|
+
color: config.border?.color || '#ffffff',
|
|
465
|
+
applyHorizontal: config.border?.applyHorizontal ?? true,
|
|
466
|
+
applyVertical: config.border?.applyVertical ?? false,
|
|
467
|
+
...config.border,
|
|
468
|
+
size: val
|
|
469
|
+
}
|
|
470
|
+
})}
|
|
471
|
+
unit="%"
|
|
472
|
+
/>
|
|
473
|
+
|
|
474
|
+
<div className="flex gap-4">
|
|
475
|
+
<label className="flex items-center gap-2 text-xs text-zinc-300 cursor-pointer">
|
|
476
|
+
<input
|
|
477
|
+
type="checkbox"
|
|
478
|
+
checked={config.border?.applyHorizontal ?? true}
|
|
479
|
+
onChange={(e) => updateConfig({
|
|
480
|
+
border: {
|
|
481
|
+
color: config.border?.color || '#ffffff',
|
|
482
|
+
size: config.border?.size ?? 0,
|
|
483
|
+
applyVertical: config.border?.applyVertical ?? false,
|
|
484
|
+
...config.border,
|
|
485
|
+
applyHorizontal: e.target.checked
|
|
486
|
+
}
|
|
487
|
+
})}
|
|
488
|
+
className="rounded bg-zinc-700 border-zinc-600"
|
|
489
|
+
/>
|
|
490
|
+
{t('panels.borderHorizontal')}
|
|
491
|
+
</label>
|
|
492
|
+
<label className="flex items-center gap-2 text-xs text-zinc-300 cursor-pointer">
|
|
493
|
+
<input
|
|
494
|
+
type="checkbox"
|
|
495
|
+
checked={config.border?.applyVertical ?? false}
|
|
496
|
+
onChange={(e) => updateConfig({
|
|
497
|
+
border: {
|
|
498
|
+
color: config.border?.color || '#ffffff',
|
|
499
|
+
size: config.border?.size ?? 0,
|
|
500
|
+
applyHorizontal: config.border?.applyHorizontal ?? true,
|
|
501
|
+
...config.border,
|
|
502
|
+
applyVertical: e.target.checked
|
|
503
|
+
}
|
|
504
|
+
})}
|
|
505
|
+
className="rounded bg-zinc-700 border-zinc-600"
|
|
506
|
+
/>
|
|
507
|
+
{t('panels.borderVertical')}
|
|
508
|
+
</label>
|
|
509
|
+
</div>
|
|
510
|
+
|
|
511
|
+
<div className="space-y-2">
|
|
512
|
+
<span className="text-xs text-zinc-400 block">{t('panels.borderColor')}</span>
|
|
513
|
+
<div className="flex items-center gap-2">
|
|
514
|
+
<input
|
|
515
|
+
type="color"
|
|
516
|
+
value={config.border?.color ?? '#ffffff'}
|
|
517
|
+
onChange={(e) => updateConfig({
|
|
518
|
+
border: {
|
|
519
|
+
size: config.border?.size ?? 0,
|
|
520
|
+
applyHorizontal: config.border?.applyHorizontal ?? true,
|
|
521
|
+
applyVertical: config.border?.applyVertical ?? false,
|
|
522
|
+
...config.border,
|
|
523
|
+
color: e.target.value
|
|
524
|
+
}
|
|
525
|
+
})}
|
|
526
|
+
className="w-8 h-8 rounded cursor-pointer bg-transparent border-0 p-0"
|
|
527
|
+
/>
|
|
528
|
+
<input
|
|
529
|
+
type="text"
|
|
530
|
+
value={config.border?.color ?? '#ffffff'}
|
|
531
|
+
onChange={(e) => updateConfig({
|
|
532
|
+
border: {
|
|
533
|
+
size: config.border?.size ?? 0,
|
|
534
|
+
applyHorizontal: config.border?.applyHorizontal ?? true,
|
|
535
|
+
applyVertical: config.border?.applyVertical ?? false,
|
|
536
|
+
...config.border,
|
|
537
|
+
color: e.target.value
|
|
538
|
+
}
|
|
539
|
+
})}
|
|
540
|
+
className="flex-1 bg-zinc-800 text-zinc-300 px-2 py-1.5 rounded text-xs border border-zinc-700"
|
|
541
|
+
/>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
)}
|
|
546
|
+
|
|
547
|
+
{activeTool === 'crop' && (
|
|
548
|
+
<div className="flex flex-col gap-4">
|
|
549
|
+
<div className="flex justify-between items-center">
|
|
550
|
+
<div className="text-xs text-zinc-400">{t('panels.aspectRatio')}</div>
|
|
551
|
+
<button
|
|
552
|
+
onClick={() => setIsAspectRatioManagerOpen(true)}
|
|
553
|
+
className="text-zinc-400 hover:text-zinc-200 p-1 rounded hover:bg-zinc-800 transition-colors"
|
|
554
|
+
title={t('common.manageRatios')}
|
|
555
|
+
>
|
|
556
|
+
<Settings size={14} />
|
|
557
|
+
</button>
|
|
558
|
+
</div>
|
|
559
|
+
<div className="grid grid-cols-3 gap-2">
|
|
560
|
+
{aspectRatios.map(ratio => (
|
|
561
|
+
<button
|
|
562
|
+
key={ratio.id}
|
|
563
|
+
className={`py-2 rounded text-xs border ${
|
|
564
|
+
config.crop?.aspectRatio === ratio.value
|
|
565
|
+
? 'bg-blue-600 border-blue-500 text-white'
|
|
566
|
+
: 'bg-zinc-800 border-zinc-700 text-zinc-300 hover:bg-zinc-700'
|
|
567
|
+
}`}
|
|
568
|
+
onClick={() => updateConfig({ crop: { ...config.crop, aspectRatio: ratio.value, x: 0, y: 0, width: 100, height: 100 } })}
|
|
569
|
+
>
|
|
570
|
+
{ratio.value === 'Free' ? t('panels.free') : ratio.label}
|
|
571
|
+
</button>
|
|
572
|
+
))}
|
|
573
|
+
</div>
|
|
574
|
+
|
|
575
|
+
<button
|
|
576
|
+
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded text-sm font-medium mt-4"
|
|
577
|
+
onClick={() => {
|
|
578
|
+
if (cropRect.width > 0 && cropRect.height > 0) {
|
|
579
|
+
applyCrop();
|
|
580
|
+
} else {
|
|
581
|
+
alert('请先选择裁剪区域');
|
|
582
|
+
}
|
|
583
|
+
}}
|
|
584
|
+
>
|
|
585
|
+
应用裁剪
|
|
586
|
+
</button>
|
|
587
|
+
|
|
588
|
+
<button
|
|
589
|
+
className="w-full bg-zinc-800 hover:bg-zinc-700 text-zinc-300 py-2 rounded text-sm"
|
|
590
|
+
onClick={() => {
|
|
591
|
+
updateConfig({ crop: undefined });
|
|
592
|
+
}}
|
|
593
|
+
>
|
|
594
|
+
取消裁剪
|
|
595
|
+
</button>
|
|
596
|
+
|
|
597
|
+
<p className="text-[10px] text-zinc-500 mt-2">
|
|
598
|
+
* {t('panels.cropWarning')}
|
|
599
|
+
</p>
|
|
600
|
+
|
|
601
|
+
<AspectRatioDialog
|
|
602
|
+
isOpen={isAspectRatioManagerOpen}
|
|
603
|
+
onClose={() => setIsAspectRatioManagerOpen(false)}
|
|
604
|
+
/>
|
|
605
|
+
</div>
|
|
606
|
+
)}
|
|
607
|
+
|
|
608
|
+
{activeTool === 'rotate' && (
|
|
609
|
+
<div className="flex flex-col gap-6">
|
|
610
|
+
<div className="flex justify-between gap-2">
|
|
611
|
+
<button
|
|
612
|
+
onClick={() => {
|
|
613
|
+
// Normalize to -180 ~ 180
|
|
614
|
+
let newRot = (config.rotation - 90) % 360;
|
|
615
|
+
if (newRot > 180) newRot -= 360;
|
|
616
|
+
if (newRot <= -180) newRot += 360;
|
|
617
|
+
updateConfig({ rotation: newRot });
|
|
618
|
+
}}
|
|
619
|
+
className="flex-1 bg-zinc-800 text-zinc-300 py-2 rounded text-sm hover:bg-zinc-700"
|
|
620
|
+
>
|
|
621
|
+
{t('panels.rotateLeft')}
|
|
622
|
+
</button>
|
|
623
|
+
<button
|
|
624
|
+
onClick={() => {
|
|
625
|
+
// Normalize to -180 ~ 180
|
|
626
|
+
let newRot = (config.rotation + 90) % 360;
|
|
627
|
+
if (newRot > 180) newRot -= 360;
|
|
628
|
+
if (newRot <= -180) newRot += 360;
|
|
629
|
+
updateConfig({ rotation: newRot });
|
|
630
|
+
}}
|
|
631
|
+
className="flex-1 bg-zinc-800 text-zinc-300 py-2 rounded text-sm hover:bg-zinc-700"
|
|
632
|
+
>
|
|
633
|
+
{t('panels.rotateRight')}
|
|
634
|
+
</button>
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
<button
|
|
638
|
+
className={`flex items-center justify-center gap-2 py-2 rounded text-sm transition-colors border ${
|
|
639
|
+
isStraightenToolActive
|
|
640
|
+
? 'text-blue-500 bg-zinc-800 border-blue-500/50'
|
|
641
|
+
: 'bg-zinc-800 text-zinc-300 hover:bg-zinc-700 border-transparent'
|
|
642
|
+
}`}
|
|
643
|
+
onClick={() => setStraightenToolActive(!isStraightenToolActive)}
|
|
644
|
+
>
|
|
645
|
+
<Ruler size={16} />
|
|
646
|
+
{t('panels.straighten')}
|
|
647
|
+
</button>
|
|
648
|
+
|
|
649
|
+
{isStraightenToolActive && (
|
|
650
|
+
<p className="text-[10px] text-zinc-500">
|
|
651
|
+
* {t('panels.straightenTip')}
|
|
652
|
+
</p>
|
|
653
|
+
)}
|
|
654
|
+
|
|
655
|
+
<SliderControl
|
|
656
|
+
label={t('panels.rotationAngle')}
|
|
657
|
+
value={config.rotation}
|
|
658
|
+
min={-180} max={180} step={0.5}
|
|
659
|
+
onChange={(val) => updateConfig({ rotation: val })}
|
|
660
|
+
unit="°"
|
|
661
|
+
/>
|
|
662
|
+
</div>
|
|
663
|
+
)}
|
|
664
|
+
|
|
665
|
+
{/* 添加常用尺寸对话框 */}
|
|
666
|
+
{isAddSizeDialogOpen && (
|
|
667
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
668
|
+
<div className="bg-zinc-900 border border-zinc-800 rounded-lg p-5 w-80">
|
|
669
|
+
<div className="flex justify-between items-center mb-4">
|
|
670
|
+
<h3 className="text-zinc-100 font-medium">添加常用尺寸</h3>
|
|
671
|
+
<button
|
|
672
|
+
onClick={() => setIsAddSizeDialogOpen(false)}
|
|
673
|
+
className="text-zinc-400 hover:text-zinc-200"
|
|
674
|
+
>
|
|
675
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
676
|
+
</button>
|
|
677
|
+
</div>
|
|
678
|
+
|
|
679
|
+
<div className="space-y-4">
|
|
680
|
+
<div className="space-y-2">
|
|
681
|
+
<div className="flex items-center gap-4">
|
|
682
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
683
|
+
<input
|
|
684
|
+
type="radio"
|
|
685
|
+
name="addSizeMode"
|
|
686
|
+
value="width"
|
|
687
|
+
checked={addSizeMode === 'width'}
|
|
688
|
+
onChange={() => setAddSizeMode('width')}
|
|
689
|
+
className="accent-blue-500"
|
|
690
|
+
/>
|
|
691
|
+
<span className="text-zinc-300 text-sm">按宽度</span>
|
|
692
|
+
</label>
|
|
693
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
694
|
+
<input
|
|
695
|
+
type="radio"
|
|
696
|
+
name="addSizeMode"
|
|
697
|
+
value="height"
|
|
698
|
+
checked={addSizeMode === 'height'}
|
|
699
|
+
onChange={() => setAddSizeMode('height')}
|
|
700
|
+
className="accent-blue-500"
|
|
701
|
+
/>
|
|
702
|
+
<span className="text-zinc-300 text-sm">按高度</span>
|
|
703
|
+
</label>
|
|
704
|
+
</div>
|
|
705
|
+
</div>
|
|
706
|
+
|
|
707
|
+
{addSizeMode === 'width' ? (
|
|
708
|
+
<div className="flex items-center gap-2">
|
|
709
|
+
<input
|
|
710
|
+
type="number"
|
|
711
|
+
placeholder="宽度"
|
|
712
|
+
className="flex-1 bg-zinc-800 text-zinc-100 p-2 rounded border border-zinc-700 focus:border-blue-500 outline-none text-sm"
|
|
713
|
+
value={addSizeValue}
|
|
714
|
+
onChange={(e) => setAddSizeValue(e.target.value)}
|
|
715
|
+
/>
|
|
716
|
+
<span className="text-zinc-400">×</span>
|
|
717
|
+
<span className="text-zinc-400">对应高度</span>
|
|
718
|
+
</div>
|
|
719
|
+
) : (
|
|
720
|
+
<div className="flex items-center gap-2">
|
|
721
|
+
<span className="text-zinc-400">对应宽度</span>
|
|
722
|
+
<span className="text-zinc-400">×</span>
|
|
723
|
+
<input
|
|
724
|
+
type="number"
|
|
725
|
+
placeholder="高度"
|
|
726
|
+
className="flex-1 bg-zinc-800 text-zinc-100 p-2 rounded border border-zinc-700 focus:border-blue-500 outline-none text-sm"
|
|
727
|
+
value={addSizeValue}
|
|
728
|
+
onChange={(e) => setAddSizeValue(e.target.value)}
|
|
729
|
+
/>
|
|
730
|
+
</div>
|
|
731
|
+
)}
|
|
732
|
+
|
|
733
|
+
<div className="flex gap-3 mt-6">
|
|
734
|
+
<button
|
|
735
|
+
onClick={() => {
|
|
736
|
+
// 添加尺寸逻辑
|
|
737
|
+
const value = parseInt(addSizeValue);
|
|
738
|
+
if (!isNaN(value) && value > 0) {
|
|
739
|
+
// 根据原始图片比例计算尺寸
|
|
740
|
+
const calculated = calculateSize(value, addSizeMode);
|
|
741
|
+
const newSize = {
|
|
742
|
+
width: calculated.width,
|
|
743
|
+
height: calculated.height,
|
|
744
|
+
mode: addSizeMode
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
// 检查尺寸是否已存在
|
|
748
|
+
const isDuplicate = presetSizes.some(
|
|
749
|
+
(size) => size.width === newSize.width && size.height === newSize.height
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
if (!isDuplicate) {
|
|
753
|
+
// 添加新尺寸并按宽度从小到大排序
|
|
754
|
+
const updatedSizes = [...presetSizes, newSize].sort((a, b) => a.width - b.width);
|
|
755
|
+
setPresetSizes(updatedSizes);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// 重置状态并关闭对话框
|
|
759
|
+
setAddSizeValue('');
|
|
760
|
+
setIsAddSizeDialogOpen(false);
|
|
761
|
+
}
|
|
762
|
+
}}
|
|
763
|
+
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white py-2 rounded text-sm font-medium"
|
|
764
|
+
>
|
|
765
|
+
确定
|
|
766
|
+
</button>
|
|
767
|
+
<button
|
|
768
|
+
onClick={() => {
|
|
769
|
+
// 取消逻辑
|
|
770
|
+
setAddSizeValue('');
|
|
771
|
+
setIsAddSizeDialogOpen(false);
|
|
772
|
+
}}
|
|
773
|
+
className="flex-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-200 py-2 rounded text-sm"
|
|
774
|
+
>
|
|
775
|
+
取消
|
|
776
|
+
</button>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
)}
|
|
782
|
+
|
|
783
|
+
{/* 删除常用尺寸对话框 */}
|
|
784
|
+
{isDeleteSizeDialogOpen && (
|
|
785
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
786
|
+
<div className="bg-zinc-900 border border-zinc-800 rounded-lg p-5 w-80">
|
|
787
|
+
<div className="flex justify-between items-center mb-4">
|
|
788
|
+
<h3 className="text-zinc-100 font-medium">删除常用尺寸</h3>
|
|
789
|
+
<button
|
|
790
|
+
onClick={() => setIsDeleteSizeDialogOpen(false)}
|
|
791
|
+
className="text-zinc-400 hover:text-zinc-200"
|
|
792
|
+
>
|
|
793
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
794
|
+
</button>
|
|
795
|
+
</div>
|
|
796
|
+
|
|
797
|
+
<div className="space-y-4">
|
|
798
|
+
<div className="space-y-2">
|
|
799
|
+
<div className="flex items-center gap-4">
|
|
800
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
801
|
+
<input
|
|
802
|
+
type="radio"
|
|
803
|
+
name="deleteSizeMode"
|
|
804
|
+
value="width"
|
|
805
|
+
checked={deleteSizeMode === 'width'}
|
|
806
|
+
onChange={() => {
|
|
807
|
+
setDeleteSizeMode('width');
|
|
808
|
+
setSelectedSizeToDelete('');
|
|
809
|
+
}}
|
|
810
|
+
className="accent-blue-500"
|
|
811
|
+
/>
|
|
812
|
+
<span className="text-zinc-300 text-sm">按宽度</span>
|
|
813
|
+
</label>
|
|
814
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
815
|
+
<input
|
|
816
|
+
type="radio"
|
|
817
|
+
name="deleteSizeMode"
|
|
818
|
+
value="height"
|
|
819
|
+
checked={deleteSizeMode === 'height'}
|
|
820
|
+
onChange={() => {
|
|
821
|
+
setDeleteSizeMode('height');
|
|
822
|
+
setSelectedSizeToDelete('');
|
|
823
|
+
}}
|
|
824
|
+
className="accent-blue-500"
|
|
825
|
+
/>
|
|
826
|
+
<span className="text-zinc-300 text-sm">按高度</span>
|
|
827
|
+
</label>
|
|
828
|
+
</div>
|
|
829
|
+
</div>
|
|
830
|
+
|
|
831
|
+
<div>
|
|
832
|
+
<select
|
|
833
|
+
className="w-full bg-zinc-800 text-zinc-100 p-2 rounded border border-zinc-700 focus:border-blue-500 outline-none text-sm"
|
|
834
|
+
value={selectedSizeToDelete}
|
|
835
|
+
onChange={(e) => setSelectedSizeToDelete(e.target.value)}
|
|
836
|
+
>
|
|
837
|
+
<option value="">选择要删除的尺寸</option>
|
|
838
|
+
{presetSizes
|
|
839
|
+
.filter((size) => size.mode === deleteSizeMode)
|
|
840
|
+
.map((size) => (
|
|
841
|
+
<option key={`${size.width}x${size.height}`} value={`${size.width}x${size.height}`}>
|
|
842
|
+
{formatSizeDisplay(size.mode === 'width' ? size.width : size.height, size.mode)}
|
|
843
|
+
</option>
|
|
844
|
+
))}
|
|
845
|
+
</select>
|
|
846
|
+
</div>
|
|
847
|
+
|
|
848
|
+
<div className="flex gap-3 mt-6">
|
|
849
|
+
<button
|
|
850
|
+
onClick={() => {
|
|
851
|
+
// 删除尺寸逻辑
|
|
852
|
+
if (selectedSizeToDelete) {
|
|
853
|
+
const [width, height] = selectedSizeToDelete.split('x').map(Number);
|
|
854
|
+
setPresetSizes(
|
|
855
|
+
presetSizes.filter(
|
|
856
|
+
(size) => size.width !== width || size.height !== height
|
|
857
|
+
)
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
// 重置状态并关闭对话框
|
|
861
|
+
setSelectedSizeToDelete('');
|
|
862
|
+
setIsDeleteSizeDialogOpen(false);
|
|
863
|
+
}
|
|
864
|
+
}}
|
|
865
|
+
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white py-2 rounded text-sm font-medium"
|
|
866
|
+
>
|
|
867
|
+
确定
|
|
868
|
+
</button>
|
|
869
|
+
<button
|
|
870
|
+
onClick={() => {
|
|
871
|
+
// 取消逻辑
|
|
872
|
+
setSelectedSizeToDelete('');
|
|
873
|
+
setIsDeleteSizeDialogOpen(false);
|
|
874
|
+
}}
|
|
875
|
+
className="flex-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-200 py-2 rounded text-sm"
|
|
876
|
+
>
|
|
877
|
+
取消
|
|
878
|
+
</button>
|
|
879
|
+
</div>
|
|
880
|
+
</div>
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
)}
|
|
884
|
+
</div>
|
|
885
|
+
);
|
|
886
|
+
};
|