diff --git a/Cunkebao/app/content/[id]/materials/edit/[materialId]/page.tsx b/Cunkebao/app/content/[id]/materials/edit/[materialId]/page.tsx index f22c944f..50d6cf24 100644 --- a/Cunkebao/app/content/[id]/materials/edit/[materialId]/page.tsx +++ b/Cunkebao/app/content/[id]/materials/edit/[materialId]/page.tsx @@ -15,6 +15,7 @@ import { showToast } from "@/lib/toast" import Image from "next/image" import { Input } from "@/components/ui/input" import { cn } from "@/lib/utils" +import { useRef } from "react" interface ApiResponse { code: number @@ -77,6 +78,7 @@ export default function EditMaterialPage({ params }: { params: Promise<{ id: str const [iconUrl, setIconUrl] = useState("") const [videoUrl, setVideoUrl] = useState("") const [comment, setComment] = useState("") + const fileInputRef = useRef(null) // 获取素材详情 useEffect(() => { @@ -153,22 +155,56 @@ export default function EditMaterialPage({ params }: { params: Promise<{ id: str fetchMaterialDetail() }, [resolvedParams.materialId, router]) - // 模拟上传图片 + // 替换handleUploadImage为: const handleUploadImage = () => { - // 这里应该是真实的图片上传逻辑 - // 为了演示,这里模拟添加一些示例图片URL - const mockImageUrls = [ - "https://picsum.photos/id/237/200/300", - "https://picsum.photos/id/238/200/300", - "https://picsum.photos/id/239/200/300" - ] - - const randomIndex = Math.floor(Math.random() * mockImageUrls.length) - const newImage = mockImageUrls[randomIndex] - - if (!images.includes(newImage)) { - setImages([...images, newImage]) - setPreviewUrls([...previewUrls, newImage]) + if (images.length >= 9) { + showToast("最多只能上传9张图片", "error") + return + } + fileInputRef.current?.click() + } + + // 新增真实上传逻辑 + const handleFileChange = async (event: React.ChangeEvent) => { + if (images.length >= 9) { + showToast("最多只能上传9张图片", "error") + return + } + const file = event.target.files?.[0] + if (!file) return + + if (!file.type.startsWith('image/')) { + showToast("请选择图片文件", "error") + return + } + + showToast("正在上传图片...", "loading") + const formData = new FormData() + formData.append("file", file) + + try { + const token = localStorage.getItem('token'); + const headers: HeadersInit = {} + if (token) headers['Authorization'] = `Bearer ${token}` + + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/attachment/upload`, { + method: 'POST', + headers, + body: formData, + }) + + const result = await response.json() + if (result.code === 200 && result.data?.url) { + setImages((prev) => [...prev, result.data.url]) + setPreviewUrls((prev) => [...prev, result.data.url]) + showToast("图片上传成功", "success") + } else { + showToast(result.msg || "图片上传失败", "error") + } + } catch (error: any) { + showToast(error?.message || "图片上传失败", "error") + } finally { + if (fileInputRef.current) fileInputRef.current.value = '' } } @@ -450,12 +486,20 @@ export default function EditMaterialPage({ params }: { params: Promise<{ id: str variant="outline" onClick={handleUploadImage} className="w-full py-8 flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-blue-300 bg-white hover:bg-blue-50" + disabled={images.length >= 9} > 点击上传图片 - 支持 JPG、PNG 格式 + {`已上传${images.length}张,最多可上传9张`} + {previewUrls.length > 0 && (
diff --git a/Cunkebao/app/content/[id]/materials/new/page.tsx b/Cunkebao/app/content/[id]/materials/new/page.tsx index 55eeb53c..e0d75751 100644 --- a/Cunkebao/app/content/[id]/materials/new/page.tsx +++ b/Cunkebao/app/content/[id]/materials/new/page.tsx @@ -2,59 +2,166 @@ import type React from "react" -import { useState } from "react" +import { useState, useRef } from "react" import { useRouter } from "next/navigation" -import { ChevronLeft, Plus, X } from "lucide-react" +import { ChevronLeft, Plus, X, Image as ImageIcon, UploadCloud, Link, Video, FileText, Layers, CalendarDays, ChevronDown } from "lucide-react" import { Card } from "@/components/ui/card" import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Label } from "@/components/ui/label" -import { Badge } from "@/components/ui/badge" import { toast } from "@/components/ui/use-toast" +import Image from "next/image" +import { Input } from "@/components/ui/input" +import { cn } from "@/lib/utils" +import { api } from "@/lib/api" +import { showToast } from "@/lib/toast" + +interface ApiResponse { + code: number + msg: string + data: T +} + +// 素材类型枚举 +const MATERIAL_TYPES = [ + { id: 1, name: "图片", icon: ImageIcon }, + { id: 2, name: "链接", icon: Link }, + { id: 3, name: "视频", icon: Video }, + { id: 4, name: "文本", icon: FileText }, + { id: 5, name: "小程序", icon: Layers } +] export default function NewMaterialPage({ params }: { params: { id: string } }) { const router = useRouter() const [content, setContent] = useState("") - const [newTag, setNewTag] = useState("") - const [tags, setTags] = useState([]) + const [images, setImages] = useState([]) + const [previewUrls, setPreviewUrls] = useState([]) + const [materialType, setMaterialType] = useState(1) + const [url, setUrl] = useState("") + const [desc, setDesc] = useState("") + const [image, setImage] = useState("") + const [videoUrl, setVideoUrl] = useState("") + const [publishTime, setPublishTime] = useState("") + const [comment, setComment] = useState("") + const [loading, setLoading] = useState(false) + const fileInputRef = useRef(null) - const handleAddTag = () => { - if (newTag && !tags.includes(newTag)) { - setTags([...tags, newTag]) - setNewTag("") - } + // 图片上传 + const handleUploadImage = () => { + fileInputRef.current?.click() } - const handleRemoveTag = (tagToRemove: string) => { - setTags(tags.filter((tag) => tag !== tagToRemove)) - } - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!content) { - toast({ - title: "错误", - description: "请输入素材内容", - variant: "destructive", - }) + const handleFileChange = async (event: React.ChangeEvent) => { + if (images.length >= 9) { + showToast("最多只能上传9张图片", "error") return } + const file = event.target.files?.[0] + if (!file) return + + if (!file.type.startsWith('image/')) { + showToast("请选择图片文件", "error") + return + } + + const loadingToast = showToast("正在上传图片...", "loading", true) + setLoading(true) + + const formData = new FormData() + formData.append("file", file) + try { - // 模拟保存新素材 - await new Promise((resolve) => setTimeout(resolve, 1000)) - toast({ - title: "成功", - description: "新素材已创建", - }) + const token = localStorage.getItem('token'); + const headers: HeadersInit = { + // 浏览器会自动为 FormData 设置 Content-Type 为 multipart/form-data,无需手动设置 + }; + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/attachment/upload`, { + method: 'POST', + headers: headers, + body: formData, + }); + + const result: ApiResponse = await response.json(); + + if (result.code === 200 && result.data?.url) { + setImages((prev) => [...prev, result.data.url]); + setPreviewUrls((prev) => [...prev, result.data.url]); + showToast("图片上传成功", "success"); + } else { + showToast(result.msg || "图片上传失败", "error"); + } + } catch (error: any) { + showToast(error?.message || "图片上传失败", "error") + } finally { + loadingToast.remove && loadingToast.remove() + setLoading(false) + // 清空文件输入框,以便再次上传同一文件 + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } + } + + const handleRemoveImage = (indexToRemove: number) => { + setImages(images.filter((_, index) => index !== indexToRemove)) + setPreviewUrls(previewUrls.filter((_, index) => index !== indexToRemove)) + } + + // 创建素材 + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + // 校验 + if (!content) { + showToast("请输入内容", "error") + return + } + // if (!comment) { + // showToast("请输入评论内容", "error") + // return + // } + if (materialType === 1 && images.length === 0) { + showToast("请上传图片", "error") + return + } else if (materialType === 2 && (!url || !desc)) { + showToast("请输入描述和链接地址", "error") + return + } else if (materialType === 3 && (!url && !videoUrl)) { + showToast("请填写视频链接或上传视频", "error") + return + } + setLoading(true) + const loadingToast = showToast("正在创建素材...", "loading", true) + try { + const payload: any = { + libraryId: params.id, + type: materialType, + content: content, + comment: comment, + sendTime: publishTime, + } + if (materialType === 1) { + payload.resUrls = images + } else if (materialType === 2) { + payload.urls = [{ desc, image, url }] + } else if (materialType === 3) { + payload.urls = videoUrl ? [videoUrl] : [] + } + const response = await api.post('/v1/content/library/create-item', payload) + if (response.code === 200) { + showToast("创建成功", "success") router.push(`/content/${params.id}/materials`) - } catch (error) { - console.error("Failed to create new material:", error) - toast({ - title: "错误", - description: "创建新素材失败", - variant: "destructive", - }) + } else { + showToast(response.msg || "创建失败", "error") + } + } catch (error: any) { + showToast(error?.message || "创建素材失败", "error") + } finally { + loadingToast.remove && loadingToast.remove() + setLoading(false) } } @@ -70,57 +177,273 @@ export default function NewMaterialPage({ params }: { params: { id: string } })
-
- -
+ + + {/* 基础信息分组 */} +
+
基础信息
+
+ +
+ setPublishTime(e.target.value)} + className="w-full h-12 rounded-2xl border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 px-4 text-base placeholder:text-gray-300" + placeholder="请选择发布时间" + style={{ width: 'auto' }} + /> + +
+
- + +
+ + +
+
+
+
+ {/* 内容信息分组(所有类型都展示内容和评论) */} +
+
内容信息
+