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
+7 -3
View File
@@ -1,4 +1,4 @@
import { get, post } from '@/lib/request' import {get, post} from '@/lib/request'
// 等级配置接口 // 等级配置接口
export interface LevelConf { export interface LevelConf {
@@ -22,6 +22,10 @@ export interface UpdateLevelParams extends Partial<CreateLevelParams> {
id: string id: string
} }
export interface ListResponse {
list: LevelConf[];
}
// 获取等级配置列表 (虽然是 list 接口,但根据以往经验,可能返回带分页的结构或者直接是数组,先假设是 PageResult 或者是 List 结构,Swagger 上没细说。通常 /list 可能是分页的。如果 swagger 没写分页参数,那可能是全量列表) // 获取等级配置列表 (虽然是 list 接口,但根据以往经验,可能返回带分页的结构或者直接是数组,先假设是 PageResult 或者是 List 结构,Swagger 上没细说。通常 /list 可能是分页的。如果 swagger 没写分页参数,那可能是全量列表)
// 根据 swagger "/config/level/list" 没有 parameters,大概率是全量列表。 // 根据 swagger "/config/level/list" 没有 parameters,大概率是全量列表。
// 假设返回结构 { data: LevelConf[], msg: string, code: number } // 假设返回结构 { data: LevelConf[], msg: string, code: number }
@@ -38,12 +42,12 @@ export function updateLevelConf(data: UpdateLevelParams) {
// 获取等级配置详情 // 获取等级配置详情
export function getLevelConfDetail(id: string) { export function getLevelConfDetail(id: string) {
return get<{ data: LevelConf }>('/config/level/detail', { id }) return get<{ data: LevelConf }>('/config/level/detail', {id})
} }
// 获取等级配置列表 // 获取等级配置列表
export function getLevelConfList() { export function getLevelConfList() {
return get<{ data: LevelConf[] }>('/config/level/list') return get<{ data: ListResponse }>('/config/level/list')
} }
// 删除等级配置 (Swagger 里没看到 delete 接口,先不加) // 删除等级配置 (Swagger 里没看到 delete 接口,先不加)
+11
View File
@@ -159,7 +159,18 @@ export function addWiki(data: CreateWikiParams) {
return post<{ msg: string }>('/wiki/add', data) return post<{ msg: string }>('/wiki/add', data)
} }
// 修改百科 // 修改百科
export function updateWiki(data: UpdateWikiParams) { export function updateWiki(data: UpdateWikiParams) {
return post<{ msg: string }>('/wiki/update', data) 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 || [] const list = res.data?.list || []
// Map backend snake_case to camelCase if necessary (for robustness) // Map backend snake_case to camelCase if necessary (for robustness)
const mappedList = list.map((item: any) => ({ const mappedList = list.map((item: Topic) => ({
...item, ...item,
startTime: (item.startTime || item.start_time) ? String(item.startTime || item.start_time) : undefined, startTime: (item.startTime) ? String(item.startTime) : undefined,
endTime: (item.endTime || item.end_time) ? String(item.endTime || item.end_time) : undefined endTime: (item.endTime) ? String(item.endTime) : undefined
})) as Topic[] })) as Topic[]
setTopics(Array.isArray(mappedList) ? mappedList : []) setTopics(Array.isArray(mappedList) ? mappedList : [])
@@ -108,15 +108,15 @@ export default function TopicsPage() {
const handleEdit = async (topic: Topic) => { const handleEdit = async (topic: Topic) => {
try { try {
const res = await getTopicDetail(topic.id) 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({ setFormData({
id: detail.id, id: detail.id,
title: detail.title || '', title: detail.title || '',
remark: detail.remark || '', remark: detail.remark || '',
// backend might return start_time, map to startTime // backend might return start_time, map to startTime
startTime: detail.startTime || detail.start_time || '', startTime: detail.startTime || '',
endTime: detail.endTime || detail.end_time || '', endTime: detail.endTime || '',
}) })
setIsEdit(true) setIsEdit(true)
setDialogOpen(true) setDialogOpen(true)
+2 -2
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -214,7 +214,7 @@ export default function BadgeConfigPage() {
setSubmitting(true) setSubmitting(true)
try { try {
const { icon, ...payload } = formData const { ...payload } = formData
if (isEdit && formData.id) { if (isEdit && formData.id) {
await updateBadge(payload) await updateBadge(payload)
+2 -8
View File
@@ -53,18 +53,12 @@ export default function LevelConfigPage() {
setLoading(true) setLoading(true)
try { try {
const res = await getLevelConfList() const res = await getLevelConfList()
console.log('等级配置接口返回:', res)
let list: LevelConf[] = [] let list: LevelConf[] = []
if (res && Array.isArray(res.data)) { if (res && res.data && Array.isArray(res.data.list)) {
list = res.data list = res.data.list
} 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[]
} }
// Sort by level // Sort by level
list.sort((a, b) => a.level - b.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 { Plus, Search, Pencil, Trash2, Leaf, Sun, Droplets, Star, Eye, X } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -40,6 +40,8 @@ import {
type Wiki, type Wiki,
type WikiClass, type WikiClass,
type WikiPageParams, type WikiPageParams,
uploadWikiImg,
deleteWiki,
} from '@/api/wiki' } from '@/api/wiki'
import { uploadFile, type SystemOss } from '@/api/system' import { uploadFile, type SystemOss } from '@/api/system'
@@ -74,6 +76,14 @@ interface WikiFormData {
isHot: number isHot: number
} }
const LIGHT_TYPES: Record<string, string> = {
full_sun: '全日照',
partial_sun: '半日照',
scattered_light: '明亮散射光',
semi_shade: '半阴',
shade: '耐阴',
}
const defaultFormData: WikiFormData = { const defaultFormData: WikiFormData = {
name: '', name: '',
latinName: '', latinName: '',
@@ -124,6 +134,11 @@ export default function PlantsPage() {
const [isEdit, setIsEdit] = useState(false) const [isEdit, setIsEdit] = useState(false)
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
const [uploading, setUploading] = 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 () => { const fetchPlants = async () => {
@@ -133,6 +148,7 @@ export default function PlantsPage() {
const res = await getWikiPage(searchParams) const res = await getWikiPage(searchParams)
setPlants(res.data?.list || []) setPlants(res.data?.list || [])
setTotal(res.data?.total || 0) setTotal(res.data?.total || 0)
setSelectedIds([])
} catch (err) { } catch (err) {
console.error('获取植物列表失败:', err) console.error('获取植物列表失败:', err)
setError('获取植物列表失败') setError('获取植物列表失败')
@@ -241,6 +257,45 @@ export default function PlantsPage() {
setDeleteDialogOpen(true) 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 handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files 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 () => { const handleSubmit = async () => {
if (!formData.name) { if (!formData.name) {
@@ -292,6 +379,8 @@ export default function PlantsPage() {
} }
if (isEdit && formData.id) { if (isEdit && formData.id) {
await updateWiki({ ...data, id: formData.id }) await updateWiki({ ...data, id: formData.id })
// Sync images using the dedicated API
await uploadWikiImg({ id: formData.id, ossIds: formData.ossIds })
} else { } else {
await addWiki(data) await addWiki(data)
} }
@@ -328,11 +417,19 @@ export default function PlantsPage() {
<h1 className="text-3xl font-bold tracking-tight"></h1> <h1 className="text-3xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p> <p className="text-muted-foreground"></p>
</div> </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}> <Button onClick={handleAdd}>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
</Button> </Button>
</div> </div>
</div>
{/* 错误提示 */} {/* 错误提示 */}
{error && ( {error && (
@@ -393,6 +490,12 @@ export default function PlantsPage() {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <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> <TableHead></TableHead>
<TableHead></TableHead> <TableHead></TableHead>
@@ -411,6 +514,12 @@ export default function PlantsPage() {
) : ( ) : (
plants.map(plant => ( plants.map(plant => (
<TableRow key={plant.id}> <TableRow key={plant.id}>
<TableCell>
<Checkbox
checked={selectedIds.includes(plant.id)}
onCheckedChange={(checked) => handleSelectRow(plant.id, checked as boolean)}
/>
</TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{plant.imgList?.[0]?.url ? ( {plant.imgList?.[0]?.url ? (
@@ -420,8 +529,21 @@ export default function PlantsPage() {
className="h-12 w-12 rounded-lg object-cover" 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"> <div
<Leaf className="h-6 w-6 text-green-600" /> 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>
)} )}
<div> <div>
@@ -446,7 +568,7 @@ export default function PlantsPage() {
{plant.lightType && ( {plant.lightType && (
<span className="flex items-center gap-0.5"> <span className="flex items-center gap-0.5">
<Sun className="h-3 w-3" /> <Sun className="h-3 w-3" />
{plant.lightType} {LIGHT_TYPES[plant.lightType] || plant.lightType}
</span> </span>
)} )}
{plant.lifeCycle && ( {plant.lifeCycle && (
@@ -618,10 +740,11 @@ export default function PlantsPage() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<Input <Textarea
value={formData.distributionArea} value={formData.distributionArea}
onChange={e => setFormData({ ...formData, distributionArea: e.target.value })} onChange={e => setFormData({ ...formData, distributionArea: e.target.value })}
placeholder="如:热带亚洲" placeholder="如:热带亚洲"
rows={2}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@@ -714,52 +837,58 @@ export default function PlantsPage() {
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<Input <Textarea
value={formData.foliageType} value={formData.foliageType}
onChange={e => setFormData({ ...formData, foliageType: e.target.value })} onChange={e => setFormData({ ...formData, foliageType: e.target.value })}
placeholder="如:常绿" placeholder="如:常绿"
rows={2}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<Input <Textarea
value={formData.foliageColor} value={formData.foliageColor}
onChange={e => setFormData({ ...formData, foliageColor: e.target.value })} onChange={e => setFormData({ ...formData, foliageColor: e.target.value })}
placeholder="如:绿色" placeholder="如:绿色"
rows={2}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<Input <Textarea
value={formData.foliageShape} value={formData.foliageShape}
onChange={e => setFormData({ ...formData, foliageShape: e.target.value })} onChange={e => setFormData({ ...formData, foliageShape: e.target.value })}
placeholder="如:心形" placeholder="如:心形"
rows={2}
/> />
</div> </div>
</div> </div>
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<Input <Textarea
value={formData.floweringPeriod} value={formData.floweringPeriod}
onChange={e => setFormData({ ...formData, floweringPeriod: e.target.value })} onChange={e => setFormData({ ...formData, floweringPeriod: e.target.value })}
placeholder="如:春季" placeholder="如:春季"
rows={2}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<Input <Textarea
value={formData.floweringColor} value={formData.floweringColor}
onChange={e => setFormData({ ...formData, floweringColor: e.target.value })} onChange={e => setFormData({ ...formData, floweringColor: e.target.value })}
placeholder="如:白色" placeholder="如:白色"
rows={2}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<Input <Textarea
value={formData.floweringShape} value={formData.floweringShape}
onChange={e => setFormData({ ...formData, floweringShape: e.target.value })} onChange={e => setFormData({ ...formData, floweringShape: e.target.value })}
placeholder="如:佛焰苞" placeholder="如:佛焰苞"
rows={2}
/> />
</div> </div>
</div> </div>
@@ -774,18 +903,20 @@ export default function PlantsPage() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<Input <Textarea
value={formData.fruit} value={formData.fruit}
onChange={e => setFormData({ ...formData, fruit: e.target.value })} onChange={e => setFormData({ ...formData, fruit: e.target.value })}
placeholder="如:浆果" placeholder="如:浆果"
rows={2}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label></Label> <Label></Label>
<Input <Textarea
value={formData.stem} value={formData.stem}
onChange={e => setFormData({ ...formData, stem: e.target.value })} onChange={e => setFormData({ ...formData, stem: e.target.value })}
placeholder="如:藤本" placeholder="如:藤本"
rows={2}
/> />
</div> </div>
</div> </div>
@@ -823,9 +954,9 @@ export default function PlantsPage() {
<SelectValue placeholder="选择光照类型" /> <SelectValue placeholder="选择光照类型" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="full_sun"></SelectItem> {Object.entries(LIGHT_TYPES).map(([value, label]) => (
<SelectItem value="partial_sun"></SelectItem> <SelectItem key={value} value={value}>{label}</SelectItem>
<SelectItem value="shade"></SelectItem> ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -963,7 +1094,7 @@ export default function PlantsPage() {
<div><span className="text-muted-foreground"></span>{selectedPlant.lifeCycle}</div> <div><span className="text-muted-foreground"></span>{selectedPlant.lifeCycle}</div>
)} )}
{selectedPlant.lightType && ( {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 && ( {selectedPlant.optimalTempPeriod && (
<div><span className="text-muted-foreground"></span>{selectedPlant.optimalTempPeriod}</div> <div><span className="text-muted-foreground"></span>{selectedPlant.optimalTempPeriod}</div>
@@ -993,19 +1124,31 @@ export default function PlantsPage() {
<DialogHeader> <DialogHeader>
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
<DialogDescription> <DialogDescription>
"{selectedPlant?.name}" {selectedPlant
? `确定要删除植物 "${selectedPlant.name}" 吗?此操作不可撤销。`
: `确定要删除选中的 ${selectedIds.length} 个植物吗?此操作不可撤销。`
}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}> <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
</Button> </Button>
<Button variant="destructive" onClick={() => setDeleteDialogOpen(false)}> <Button variant="destructive" onClick={handleDeleteExecute}>
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Hidden Input for List Upload */}
<Input
ref={listUploadInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleListFileUpload}
/>
</div> </div>
) )
} }