From 7252738ef0b2d491912d502a6a93aed058acd79b Mon Sep 17 00:00:00 2001 From: Blizzard Date: Wed, 25 Feb 2026 14:10:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=91=E6=8D=A2=E4=B8=AD=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/exchange.ts | 104 ++++++ src/api/post.ts | 5 + src/pages/plant/community/Post.tsx | 98 ++++- src/pages/plant/exchange/Item.tsx | 581 +++++++++++++++++++++++++++++ src/pages/plant/exchange/Order.tsx | 480 ++++++++++++++++++++++++ src/pages/plant/exchange/index.tsx | 6 + 6 files changed, 1260 insertions(+), 14 deletions(-) create mode 100644 src/api/exchange.ts create mode 100644 src/pages/plant/exchange/Item.tsx create mode 100644 src/pages/plant/exchange/Order.tsx create mode 100644 src/pages/plant/exchange/index.tsx diff --git a/src/api/exchange.ts b/src/api/exchange.ts new file mode 100644 index 0000000..6b7a8ad --- /dev/null +++ b/src/api/exchange.ts @@ -0,0 +1,104 @@ +import {post, type PageResult, type PageParams } from '@/lib/request' + +// ==================== Types ==================== + +export interface ExchangeItem { + id: string + name: string + description: string + imageId: string + type: 'PHYSICAL' | 'VIRTUAL' | 'COUPON' + costSunlight: number + stock: number + limitPerUser: number + status: number // 1=上架 2=下架 + sort: number + startTime: string | null + endTime: string | null + image?: { id: string; url: string } + createdAt: string + updatedAt: string +} + +export interface ExchangeOrder { + id: string + userId: string + itemId: string + itemName: string + costSunlight: number + quantity: number + status: number // 1=待处理 2=处理中 3=已发货 4=已完成 5=已取消 + itemType: string + recipientName: string + phone: string + address: string + trackingNo: string + remark: string + completedAt: string | null + item?: ExchangeItem + createdAt: string + updatedAt: string +} + +// ==================== Item API ==================== + +export interface CreateItemParams { + name: string + description: string + imageId?: string + type: string + costSunlight: number + stock: number + limitPerUser: number + status: number + sort: number + startTime?: string | null + endTime?: string | null +} + +export interface UpdateItemParams extends Partial { + id: string +} + +export interface ItemListParams extends PageParams { + type?: string + status?: number +} + +export function getItemList(data: ItemListParams) { + return post<{ data: PageResult }>('/exchange/item/list', data) +} + +export function createItem(data: CreateItemParams) { + return post('/exchange/item/create', data) +} + +export function updateItem(data: UpdateItemParams) { + return post('/exchange/item/update', data) +} + +export function deleteItem(id: string) { + return post('/exchange/item/delete', { id }) +} + +// ==================== Order API ==================== + +export interface OrderListParams extends PageParams { + status?: number + userId?: string +} + +export interface UpdateOrderParams { + id: string + status: number + trackingNo?: string + remark?: string +} + +export function getOrderList(data: OrderListParams) { + return post<{ data: PageResult }>('/exchange/order/list', data) +} + +export function updateOrderStatus(data: UpdateOrderParams) { + return post('/exchange/order/update', data) +} diff --git a/src/api/post.ts b/src/api/post.ts index 4b4070c..40c61dc 100644 --- a/src/api/post.ts +++ b/src/api/post.ts @@ -66,3 +66,8 @@ export function likePost(id: string, type: 1 | 2) { export function commentPost(data: CreateCommentParams) { return post<{ msg: string }>('/post/comment', data) } + +// 删除帖子(支持批量) +export function deletePost(ids: string[]) { + return post<{ msg: string }>('/post/delete', { ids }) +} diff --git a/src/pages/plant/community/Post.tsx b/src/pages/plant/community/Post.tsx index b264708..96845d4 100644 --- a/src/pages/plant/community/Post.tsx +++ b/src/pages/plant/community/Post.tsx @@ -1,9 +1,10 @@ import { useState, useEffect } from 'react' -import { Search, Eye, Heart, MessageCircle, Trash2, ShieldCheck, User } from 'lucide-react' +import { Search, Eye, Heart, MessageCircle, Trash2, ShieldCheck, User, Loader2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Checkbox } from '@/components/ui/checkbox' import { Table, TableBody, @@ -33,6 +34,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' // import { Separator } from '@/components/ui/separator' import { getPostPage, + deletePost, type Post, type PostPageParams, } from '@/api/post' @@ -52,6 +54,8 @@ export default function PostsPage() { const [previewDialogOpen, setPreviewDialogOpen] = useState(false) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [selectedPost, setSelectedPost] = useState(null) + const [selectedIds, setSelectedIds] = useState([]) + const [deleting, setDeleting] = useState(false) // 获取帖子列表 const fetchPosts = async () => { @@ -85,16 +89,53 @@ export default function PostsPage() { setPreviewDialogOpen(true) } - // 处理删除确认 + // 处理删除确认(单个) const handleDeleteConfirm = (post: Post) => { setSelectedPost(post) setDeleteDialogOpen(true) } - // 执行删除 (Mock) - const handleDelete = () => { - alert('删除功能开发中') - setDeleteDialogOpen(false) + // 处理批量删除确认 + const handleBatchDeleteConfirm = () => { + setSelectedPost(null) // null means batch mode + setDeleteDialogOpen(true) + } + + // 执行删除 + const handleDelete = async () => { + const ids = selectedPost ? [selectedPost.id] : selectedIds + if (ids.length === 0) return + + setDeleting(true) + try { + await deletePost(ids) + setDeleteDialogOpen(false) + setSelectedPost(null) + setSelectedIds([]) + fetchPosts() + } catch (error) { + console.error('删除帖子失败:', error) + } finally { + setDeleting(false) + } + } + + // 选择相关 + const isAllSelected = posts.length > 0 && selectedIds.length === posts.length + const isSomeSelected = selectedIds.length > 0 && selectedIds.length < posts.length + + const toggleSelectAll = () => { + if (isAllSelected) { + setSelectedIds([]) + } else { + setSelectedIds(posts.map(p => p.id)) + } + } + + const toggleSelect = (id: string) => { + setSelectedIds(prev => + prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] + ) } // 审核 (Mock) @@ -164,6 +205,18 @@ export default function PostsPage() { 搜索 + {selectedIds.length > 0 && ( +
+ 已选择 {selectedIds.length} + + +
+ )} @@ -186,7 +239,13 @@ export default function PostsPage() { - 帖子内容 + + + + 帖子内容 发布者 互动数据 状态 @@ -197,7 +256,7 @@ export default function PostsPage() { {posts.length === 0 ? ( - +
@@ -214,8 +273,14 @@ export default function PostsPage() { const timeDisplay = post.createdAtStr || post.createdAt?.replace('T', ' ').slice(0, 16) || '-' return ( - - + + + toggleSelect(post.id)} + /> + +
{post.title}
@@ -505,14 +570,19 @@ export default function PostsPage() { - 确认操作 + 确认删除 - 确定要删除帖子"{selectedPost?.title}"吗?删除后无法恢复。 + {selectedPost + ? <>确定要删除帖子「{selectedPost.title}」吗?删除后无法恢复。 + : <>确定要删除选中的 {selectedIds.length} 条帖子吗?删除后无法恢复。 + } - - + + diff --git a/src/pages/plant/exchange/Item.tsx b/src/pages/plant/exchange/Item.tsx new file mode 100644 index 0000000..8d07043 --- /dev/null +++ b/src/pages/plant/exchange/Item.tsx @@ -0,0 +1,581 @@ +import { useState, useEffect, useCallback } from 'react' +import { + Plus, Search, MoreHorizontal, Pencil, Trash2, Gift, Package, Sun, Archive, + ArrowUpDown, Clock, Loader2 +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Badge } from '@/components/ui/badge' +import { Textarea } from '@/components/ui/textarea' +import { + Table, TableBody, TableCell, TableHead, TableHeader, TableRow, +} from '@/components/ui/table' +import { + Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, +} from '@/components/ui/dialog' +import { + DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from '@/components/ui/select' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { cn, formatDate } from '@/lib/utils' +import { + getItemList, createItem, updateItem, deleteItem, + type ExchangeItem, type CreateItemParams +} from '@/api/exchange' + +// ==================== Form Types ==================== + +interface ItemFormData { + id?: string + name: string + description: string + type: string + costSunlight: number + stock: number + limitPerUser: number + status: number + sort: number + startTime: string + endTime: string +} + +const defaultFormData: ItemFormData = { + name: '', + description: '', + type: 'PHYSICAL', + costSunlight: 0, + stock: -1, + limitPerUser: 0, + status: 1, + sort: 0, + startTime: '', + endTime: '', +} + +// ==================== Main Component ==================== + +export default function ExchangeItemPage() { + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(true) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [pageSize] = useState(10) + const [searchTerm, setSearchTerm] = useState('') + const [statusFilter, setStatusFilter] = useState('all') + const [typeFilter, setTypeFilter] = useState('all') + + const [dialogOpen, setDialogOpen] = useState(false) + const [isEdit, setIsEdit] = useState(false) + const [formData, setFormData] = useState(defaultFormData) + const [submitting, setSubmitting] = useState(false) + + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [itemToDelete, setItemToDelete] = useState(null) + + // Fetch list + const fetchItems = useCallback(async () => { + setLoading(true) + try { + const params: Record = { + current: page, + pageSize, + keyword: searchTerm || undefined, + } + if (statusFilter !== 'all') params.status = parseInt(statusFilter) + if (typeFilter !== 'all') params.type = typeFilter + + const res = await getItemList(params as any) + if (res?.data) { + setItems(res.data.list || []) + setTotal(res.data.total || 0) + } + } catch (error) { + console.error('获取商品列表失败:', error) + } finally { + setLoading(false) + } + }, [page, pageSize, searchTerm, statusFilter, typeFilter]) + + useEffect(() => { + fetchItems() + }, [fetchItems]) + + // Stats + const onShelfCount = items.filter(i => i.status === 1).length + const lowStockCount = items.filter(i => i.stock >= 0 && i.stock <= 5).length + + // Open create dialog + const handleAdd = () => { + setIsEdit(false) + setFormData(defaultFormData) + setDialogOpen(true) + } + + // Open edit dialog + const handleEdit = (item: ExchangeItem) => { + setIsEdit(true) + setFormData({ + id: item.id, + name: item.name, + description: item.description || '', + type: item.type, + costSunlight: item.costSunlight, + stock: item.stock, + limitPerUser: item.limitPerUser, + status: item.status, + sort: item.sort, + startTime: item.startTime ? item.startTime.slice(0, 16) : '', + endTime: item.endTime ? item.endTime.slice(0, 16) : '', + }) + setDialogOpen(true) + } + + // Submit form + const handleSubmit = async () => { + if (!formData.name || formData.costSunlight <= 0) return + + setSubmitting(true) + try { + const data: CreateItemParams = { + name: formData.name, + description: formData.description, + type: formData.type, + costSunlight: formData.costSunlight, + stock: formData.stock, + limitPerUser: formData.limitPerUser, + status: formData.status, + sort: formData.sort, + startTime: formData.startTime ? new Date(formData.startTime).toISOString() : null, + endTime: formData.endTime ? new Date(formData.endTime).toISOString() : null, + } + + if (isEdit && formData.id) { + await updateItem({ ...data, id: formData.id }) + } else { + await createItem(data) + } + setDialogOpen(false) + fetchItems() + } catch (error) { + console.error('保存商品失败:', error) + } finally { + setSubmitting(false) + } + } + + // Delete + const handleDelete = async () => { + if (!itemToDelete) return + try { + await deleteItem(itemToDelete.id) + setDeleteDialogOpen(false) + setItemToDelete(null) + fetchItems() + } catch (error) { + console.error('删除商品失败:', error) + } + } + + // Helpers + const getStatusBadge = (status: number) => { + return status === 1 + ? 上架中 + : 已下架 + } + + const getTypeBadge = (type: string) => { + switch (type) { + case 'PHYSICAL': return 实物 + case 'VIRTUAL': return 虚拟 + case 'COUPON': return 优惠券 + default: return {type} + } + } + + const totalPages = Math.ceil(total / pageSize) + + return ( +
+ {/* Stats Cards */} +
+ + + 商品总数 + + + +
{total}
+

所有兑换商品

+
+
+ + + 上架商品 + + + +
{onShelfCount}
+

当前可兑换的商品

+
+
+ + + 低库存预警 + + + +
{lowStockCount}
+

库存≤5的商品

+
+
+ + + 快速添加 +
+ +
+
+ +
New
+

点击添加新兑换商品

+
+
+
+ + {/* Main Table */} + + +
+ + 商品列表 + {total} + + 管理兑换中心的所有商品,设置价格、库存和活动时间 +
+
+
+ + { setSearchTerm(e.target.value); setPage(1) }} + /> +
+ + + +
+
+ + {loading ? ( +
+ +

正在加载...

+
+ ) : ( + <> +
+
+ + + 商品信息 + 类型 + +
+ + 阳光值 +
+
+ 库存 + 限兑 + 活动时间 + 状态 + +
+ + 排序 +
+
+ +
+
+ + {items.length === 0 ? ( + + +
+ +

暂无商品数据

+

点击"添加商品"创建第一个兑换商品

+
+
+
+ ) : ( + items.map((item, index) => ( + + +
+ {item.image ? ( + {item.name} + ) : ( +
+ +
+ )} +
+

{item.name}

+ {item.description && ( +

{item.description}

+ )} +
+
+
+ {getTypeBadge(item.type)} + + {item.costSunlight} + + + {item.stock < 0 ? ( + 无限 + ) : ( + + {item.stock} + + )} + + + {item.limitPerUser > 0 ? ( + {item.limitPerUser}次/人 + ) : ( + 不限 + )} + + + {item.startTime || item.endTime ? ( +
+ + + {item.startTime ? formatDate(item.startTime) : '不限'} + {' ~ '} + {item.endTime ? formatDate(item.endTime) : '不限'} + +
+ ) : ( + 长期 + )} +
+ {getStatusBadge(item.status)} + + {item.sort} + + + + + + + + handleEdit(item)}> + 编辑 + + { setItemToDelete(item); setDeleteDialogOpen(true) }} + > + 删除 + + + + +
+ )) + )} +
+
+ + + {/* Pagination */} + {totalPages > 1 && ( +
+

+ 显示第 {(page - 1) * pageSize + 1}-{Math.min(page * pageSize, total)} 条,共 {total} 条 +

+
+ + + {page} / {totalPages} + + +
+
+ )} + + )} + + + + {/* Create / Edit Dialog */} + + + + +
+ {isEdit ? : } +
+ {isEdit ? '编辑商品' : '添加商品'} +
+ + {isEdit ? '修改兑换商品信息' : '创建新的兑换商品,设置价格和库存'} + +
+
+ {/* Name */} +
+ + setFormData(p => ({ ...p, name: e.target.value }))} + placeholder="如:多肉植物盲盒" className="h-10 bg-muted/30 focus:bg-background" /> +
+ + {/* Description */} +
+ +