3af4878d
杨鑫
产品 标签 关联
|
1
2
3
4
5
6
7
8
9
10
11
12
|
import React, { useRef, useState } from "react";
import { Plus, X } from "lucide-react";
import { toast } from "sonner";
import { cn } from "./utils";
import { PICTURE_UPLOAD_MAX_BYTES, resolvePictureUrlForDisplay, uploadImageFile } from "../../services/imageUploadService";
export type ImageUrlUploadProps = {
value: string;
onChange: (url: string) => void;
disabled?: boolean;
/** 辅助说明,显示在方框下方 */
hint?: string;
|
b04bc3a5
杨鑫
打印日志 重新打印
|
13
|
/** 空状态主文案(默认无,仅加号;需要时传入如 "Click to upload") */
|
3af4878d
杨鑫
产品 标签 关联
|
14
15
16
17
18
19
20
|
emptyLabel?: string;
accept?: string;
/** 默认 5MB,与平台 picture 上传接口一致 */
maxSizeMb?: number;
className?: string;
/** 上传区域宽度(tailwind),默认 max-w-[200px] */
boxClassName?: string;
|
923d50c0
杨鑫
更新bug
|
21
22
|
/** 固定上传区边长(px);优先于仅依赖 Tailwind 的 boxClassName,避免预构建 CSS 缺类名时撑满整行 */
boxSizePx?: number;
|
3af4878d
杨鑫
产品 标签 关联
|
23
24
25
26
27
28
29
30
31
32
33
|
/** 传给后端的 multipart `subDir`(如 category、product) */
uploadSubDir?: string;
/** 明确单图:隐藏多选、提示文案 */
oneImageOnly?: boolean;
};
export function ImageUrlUpload({
value,
onChange,
disabled,
hint,
|
b04bc3a5
杨鑫
打印日志 重新打印
|
34
|
emptyLabel = "",
|
3af4878d
杨鑫
产品 标签 关联
|
35
36
37
38
|
accept = "image/jpeg,image/png,image/webp,image/gif",
maxSizeMb = PICTURE_UPLOAD_MAX_BYTES / (1024 * 1024),
className,
boxClassName,
|
923d50c0
杨鑫
更新bug
|
39
|
boxSizePx,
|
3af4878d
杨鑫
产品 标签 关联
|
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
uploadSubDir,
oneImageOnly,
}: ImageUrlUploadProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [uploading, setUploading] = useState(false);
const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
e.target.value = "";
if (!file) return;
if (!file.type.startsWith("image/")) {
toast.error("Please select an image file.");
return;
}
if (file.size > maxSizeMb * 1024 * 1024) {
toast.error(`Image must be ${maxSizeMb} MB or smaller.`);
return;
}
setUploading(true);
try {
const url = await uploadImageFile(file, { subDir: uploadSubDir });
onChange(url);
toast.success("Image uploaded.");
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
toast.error("Upload failed", { description: msg || undefined });
} finally {
setUploading(false);
}
};
const busy = disabled || uploading;
const openPicker = () => {
if (!busy) inputRef.current?.click();
};
|
923d50c0
杨鑫
更新bug
|
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
const fixedSize = typeof boxSizePx === "number" && boxSizePx > 0 ? Math.round(boxSizePx) : 0;
const hasCustomBox = fixedSize > 0 || Boolean(boxClassName?.trim());
const boxSizeStyle: React.CSSProperties | undefined =
fixedSize > 0
? {
width: fixedSize,
height: fixedSize,
minWidth: fixedSize,
minHeight: fixedSize,
maxWidth: fixedSize,
maxHeight: fixedSize,
boxSizing: "border-box",
}
: undefined;
|
63289723
杨鑫
提交
|
92
|
const boxShell = hasCustomBox
|
923d50c0
杨鑫
更新bug
|
93
|
? "box-border shrink-0 rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
63289723
杨鑫
提交
|
94
|
: "w-full max-w-[200px] aspect-square rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2";
|
3af4878d
杨鑫
产品 标签 关联
|
95
96
|
return (
|
923d50c0
杨鑫
更新bug
|
97
|
<div className={cn("space-y-2", hasCustomBox && "w-fit max-w-full", className)}>
|
3af4878d
杨鑫
产品 标签 关联
|
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
<input
ref={inputRef}
type="file"
accept={accept}
className="sr-only"
disabled={busy}
multiple={false}
onChange={onFileChange}
/>
{!value ? (
<button
type="button"
disabled={busy}
onClick={openPicker}
|
b04bc3a5
杨鑫
打印日志 重新打印
|
113
|
aria-label={emptyLabel || "Upload image"}
|
923d50c0
杨鑫
更新bug
|
114
|
style={boxSizeStyle}
|
3af4878d
杨鑫
产品 标签 关联
|
115
|
className={cn(
|
63289723
杨鑫
提交
|
116
117
|
boxShell,
hasCustomBox ? boxClassName : null,
|
b04bc3a5
杨鑫
打印日志 重新打印
|
118
119
120
121
|
"flex border-2 border-dashed border-gray-300 bg-gray-50/80 text-gray-400",
emptyLabel && !uploading
? "flex-col items-center justify-center gap-2"
: "items-center justify-center",
|
3af4878d
杨鑫
产品 标签 关联
|
122
123
|
"hover:border-gray-400 hover:bg-gray-100/90 hover:text-gray-500",
"disabled:pointer-events-none disabled:opacity-50",
|
3af4878d
杨鑫
产品 标签 关联
|
124
125
|
)}
>
|
b04bc3a5
杨鑫
打印日志 重新打印
|
126
127
128
129
|
{uploading ? (
<span className="px-3 text-center text-sm font-normal text-gray-500">Uploading…</span>
) : (
<>
|
923d50c0
杨鑫
更新bug
|
130
131
132
133
134
135
136
|
<Plus
className={cn(
"shrink-0 stroke-[1.25]",
fixedSize > 0 && fixedSize <= 120 ? "h-8 w-8" : "h-10 w-10",
)}
aria-hidden
/>
|
b04bc3a5
杨鑫
打印日志 重新打印
|
137
138
139
140
141
142
143
|
{emptyLabel ? (
<span className="px-3 text-center text-sm font-normal leading-tight text-gray-400">
{emptyLabel}
</span>
) : null}
</>
)}
|
3af4878d
杨鑫
产品 标签 关联
|
144
145
146
|
</button>
) : (
<div
|
923d50c0
杨鑫
更新bug
|
147
|
style={boxSizeStyle}
|
3af4878d
杨鑫
产品 标签 关联
|
148
|
className={cn(
|
63289723
杨鑫
提交
|
149
150
151
|
"group relative overflow-hidden border-2 border-dashed border-gray-300 bg-gray-50/80",
boxShell,
hasCustomBox ? boxClassName : null,
|
3af4878d
杨鑫
产品 标签 关联
|
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
|
)}
>
<button
type="button"
disabled={busy}
onClick={openPicker}
className="relative h-full w-full p-0"
aria-label="Replace image"
>
<img
src={resolvePictureUrlForDisplay(value)}
alt=""
className="h-full w-full object-contain"
onError={(ev) => {
(ev.target as HTMLImageElement).style.opacity = "0.2";
}}
/>
<span className="absolute inset-0 flex items-center justify-center bg-black/0 text-sm font-medium text-white opacity-0 transition group-hover:bg-black/45 group-hover:opacity-100">
Click to replace
</span>
</button>
<button
type="button"
disabled={busy}
onClick={(e) => {
e.stopPropagation();
onChange("");
}}
className="absolute right-1.5 top-1.5 flex h-7 w-7 items-center justify-center rounded-full bg-white/95 text-gray-600 shadow-sm ring-1 ring-gray-200 transition hover:bg-white hover:text-gray-900 disabled:opacity-50"
aria-label="Remove image"
>
<X className="h-4 w-4" />
</button>
</div>
)}
{oneImageOnly ? (
<p className="text-xs text-muted-foreground">One image only. Replace or clear to change.</p>
) : null}
{hint ? <p className="text-xs text-muted-foreground">{hint}</p> : null}
</div>
);
}
|