feat: 前端剩余页面全面对接后端 API(Phase 2 完成)
为品牌方端(8页)、代理商端(10页)、达人端(6页)共24个页面添加真实API调用: - 每页新增 USE_MOCK 条件分支,开发环境使用 mock 数据,生产环境调用真实 API - 添加 loading 骨架屏、error toast 提示、submitting 状态管理 - 数据映射:TaskResponse → 页面视图模型,处理类型差异 - 审核操作(通过/驳回/强制通过)对接 api.reviewScript/reviewVideo - Brief/规则/AI配置对接 api.getBrief/updateBrief/listForbiddenWords 等 - 申诉/历史/额度管理对接 api.listTasks + 状态过滤映射 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
54eaa54966
commit
a8be7bbca9
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
@@ -18,14 +18,47 @@ import {
|
||||
Download,
|
||||
File,
|
||||
Send,
|
||||
Image as ImageIcon
|
||||
Image as ImageIcon,
|
||||
Loader2
|
||||
} from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
import { USE_MOCK } from '@/contexts/AuthContext'
|
||||
import type { TaskResponse } from '@/types/task'
|
||||
|
||||
// 申诉状态类型
|
||||
type AppealStatus = 'pending' | 'processing' | 'approved' | 'rejected'
|
||||
|
||||
// 申诉详情类型
|
||||
interface AppealDetail {
|
||||
id: string
|
||||
taskId: string
|
||||
taskTitle: string
|
||||
creatorId: string
|
||||
creatorName: string
|
||||
creatorAvatar: string
|
||||
type: 'ai' | 'agency'
|
||||
contentType: 'script' | 'video'
|
||||
reason: string
|
||||
content: string
|
||||
status: AppealStatus
|
||||
createdAt: string
|
||||
appealCount: number
|
||||
attachments: { id: string; name: string; size: string; type: string }[]
|
||||
originalIssue: {
|
||||
type: string
|
||||
title: string
|
||||
description: string
|
||||
location: string
|
||||
}
|
||||
taskInfo: {
|
||||
projectName: string
|
||||
scriptFileName: string
|
||||
scriptFileSize: string
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟申诉详情数据
|
||||
const mockAppealDetail = {
|
||||
const mockAppealDetail: AppealDetail = {
|
||||
id: 'appeal-001',
|
||||
taskId: 'task-001',
|
||||
taskTitle: '夏日护肤推广脚本',
|
||||
@@ -38,6 +71,7 @@ const mockAppealDetail = {
|
||||
content: '脚本中提到的"某品牌"是泛指,并非特指竞品,AI系统可能误解了语境。我在脚本中使用的是泛化表述,并没有提及任何具体的竞品名称。请代理商重新审核此处,谢谢!',
|
||||
status: 'pending' as AppealStatus,
|
||||
createdAt: '2026-02-06 10:30',
|
||||
appealCount: 1,
|
||||
// 附件
|
||||
attachments: [
|
||||
{ id: 'att-001', name: '品牌授权证明.pdf', size: '1.2 MB', type: 'pdf' },
|
||||
@@ -58,6 +92,66 @@ const mockAppealDetail = {
|
||||
},
|
||||
}
|
||||
|
||||
// Derive a UI-compatible appeal detail from a TaskResponse
|
||||
function mapTaskToAppealDetail(task: TaskResponse) {
|
||||
const isVideoStage = task.stage.startsWith('video')
|
||||
const contentType: 'script' | 'video' = isVideoStage ? 'video' : 'script'
|
||||
const type: 'ai' | 'agency' = task.stage.includes('ai') ? 'ai' : 'agency'
|
||||
|
||||
let status: AppealStatus = 'pending'
|
||||
if (task.stage === 'completed') {
|
||||
status = 'approved'
|
||||
} else if (task.stage === 'rejected') {
|
||||
status = 'rejected'
|
||||
} else if (task.stage.includes('review')) {
|
||||
status = 'processing'
|
||||
}
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
}).replace(/\//g, '-')
|
||||
}
|
||||
|
||||
// Extract original issue from AI results if available
|
||||
const aiResult = isVideoStage ? task.video_ai_result : task.script_ai_result
|
||||
const agencyComment = isVideoStage ? task.video_agency_comment : task.script_agency_comment
|
||||
const originalIssueTitle = aiResult?.violations?.[0]?.type || agencyComment || '审核问题'
|
||||
const originalIssueDesc = aiResult?.violations?.[0]?.content || agencyComment || ''
|
||||
const originalIssueLocation = aiResult?.violations?.[0]?.source || ''
|
||||
|
||||
return {
|
||||
id: task.id,
|
||||
taskId: task.id,
|
||||
taskTitle: task.name,
|
||||
creatorId: task.creator.id,
|
||||
creatorName: task.creator.name,
|
||||
creatorAvatar: task.creator.name.charAt(0),
|
||||
type,
|
||||
contentType,
|
||||
reason: task.appeal_reason || '申诉',
|
||||
content: task.appeal_reason || '',
|
||||
status,
|
||||
createdAt: formatDate(task.updated_at),
|
||||
appealCount: task.appeal_count,
|
||||
attachments: [] as { id: string; name: string; size: string; type: string }[],
|
||||
originalIssue: {
|
||||
type: type === 'ai' ? 'ai' : 'agency',
|
||||
title: originalIssueTitle,
|
||||
description: originalIssueDesc,
|
||||
location: originalIssueLocation,
|
||||
},
|
||||
taskInfo: {
|
||||
projectName: task.project.name,
|
||||
scriptFileName: isVideoStage
|
||||
? (task.video_file_name || '视频文件')
|
||||
: (task.script_file_name || '脚本文件'),
|
||||
scriptFileSize: '-',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 状态配置
|
||||
const statusConfig: Record<AppealStatus, { label: string; color: string; bgColor: string; icon: React.ElementType }> = {
|
||||
pending: { label: '待处理', color: 'text-accent-amber', bgColor: 'bg-accent-amber/15', icon: Clock },
|
||||
@@ -70,9 +164,35 @@ export default function AgencyAppealDetailPage() {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const params = useParams()
|
||||
const [appeal] = useState(mockAppealDetail)
|
||||
const taskId = params.id as string
|
||||
|
||||
const [appeal, setAppeal] = useState(mockAppealDetail)
|
||||
const [replyContent, setReplyContent] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const fetchAppeal = useCallback(async () => {
|
||||
if (USE_MOCK) {
|
||||
setAppeal(mockAppealDetail)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const task = await api.getTask(taskId)
|
||||
setAppeal(mapTaskToAppealDetail(task))
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch appeal detail:', err)
|
||||
toast.error('加载申诉详情失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [taskId, toast])
|
||||
|
||||
useEffect(() => {
|
||||
fetchAppeal()
|
||||
}, [fetchAppeal])
|
||||
|
||||
const status = statusConfig[appeal.status]
|
||||
const StatusIcon = status.icon
|
||||
@@ -83,10 +203,26 @@ export default function AgencyAppealDetailPage() {
|
||||
return
|
||||
}
|
||||
setIsSubmitting(true)
|
||||
// 模拟提交
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
toast.success('申诉已通过')
|
||||
router.push('/agency/appeals')
|
||||
|
||||
try {
|
||||
if (USE_MOCK) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
} else {
|
||||
// Determine if this is script or video review based on the appeal's content type
|
||||
const isVideo = appeal.contentType === 'video'
|
||||
if (isVideo) {
|
||||
await api.reviewVideo(taskId, { action: 'pass', comment: replyContent })
|
||||
} else {
|
||||
await api.reviewScript(taskId, { action: 'pass', comment: replyContent })
|
||||
}
|
||||
}
|
||||
toast.success('申诉已通过')
|
||||
router.push('/agency/appeals')
|
||||
} catch (err) {
|
||||
console.error('Failed to approve appeal:', err)
|
||||
toast.error('操作失败,请重试')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReject = async () => {
|
||||
@@ -95,10 +231,34 @@ export default function AgencyAppealDetailPage() {
|
||||
return
|
||||
}
|
||||
setIsSubmitting(true)
|
||||
// 模拟提交
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
toast.success('申诉已驳回')
|
||||
router.push('/agency/appeals')
|
||||
|
||||
try {
|
||||
if (USE_MOCK) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
} else {
|
||||
const isVideo = appeal.contentType === 'video'
|
||||
if (isVideo) {
|
||||
await api.reviewVideo(taskId, { action: 'reject', comment: replyContent })
|
||||
} else {
|
||||
await api.reviewScript(taskId, { action: 'reject', comment: replyContent })
|
||||
}
|
||||
}
|
||||
toast.success('申诉已驳回')
|
||||
router.push('/agency/appeals')
|
||||
} catch (err) {
|
||||
console.error('Failed to reject appeal:', err)
|
||||
toast.error('操作失败,请重试')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-text-tertiary">
|
||||
<Loader2 size={32} className="animate-spin mb-4" />
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -186,7 +346,9 @@ export default function AgencyAppealDetailPage() {
|
||||
<span className="font-medium text-text-primary">{appeal.originalIssue.title}</span>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary">{appeal.originalIssue.description}</p>
|
||||
<p className="text-xs text-text-tertiary mt-2">位置: {appeal.originalIssue.location}</p>
|
||||
{appeal.originalIssue.location && (
|
||||
<p className="text-xs text-text-tertiary mt-2">位置: {appeal.originalIssue.location}</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -209,6 +371,10 @@ export default function AgencyAppealDetailPage() {
|
||||
<span className="text-sm text-text-tertiary">详细说明</span>
|
||||
<p className="text-text-primary mt-1 leading-relaxed">{appeal.content}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-text-tertiary">申诉次数</span>
|
||||
<p className="text-text-primary mt-1">{appeal.appealCount} 次</p>
|
||||
</div>
|
||||
|
||||
{/* 附件 */}
|
||||
{appeal.attachments.length > 0 && (
|
||||
@@ -303,7 +469,7 @@ export default function AgencyAppealDetailPage() {
|
||||
onClick={handleApprove}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<CheckCircle size={16} />
|
||||
{isSubmitting ? <Loader2 size={16} className="animate-spin" /> : <CheckCircle size={16} />}
|
||||
通过申诉
|
||||
</Button>
|
||||
<Button
|
||||
@@ -312,7 +478,7 @@ export default function AgencyAppealDetailPage() {
|
||||
onClick={handleReject}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<XCircle size={16} />
|
||||
{isSubmitting ? <Loader2 size={16} className="animate-spin" /> : <XCircle size={16} />}
|
||||
驳回申诉
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user