feat: 修复百科添加页面的问题
This commit is contained in:
+5
-1
@@ -22,6 +22,10 @@ export interface UpdateLevelParams extends Partial<CreateLevelParams> {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface ListResponse {
|
||||
list: LevelConf[];
|
||||
}
|
||||
|
||||
// 获取等级配置列表 (虽然是 list 接口,但根据以往经验,可能返回带分页的结构或者直接是数组,先假设是 PageResult 或者是 List 结构,Swagger 上没细说。通常 /list 可能是分页的。如果 swagger 没写分页参数,那可能是全量列表)
|
||||
// 根据 swagger "/config/level/list" 没有 parameters,大概率是全量列表。
|
||||
// 假设返回结构 { data: LevelConf[], msg: string, code: number }
|
||||
@@ -43,7 +47,7 @@ export function getLevelConfDetail(id: string) {
|
||||
|
||||
// 获取等级配置列表
|
||||
export function getLevelConfList() {
|
||||
return get<{ data: LevelConf[] }>('/config/level/list')
|
||||
return get<{ data: ListResponse }>('/config/level/list')
|
||||
}
|
||||
|
||||
// 删除等级配置 (Swagger 里没看到 delete 接口,先不加)
|
||||
|
||||
@@ -159,7 +159,18 @@ export function addWiki(data: CreateWikiParams) {
|
||||
return post<{ msg: string }>('/wiki/add', data)
|
||||
}
|
||||
|
||||
|
||||
// 修改百科
|
||||
export function updateWiki(data: UpdateWikiParams) {
|
||||
return post<{ msg: string }>('/wiki/update', data)
|
||||
}
|
||||
|
||||
// 上传百科图片 (列表页快捷上传)
|
||||
export function uploadWikiImg(data: { id: string; ossIds: string[] }) {
|
||||
return post<{ msg: string }>('/wiki/uploadImg', data)
|
||||
}
|
||||
|
||||
// 删除百科
|
||||
export function deleteWiki(ids: string[]) {
|
||||
return post<{ msg: string }>('/wiki/delete', { ids })
|
||||
}
|
||||
|
||||
@@ -71,10 +71,10 @@ export default function TopicsPage() {
|
||||
const list = res.data?.list || []
|
||||
|
||||
// Map backend snake_case to camelCase if necessary (for robustness)
|
||||
const mappedList = list.map((item: any) => ({
|
||||
const mappedList = list.map((item: Topic) => ({
|
||||
...item,
|
||||
startTime: (item.startTime || item.start_time) ? String(item.startTime || item.start_time) : undefined,
|
||||
endTime: (item.endTime || item.end_time) ? String(item.endTime || item.end_time) : undefined
|
||||
startTime: (item.startTime) ? String(item.startTime) : undefined,
|
||||
endTime: (item.endTime) ? String(item.endTime) : undefined
|
||||
})) as Topic[]
|
||||
|
||||
setTopics(Array.isArray(mappedList) ? mappedList : [])
|
||||
@@ -108,15 +108,15 @@ export default function TopicsPage() {
|
||||
const handleEdit = async (topic: Topic) => {
|
||||
try {
|
||||
const res = await getTopicDetail(topic.id)
|
||||
const detail = res.data as any // Cast to any to handle potential snake_case from backend
|
||||
const detail = res.data as Topic // Cast to any to handle potential snake_case from backend
|
||||
|
||||
setFormData({
|
||||
id: detail.id,
|
||||
title: detail.title || '',
|
||||
remark: detail.remark || '',
|
||||
// backend might return start_time, map to startTime
|
||||
startTime: detail.startTime || detail.start_time || '',
|
||||
endTime: detail.endTime || detail.end_time || '',
|
||||
startTime: detail.startTime || '',
|
||||
endTime: detail.endTime || '',
|
||||
})
|
||||
setIsEdit(true)
|
||||
setDialogOpen(true)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Award, Trophy, Medal, Grid, Plus, Edit2, Trash2, Eye, Upload, X, Crown, Sprout, Star, Sparkles, Search, ChevronRight, ChevronDown, Layers, Hash } from 'lucide-react'
|
||||
import { Award, Trophy, Medal, Grid, Plus, Edit2, Trash2, Eye, Upload, X, Sprout, Star, ChevronRight, ChevronDown, Layers, Hash } from 'lucide-react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -214,7 +214,7 @@ export default function BadgeConfigPage() {
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const { icon, ...payload } = formData
|
||||
const { ...payload } = formData
|
||||
|
||||
if (isEdit && formData.id) {
|
||||
await updateBadge(payload)
|
||||
|
||||
@@ -53,18 +53,12 @@ export default function LevelConfigPage() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getLevelConfList()
|
||||
console.log('等级配置接口返回:', res)
|
||||
|
||||
let list: LevelConf[] = []
|
||||
|
||||
if (res && Array.isArray(res.data)) {
|
||||
list = res.data
|
||||
} else if (res && res.data && Array.isArray((res.data as any).list)) {
|
||||
list = (res.data as any).list
|
||||
} else if (Array.isArray(res)) {
|
||||
list = res as unknown as LevelConf[]
|
||||
if (res && res.data && Array.isArray(res.data.list)) {
|
||||
list = res.data.list
|
||||
}
|
||||
|
||||
// Sort by level
|
||||
list.sort((a, b) => a.level - b.level)
|
||||
|
||||
|
||||
+162
-19
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Plus, Search, Pencil, Trash2, Leaf, Sun, Droplets, Star, Eye, X } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
type Wiki,
|
||||
type WikiClass,
|
||||
type WikiPageParams,
|
||||
uploadWikiImg,
|
||||
deleteWiki,
|
||||
} from '@/api/wiki'
|
||||
import { uploadFile, type SystemOss } from '@/api/system'
|
||||
|
||||
@@ -74,6 +76,14 @@ interface WikiFormData {
|
||||
isHot: number
|
||||
}
|
||||
|
||||
const LIGHT_TYPES: Record<string, string> = {
|
||||
full_sun: '全日照',
|
||||
partial_sun: '半日照',
|
||||
scattered_light: '明亮散射光',
|
||||
semi_shade: '半阴',
|
||||
shade: '耐阴',
|
||||
}
|
||||
|
||||
const defaultFormData: WikiFormData = {
|
||||
name: '',
|
||||
latinName: '',
|
||||
@@ -124,6 +134,11 @@ export default function PlantsPage() {
|
||||
const [isEdit, setIsEdit] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
|
||||
// List Upload State
|
||||
const listUploadInputRef = useRef<HTMLInputElement>(null)
|
||||
const [uploadingWikiId, setUploadingWikiId] = useState<string | null>(null)
|
||||
|
||||
// 获取植物列表
|
||||
const fetchPlants = async () => {
|
||||
@@ -133,6 +148,7 @@ export default function PlantsPage() {
|
||||
const res = await getWikiPage(searchParams)
|
||||
setPlants(res.data?.list || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setSelectedIds([])
|
||||
} catch (err) {
|
||||
console.error('获取植物列表失败:', err)
|
||||
setError('获取植物列表失败')
|
||||
@@ -241,6 +257,45 @@ export default function PlantsPage() {
|
||||
setDeleteDialogOpen(true)
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedIds.length === 0) return
|
||||
setSelectedPlant(null)
|
||||
setDeleteDialogOpen(true)
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
const handleDeleteExecute = async () => {
|
||||
try {
|
||||
const ids = selectedPlant ? [selectedPlant.id] : selectedIds
|
||||
await deleteWiki(ids)
|
||||
setDeleteDialogOpen(false)
|
||||
fetchPlants()
|
||||
setSelectedPlant(null)
|
||||
// fetchPlants already clears selectedIds
|
||||
} catch (e) {
|
||||
console.error('删除失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择全部
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedIds(plants.map(p => p.id))
|
||||
} else {
|
||||
setSelectedIds([])
|
||||
}
|
||||
}
|
||||
|
||||
// 选择单行
|
||||
const handleSelectRow = (id: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedIds(prev => [...prev, id])
|
||||
} else {
|
||||
setSelectedIds(prev => prev.filter(i => i !== id))
|
||||
}
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files
|
||||
@@ -272,6 +327,38 @@ export default function PlantsPage() {
|
||||
}))
|
||||
}
|
||||
|
||||
// 列表页点击上传
|
||||
const handleListImageClick = (plant: Wiki) => {
|
||||
if (!plant.imgList?.[0]?.url) {
|
||||
setUploadingWikiId(plant.id)
|
||||
listUploadInputRef.current?.click()
|
||||
}
|
||||
}
|
||||
|
||||
const handleListFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file || !uploadingWikiId) return
|
||||
|
||||
try {
|
||||
const res = await uploadFile(file)
|
||||
const ossId = res.data.file.id
|
||||
|
||||
await uploadWikiImg({
|
||||
id: uploadingWikiId,
|
||||
ossIds: [ossId]
|
||||
})
|
||||
|
||||
fetchPlants()
|
||||
} catch (err) {
|
||||
console.error('上传图片失败:', err)
|
||||
} finally {
|
||||
setUploadingWikiId(null)
|
||||
if (listUploadInputRef.current) {
|
||||
listUploadInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.name) {
|
||||
@@ -292,6 +379,8 @@ export default function PlantsPage() {
|
||||
}
|
||||
if (isEdit && formData.id) {
|
||||
await updateWiki({ ...data, id: formData.id })
|
||||
// Sync images using the dedicated API
|
||||
await uploadWikiImg({ id: formData.id, ossIds: formData.ossIds })
|
||||
} else {
|
||||
await addWiki(data)
|
||||
}
|
||||
@@ -328,11 +417,19 @@ export default function PlantsPage() {
|
||||
<h1 className="text-3xl font-bold tracking-tight">植物百科</h1>
|
||||
<p className="text-muted-foreground">管理植物百科信息</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{selectedIds.length > 0 && (
|
||||
<Button variant="destructive" onClick={handleBatchDelete}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
批量删除 ({selectedIds.length})
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleAdd}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新增植物
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
@@ -393,6 +490,12 @@ export default function PlantsPage() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[50px]">
|
||||
<Checkbox
|
||||
checked={selectedIds.length === plants.length && plants.length > 0}
|
||||
onCheckedChange={(checked) => handleSelectAll(checked as boolean)}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>植物信息</TableHead>
|
||||
<TableHead>分类</TableHead>
|
||||
<TableHead>特性</TableHead>
|
||||
@@ -411,6 +514,12 @@ export default function PlantsPage() {
|
||||
) : (
|
||||
plants.map(plant => (
|
||||
<TableRow key={plant.id}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedIds.includes(plant.id)}
|
||||
onCheckedChange={(checked) => handleSelectRow(plant.id, checked as boolean)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
{plant.imgList?.[0]?.url ? (
|
||||
@@ -420,8 +529,21 @@ export default function PlantsPage() {
|
||||
className="h-12 w-12 rounded-lg object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-12 w-12 rounded-lg bg-green-100 dark:bg-green-900/20 flex items-center justify-center">
|
||||
<Leaf className="h-6 w-6 text-green-600" />
|
||||
<div
|
||||
className="h-12 w-12 rounded-lg bg-green-100 dark:bg-green-900/20 flex items-center justify-center cursor-pointer hover:bg-green-200 transition-colors relative group"
|
||||
onClick={() => handleListImageClick(plant)}
|
||||
title="点击上传图片"
|
||||
>
|
||||
{uploadingWikiId === plant.id ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-green-600"></div>
|
||||
) : (
|
||||
<>
|
||||
<Leaf className="h-6 w-6 text-green-600 group-hover:opacity-50 transition-opacity" />
|
||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Plus className="h-5 w-5 text-green-700 font-bold" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
@@ -446,7 +568,7 @@ export default function PlantsPage() {
|
||||
{plant.lightType && (
|
||||
<span className="flex items-center gap-0.5">
|
||||
<Sun className="h-3 w-3" />
|
||||
{plant.lightType}
|
||||
{LIGHT_TYPES[plant.lightType] || plant.lightType}
|
||||
</span>
|
||||
)}
|
||||
{plant.lifeCycle && (
|
||||
@@ -618,10 +740,11 @@ export default function PlantsPage() {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>分布区域</Label>
|
||||
<Input
|
||||
<Textarea
|
||||
value={formData.distributionArea}
|
||||
onChange={e => setFormData({ ...formData, distributionArea: e.target.value })}
|
||||
placeholder="如:热带亚洲"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -714,52 +837,58 @@ export default function PlantsPage() {
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>叶片类型</Label>
|
||||
<Input
|
||||
<Textarea
|
||||
value={formData.foliageType}
|
||||
onChange={e => setFormData({ ...formData, foliageType: e.target.value })}
|
||||
placeholder="如:常绿"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>叶片颜色</Label>
|
||||
<Input
|
||||
<Textarea
|
||||
value={formData.foliageColor}
|
||||
onChange={e => setFormData({ ...formData, foliageColor: e.target.value })}
|
||||
placeholder="如:绿色"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>叶片形状</Label>
|
||||
<Input
|
||||
<Textarea
|
||||
value={formData.foliageShape}
|
||||
onChange={e => setFormData({ ...formData, foliageShape: e.target.value })}
|
||||
placeholder="如:心形"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>花期</Label>
|
||||
<Input
|
||||
<Textarea
|
||||
value={formData.floweringPeriod}
|
||||
onChange={e => setFormData({ ...formData, floweringPeriod: e.target.value })}
|
||||
placeholder="如:春季"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>花色</Label>
|
||||
<Input
|
||||
<Textarea
|
||||
value={formData.floweringColor}
|
||||
onChange={e => setFormData({ ...formData, floweringColor: e.target.value })}
|
||||
placeholder="如:白色"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>花形</Label>
|
||||
<Input
|
||||
<Textarea
|
||||
value={formData.floweringShape}
|
||||
onChange={e => setFormData({ ...formData, floweringShape: e.target.value })}
|
||||
placeholder="如:佛焰苞"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -774,18 +903,20 @@ export default function PlantsPage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>果实</Label>
|
||||
<Input
|
||||
<Textarea
|
||||
value={formData.fruit}
|
||||
onChange={e => setFormData({ ...formData, fruit: e.target.value })}
|
||||
placeholder="如:浆果"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>茎</Label>
|
||||
<Input
|
||||
<Textarea
|
||||
value={formData.stem}
|
||||
onChange={e => setFormData({ ...formData, stem: e.target.value })}
|
||||
placeholder="如:藤本"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -823,9 +954,9 @@ export default function PlantsPage() {
|
||||
<SelectValue placeholder="选择光照类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="full_sun">全日照</SelectItem>
|
||||
<SelectItem value="partial_sun">半日照</SelectItem>
|
||||
<SelectItem value="shade">耐阴</SelectItem>
|
||||
{Object.entries(LIGHT_TYPES).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>{label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -963,7 +1094,7 @@ export default function PlantsPage() {
|
||||
<div><span className="text-muted-foreground">生命周期:</span>{selectedPlant.lifeCycle}</div>
|
||||
)}
|
||||
{selectedPlant.lightType && (
|
||||
<div><span className="text-muted-foreground">光照:</span>{selectedPlant.lightType}</div>
|
||||
<div><span className="text-muted-foreground">光照:</span>{LIGHT_TYPES[selectedPlant.lightType] || selectedPlant.lightType}</div>
|
||||
)}
|
||||
{selectedPlant.optimalTempPeriod && (
|
||||
<div><span className="text-muted-foreground">适温:</span>{selectedPlant.optimalTempPeriod}</div>
|
||||
@@ -993,19 +1124,31 @@ export default function PlantsPage() {
|
||||
<DialogHeader>
|
||||
<DialogTitle>确认删除</DialogTitle>
|
||||
<DialogDescription>
|
||||
确定要删除植物 "{selectedPlant?.name}" 吗?此操作不可撤销。
|
||||
{selectedPlant
|
||||
? `确定要删除植物 "${selectedPlant.name}" 吗?此操作不可撤销。`
|
||||
: `确定要删除选中的 ${selectedIds.length} 个植物吗?此操作不可撤销。`
|
||||
}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={() => setDeleteDialogOpen(false)}>
|
||||
<Button variant="destructive" onClick={handleDeleteExecute}>
|
||||
删除
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Hidden Input for List Upload */}
|
||||
<Input
|
||||
ref={listUploadInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleListFileUpload}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user