652 lines
25 KiB
TypeScript
652 lines
25 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect, useRef } from "react"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { DashboardShell } from "@/components/dashboard-shell"
|
||
import { DashboardHeader } from "@/components/dashboard-header"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { ArrowLeft, Edit, AlertTriangle, FileText, Play, Pause, Volume2, VolumeX, Music, Download, Factory } from "lucide-react"
|
||
import Link from "next/link"
|
||
import { AddPrintBatchDialog } from "@/components/songs/add-print-batch-dialog"
|
||
import { ExportCardsDialog } from "@/components/songs/export-cards-dialog"
|
||
import { ExportBatchDialog } from "@/components/songs/export-batch-dialog"
|
||
import { useToast } from "@/components/ui/use-toast"
|
||
import { getSong, getSongBatches, markBatchAsProduced, SongBatch } from "@/lib/api/songs"
|
||
import { Song } from "@/lib/api/types"
|
||
import { Skeleton } from "@/components/ui/skeleton"
|
||
import { Slider } from "@/components/ui/slider"
|
||
import { useParams } from "next/navigation"
|
||
import { format } from "date-fns"
|
||
|
||
export default function SongDetailPage() {
|
||
const params = useParams()
|
||
const songId = params.id as string
|
||
const { toast } = useToast()
|
||
const [song, setSong] = useState<Song | null>(null)
|
||
const [batches, setBatches] = useState<SongBatch[]>([])
|
||
const [isLoading, setIsLoading] = useState(true)
|
||
const [isBatchesLoading, setIsBatchesLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [isPlaying, setIsPlaying] = useState(false)
|
||
const [isAudioLoading, setIsAudioLoading] = useState(false)
|
||
const [volume, setVolume] = useState(80)
|
||
const [isMuted, setIsMuted] = useState(false)
|
||
|
||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||
|
||
// 加载歌曲数据
|
||
useEffect(() => {
|
||
const fetchSong = async () => {
|
||
if (!songId) return
|
||
|
||
setIsLoading(true)
|
||
try {
|
||
const songData = await getSong(songId)
|
||
setSong(songData)
|
||
setError(null)
|
||
} catch (error) {
|
||
console.error("获取歌曲失败:", error)
|
||
setError(error instanceof Error ? error.message : "未知错误")
|
||
toast({
|
||
title: "获取歌曲失败",
|
||
description: error instanceof Error ? error.message : "无法加载歌曲数据,请稍后重试",
|
||
variant: "destructive",
|
||
})
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
|
||
fetchSong()
|
||
}, [songId, toast])
|
||
|
||
// 加载批次数据
|
||
useEffect(() => {
|
||
const fetchBatches = async () => {
|
||
if (!songId) return
|
||
|
||
setIsBatchesLoading(true)
|
||
try {
|
||
const batchData = await getSongBatches(songId)
|
||
setBatches(batchData)
|
||
} catch (error) {
|
||
console.error("获取批次数据失败:", error)
|
||
toast({
|
||
title: "获取批次数据失败",
|
||
description: error instanceof Error ? error.message : "无法加载批次数据,请稍后重试",
|
||
variant: "destructive",
|
||
})
|
||
} finally {
|
||
setIsBatchesLoading(false)
|
||
}
|
||
}
|
||
|
||
fetchBatches()
|
||
}, [songId, toast])
|
||
|
||
// 刷新批次数据的函数
|
||
const refreshBatches = async () => {
|
||
if (!songId) return
|
||
|
||
setIsBatchesLoading(true)
|
||
try {
|
||
const batchData = await getSongBatches(songId)
|
||
setBatches(batchData)
|
||
} catch (error) {
|
||
console.error("刷新批次数据失败:", error)
|
||
} finally {
|
||
setIsBatchesLoading(false)
|
||
}
|
||
}
|
||
|
||
// 初始化音频播放器 - 现在只创建但不自动播放
|
||
useEffect(() => {
|
||
if (song?.audioUrl && !audioRef.current) {
|
||
audioRef.current = new Audio(song.audioUrl)
|
||
|
||
const audio = audioRef.current
|
||
|
||
// 音频加载完成时 - 不再自动播放
|
||
audio.addEventListener('canplay', () => {
|
||
setIsAudioLoading(false)
|
||
})
|
||
|
||
// 音频播放结束时
|
||
audio.addEventListener('ended', () => {
|
||
setIsPlaying(false)
|
||
})
|
||
|
||
// 音频播放错误时
|
||
audio.addEventListener('error', () => {
|
||
setIsAudioLoading(false)
|
||
setIsPlaying(false)
|
||
toast({
|
||
title: "播放错误",
|
||
description: "无法播放该音频文件",
|
||
variant: "destructive",
|
||
})
|
||
})
|
||
|
||
// 设置音量
|
||
audio.volume = volume / 100
|
||
audio.muted = isMuted
|
||
}
|
||
|
||
return () => {
|
||
if (audioRef.current) {
|
||
const audio = audioRef.current
|
||
audio.pause()
|
||
audio.src = ''
|
||
|
||
audio.removeEventListener('canplay', () => {})
|
||
audio.removeEventListener('ended', () => {})
|
||
audio.removeEventListener('error', () => {})
|
||
audioRef.current = null
|
||
}
|
||
}
|
||
}, [song?.audioUrl, toast, volume, isMuted])
|
||
|
||
// 监听音量变化
|
||
useEffect(() => {
|
||
if (audioRef.current) {
|
||
audioRef.current.volume = volume / 100
|
||
}
|
||
}, [volume])
|
||
|
||
// 监听静音状态
|
||
useEffect(() => {
|
||
if (audioRef.current) {
|
||
audioRef.current.muted = isMuted
|
||
}
|
||
}, [isMuted])
|
||
|
||
// 处理播放/暂停
|
||
const togglePlay = () => {
|
||
if (!song?.audioUrl) {
|
||
toast({
|
||
title: "无法播放",
|
||
description: "该歌曲没有可播放的音频文件",
|
||
variant: "destructive",
|
||
})
|
||
return
|
||
}
|
||
|
||
if (isPlaying) {
|
||
audioRef.current?.pause()
|
||
setIsPlaying(false)
|
||
} else {
|
||
setIsAudioLoading(true)
|
||
|
||
if (audioRef.current) {
|
||
audioRef.current.play()
|
||
.then(() => {
|
||
setIsPlaying(true)
|
||
setIsAudioLoading(false)
|
||
})
|
||
.catch((error) => {
|
||
console.error('播放失败:', error)
|
||
setIsAudioLoading(false)
|
||
toast({
|
||
title: "播放失败",
|
||
description: "无法播放音频,请稍后重试",
|
||
variant: "destructive",
|
||
})
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 切换静音状态
|
||
const toggleMute = () => {
|
||
setIsMuted(!isMuted)
|
||
}
|
||
|
||
// 处理音量变化
|
||
const handleVolumeChange = (values: number[]) => {
|
||
const newVolume = values[0]
|
||
setVolume(newVolume)
|
||
|
||
// 如果音量从0调高,则取消静音
|
||
if (newVolume > 0 && isMuted) {
|
||
setIsMuted(false)
|
||
}
|
||
// 如果音量调到0,则设置为静音
|
||
else if (newVolume === 0 && !isMuted) {
|
||
setIsMuted(true)
|
||
}
|
||
}
|
||
|
||
// 标记批次为已生产
|
||
const handleMarkAsProduced = async (batchId: number) => {
|
||
try {
|
||
setIsBatchesLoading(true);
|
||
await markBatchAsProduced(batchId);
|
||
toast({
|
||
title: "操作成功",
|
||
description: "批次已成功标记为已生产",
|
||
});
|
||
await refreshBatches();
|
||
} catch (error) {
|
||
console.error("标记批次为已生产失败:", error);
|
||
toast({
|
||
title: "操作失败",
|
||
description: error instanceof Error ? error.message : "无法标记批次为已生产,请稍后重试",
|
||
variant: "destructive",
|
||
});
|
||
} finally {
|
||
setIsBatchesLoading(false);
|
||
}
|
||
};
|
||
|
||
if (error) {
|
||
return (
|
||
<DashboardShell>
|
||
<div className="flex flex-col items-center justify-center h-[60vh]">
|
||
<AlertTriangle className="h-16 w-16 text-red-500 mb-4" />
|
||
<h1 className="text-2xl font-bold mb-2">歌曲不存在</h1>
|
||
<p className="text-gray-500 mb-6">找不到ID为 {songId} 的歌曲</p>
|
||
<Button asChild>
|
||
<Link href="/songs">
|
||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||
返回歌曲列表
|
||
</Link>
|
||
</Button>
|
||
</div>
|
||
</DashboardShell>
|
||
)
|
||
}
|
||
|
||
if (isLoading || !song) {
|
||
return (
|
||
<DashboardShell>
|
||
<div className="flex items-center mb-6">
|
||
<Button variant="ghost" size="sm" className="mr-4" asChild>
|
||
<Link href="/songs">
|
||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||
返回列表
|
||
</Link>
|
||
</Button>
|
||
<div className="w-full">
|
||
<Skeleton className="h-10 w-64 mb-2" />
|
||
<Skeleton className="h-5 w-32" />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-6 md:grid-cols-3">
|
||
<Skeleton className="h-[400px]" />
|
||
<Skeleton className="h-[400px] md:col-span-2" />
|
||
</div>
|
||
</DashboardShell>
|
||
)
|
||
}
|
||
|
||
const isPublished = song.status === "已发布";
|
||
|
||
return (
|
||
<DashboardShell>
|
||
<div className="absolute top-0 right-0 w-1/2 h-48 bg-gradient-to-bl from-pink-200 via-purple-200 to-transparent opacity-20 rounded-bl-full" />
|
||
|
||
<div className="flex items-center mb-6">
|
||
<Button variant="ghost" size="sm" className="mr-4" asChild>
|
||
<Link href="/songs">
|
||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||
返回列表
|
||
</Link>
|
||
</Button>
|
||
<DashboardHeader heading={song.name} text={`歌曲ID: ${song.id}`}>
|
||
<div className="flex space-x-2 ml-auto">
|
||
{song.audioUrl && (
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className={`${
|
||
isPlaying
|
||
? "bg-pink-100 text-pink-700 hover:bg-pink-200"
|
||
: "hover:bg-pink-50 hover:text-pink-700"
|
||
}`}
|
||
onClick={togglePlay}
|
||
disabled={isAudioLoading}
|
||
>
|
||
{isAudioLoading ? (
|
||
<>
|
||
<div className="h-4 w-4 mr-2 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||
加载中...
|
||
</>
|
||
) : isPlaying ? (
|
||
<>
|
||
<Pause className="h-4 w-4 mr-2" />
|
||
暂停播放
|
||
</>
|
||
) : (
|
||
<>
|
||
<Play className="h-4 w-4 mr-2" />
|
||
播放歌曲
|
||
</>
|
||
)}
|
||
</Button>
|
||
)}
|
||
|
||
{!isPublished && (
|
||
<Button
|
||
asChild
|
||
className="bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700 transition-all duration-300 shadow-md hover:shadow-lg"
|
||
>
|
||
<Link href={`/songs/edit/${songId}`}>
|
||
<Edit className="mr-2 h-4 w-4" />
|
||
编辑歌曲
|
||
</Link>
|
||
</Button>
|
||
)}
|
||
<ExportCardsDialog songId={song.id} />
|
||
</div>
|
||
</DashboardHeader>
|
||
</div>
|
||
|
||
{/* 音频控制器 - 仅当歌曲有音频URL且正在播放时显示 */}
|
||
{song.audioUrl && isPlaying && (
|
||
<div className="mb-4">
|
||
<div className="bg-pink-50 p-3 rounded-lg border border-pink-100 flex items-center">
|
||
<div className="flex-shrink-0 mr-3">
|
||
<Music className="h-5 w-5 text-pink-500" />
|
||
</div>
|
||
<div className="flex-grow">
|
||
<div className="flex items-center space-x-4">
|
||
<div className="flex-grow">
|
||
<p className="text-sm font-medium text-pink-700 truncate">
|
||
正在播放: {song.name}
|
||
</p>
|
||
<p className="text-xs text-pink-500 truncate">
|
||
{song.composer}
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center space-x-3">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8 rounded-full hover:bg-pink-100"
|
||
onClick={toggleMute}
|
||
>
|
||
{isMuted ? <VolumeX className="h-4 w-4 text-pink-500" /> : <Volume2 className="h-4 w-4 text-pink-500" />}
|
||
</Button>
|
||
<div className="w-32">
|
||
<Slider
|
||
value={[volume]}
|
||
min={0}
|
||
max={100}
|
||
step={1}
|
||
onValueChange={handleVolumeChange}
|
||
className="h-1"
|
||
/>
|
||
</div>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8 rounded-full bg-pink-200 hover:bg-pink-300 text-pink-700"
|
||
onClick={togglePlay}
|
||
>
|
||
<Pause className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<Tabs defaultValue="details" className="space-y-4">
|
||
<TabsList>
|
||
<TabsTrigger value="details">歌曲详情</TabsTrigger>
|
||
<TabsTrigger value="batches">批次管理</TabsTrigger>
|
||
<TabsTrigger value="analytics">数据分析</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="details" className="space-y-4">
|
||
<div className="grid gap-6 md:grid-cols-3">
|
||
<Card className="md:col-span-1 border-none shadow-lg bg-white">
|
||
<CardHeader>
|
||
<CardTitle className="text-lg font-bold">歌曲预览</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="flex justify-center">
|
||
<div className="relative w-full aspect-square max-w-[300px] rounded-lg overflow-hidden bg-gray-100 flex items-center justify-center group">
|
||
<img src={song.image || "/placeholder.svg"} alt={song.name} className="object-cover" />
|
||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3">
|
||
<div className="flex flex-wrap gap-2">
|
||
<Badge className={`${isPublished ? "bg-green-500" : "bg-gray-500"}`}>{song.status}</Badge>
|
||
{song.rarity && <Badge className="bg-purple-500">{song.rarity}</Badge>}
|
||
{song.cardType && <Badge className="bg-blue-500">{song.cardType}</Badge>}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="md:col-span-2 border-none shadow-lg bg-gradient-to-br from-white to-purple-50">
|
||
<CardHeader>
|
||
<CardTitle className="text-lg font-bold">歌曲详情</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">作曲</p>
|
||
<p className="font-medium">{song.composer}</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">作词</p>
|
||
<p className="font-medium">{song.lyricist}</p>
|
||
</div>
|
||
{song.arrangement && (
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">编曲</p>
|
||
<p className="font-medium">{song.arrangement}</p>
|
||
</div>
|
||
)}
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">时长</p>
|
||
<p className="font-medium">{song.duration}</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">发布日期</p>
|
||
<p className="font-medium">{song.releaseDate || "尚未发布"}</p>
|
||
</div>
|
||
{song.genre && (
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">风格</p>
|
||
<p className="font-medium">{song.genre}</p>
|
||
</div>
|
||
)}
|
||
{song.bpm && (
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">BPM</p>
|
||
<p className="font-medium">{song.bpm}</p>
|
||
</div>
|
||
)}
|
||
{song.price && (
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">价格</p>
|
||
<p className="font-medium">¥{song.price}</p>
|
||
</div>
|
||
)}
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">激活数量</p>
|
||
<p className="font-medium">{song.cardsCount || 0}</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">印刷批次</p>
|
||
<p className="font-medium">{song.batchesCount || 0}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{song.description && (
|
||
<div className="mt-4">
|
||
<p className="text-sm font-medium text-gray-500 mb-1">描述</p>
|
||
<p className="text-sm">{song.description}</p>
|
||
</div>
|
||
)}
|
||
|
||
{song.lyrics && (
|
||
<div className="mt-4">
|
||
<p className="text-sm font-medium text-gray-500 mb-1">歌词</p>
|
||
<div className="text-sm max-h-40 overflow-y-auto p-3 bg-gray-50 rounded">
|
||
<pre className="whitespace-pre-wrap font-sans">{song.lyrics}</pre>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{isPublished && (
|
||
<div className="mt-6 p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-start">
|
||
<AlertTriangle className="h-5 w-5 text-amber-500 mr-2 flex-shrink-0 mt-0.5" />
|
||
<p className="text-sm text-amber-700">该歌曲已发布,基本属性不可修改。您仍可以增加印刷数量。</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="batches" className="space-y-4">
|
||
<Card className="border-none shadow-lg bg-gradient-to-br from-white to-blue-50">
|
||
<CardHeader className="flex flex-row items-center justify-between">
|
||
<div>
|
||
<CardTitle className="text-lg font-bold">印刷批次</CardTitle>
|
||
<CardDescription>管理歌曲卡牌的印刷批次和卡牌ID</CardDescription>
|
||
</div>
|
||
<AddPrintBatchDialog songId={song.id} isPublished={isPublished} onBatchCreated={refreshBatches} />
|
||
</CardHeader>
|
||
<CardContent>
|
||
{/* 如果没有批次数据,显示空状态 */}
|
||
{isBatchesLoading ? (
|
||
<div className="space-y-3">
|
||
<div className="flex justify-between mb-4">
|
||
{Array.from({ length: 6 }).map((_, i) => (
|
||
<Skeleton key={i} className="h-4 w-16" />
|
||
))}
|
||
</div>
|
||
{Array.from({ length: 3 }).map((_, i) => (
|
||
<div key={i} className="flex justify-between py-3 border-b">
|
||
{Array.from({ length: 6 }).map((_, j) => (
|
||
<Skeleton key={j} className="h-4 w-16" />
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : batches.length === 0 ? (
|
||
<div className="text-center py-8">
|
||
<p className="text-gray-500">暂无批次数据</p>
|
||
</div>
|
||
) : (
|
||
<div className="rounded-md border">
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="bg-gray-50 border-b">
|
||
<th className="py-3 px-4 text-left text-sm font-medium text-gray-500">批次ID</th>
|
||
<th className="py-3 px-4 text-left text-sm font-medium text-gray-500">创建日期</th>
|
||
<th className="py-3 px-4 text-left text-sm font-medium text-gray-500">数量</th>
|
||
<th className="py-3 px-4 text-left text-sm font-medium text-gray-500">起始ID</th>
|
||
<th className="py-3 px-4 text-left text-sm font-medium text-gray-500">结束ID</th>
|
||
<th className="py-3 px-4 text-left text-sm font-medium text-gray-500">状态</th>
|
||
<th className="py-3 px-4 text-right text-sm font-medium text-gray-500">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{batches.map((batch) => (
|
||
<tr key={batch.id} className="border-b">
|
||
<td className="py-3 px-4 text-left text-sm font-medium text-gray-500">{batch.id}</td>
|
||
<td className="py-3 px-4 text-left text-sm font-medium text-gray-500">{format(new Date(batch.created_at), 'yyyy-MM-dd')}</td>
|
||
<td className="py-3 px-4 text-left text-sm font-medium text-gray-500">{batch.quantity}</td>
|
||
<td className="py-3 px-4 text-left text-sm font-medium text-gray-500">{batch.start_id}</td>
|
||
<td className="py-3 px-4 text-left text-sm font-medium text-gray-500">{batch.end_id}</td>
|
||
<td className="py-3 px-4 text-left text-sm font-medium text-gray-500">
|
||
<Badge className={getBatchStatusColor(batch.status)}>
|
||
{batch.status_display || getBatchStatusText(batch.status)}
|
||
</Badge>
|
||
</td>
|
||
<td className="py-3 px-4 text-right">
|
||
<div className="flex justify-end space-x-2">
|
||
<ExportBatchDialog batchId={batch.id} />
|
||
{batch.status !== 'produced' && batch.status !== 'published' && (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-8 hover:bg-green-50 hover:text-green-600"
|
||
onClick={() => handleMarkAsProduced(batch.id)}
|
||
>
|
||
<Factory className="h-4 w-4 mr-1" />
|
||
生产
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="analytics" className="space-y-4">
|
||
<Card className="border-none shadow-lg bg-gradient-to-br from-white to-green-50">
|
||
<CardHeader>
|
||
<CardTitle className="text-lg font-bold">数据分析</CardTitle>
|
||
<CardDescription>查看歌曲卡牌的激活数据和使用情况</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid gap-6 md:grid-cols-2">
|
||
<div className="h-[300px] bg-gray-50 rounded-lg flex items-center justify-center">
|
||
<p className="text-gray-500">激活数据图表</p>
|
||
</div>
|
||
<div className="h-[300px] bg-gray-50 rounded-lg flex items-center justify-center">
|
||
<p className="text-gray-500">地区分布图表</p>
|
||
</div>
|
||
<div className="md:col-span-2 h-[300px] bg-gray-50 rounded-lg flex items-center justify-center">
|
||
<p className="text-gray-500">时间趋势图表</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</DashboardShell>
|
||
)
|
||
}
|
||
|
||
// 获取批次状态颜色
|
||
function getBatchStatusColor(status: string): string {
|
||
switch (status) {
|
||
case 'draft':
|
||
return 'bg-gray-500';
|
||
case 'exported':
|
||
return 'bg-blue-500';
|
||
case 'in_production':
|
||
return 'bg-amber-500';
|
||
case 'produced':
|
||
return 'bg-purple-500';
|
||
case 'published':
|
||
return 'bg-green-500';
|
||
default:
|
||
return 'bg-gray-500';
|
||
}
|
||
}
|
||
|
||
// 获取批次状态文本
|
||
function getBatchStatusText(status: string): string {
|
||
switch (status) {
|
||
case 'draft':
|
||
return '草稿';
|
||
case 'exported':
|
||
return '已导出';
|
||
case 'in_production':
|
||
return '生产中';
|
||
case 'produced':
|
||
return '已生产';
|
||
case 'published':
|
||
return '已发布';
|
||
default:
|
||
return status;
|
||
}
|
||
}
|