feat: 修复百科添加页面的问题

This commit is contained in:
Blizzard
2026-02-14 14:32:16 +08:00
parent 5156255c77
commit 7d52705e05
6 changed files with 194 additions and 42 deletions
+5 -1
View File
@@ -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 接口,先不加)
+11
View File
@@ -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 })
}
+6 -6
View File
@@ -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)
+2 -2
View File
@@ -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)
+2 -8
View File
@@ -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
View File
@@ -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>
)
}