667 lines
25 KiB
TypeScript
667 lines
25 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
||
import { materialsApi, subjectsApi, type MaterialFile, type MaterialQueryParams, type MaterialSubject, type MaterialCreateRequest, type MaterialUpdateRequest } from '@/api'
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Badge } from '@/components/ui/badge'
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from '@/components/ui/table'
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from '@/components/ui/select'
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from '@/components/ui/dialog'
|
||
import { PaginationNavigator } from '@/components/ui/pagination'
|
||
import { Label } from '@/components/ui/label'
|
||
import { Textarea } from '@/components/ui/textarea'
|
||
import { Loader2, Plus, Pencil, Trash2, Search, Download, Eye } from 'lucide-react'
|
||
import { format } from 'date-fns'
|
||
|
||
// 文件类型配置
|
||
const FILE_TYPE_CONFIG = {
|
||
pdf: { icon: '📄', color: 'bg-red-100 text-red-800', label: 'PDF' },
|
||
word: { icon: '📝', color: 'bg-blue-100 text-blue-800', label: 'Word' },
|
||
ppt: { icon: '📊', color: 'bg-orange-100 text-orange-800', label: 'PPT' },
|
||
} as const
|
||
|
||
// 格式化文件大小
|
||
const formatFileSize = (bytes: number) => {
|
||
if (bytes === 0) return '0 B'
|
||
const k = 1024
|
||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||
}
|
||
|
||
export default function Materials() {
|
||
const [materials, setMaterials] = useState<MaterialFile[]>([])
|
||
const [subjects, setSubjects] = useState<MaterialSubject[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [total, setTotal] = useState(0)
|
||
const [page, setPage] = useState(1)
|
||
const pageSize = 10
|
||
|
||
// 筛选
|
||
const [subjectFilter, setSubjectFilter] = useState<string>('all')
|
||
const [fileTypeFilter, setFileTypeFilter] = useState<string>('all')
|
||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||
const [keyword, setKeyword] = useState('')
|
||
|
||
// 创建/编辑对话框
|
||
const [dialogOpen, setDialogOpen] = useState(false)
|
||
const [editingMaterial, setEditingMaterial] = useState<MaterialFile | null>(null)
|
||
const [formData, setFormData] = useState<MaterialCreateRequest & { status?: 'active' | 'inactive' }>({
|
||
subject_id: '',
|
||
title: '',
|
||
description: '',
|
||
file_type: 'pdf',
|
||
file_size: 0,
|
||
file_url: '',
|
||
file_name: '',
|
||
tags: [],
|
||
})
|
||
const [dialogLoading, setDialogLoading] = useState(false)
|
||
|
||
// 删除确认对话框
|
||
const [deleteOpen, setDeleteOpen] = useState(false)
|
||
const [deletingMaterial, setDeletingMaterial] = useState<MaterialFile | null>(null)
|
||
const [deleteLoading, setDeleteLoading] = useState(false)
|
||
|
||
// 详情对话框
|
||
const [detailOpen, setDetailOpen] = useState(false)
|
||
const [selectedMaterial, setSelectedMaterial] = useState<MaterialFile | null>(null)
|
||
const [detailLoading, setDetailLoading] = useState(false)
|
||
|
||
// 加载资料列表
|
||
const loadMaterials = useCallback(async () => {
|
||
setLoading(true)
|
||
try {
|
||
const params: MaterialQueryParams = {
|
||
page,
|
||
page_size: pageSize,
|
||
keyword: keyword || undefined,
|
||
subject_id: subjectFilter !== 'all' ? subjectFilter : undefined,
|
||
file_type: fileTypeFilter !== 'all' ? fileTypeFilter as any : undefined,
|
||
status: statusFilter !== 'all' ? statusFilter : undefined,
|
||
}
|
||
const response = await materialsApi.getMaterials(params)
|
||
const data = response.data.data
|
||
setMaterials(data.list || [])
|
||
setTotal(data.total || 0)
|
||
} catch (error) {
|
||
console.error('Failed to load materials:', error)
|
||
setMaterials([])
|
||
setTotal(0)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [page, pageSize, keyword, subjectFilter, fileTypeFilter, statusFilter])
|
||
|
||
// 加载学科列表(用于筛选和表单)
|
||
const loadSubjects = useCallback(async () => {
|
||
try {
|
||
const response = await subjectsApi.getSubjects()
|
||
const data = response.data.data as MaterialSubject[] | { list: MaterialSubject[] } | undefined
|
||
const list = Array.isArray(data) ? data : (data?.list || [])
|
||
setSubjects(list)
|
||
} catch (error) {
|
||
console.error('Failed to load subjects:', error)
|
||
}
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
loadSubjects()
|
||
}, [loadSubjects])
|
||
|
||
useEffect(() => {
|
||
loadMaterials()
|
||
}, [loadMaterials])
|
||
|
||
// 搜索
|
||
const handleSearch = () => {
|
||
setPage(1)
|
||
loadMaterials()
|
||
}
|
||
|
||
// 打开创建对话框
|
||
const handleOpenCreate = () => {
|
||
setEditingMaterial(null)
|
||
setFormData({
|
||
subject_id: subjects[0]?.id || '',
|
||
title: '',
|
||
description: '',
|
||
file_type: 'pdf',
|
||
file_size: 0,
|
||
file_url: '',
|
||
file_name: '',
|
||
tags: [],
|
||
})
|
||
setDialogOpen(true)
|
||
}
|
||
|
||
// 打开编辑对话框
|
||
const handleOpenEdit = (material: MaterialFile) => {
|
||
setEditingMaterial(material)
|
||
setFormData({
|
||
subject_id: material.subject_id,
|
||
title: material.title,
|
||
description: material.description || '',
|
||
file_type: material.file_type,
|
||
file_size: material.file_size,
|
||
file_url: material.file_url,
|
||
file_name: material.file_name || '',
|
||
tags: material.tags || [],
|
||
})
|
||
setDialogOpen(true)
|
||
}
|
||
|
||
// 保存资料
|
||
const handleSave = async () => {
|
||
if (!formData.subject_id || !formData.title?.trim() || !formData.file_url?.trim()) return
|
||
setDialogLoading(true)
|
||
try {
|
||
if (editingMaterial) {
|
||
const updateData: MaterialUpdateRequest = { ...formData }
|
||
await materialsApi.updateMaterial(editingMaterial.id, updateData)
|
||
} else {
|
||
await materialsApi.createMaterial(formData)
|
||
}
|
||
setDialogOpen(false)
|
||
loadMaterials()
|
||
} catch (error) {
|
||
console.error('Failed to save material:', error)
|
||
} finally {
|
||
setDialogLoading(false)
|
||
}
|
||
}
|
||
|
||
// 打开删除确认
|
||
const handleOpenDelete = (material: MaterialFile) => {
|
||
setDeletingMaterial(material)
|
||
setDeleteOpen(true)
|
||
}
|
||
|
||
// 确认删除
|
||
const handleConfirmDelete = async () => {
|
||
if (!deletingMaterial) return
|
||
setDeleteLoading(true)
|
||
try {
|
||
await materialsApi.deleteMaterial(deletingMaterial.id)
|
||
setDeleteOpen(false)
|
||
loadMaterials()
|
||
} catch (error) {
|
||
console.error('Failed to delete material:', error)
|
||
} finally {
|
||
setDeleteLoading(false)
|
||
}
|
||
}
|
||
|
||
// 查看详情
|
||
const handleViewDetail = async (material: MaterialFile) => {
|
||
setDetailLoading(true)
|
||
setDetailOpen(true)
|
||
try {
|
||
const response = await materialsApi.getMaterialById(material.id)
|
||
setSelectedMaterial(response.data.data)
|
||
} catch (error) {
|
||
console.error('Failed to load material detail:', error)
|
||
setSelectedMaterial(null)
|
||
} finally {
|
||
setDetailLoading(false)
|
||
}
|
||
}
|
||
|
||
// 格式化日期
|
||
const formatDate = (dateStr: string) => {
|
||
try {
|
||
return format(new Date(dateStr), 'yyyy-MM-dd HH:mm')
|
||
} catch {
|
||
return dateStr
|
||
}
|
||
}
|
||
|
||
const totalPages = Math.ceil(total / pageSize)
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<h1 className="text-3xl font-bold">学习资料管理</h1>
|
||
<p className="text-muted-foreground">管理各学科的学习资料文件</p>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||
<div>
|
||
<CardTitle>资料列表</CardTitle>
|
||
<CardDescription>共 {total} 个资料</CardDescription>
|
||
</div>
|
||
<Button onClick={handleOpenCreate}>
|
||
<Plus className="h-4 w-4 mr-2" />
|
||
添加资料
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{/* 筛选栏 */}
|
||
<div className="flex flex-wrap gap-4 mb-4">
|
||
<Input
|
||
placeholder="搜索资料标题..."
|
||
value={keyword}
|
||
onChange={(e) => setKeyword(e.target.value)}
|
||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||
className="w-[200px]"
|
||
/>
|
||
<Select value={subjectFilter} onValueChange={(v) => { setSubjectFilter(v); setPage(1); }}>
|
||
<SelectTrigger className="w-[150px]">
|
||
<SelectValue placeholder="学科筛选" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">全部学科</SelectItem>
|
||
{subjects.map((s) => (
|
||
<SelectItem key={s.id} value={s.id}>
|
||
{s.icon} {s.name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<Select value={fileTypeFilter} onValueChange={(v) => { setFileTypeFilter(v); setPage(1); }}>
|
||
<SelectTrigger className="w-[120px]">
|
||
<SelectValue placeholder="文件类型" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">全部类型</SelectItem>
|
||
<SelectItem value="pdf">PDF</SelectItem>
|
||
<SelectItem value="word">Word</SelectItem>
|
||
<SelectItem value="ppt">PPT</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
<Select value={statusFilter} onValueChange={(v) => { setStatusFilter(v); setPage(1); }}>
|
||
<SelectTrigger className="w-[120px]">
|
||
<SelectValue placeholder="状态筛选" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">全部状态</SelectItem>
|
||
<SelectItem value="active">启用</SelectItem>
|
||
<SelectItem value="inactive">禁用</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
<Button variant="outline" onClick={handleSearch}>
|
||
<Search className="h-4 w-4 mr-2" />
|
||
搜索
|
||
</Button>
|
||
<Button variant="outline" onClick={loadMaterials}>
|
||
<Loader2 className="h-4 w-4 mr-2" />
|
||
刷新
|
||
</Button>
|
||
</div>
|
||
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className="w-[80px]">类型</TableHead>
|
||
<TableHead>标题</TableHead>
|
||
<TableHead className="w-[120px]">学科</TableHead>
|
||
<TableHead className="w-[100px]">大小</TableHead>
|
||
<TableHead className="w-[80px]">下载</TableHead>
|
||
<TableHead className="w-[100px]">状态</TableHead>
|
||
<TableHead className="w-[150px]">创建时间</TableHead>
|
||
<TableHead className="w-[150px]">操作</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{loading ? (
|
||
<TableRow>
|
||
<TableCell colSpan={8} className="text-center py-8">
|
||
<Loader2 className="h-6 w-6 animate-spin mx-auto text-muted-foreground" />
|
||
<p className="text-muted-foreground mt-2">加载中...</p>
|
||
</TableCell>
|
||
</TableRow>
|
||
) : materials.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
|
||
暂无数据
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
materials.map((material) => {
|
||
const typeConfig = FILE_TYPE_CONFIG[material.file_type] || FILE_TYPE_CONFIG.pdf
|
||
const subject = subjects.find((s) => s.id === material.subject_id)
|
||
return (
|
||
<TableRow key={material.id}>
|
||
<TableCell>
|
||
<Badge className={typeConfig.color}>
|
||
{typeConfig.icon} {typeConfig.label}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="max-w-[300px]">
|
||
<p className="font-medium truncate">{material.title}</p>
|
||
<p className="text-xs text-muted-foreground truncate">
|
||
{material.file_name || '未命名文件'}
|
||
</p>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
{subject ? (
|
||
<span>{subject.icon} {subject.name}</span>
|
||
) : (
|
||
<span className="text-muted-foreground">-</span>
|
||
)}
|
||
</TableCell>
|
||
<TableCell>{formatFileSize(material.file_size)}</TableCell>
|
||
<TableCell>
|
||
<div className="flex items-center gap-1">
|
||
<Download className="h-3 w-3 text-muted-foreground" />
|
||
{material.download_count}
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Badge variant={material.status === 'active' ? 'default' : 'outline'}>
|
||
{material.status === 'active' ? '启用' : '禁用'}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell className="text-sm text-muted-foreground">
|
||
{formatDate(material.created_at)}
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="flex items-center gap-1">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => handleViewDetail(material)}
|
||
>
|
||
<Eye className="h-4 w-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => handleOpenEdit(material)}
|
||
>
|
||
<Pencil className="h-4 w-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => handleOpenDelete(material)}
|
||
>
|
||
<Trash2 className="h-4 w-4 text-destructive" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
)
|
||
})
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
|
||
{totalPages > 1 && (
|
||
<div className="mt-4">
|
||
<PaginationNavigator
|
||
currentPage={page}
|
||
totalPages={totalPages}
|
||
onPageChange={setPage}
|
||
/>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 创建/编辑对话框 */}
|
||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||
<DialogContent className="sm:max-w-[600px]">
|
||
<DialogHeader>
|
||
<DialogTitle>
|
||
{editingMaterial ? '编辑资料' : '添加资料'}
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
{editingMaterial ? '修改资料信息' : '创建新的学习资料'}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-4 py-4 max-h-[60vh] overflow-y-auto">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="subject_id">所属学科 *</Label>
|
||
<Select
|
||
value={formData.subject_id}
|
||
onValueChange={(v) => setFormData({ ...formData, subject_id: v })}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="选择学科" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{subjects.map((s) => (
|
||
<SelectItem key={s.id} value={s.id}>
|
||
{s.icon} {s.name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="title">资料标题 *</Label>
|
||
<Input
|
||
id="title"
|
||
value={formData.title || ''}
|
||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||
placeholder="如:高等数学期末复习资料"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="description">描述</Label>
|
||
<Textarea
|
||
id="description"
|
||
value={formData.description || ''}
|
||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||
placeholder="资料描述(可选)"
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="file_type">文件类型 *</Label>
|
||
<Select
|
||
value={formData.file_type}
|
||
onValueChange={(v) => setFormData({ ...formData, file_type: v as any })}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="pdf">📄 PDF</SelectItem>
|
||
<SelectItem value="word">📝 Word</SelectItem>
|
||
<SelectItem value="ppt">📊 PPT</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="file_size">文件大小 (bytes)</Label>
|
||
<Input
|
||
id="file_size"
|
||
type="number"
|
||
value={formData.file_size || 0}
|
||
onChange={(e) => setFormData({ ...formData, file_size: parseInt(e.target.value) || 0 })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="file_name">原始文件名</Label>
|
||
<Input
|
||
id="file_name"
|
||
value={formData.file_name || ''}
|
||
onChange={(e) => setFormData({ ...formData, file_name: e.target.value })}
|
||
placeholder="如:高等数学-期末复习-2024.pdf"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="file_url">文件链接 *</Label>
|
||
<Input
|
||
id="file_url"
|
||
value={formData.file_url || ''}
|
||
onChange={(e) => setFormData({ ...formData, file_url: e.target.value })}
|
||
placeholder="https://example.com/path/to/file.pdf"
|
||
/>
|
||
</div>
|
||
|
||
{editingMaterial && (
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
id="status"
|
||
type="checkbox"
|
||
checked={formData.status === 'active' || formData.status === undefined}
|
||
onChange={(e) => setFormData({ ...formData, status: e.target.checked ? 'active' : 'inactive' })}
|
||
className="rounded"
|
||
/>
|
||
<Label htmlFor="status">启用此资料</Label>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setDialogOpen(false)}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
onClick={handleSave}
|
||
disabled={dialogLoading || !formData.subject_id || !formData.title?.trim() || !formData.file_url?.trim()}
|
||
>
|
||
{dialogLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||
{editingMaterial ? '保存' : '创建'}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* 删除确认对话框 */}
|
||
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>确认删除</DialogTitle>
|
||
<DialogDescription>
|
||
确定要删除资料「{deletingMaterial?.title}」吗?此操作不可恢复。
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setDeleteOpen(false)}>
|
||
取消
|
||
</Button>
|
||
<Button variant="destructive" onClick={handleConfirmDelete}>
|
||
{deleteLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||
删除
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* 详情对话框 */}
|
||
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
||
<DialogContent className="sm:max-w-[600px]">
|
||
<DialogHeader>
|
||
<DialogTitle>资料详情</DialogTitle>
|
||
</DialogHeader>
|
||
{detailLoading ? (
|
||
<div className="flex items-center justify-center py-8">
|
||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||
</div>
|
||
) : selectedMaterial ? (
|
||
<div className="space-y-4">
|
||
<div className="flex items-start gap-4">
|
||
<div className="text-4xl">
|
||
{FILE_TYPE_CONFIG[selectedMaterial.file_type]?.icon || '📄'}
|
||
</div>
|
||
<div>
|
||
<h3 className="font-semibold text-lg">{selectedMaterial.title}</h3>
|
||
<p className="text-sm text-muted-foreground">{selectedMaterial.file_name}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||
<div>
|
||
<span className="text-muted-foreground">所属学科:</span>
|
||
<span>
|
||
{subjects.find((s) => s.id === selectedMaterial.subject_id)?.name || '-'}
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-muted-foreground">文件类型:</span>
|
||
<span>{FILE_TYPE_CONFIG[selectedMaterial.file_type]?.label || selectedMaterial.file_type}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-muted-foreground">文件大小:</span>
|
||
<span>{formatFileSize(selectedMaterial.file_size)}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-muted-foreground">下载次数:</span>
|
||
<span>{selectedMaterial.download_count}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-muted-foreground">状态:</span>
|
||
<Badge variant={selectedMaterial.status === 'active' ? 'default' : 'outline'}>
|
||
{selectedMaterial.status === 'active' ? '启用' : '禁用'}
|
||
</Badge>
|
||
</div>
|
||
<div>
|
||
<span className="text-muted-foreground">创建时间:</span>
|
||
<span>{formatDate(selectedMaterial.created_at)}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{selectedMaterial.description && (
|
||
<div>
|
||
<span className="text-muted-foreground">描述:</span>
|
||
<p className="mt-1 text-sm">{selectedMaterial.description}</p>
|
||
</div>
|
||
)}
|
||
|
||
{selectedMaterial.tags && selectedMaterial.tags.length > 0 && (
|
||
<div>
|
||
<span className="text-muted-foreground">标签:</span>
|
||
<div className="flex flex-wrap gap-1 mt-1">
|
||
{selectedMaterial.tags.map((tag, i) => (
|
||
<Badge key={i} variant="secondary">{tag}</Badge>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="pt-4">
|
||
<Label className="text-muted-foreground">文件链接:</Label>
|
||
<a
|
||
href={selectedMaterial.file_url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="block mt-1 text-sm text-blue-600 hover:underline break-all"
|
||
>
|
||
{selectedMaterial.file_url}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<p className="text-center text-muted-foreground py-8">无法加载详情</p>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
)
|
||
} |