'use client' import React, { useState, useEffect, useCallback } from 'react' import { useRouter } from 'next/navigation' import { ArrowLeft, AlertCircle, CheckCircle, Clock, XCircle, Send, Info, Loader2 } from 'lucide-react' import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout' import { Button } from '@/components/ui/Button' import { cn } from '@/lib/utils' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import { useToast } from '@/components/ui/Toast' import type { TaskResponse } from '@/types/task' // 申请状态类型 type RequestStatus = 'none' | 'pending' | 'approved' | 'rejected' // 任务申诉次数数据 interface TaskAppealQuota { id: string taskName: string agencyName: string remaining: number used: number requestStatus: RequestStatus requestTime?: string } // 模拟任务申诉次数数据 const mockTaskQuotas: TaskAppealQuota[] = [ { id: '1', taskName: '618美妆推广视频', agencyName: '星辰传媒', remaining: 1, used: 0, requestStatus: 'none', }, { id: '2', taskName: '双11护肤品种草', agencyName: '星辰传媒', remaining: 0, used: 1, requestStatus: 'pending', requestTime: '2024-02-05 14:30', }, { id: '3', taskName: '春节限定礼盒开箱', agencyName: '晨曦文化', remaining: 2, used: 0, requestStatus: 'approved', requestTime: '2024-02-04 10:15', }, { id: '4', taskName: '情人节香水测评', agencyName: '晨曦文化', remaining: 0, used: 1, requestStatus: 'rejected', requestTime: '2024-02-03 16:20', }, ] // 将 TaskResponse 映射为 TaskAppealQuota function mapTaskToQuota(task: TaskResponse): TaskAppealQuota { // Default quota is 1 per task const defaultQuota = 1 const remaining = Math.max(0, defaultQuota - task.appeal_count) // Determine request status based on task state let requestStatus: RequestStatus = 'none' if (task.is_appeal && task.appeal_count > 0) { requestStatus = 'pending' } return { id: task.id, taskName: task.name, agencyName: task.agency?.name || '未知代理商', remaining, used: task.appeal_count, requestStatus, } } // 状态标签组件 function StatusBadge({ status }: { status: RequestStatus }) { const config = { none: { label: '', icon: null, className: '' }, pending: { label: '申请中', icon: Clock, className: 'bg-accent-amber/15 text-accent-amber', }, approved: { label: '已同意', icon: CheckCircle, className: 'bg-accent-green/15 text-accent-green', }, rejected: { label: '已拒绝', icon: XCircle, className: 'bg-accent-coral/15 text-accent-coral', }, } const { label, icon: Icon, className } = config[status] if (status === 'none') return null return ( {Icon && } {label} ) } // 骨架屏组件 function QuotaSkeleton() { return (
) } // 任务卡片组件 function TaskQuotaCard({ task, onRequestIncrease, requesting, }: { task: TaskAppealQuota onRequestIncrease: (taskId: string) => void requesting: boolean }) { const canRequest = task.requestStatus === 'none' || task.requestStatus === 'rejected' return (
{/* 任务信息 */}

{task.taskName}

{task.agencyName}

{/* 申诉次数 */}
{task.remaining} 剩余次数
{task.used} 已使用
{/* 操作按钮 */}
{task.requestTime && ( {task.requestStatus === 'pending' ? '申请时间:' : '处理时间:'} {task.requestTime} )} {!task.requestTime && } {canRequest ? ( ) : task.requestStatus === 'pending' ? ( 等待代理商处理... ) : null}
) } export default function AppealQuotaPage() { const router = useRouter() const toast = useToast() const [tasks, setTasks] = useState([]) const [loading, setLoading] = useState(true) const [requestingTaskId, setRequestingTaskId] = useState(null) const loadQuotas = useCallback(async () => { if (USE_MOCK) { setTasks(mockTaskQuotas) setLoading(false) return } try { setLoading(true) const response = await api.listTasks(1, 100) const mapped = response.items.map(mapTaskToQuota) setTasks(mapped) } catch (err) { console.error('加载申诉次数失败:', err) toast.error('加载申诉次数信息失败,请稍后重试') } finally { setLoading(false) } }, [toast]) useEffect(() => { loadQuotas() }, [loadQuotas]) // 申请增加申诉次数 const handleRequestIncrease = async (taskId: string) => { if (USE_MOCK) { setTasks(prev => prev.map(task => task.id === taskId ? { ...task, requestStatus: 'pending' as RequestStatus, requestTime: new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }), } : task ) ) toast.success('申请已发送,等待代理商处理') return } try { setRequestingTaskId(taskId) await api.increaseAppealCount(taskId) toast.success('申请已发送,等待代理商处理') // Update local state optimistically setTasks(prev => prev.map(task => task.id === taskId ? { ...task, requestStatus: 'pending' as RequestStatus, requestTime: new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }), } : task ) ) } catch (err) { console.error('申请增加申诉次数失败:', err) toast.error('申请失败,请稍后重试') } finally { setRequestingTaskId(null) } } // 统计数据 const totalRemaining = tasks.reduce((sum, t) => sum + t.remaining, 0) const totalUsed = tasks.reduce((sum, t) => sum + t.used, 0) const pendingRequests = tasks.filter(t => t.requestStatus === 'pending').length return (
{/* 顶部栏 */}

申诉次数

查看各任务的申诉次数,可向代理商申请增加

{/* 统计卡片 */} {loading ? (
{[1, 2, 3].map(i => (
))}
) : (
{totalRemaining} 总剩余次数
{totalUsed} 总已使用
{pendingRequests} 待处理申请
)} {/* 规则说明 */}
申诉次数规则 每个任务初始有 1 次申诉机会,不同任务独立计算。如需更多次数,可点击“申请增加”向代理商发送请求,无需填写理由。代理商可增加的次数无上限。
{/* 任务列表 */}

任务申诉次数 {!loading && `(${tasks.length})`}

{loading ? ( <> ) : tasks.length > 0 ? ( tasks.map(task => ( )) ) : (

暂无任务数据

)}
) }