"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useDropzone } from "react-dropzone"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import type { MediaAsset, MediaKind, MediaSelection } from "@/types/media"; import { emitAssetsChanged } from "@/lib/events"; export type PickerTab = "upload" | "recent" | "link" | "icon"; interface ImagePickerDialogProps { open: boolean; defaultTab: PickerTab; mediaType: MediaKind; documentId: string; workspaceId: string; onClose: () => void; onSelect: (selection: MediaSelection) => void; } const ICON_PRESETS: Array<{ id: string; label: string; url: string }> = [ { id: "progress", label: "进度波纹", url: "https://media.giphy.com/media/QBd2kLB5qDmysEXre9/giphy.gif" }, { id: "heartbeat", label: "心跳", url: "https://media.giphy.com/media/HEPwfdu6T6svpPE1eN/giphy.gif" }, { id: "sparkles", label: "闪光", url: "https://media.giphy.com/media/f9jQLaPmuFKfa7E55Q/giphy.gif" }, { id: "loading", label: "加载", url: "https://media.giphy.com/media/52qtwCtj9OLTi/giphy.gif" }, ]; const MEDIA_TABS: Record = { image: ["upload", "recent", "link", "icon"], video: ["upload", "recent", "link"], audio: ["upload", "recent", "link"], file: ["upload", "recent", "link"], }; const TAB_LABELS: Record = { upload: "上传", recent: "最近", link: "链接", icon: "动态图标", }; const MEDIA_ACCEPTS: Record | null> = { image: { "image/*": [] }, video: { "video/*": [] }, audio: { "audio/*": [] }, file: null, }; const MEDIA_TITLES: Record = { image: "选择图片", video: "选择视频", audio: "选择音频", file: "选择文件", }; const formatBytes = (size?: number | null) => { if (!size || size <= 0) { return ""; } const units = ["B", "KB", "MB", "GB", "TB"]; let value = size; let index = 0; while (value >= 1024 && index < units.length - 1) { value /= 1024; index += 1; } return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[index]}`; }; const guessFileNameFromUrl = (url: string) => { try { const parsed = new URL(url); const name = parsed.pathname.split("/").filter(Boolean).pop(); if (name) { return decodeURIComponent(name); } } catch { const segments = url.split("?")[0]?.split("/") ?? []; const name = segments.pop(); if (name) { return decodeURIComponent(name); } } return "未命名资源"; }; export function ImagePickerDialog({ open, defaultTab, mediaType, documentId, workspaceId, onClose, onSelect, }: ImagePickerDialogProps) { const [tab, setTab] = useState(defaultTab); const availableTabs = MEDIA_TABS[mediaType]; const [recent, setRecent] = useState([]); const [recentLoading, setRecentLoading] = useState(false); const [linkUrl, setLinkUrl] = useState(""); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (open) { const availableTabs = MEDIA_TABS[mediaType]; const fallback = availableTabs.includes(defaultTab) ? defaultTab : availableTabs[0]; setTab(fallback); } }, [defaultTab, mediaType, open]); const fetchRecent = useCallback(async () => { if (!workspaceId) { return; } setRecentLoading(true); try { const response = await fetch( `/api/media/assets?workspaceId=${workspaceId}&assetType=${mediaType}`, ); if (!response.ok) { throw new Error("无法获取最近上传"); } const payload = (await response.json()) as { items: MediaAsset[] }; setRecent(payload.items ?? []); } catch (err) { console.error(err); setRecent([]); } finally { setRecentLoading(false); } }, [workspaceId, mediaType]); useEffect(() => { if (open && tab === "recent") { void fetchRecent(); } }, [open, tab, fetchRecent]); const handleUpload = useCallback( async (file: File) => { if (!workspaceId || !documentId) { setError("缺少必要参数"); return; } setUploading(true); setError(null); try { const form = new FormData(); form.append("file", file); form.append("workspaceId", workspaceId); form.append("documentId", documentId); const response = await fetch("/api/media/upload", { method: "POST", body: form, }); if (!response.ok) { const payload = await response.json().catch(() => null); throw new Error(payload?.error ?? "上传失败"); } const payload = (await response.json()) as { asset: MediaAsset }; if (!payload.asset?.file_url) { throw new Error("返回数据缺少文件地址"); } emitAssetsChanged(documentId, payload.asset); onSelect({ assetId: payload.asset.id, fileUrl: payload.asset.file_url, thumbnailUrl: payload.asset.thumbnail_url, assetType: payload.asset.asset_type, fileName: payload.asset.file_name, fileSize: payload.asset.file_size, mimeType: payload.asset.mime_type, }); onClose(); } catch (err) { setError((err as Error).message); } finally { setUploading(false); } }, [documentId, onClose, onSelect, workspaceId], ); const onDrop = useCallback( (acceptedFiles: File[]) => { if (acceptedFiles.length === 0) return; void handleUpload(acceptedFiles[0]); }, [handleUpload], ); const acceptConfig = MEDIA_ACCEPTS[mediaType]; const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: acceptConfig ?? undefined, maxFiles: 1, }); const handleLinkSubmit = async () => { if (!linkUrl.trim()) return; try { const targetUrl = linkUrl.trim(); const response = await fetch("/api/media/assets", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ workspaceId, documentId, fileUrl: targetUrl, thumbnailUrl: targetUrl, assetType: mediaType, fileName: guessFileNameFromUrl(targetUrl), }), }); if (!response.ok) { const payload = await response.json().catch(() => null); throw new Error(payload?.error ?? "保存失败"); } const payload = (await response.json()) as { asset: MediaAsset }; if (!payload.asset?.file_url) { throw new Error("缺少图片地址"); } emitAssetsChanged(documentId, payload.asset); onSelect({ assetId: payload.asset.id, fileUrl: payload.asset.file_url, thumbnailUrl: payload.asset.thumbnail_url, assetType: payload.asset.asset_type, fileName: payload.asset.file_name, fileSize: payload.asset.file_size, mimeType: payload.asset.mime_type, }); setLinkUrl(""); onClose(); } catch (err) { setError((err as Error).message); } }; const renderRecent = () => { if (recentLoading) { return
加载最近上传中...
; } if (recent.length === 0) { return
暂无最近上传的{MEDIA_TITLES[mediaType].replace("选择", "")}
; } return (
{recent.map((asset) => ( ))}
); }; const renderIcons = () => (
{ICON_PRESETS.map((icon) => ( ))}
); const helperText = useMemo(() => { const label = MEDIA_TITLES[mediaType].replace("选择", ""); switch (tab) { case "upload": return `支持拖拽或点击上传${label},单个文件不超过 200MB`; case "recent": return `最近 12 个${label},支持一键选取`; case "link": return `输入${label}直链或 CDN 地址`; case "icon": return "选择 Wolai 常用动态图标"; default: return ""; } }, [mediaType, tab]); return ( !next && onClose()}> {MEDIA_TITLES[mediaType]} {helperText} {error &&
{error}
} setTab(value as PickerTab)}> {availableTabs.map((tabKey) => ( {TAB_LABELS[tabKey]} ))} {availableTabs.includes("upload") && (
{uploading ? "上传中..." : "拖拽文件到此处,或点击选择文件"}
)} {availableTabs.includes("recent") && (
最近上传的{MEDIA_TITLES[mediaType].replace("选择", "")}
{renderRecent()}
)} {availableTabs.includes("link") && (
setLinkUrl(event.target.value)} placeholder={`输入${MEDIA_TITLES[mediaType].replace("选择", "")}链接,例如 https://example.com/file`} />
)} {availableTabs.includes("icon") && {renderIcons()}}
); }