mirror of
https://git.oceanpay.cc/danial/kami_apple_exchage.git
synced 2025-12-18 21:23:49 +00:00
- 新增链接权重字段,支持1-100范围设置 - 修改轮询算法为基于权重的选择机制 - 更新链接API接口返回统一使用LinkInfo模型 - 添加更新链接权重的PATCH端点 - 调整链接仓库查询逻辑,只包含激活状态链接 - 迁移链接相关Pydantic模型到task模块统一管理 - 修改分页响应格式为通用PaginatedResponse包装 - 禁用OpenTelemetry监控配置
446 lines
25 KiB
TypeScript
446 lines
25 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useMemo } from "react";
|
||
import {
|
||
RefreshCw,
|
||
User,
|
||
Link,
|
||
Clock,
|
||
CheckCircle,
|
||
XCircle,
|
||
AlertCircle,
|
||
Loader2,
|
||
Gift,
|
||
ChevronLeft,
|
||
ChevronRight
|
||
} from "lucide-react";
|
||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/animate-ui/base/tooltip";
|
||
import { useGetTaskListApiV1TasksListGet } from "@/lib/api/generated/task-management.gen";
|
||
import { AppleButton } from "@/components/ui/apple-button";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { AnimatedNumber } from "@/components/ui/animated-number";
|
||
import { Progress } from "@/components/animate-ui/radix/progress";
|
||
import { GiftCardInput } from "./gift-card-input";
|
||
import { cn } from "@/lib/utils";
|
||
import { OrderTaskStatus } from "@/lib/api/generated/schemas";
|
||
|
||
interface TaskListProps {
|
||
refreshEnabled?: boolean;
|
||
refreshInterval?: number;
|
||
className?: string;
|
||
}
|
||
|
||
export function TaskList({ refreshEnabled = false, refreshInterval = 5000, className }: TaskListProps) {
|
||
const [isLocalRefreshing, setIsLocalRefreshing] = useState(false);
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
const ITEMS_PER_PAGE = 12;
|
||
|
||
// 获取任务列表
|
||
const {
|
||
data: taskListData,
|
||
isLoading,
|
||
error,
|
||
refetch
|
||
} = useGetTaskListApiV1TasksListGet(
|
||
{
|
||
query: {
|
||
enabled: true,
|
||
refetchInterval: refreshEnabled ? refreshInterval : false,
|
||
retry: 2,
|
||
staleTime: 30000,
|
||
},
|
||
}
|
||
);
|
||
|
||
const handleRefresh = () => {
|
||
setIsLocalRefreshing(true);
|
||
refetch();
|
||
setTimeout(() => setIsLocalRefreshing(false), 1000);
|
||
};
|
||
|
||
const tasks = taskListData?.tasks || [];
|
||
|
||
// 排序:waiting_gift_card 状态的任务优先
|
||
const sortedTasks = useMemo(() => {
|
||
return [...tasks].sort((a, b) => {
|
||
if (a.status === "waiting_gift_card" && b.status !== "waiting_gift_card") {
|
||
return -1; // a 排在前面
|
||
}
|
||
if (a.status !== "waiting_gift_card" && b.status === "waiting_gift_card") {
|
||
return 1; // b 排在前面
|
||
}
|
||
return 0; // 保持原有顺序
|
||
});
|
||
}, [tasks]);
|
||
|
||
// 分页逻辑
|
||
const totalPages = Math.ceil(sortedTasks.length / ITEMS_PER_PAGE);
|
||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
||
const endIndex = startIndex + ITEMS_PER_PAGE;
|
||
const paginatedTasks = sortedTasks.slice(startIndex, endIndex);
|
||
|
||
// 截断错误信息
|
||
const truncateErrorMessage = (message: string, maxLength: number = 50) => {
|
||
if (message.length <= maxLength) return message;
|
||
return message.substring(0, maxLength) + "...";
|
||
};
|
||
|
||
// 格式化时间
|
||
const formatTime = (timeString: string) => {
|
||
try {
|
||
return new Date(timeString).toLocaleString('zh-CN', {
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
} catch {
|
||
return '未知时间';
|
||
}
|
||
};
|
||
|
||
// 获取状态颜色
|
||
const getStatusColor = (status: OrderTaskStatus) => {
|
||
const colorMap: Record<OrderTaskStatus, string> = {
|
||
pending: "bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300",
|
||
running: "bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300",
|
||
success: "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300",
|
||
failed: "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300",
|
||
waiting_gift_card: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300",
|
||
gift_card_received: "bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-300",
|
||
};
|
||
return colorMap[status] || "bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300";
|
||
};
|
||
|
||
// 获取状态文本
|
||
const getStatusText = (status: OrderTaskStatus) => {
|
||
const textMap: Record<OrderTaskStatus, string> = {
|
||
pending: "等待中",
|
||
running: "运行中",
|
||
success: "成功",
|
||
failed: "失败",
|
||
waiting_gift_card: "等待礼品卡",
|
||
gift_card_received: "礼品卡已接收",
|
||
};
|
||
return textMap[status] || status;
|
||
};
|
||
|
||
// 获取状态图标
|
||
const getStatusIcon = (status: OrderTaskStatus) => {
|
||
const iconMap: Record<OrderTaskStatus, React.ReactNode> = {
|
||
pending: <Clock className="h-4 w-4" />,
|
||
running: <Loader2 className="h-4 w-4 animate-spin" />,
|
||
success: <CheckCircle className="h-4 w-4" />,
|
||
failed: <XCircle className="h-4 w-4" />,
|
||
waiting_gift_card: <Gift className="h-4 w-4" />,
|
||
gift_card_received: <CheckCircle className="h-4 w-4" />,
|
||
};
|
||
return iconMap[status] || <Clock className="h-4 w-4" />;
|
||
};
|
||
|
||
return (
|
||
<div className={cn("space-y-6", className)}>
|
||
{/* 任务列表卡片 */}
|
||
<div className="apple-glass-card rounded-2xl p-6 transition-all duration-300">
|
||
{/* 头部 */}
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-purple-600">
|
||
<User className="w-5 h-5 text-white" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||
任务列表
|
||
</h3>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
实时任务处理状态 • 共 {tasks.length} 个任务
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<AppleButton
|
||
variant="outline"
|
||
size="icon"
|
||
onClick={handleRefresh}
|
||
disabled={isLoading || isLocalRefreshing}
|
||
className="w-10 h-10 rounded-xl"
|
||
>
|
||
<RefreshCw
|
||
className={cn("h-4 w-4", (isLoading || isLocalRefreshing) && "animate-spin")}
|
||
/>
|
||
</AppleButton>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 内容区域 */}
|
||
<div className="space-y-4">
|
||
{isLoading && (
|
||
<div className="flex items-center justify-center py-8">
|
||
<div className="flex items-center gap-3">
|
||
<Loader2 className="h-6 w-6 animate-spin text-blue-600" />
|
||
<span className="text-sm text-gray-600 dark:text-gray-400">正在加载任务列表...</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{error ? (
|
||
<div className="flex items-center justify-center py-8">
|
||
<div className="flex items-center gap-3 text-red-600">
|
||
<AlertCircle className="h-6 w-6" />
|
||
<span className="text-sm">加载失败: {error instanceof Error ? error.message : String(error)}</span>
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
|
||
{!isLoading && !error && tasks.length === 0 && (
|
||
<div className="flex items-center justify-center py-8">
|
||
<div className="text-center">
|
||
<Clock className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">暂无任务</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{!isLoading && !error && tasks.length > 0 && (
|
||
<>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{paginatedTasks.map((task) => (
|
||
<div
|
||
key={task.task_id}
|
||
className="bg-white dark:bg-gray-800 rounded-xl p-4 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-all duration-200"
|
||
>
|
||
{/* 任务头部 */}
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex items-center gap-2">
|
||
{getStatusIcon(task.status)}
|
||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||
任务 {task.task_id.slice(-8)}
|
||
</span>
|
||
</div>
|
||
<Badge className={cn("px-2 py-1 rounded-full text-xs font-medium", getStatusColor(task.status))}>
|
||
{getStatusText(task.status)}
|
||
</Badge>
|
||
</div>
|
||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||
{formatTime(task.created_at)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 用户信息 */}
|
||
{task.user_info && (
|
||
<div className="space-y-2 mb-3 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||
<div className="flex items-center gap-2">
|
||
<User className="h-4 w-4 text-gray-500" />
|
||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||
{task.user_info.first_name} {task.user_info.last_name}
|
||
</span>
|
||
</div>
|
||
{/* 用户邮箱 */}
|
||
{task.user_info.email && (
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
|
||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||
{task.user_info.email}
|
||
</span>
|
||
</div>
|
||
)}
|
||
{/* 用户ID */}
|
||
{task.user_info?.first_name && (
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-3 h-3 rounded-full bg-gray-400"></div>
|
||
<span className="text-xs text-gray-500 dark:text-gray-500">
|
||
用户昵称: {task.user_info?.first_name} {task.user_info?.last_name}
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 链接信息 */}
|
||
{task.link_info && (
|
||
<div className="space-y-2 mb-3">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-sm font-medium text-green-600">
|
||
${task.link_info.amount || 0}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
{/* 链接 URL */}
|
||
{task.link_info.url && (
|
||
<div className="flex items-start gap-2">
|
||
<Link className="h-3 w-3 text-gray-400 mt-0.5" />
|
||
<a
|
||
href={task.link_info.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline break-all"
|
||
>
|
||
{task.link_info.url}
|
||
</a>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 进度条 */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||
任务进度
|
||
</span>
|
||
<span className="text-xs font-medium text-gray-900 dark:text-gray-100">
|
||
<AnimatedNumber
|
||
value={task.progress}
|
||
duration={500}
|
||
decimals={1}
|
||
/>%
|
||
</span>
|
||
</div>
|
||
<Progress
|
||
value={task.progress}
|
||
className={cn(
|
||
"h-2",
|
||
task.progress === 100
|
||
? "[&>[data-slot='progress-indicator']]:bg-green-600"
|
||
: task.progress > 0 ? "[&>[data-slot='progress-indicator']]:bg-blue-600" : "[&>[data-slot='progress-indicator']]:bg-gray-400"
|
||
)}
|
||
/>
|
||
</div>
|
||
|
||
{/* 礼品卡输入框 - 只在等待礼品卡状态显示 */}
|
||
{task.status === "waiting_gift_card" && task.link_info?.amount && (
|
||
<div className="mt-4">
|
||
<GiftCardInput
|
||
taskId={task.task_id}
|
||
amount={task.link_info.amount}
|
||
updatedAt={task.updated_at}
|
||
onSubmit={(success) => {
|
||
if (success) {
|
||
refetch();
|
||
}
|
||
}}
|
||
triggerButton={
|
||
<AppleButton variant="default" size="sm" className="w-full flex items-center gap-2 apple-glass-button">
|
||
<Gift className="h-4 w-4" />
|
||
提交礼品卡 (${task.link_info.amount})
|
||
</AppleButton>
|
||
}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* 错误信息 */}
|
||
{task.error_message && (
|
||
<div className="mt-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-red-200 dark:border-red-800 shadow-sm">
|
||
<div className="flex items-start gap-2">
|
||
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||
<div className="flex-1 min-w-0">
|
||
{task.error_message.length > 50 ? (
|
||
<Tooltip>
|
||
<TooltipTrigger>
|
||
<span className="text-sm text-red-700 dark:text-red-300 cursor-help font-medium break-words">
|
||
{truncateErrorMessage(task.error_message)}
|
||
</span>
|
||
</TooltipTrigger>
|
||
<TooltipContent className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 shadow-lg">
|
||
<p className="max-w-xs text-sm font-medium leading-relaxed break-words">{task.error_message}</p>
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
) : (
|
||
<span className="text-sm text-red-700 dark:text-red-300 font-medium break-words">
|
||
{task.error_message}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* 分页控件 */}
|
||
{totalPages > 1 && (
|
||
<div className="flex items-center justify-between pt-4 mt-4 border-t border-gray-200 dark:border-gray-700">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||
第 {currentPage} 页,共 {totalPages} 页
|
||
</span>
|
||
<span className="text-sm text-gray-500 dark:text-gray-500">
|
||
({sortedTasks.length} 个任务)
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
<AppleButton
|
||
variant="outline"
|
||
size="icon"
|
||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||
disabled={currentPage === 1}
|
||
className="w-8 h-8 rounded-lg"
|
||
>
|
||
<ChevronLeft className="h-4 w-4" />
|
||
</AppleButton>
|
||
|
||
<div className="flex items-center gap-1">
|
||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||
let pageNum;
|
||
if (totalPages <= 5) {
|
||
pageNum = i + 1;
|
||
} else if (currentPage <= 3) {
|
||
pageNum = i + 1;
|
||
} else if (currentPage >= totalPages - 2) {
|
||
pageNum = totalPages - 4 + i;
|
||
} else {
|
||
pageNum = currentPage - 2 + i;
|
||
}
|
||
|
||
return (
|
||
<AppleButton
|
||
key={pageNum}
|
||
variant={currentPage === pageNum ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => setCurrentPage(pageNum)}
|
||
className={`w-8 h-8 rounded-lg ${currentPage === pageNum ? 'apple-glass-button' : ''}`}
|
||
>
|
||
{pageNum}
|
||
</AppleButton>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<AppleButton
|
||
variant="outline"
|
||
size="icon"
|
||
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
||
disabled={currentPage === totalPages}
|
||
className="w-8 h-8 rounded-lg"
|
||
>
|
||
<ChevronRight className="h-4 w-4" />
|
||
</AppleButton>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* 底部信息 */}
|
||
{!isLoading && !error && (
|
||
<div className="flex justify-between items-center pt-4 mt-4 border-t border-gray-200 dark:border-gray-700">
|
||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||
显示 {paginatedTasks.length} 个任务 (共 {tasks.length} 个)
|
||
</span>
|
||
{refreshEnabled && (
|
||
<span className="text-xs text-gray-500 dark:text-gray-500">
|
||
自动刷新: {refreshInterval / 1000}秒
|
||
</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
} |