fix: 修复前端代码质量问题
- 创建 Toast 通知组件,替换所有 alert() 调用 - 修复 useReview hook 内存泄漏(setInterval 清理) - 移除所有 console.error 和 console.log 语句 - 为复制操作失败添加用户友好的 toast 提示 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a5a005db0c
commit
37ac749071
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import {
|
||||
@ -67,6 +68,7 @@ const statusConfig: Record<AppealStatus, { label: string; color: string; bgColor
|
||||
|
||||
export default function AgencyAppealDetailPage() {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const params = useParams()
|
||||
const [appeal] = useState(mockAppealDetail)
|
||||
const [replyContent, setReplyContent] = useState('')
|
||||
@ -77,25 +79,25 @@ export default function AgencyAppealDetailPage() {
|
||||
|
||||
const handleApprove = async () => {
|
||||
if (!replyContent.trim()) {
|
||||
alert('请填写处理意见')
|
||||
toast.error('请填写处理意见')
|
||||
return
|
||||
}
|
||||
setIsSubmitting(true)
|
||||
// 模拟提交
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
alert('申诉已通过')
|
||||
toast.success('申诉已通过')
|
||||
router.push('/agency/appeals')
|
||||
}
|
||||
|
||||
const handleReject = async () => {
|
||||
if (!replyContent.trim()) {
|
||||
alert('请填写驳回原因')
|
||||
toast.error('请填写驳回原因')
|
||||
return
|
||||
}
|
||||
setIsSubmitting(true)
|
||||
// 模拟提交
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
alert('申诉已驳回')
|
||||
toast.success('申诉已驳回')
|
||||
router.push('/agency/appeals')
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
@ -127,6 +128,7 @@ const platformRules = {
|
||||
export default function BriefConfigPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const toast = useToast()
|
||||
|
||||
// 品牌方 Brief(只读)
|
||||
const [brandBrief] = useState(mockBrandBrief)
|
||||
@ -151,7 +153,7 @@ export default function BriefConfigPage() {
|
||||
|
||||
// 下载文件
|
||||
const handleDownload = (file: BriefFile) => {
|
||||
alert(`下载文件: ${file.name}`)
|
||||
toast.info(`下载文件: ${file.name}`)
|
||||
}
|
||||
|
||||
// 预览文件
|
||||
@ -164,7 +166,7 @@ export default function BriefConfigPage() {
|
||||
setIsExporting(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
setIsExporting(false)
|
||||
alert('平台规则文档已导出!')
|
||||
toast.success('平台规则文档已导出!')
|
||||
}
|
||||
|
||||
// AI 解析
|
||||
@ -172,7 +174,7 @@ export default function BriefConfigPage() {
|
||||
setIsAIParsing(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
setIsAIParsing(false)
|
||||
alert('AI 解析完成!')
|
||||
toast.success('AI 解析完成!')
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
@ -180,7 +182,7 @@ export default function BriefConfigPage() {
|
||||
setIsSaving(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
alert('配置已保存!')
|
||||
toast.success('配置已保存!')
|
||||
}
|
||||
|
||||
// 卖点操作
|
||||
@ -243,7 +245,7 @@ export default function BriefConfigPage() {
|
||||
agencyFiles: [...prev.agencyFiles, newFile]
|
||||
}))
|
||||
setIsUploading(false)
|
||||
alert('文档上传成功!')
|
||||
toast.success('文档上传成功!')
|
||||
}
|
||||
|
||||
const removeAgencyFile = (id: string) => {
|
||||
@ -258,7 +260,7 @@ export default function BriefConfigPage() {
|
||||
}
|
||||
|
||||
const handleDownloadAgencyFile = (file: AgencyFile) => {
|
||||
alert(`下载文件: ${file.name}`)
|
||||
toast.info(`下载文件: ${file.name}`)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
@ -165,6 +166,7 @@ function StageTag({ stage }: { stage: TaskStage }) {
|
||||
}
|
||||
|
||||
export default function AgencyCreatorsPage() {
|
||||
const toast = useToast()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [showInviteModal, setShowInviteModal] = useState(false)
|
||||
const [inviteCreatorId, setInviteCreatorId] = useState('')
|
||||
@ -299,7 +301,7 @@ export default function AgencyCreatorsPage() {
|
||||
const handleConfirmAssign = () => {
|
||||
if (assignModal.creator && selectedProject) {
|
||||
const project = mockProjects.find(p => p.id === selectedProject)
|
||||
alert(`已将达人「${assignModal.creator.name}」分配到项目「${project?.name}」`)
|
||||
toast.success(`已将达人「${assignModal.creator.name}」分配到项目「${project?.name}」`)
|
||||
}
|
||||
setAssignModal({ open: false, creator: null })
|
||||
setSelectedProject('')
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
@ -120,6 +121,7 @@ const mockCurrentCompany = {
|
||||
|
||||
export default function AgencyCompanyPage() {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [formData, setFormData] = useState(mockCurrentCompany)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
@ -182,7 +184,7 @@ export default function AgencyCompanyPage() {
|
||||
// 提交验证
|
||||
const handleSubmitVerify = async () => {
|
||||
if (!verifyCode.trim()) {
|
||||
alert('请输入验证信息')
|
||||
toast.error('请输入验证信息')
|
||||
return
|
||||
}
|
||||
|
||||
@ -196,7 +198,7 @@ export default function AgencyCompanyPage() {
|
||||
setVerifyMethod(null)
|
||||
setVerifyStep(1)
|
||||
setVerifyCode('')
|
||||
alert('企业认证成功!')
|
||||
toast.success('企业认证成功')
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
@ -204,7 +206,7 @@ export default function AgencyCompanyPage() {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
setIsEditing(false)
|
||||
alert('公司信息已保存')
|
||||
toast.success('公司信息已保存')
|
||||
}
|
||||
|
||||
// 认证状态显示
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
@ -26,6 +27,7 @@ const mockUserData = {
|
||||
|
||||
export default function AgencyProfileEditPage() {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const [formData, setFormData] = useState(mockUserData)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
@ -35,8 +37,8 @@ export default function AgencyProfileEditPage() {
|
||||
await navigator.clipboard.writeText(formData.agencyId)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
} catch {
|
||||
toast.error('复制失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,7 +46,7 @@ export default function AgencyProfileEditPage() {
|
||||
setIsSaving(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
alert('个人信息已保存')
|
||||
toast.success('个人信息已保存')
|
||||
router.back()
|
||||
}
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
FileCheck
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
|
||||
// 代理商数据
|
||||
const mockAgency = {
|
||||
@ -87,6 +88,7 @@ const menuItems = [
|
||||
|
||||
// 代理商卡片组件
|
||||
function AgencyCard() {
|
||||
const toast = useToast()
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
// 复制代理商ID
|
||||
@ -95,8 +97,8 @@ function AgencyCard() {
|
||||
await navigator.clipboard.writeText(mockAgency.agencyId)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
} catch {
|
||||
toast.error('复制失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
@ -179,6 +180,7 @@ export default function AgencyReportsPage() {
|
||||
const [exportFormat, setExportFormat] = useState<'csv' | 'excel' | 'pdf'>('excel')
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const [exportSuccess, setExportSuccess] = useState(false)
|
||||
const toast = useToast()
|
||||
|
||||
const currentData = mockDataByRange[dateRange]
|
||||
|
||||
@ -199,9 +201,9 @@ export default function AgencyReportsPage() {
|
||||
downloadFile(csvContent, `${fileName}.csv`, 'text/csv')
|
||||
} else if (exportFormat === 'excel') {
|
||||
// 实际项目中会使用 xlsx 库
|
||||
alert(`Excel 文件「${fileName}.xlsx」已开始下载`)
|
||||
toast.info(`Excel 文件「${fileName}.xlsx」已开始下载`)
|
||||
} else {
|
||||
alert(`PDF 文件「${fileName}.pdf」已开始下载`)
|
||||
toast.info(`PDF 文件「${fileName}.pdf」已开始下载`)
|
||||
}
|
||||
|
||||
setIsExporting(false)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { ArrowLeft, Play, Pause, AlertTriangle, Shield, Radio } from 'lucide-react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
@ -85,6 +86,7 @@ function formatTimestamp(seconds: number): string {
|
||||
export default function ReviewPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const toast = useToast()
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [showApproveModal, setShowApproveModal] = useState(false)
|
||||
const [showRejectModal, setShowRejectModal] = useState(false)
|
||||
@ -103,7 +105,7 @@ export default function ReviewPage() {
|
||||
|
||||
const handleReject = () => {
|
||||
if (!rejectReason.trim()) {
|
||||
alert('请填写驳回原因')
|
||||
toast.error('请填写驳回原因')
|
||||
return
|
||||
}
|
||||
setShowRejectModal(false)
|
||||
@ -112,7 +114,7 @@ export default function ReviewPage() {
|
||||
|
||||
const handleForcePass = () => {
|
||||
if (!forcePassReason.trim()) {
|
||||
alert('请填写强制通过原因')
|
||||
toast.error('请填写强制通过原因')
|
||||
return
|
||||
}
|
||||
setShowForcePassModal(false)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { SuccessTag, PendingTag, WarningTag, ErrorTag } from '@/components/ui/Tag'
|
||||
@ -165,13 +166,13 @@ function ScoreTag({ score }: { score: number }) {
|
||||
type ScriptTask = typeof mockScriptTasks[0]
|
||||
type VideoTask = typeof mockVideoTasks[0]
|
||||
|
||||
function ScriptTaskCard({ task, onPreview }: { task: ScriptTask; onPreview: (task: ScriptTask) => void }) {
|
||||
function ScriptTaskCard({ task, onPreview, toast }: { task: ScriptTask; onPreview: (task: ScriptTask) => void; toast: ReturnType<typeof useToast> }) {
|
||||
const riskConfig = riskLevelConfig[task.riskLevel]
|
||||
const platform = getPlatformInfo(task.platform)
|
||||
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
alert(`下载文件: ${task.fileName}`)
|
||||
toast.info(`下载文件: ${task.fileName}`)
|
||||
}
|
||||
|
||||
const handlePreview = (e: React.MouseEvent) => {
|
||||
@ -261,13 +262,13 @@ function ScriptTaskCard({ task, onPreview }: { task: ScriptTask; onPreview: (tas
|
||||
)
|
||||
}
|
||||
|
||||
function VideoTaskCard({ task, onPreview }: { task: VideoTask; onPreview: (task: VideoTask) => void }) {
|
||||
function VideoTaskCard({ task, onPreview, toast }: { task: VideoTask; onPreview: (task: VideoTask) => void; toast: ReturnType<typeof useToast> }) {
|
||||
const riskConfig = riskLevelConfig[task.riskLevel]
|
||||
const platform = getPlatformInfo(task.platform)
|
||||
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
alert(`下载文件: ${task.fileName}`)
|
||||
toast.info(`下载文件: ${task.fileName}`)
|
||||
}
|
||||
|
||||
const handlePreview = (e: React.MouseEvent) => {
|
||||
@ -362,6 +363,7 @@ export default function AgencyReviewListPage() {
|
||||
const [activeTab, setActiveTab] = useState<'all' | 'script' | 'video'>('all')
|
||||
const [previewScript, setPreviewScript] = useState<ScriptTask | null>(null)
|
||||
const [previewVideo, setPreviewVideo] = useState<VideoTask | null>(null)
|
||||
const toast = useToast()
|
||||
|
||||
const filteredScripts = mockScriptTasks.filter(task =>
|
||||
task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
@ -462,7 +464,7 @@ export default function AgencyReviewListPage() {
|
||||
<CardContent className="space-y-3">
|
||||
{filteredScripts.length > 0 ? (
|
||||
filteredScripts.map((task) => (
|
||||
<ScriptTaskCard key={task.id} task={task} onPreview={setPreviewScript} />
|
||||
<ScriptTaskCard key={task.id} task={task} onPreview={setPreviewScript} toast={toast} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-text-tertiary">
|
||||
@ -489,7 +491,7 @@ export default function AgencyReviewListPage() {
|
||||
<CardContent className="space-y-3">
|
||||
{filteredVideos.length > 0 ? (
|
||||
filteredVideos.map((task) => (
|
||||
<VideoTaskCard key={task.id} task={task} onPreview={setPreviewVideo} />
|
||||
<VideoTaskCard key={task.id} task={task} onPreview={setPreviewVideo} toast={toast} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-text-tertiary">
|
||||
@ -536,7 +538,7 @@ export default function AgencyReviewListPage() {
|
||||
<Button variant="secondary" onClick={() => setPreviewScript(null)}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button onClick={() => alert(`下载文件: ${previewScript?.fileName}`)}>
|
||||
<Button onClick={() => toast.info(`下载文件: ${previewScript?.fileName}`)}>
|
||||
<Download size={16} />
|
||||
下载
|
||||
</Button>
|
||||
@ -581,7 +583,7 @@ export default function AgencyReviewListPage() {
|
||||
<Button variant="secondary" onClick={() => setPreviewVideo(null)}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button onClick={() => alert(`下载文件: ${previewVideo?.fileName}`)}>
|
||||
<Button onClick={() => toast.info(`下载文件: ${previewVideo?.fileName}`)}>
|
||||
<Download size={16} />
|
||||
下载
|
||||
</Button>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Modal, ConfirmModal } from '@/components/ui/Modal'
|
||||
@ -90,6 +91,7 @@ function ReviewProgressBar({ taskStatus }: { taskStatus: string }) {
|
||||
|
||||
export default function AgencyScriptReviewPage() {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const params = useParams()
|
||||
const [showApproveModal, setShowApproveModal] = useState(false)
|
||||
const [showRejectModal, setShowRejectModal] = useState(false)
|
||||
@ -103,27 +105,27 @@ export default function AgencyScriptReviewPage() {
|
||||
|
||||
const handleApprove = () => {
|
||||
setShowApproveModal(false)
|
||||
alert('已提交品牌方终审!')
|
||||
toast.success('已提交品牌方终审')
|
||||
router.push('/agency/review')
|
||||
}
|
||||
|
||||
const handleReject = () => {
|
||||
if (!rejectReason.trim()) {
|
||||
alert('请填写驳回原因')
|
||||
toast.error('请填写驳回原因')
|
||||
return
|
||||
}
|
||||
setShowRejectModal(false)
|
||||
alert('已驳回')
|
||||
toast.success('已驳回')
|
||||
router.push('/agency/review')
|
||||
}
|
||||
|
||||
const handleForcePass = () => {
|
||||
if (!forcePassReason.trim()) {
|
||||
alert('请填写强制通过原因')
|
||||
toast.error('请填写强制通过原因')
|
||||
return
|
||||
}
|
||||
setShowForcePassModal(false)
|
||||
alert('已强制通过并提交品牌方终审!')
|
||||
toast.success('已强制通过并提交品牌方终审')
|
||||
router.push('/agency/review')
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Modal, ConfirmModal } from '@/components/ui/Modal'
|
||||
@ -114,6 +115,7 @@ function RiskLevelTag({ level }: { level: string }) {
|
||||
|
||||
export default function AgencyVideoReviewPage() {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const params = useParams()
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [showApproveModal, setShowApproveModal] = useState(false)
|
||||
@ -130,27 +132,27 @@ export default function AgencyVideoReviewPage() {
|
||||
|
||||
const handleApprove = () => {
|
||||
setShowApproveModal(false)
|
||||
alert('已提交品牌方终审!')
|
||||
toast.success('已提交品牌方终审')
|
||||
router.push('/agency/review')
|
||||
}
|
||||
|
||||
const handleReject = () => {
|
||||
if (!rejectReason.trim()) {
|
||||
alert('请填写驳回原因')
|
||||
toast.error('请填写驳回原因')
|
||||
return
|
||||
}
|
||||
setShowRejectModal(false)
|
||||
alert('已驳回')
|
||||
toast.success('已驳回')
|
||||
router.push('/agency/review')
|
||||
}
|
||||
|
||||
const handleForcePass = () => {
|
||||
if (!forcePassReason.trim()) {
|
||||
alert('请填写强制通过原因')
|
||||
toast.error('请填写强制通过原因')
|
||||
return
|
||||
}
|
||||
setShowForcePassModal(false)
|
||||
alert('已强制通过并提交品牌方终审!')
|
||||
toast.success('已强制通过并提交品牌方终审')
|
||||
router.push('/agency/review')
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
@ -20,6 +21,7 @@ import {
|
||||
|
||||
export default function AgencyAccountSettingsPage() {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const [showOldPassword, setShowOldPassword] = useState(false)
|
||||
const [showNewPassword, setShowNewPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
@ -39,21 +41,21 @@ export default function AgencyAccountSettingsPage() {
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
if (!passwordForm.oldPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) {
|
||||
alert('请填写完整密码信息')
|
||||
toast.error('请填写完整密码信息')
|
||||
return
|
||||
}
|
||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||
alert('两次输入的新密码不一致')
|
||||
toast.error('两次输入的新密码不一致')
|
||||
return
|
||||
}
|
||||
if (passwordForm.newPassword.length < 8) {
|
||||
alert('新密码长度不能少于8位')
|
||||
toast.error('新密码长度不能少于8位')
|
||||
return
|
||||
}
|
||||
setIsSaving(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
alert('密码修改成功')
|
||||
toast.success('密码修改成功')
|
||||
setPasswordForm({ oldPassword: '', newPassword: '', confirmPassword: '' })
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import {
|
||||
@ -102,6 +103,7 @@ function Toggle({ checked, onChange }: { checked: boolean; onChange: (checked: b
|
||||
|
||||
export default function AgencyNotificationSettingsPage() {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const [settings, setSettings] = useState(initialSettings)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
@ -117,7 +119,7 @@ export default function AgencyNotificationSettingsPage() {
|
||||
setIsSaving(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
alert('通知设置已保存')
|
||||
toast.success('通知设置已保存')
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
import { SuccessTag, PendingTag, WarningTag } from '@/components/ui/Tag'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
@ -110,6 +111,7 @@ function StatusTag({ status }: { status: string }) {
|
||||
}
|
||||
|
||||
export default function AgenciesManagePage() {
|
||||
const toast = useToast()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [agencies, setAgencies] = useState<Agency[]>(initialAgencies)
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||
@ -231,7 +233,7 @@ export default function AgenciesManagePage() {
|
||||
.filter(p => selectedProjects.includes(p.id))
|
||||
.map(p => p.name)
|
||||
.join('、')
|
||||
alert(`已将代理商「${assignModal.agency.name}」分配到项目「${projectNames}」`)
|
||||
toast.success(`已将代理商「${assignModal.agency.name}」分配到项目「${projectNames}」`)
|
||||
}
|
||||
setAssignModal({ open: false, agency: null })
|
||||
setSelectedProjects([])
|
||||
|
||||
@ -6,6 +6,7 @@ import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { Select } from '@/components/ui/Select'
|
||||
import { SuccessTag, ErrorTag, PendingTag } from '@/components/ui/Tag'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import {
|
||||
Bot,
|
||||
Eye,
|
||||
@ -68,6 +69,7 @@ type TestResult = {
|
||||
}
|
||||
|
||||
export default function AIConfigPage() {
|
||||
const toast = useToast()
|
||||
const [provider, setProvider] = useState('oneapi')
|
||||
const [baseUrl, setBaseUrl] = useState('https://oneapi.intelligrow.cn')
|
||||
const [apiKey, setApiKey] = useState('')
|
||||
@ -133,7 +135,7 @@ export default function AIConfigPage() {
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
alert('配置已保存')
|
||||
toast.success('配置已保存')
|
||||
}
|
||||
|
||||
const getTestStatusIcon = (status: string) => {
|
||||
|
||||
@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
|
||||
import { ArrowLeft, Check, X, CheckSquare, Video, Clock } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getPlatformInfo } from '@/lib/platforms'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
|
||||
// 模拟待审核内容列表
|
||||
const mockReviewItems = [
|
||||
@ -99,6 +100,7 @@ function ReviewProgressBar({ currentStep }: { currentStep: number }) {
|
||||
|
||||
export default function FinalReviewPage() {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const [selectedItem, setSelectedItem] = useState(mockReviewItems[0])
|
||||
const [feedback, setFeedback] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
@ -108,19 +110,19 @@ export default function FinalReviewPage() {
|
||||
setIsSubmitting(true)
|
||||
// 模拟提交
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
alert('已通过审核')
|
||||
toast.success('已通过审核')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
const handleReject = async () => {
|
||||
if (!feedback.trim()) {
|
||||
alert('请填写驳回原因')
|
||||
toast.error('请填写驳回原因')
|
||||
return
|
||||
}
|
||||
setIsSubmitting(true)
|
||||
// 模拟提交
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
alert('已驳回')
|
||||
toast.success('已驳回')
|
||||
setIsSubmitting(false)
|
||||
setFeedback('')
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
@ -82,6 +83,7 @@ const strictnessOptions = [
|
||||
export default function ProjectConfigPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const toast = useToast()
|
||||
const projectId = params.id as string
|
||||
|
||||
const [brief, setBrief] = useState(mockData.brief)
|
||||
@ -100,7 +102,7 @@ export default function ProjectConfigPage() {
|
||||
setIsSaving(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
alert('配置已保存')
|
||||
toast.success('配置已保存')
|
||||
}
|
||||
|
||||
const addRequirement = () => {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
@ -40,6 +41,7 @@ const mockAgencies = [
|
||||
|
||||
export default function CreateProjectPage() {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const [projectName, setProjectName] = useState('')
|
||||
const [deadline, setDeadline] = useState('')
|
||||
const [briefFile, setBriefFile] = useState<File | null>(null)
|
||||
@ -73,14 +75,14 @@ export default function CreateProjectPage() {
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!projectName.trim() || !deadline || !briefFile || selectedAgencies.length === 0 || !selectedPlatform) {
|
||||
alert('请填写完整信息')
|
||||
toast.error('请填写完整信息')
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
// 模拟提交
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
alert('项目创建成功!')
|
||||
toast.success('项目创建成功!')
|
||||
router.push('/brand')
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import Link from 'next/link'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { SuccessTag, PendingTag, WarningTag, ErrorTag } from '@/components/ui/Tag'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import {
|
||||
FileText,
|
||||
Video,
|
||||
@ -129,6 +130,7 @@ function TaskCard({
|
||||
type: 'script' | 'video'
|
||||
onPreview: (task: ScriptTask | VideoTask, type: 'script' | 'video') => void
|
||||
}) {
|
||||
const toast = useToast()
|
||||
const href = type === 'script' ? `/brand/review/script/${task.id}` : `/brand/review/video/${task.id}`
|
||||
const platform = getPlatformInfo(task.platform)
|
||||
|
||||
@ -141,7 +143,7 @@ function TaskCard({
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
alert(`下载文件: ${task.fileName}`)
|
||||
toast.info(`下载文件: ${task.fileName}`)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -243,6 +245,7 @@ function TaskCard({
|
||||
}
|
||||
|
||||
export default function BrandReviewListPage() {
|
||||
const toast = useToast()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [activeTab, setActiveTab] = useState<'all' | 'script' | 'video'>('all')
|
||||
const [previewTask, setPreviewTask] = useState<{ task: ScriptTask | VideoTask; type: 'script' | 'video' } | null>(null)
|
||||
@ -445,7 +448,7 @@ export default function BrandReviewListPage() {
|
||||
<Button variant="secondary" onClick={() => setPreviewTask(null)}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button onClick={() => alert(`下载文件: ${previewTask?.task.fileName}`)}>
|
||||
<Button onClick={() => toast.info(`下载文件: ${previewTask?.task.fileName}`)}>
|
||||
<Download size={16} />
|
||||
下载
|
||||
</Button>
|
||||
|
||||
@ -7,6 +7,7 @@ import { Button } from '@/components/ui/Button'
|
||||
import { Modal, ConfirmModal } from '@/components/ui/Modal'
|
||||
import { SuccessTag, WarningTag, ErrorTag, PendingTag } from '@/components/ui/Tag'
|
||||
import { ReviewSteps, getBrandReviewSteps } from '@/components/ui/ReviewSteps'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import {
|
||||
ArrowLeft,
|
||||
FileText,
|
||||
@ -99,6 +100,7 @@ function ReviewProgressBar({ taskStatus }: { taskStatus: string }) {
|
||||
export default function BrandScriptReviewPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const toast = useToast()
|
||||
const [showApproveModal, setShowApproveModal] = useState(false)
|
||||
const [showRejectModal, setShowRejectModal] = useState(false)
|
||||
const [rejectReason, setRejectReason] = useState('')
|
||||
@ -109,17 +111,17 @@ export default function BrandScriptReviewPage() {
|
||||
|
||||
const handleApprove = () => {
|
||||
setShowApproveModal(false)
|
||||
alert('审核通过!')
|
||||
toast.success('审核通过')
|
||||
router.push('/brand/review')
|
||||
}
|
||||
|
||||
const handleReject = () => {
|
||||
if (!rejectReason.trim()) {
|
||||
alert('请填写驳回原因')
|
||||
toast.error('请填写驳回原因')
|
||||
return
|
||||
}
|
||||
setShowRejectModal(false)
|
||||
alert('已驳回')
|
||||
toast.success('已驳回')
|
||||
router.push('/brand/review')
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Modal, ConfirmModal } from '@/components/ui/Modal'
|
||||
@ -123,6 +124,7 @@ function RiskLevelTag({ level }: { level: string }) {
|
||||
export default function BrandVideoReviewPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const toast = useToast()
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [showApproveModal, setShowApproveModal] = useState(false)
|
||||
const [showRejectModal, setShowRejectModal] = useState(false)
|
||||
@ -135,17 +137,17 @@ export default function BrandVideoReviewPage() {
|
||||
|
||||
const handleApprove = () => {
|
||||
setShowApproveModal(false)
|
||||
alert('审核通过!')
|
||||
toast.success('审核通过!')
|
||||
router.push('/brand/review')
|
||||
}
|
||||
|
||||
const handleReject = () => {
|
||||
if (!rejectReason.trim()) {
|
||||
alert('请填写驳回原因')
|
||||
toast.error('请填写驳回原因')
|
||||
return
|
||||
}
|
||||
setShowRejectModal(false)
|
||||
alert('已驳回')
|
||||
toast.success('已驳回')
|
||||
router.push('/brand/review')
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import {
|
||||
Bell,
|
||||
Shield,
|
||||
@ -32,6 +33,7 @@ import {
|
||||
|
||||
export default function BrandSettingsPage() {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const [notifications, setNotifications] = useState({
|
||||
email: true,
|
||||
push: true,
|
||||
@ -95,7 +97,7 @@ export default function BrandSettingsPage() {
|
||||
]
|
||||
|
||||
const handleSave = () => {
|
||||
alert('设置已保存')
|
||||
toast.success('设置已保存')
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
@ -105,10 +107,10 @@ export default function BrandSettingsPage() {
|
||||
|
||||
const handleChangePassword = () => {
|
||||
if (passwordForm.new !== passwordForm.confirm) {
|
||||
alert('两次输入的密码不一致')
|
||||
toast.error('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
alert('密码修改成功')
|
||||
toast.success('密码修改成功')
|
||||
setShowPasswordModal(false)
|
||||
setPasswordForm({ current: '', new: '', confirm: '' })
|
||||
}
|
||||
@ -116,18 +118,18 @@ export default function BrandSettingsPage() {
|
||||
const handleEnable2FA = () => {
|
||||
setTwoFAEnabled(true)
|
||||
setShow2FAModal(false)
|
||||
alert('双因素认证已启用')
|
||||
toast.success('双因素认证已启用')
|
||||
}
|
||||
|
||||
const handleChangeEmail = () => {
|
||||
alert(`邮箱已更新为 ${newEmail}`)
|
||||
toast.success(`邮箱已更新为 ${newEmail}`)
|
||||
setShowEmailModal(false)
|
||||
setNewEmail('')
|
||||
setEmailCode('')
|
||||
}
|
||||
|
||||
const handleChangePhone = () => {
|
||||
alert(`手机号已更新为 ${newPhone}`)
|
||||
toast.success(`手机号已更新为 ${newPhone}`)
|
||||
setShowPhoneModal(false)
|
||||
setNewPhone('')
|
||||
setPhoneCode('')
|
||||
@ -139,11 +141,11 @@ export default function BrandSettingsPage() {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
setIsExporting(false)
|
||||
setShowExportModal(false)
|
||||
alert('导出任务已创建,完成后将通知您下载')
|
||||
toast.info('导出任务已创建,完成后将通知您下载')
|
||||
}
|
||||
|
||||
const handleRemoveDevice = (deviceId: string) => {
|
||||
alert('已移除该设备的登录状态')
|
||||
toast.success('已移除该设备的登录状态')
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -7,6 +7,7 @@ import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
|
||||
// 模拟用户数据
|
||||
const mockUser = {
|
||||
@ -21,6 +22,7 @@ const mockUser = {
|
||||
|
||||
export default function ProfileEditPage() {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [idCopied, setIdCopied] = useState(false)
|
||||
const [formData, setFormData] = useState({
|
||||
@ -37,8 +39,8 @@ export default function ProfileEditPage() {
|
||||
await navigator.clipboard.writeText(mockUser.creatorId)
|
||||
setIdCopied(true)
|
||||
setTimeout(() => setIdCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
} catch {
|
||||
toast.error('复制失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
|
||||
// 用户数据
|
||||
const mockUser = {
|
||||
@ -84,6 +85,7 @@ const menuItems = [
|
||||
|
||||
// 用户卡片组件
|
||||
function UserCard() {
|
||||
const toast = useToast()
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
// 复制达人ID
|
||||
@ -92,8 +94,8 @@ function UserCard() {
|
||||
await navigator.clipboard.writeText(mockUser.creatorId)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
} catch {
|
||||
toast.error('复制失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import {
|
||||
ArrowLeft,
|
||||
FileText,
|
||||
@ -71,14 +72,15 @@ const mockAgencyBrief = {
|
||||
export default function TaskBriefPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const toast = useToast()
|
||||
const [previewFile, setPreviewFile] = useState<AgencyBriefFile | null>(null)
|
||||
|
||||
const handleDownload = (file: AgencyBriefFile) => {
|
||||
alert(`下载文件: ${file.name}`)
|
||||
toast.info(`下载文件: ${file.name}`)
|
||||
}
|
||||
|
||||
const handleDownloadAll = () => {
|
||||
alert('下载全部文件')
|
||||
toast.info('下载全部文件')
|
||||
}
|
||||
|
||||
const requiredPoints = mockAgencyBrief.sellingPoints.filter(sp => sp.required)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import {
|
||||
Upload, Check, X, Folder, Bell, MessageCircle,
|
||||
XCircle, CheckCircle, Loader2, Scan, ArrowLeft,
|
||||
@ -389,12 +390,12 @@ function ReviewProgressBar({ task }: { task: TaskData }) {
|
||||
}
|
||||
|
||||
// Brief文档查看组件
|
||||
function AgencyBriefSection() {
|
||||
function AgencyBriefSection({ toast }: { toast: ReturnType<typeof useToast> }) {
|
||||
const [isExpanded, setIsExpanded] = useState(true)
|
||||
const [previewFile, setPreviewFile] = useState<AgencyBriefFile | null>(null)
|
||||
|
||||
const handleDownload = (file: AgencyBriefFile) => {
|
||||
alert(`下载文件: ${file.name}`)
|
||||
toast.info(`下载文件: ${file.name}`)
|
||||
}
|
||||
|
||||
const requiredPoints = mockAgencyBrief.sellingPoints.filter(sp => sp.required)
|
||||
@ -547,14 +548,14 @@ function AgencyBriefSection() {
|
||||
}
|
||||
|
||||
// 上传界面
|
||||
function UploadView({ task }: { task: TaskData }) {
|
||||
function UploadView({ task, toast }: { task: TaskData; toast: ReturnType<typeof useToast> }) {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const isScript = task.phase === 'script'
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 h-full">
|
||||
{/* Brief文档区域 - 仅脚本阶段显示 */}
|
||||
{isScript && <AgencyBriefSection />}
|
||||
{isScript && <AgencyBriefSection toast={toast} />}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@ -962,6 +963,7 @@ function ApprovedView({ task }: { task: TaskData }) {
|
||||
export default function TaskDetailPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const taskId = params.id as string
|
||||
|
||||
const taskData = allTasksData[taskId]
|
||||
@ -995,7 +997,7 @@ export default function TaskDetailPage() {
|
||||
const renderContent = () => {
|
||||
switch (taskData.stage) {
|
||||
case 'upload':
|
||||
return <UploadView task={taskData} />
|
||||
return <UploadView task={taskData} toast={toast} />
|
||||
case 'ai_reviewing':
|
||||
return <AIReviewingView task={taskData} />
|
||||
case 'ai_result':
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter, useParams, useSearchParams } from 'next/navigation'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { SuccessTag, WarningTag, ErrorTag, PendingTag } from '@/components/ui/Tag'
|
||||
@ -17,8 +18,48 @@ import {
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
Eye,
|
||||
MessageSquare
|
||||
MessageSquare,
|
||||
Download,
|
||||
File,
|
||||
Target,
|
||||
Ban,
|
||||
ChevronDown,
|
||||
ChevronUp
|
||||
} from 'lucide-react'
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
|
||||
// 代理商Brief文档(达人可查看)
|
||||
type AgencyBriefFile = {
|
||||
id: string
|
||||
name: string
|
||||
size: string
|
||||
uploadedAt: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
const mockAgencyBrief = {
|
||||
// 代理商上传的Brief文档
|
||||
files: [
|
||||
{ id: 'af1', name: '达人拍摄指南.pdf', size: '1.5MB', uploadedAt: '2026-02-02', description: '详细的拍摄流程和注意事项' },
|
||||
{ id: 'af2', name: '产品卖点话术.docx', size: '800KB', uploadedAt: '2026-02-02', description: '推荐使用的话术和表达方式' },
|
||||
{ id: 'af3', name: '品牌视觉参考.pdf', size: '3.2MB', uploadedAt: '2026-02-02', description: '视觉风格和拍摄参考示例' },
|
||||
] as AgencyBriefFile[],
|
||||
// 卖点要求
|
||||
sellingPoints: [
|
||||
{ id: 'sp1', content: 'SPF50+ PA++++', required: true },
|
||||
{ id: 'sp2', content: '轻薄质地,不油腻', required: true },
|
||||
{ id: 'sp3', content: '延展性好,易推开', required: false },
|
||||
{ id: 'sp4', content: '适合敏感肌', required: false },
|
||||
{ id: 'sp5', content: '夏日必备防晒', required: true },
|
||||
],
|
||||
// 违禁词
|
||||
blacklistWords: [
|
||||
{ id: 'bw1', word: '最好', reason: '绝对化用语' },
|
||||
{ id: 'bw2', word: '第一', reason: '绝对化用语' },
|
||||
{ id: 'bw3', word: '神器', reason: '夸大宣传' },
|
||||
{ id: 'bw4', word: '完美', reason: '绝对化用语' },
|
||||
],
|
||||
}
|
||||
|
||||
// 模拟任务数据
|
||||
const mockTask = {
|
||||
@ -105,6 +146,161 @@ function getTaskByStatus(status: string) {
|
||||
return task
|
||||
}
|
||||
|
||||
// 代理商Brief文档查看组件
|
||||
function AgencyBriefSection({ toast }: { toast: ReturnType<typeof useToast> }) {
|
||||
const [isExpanded, setIsExpanded] = useState(true)
|
||||
const [previewFile, setPreviewFile] = useState<AgencyBriefFile | null>(null)
|
||||
|
||||
const handleDownload = (file: AgencyBriefFile) => {
|
||||
toast.info(`下载文件: ${file.name}`)
|
||||
}
|
||||
|
||||
const handlePreview = (file: AgencyBriefFile) => {
|
||||
setPreviewFile(file)
|
||||
}
|
||||
|
||||
const requiredPoints = mockAgencyBrief.sellingPoints.filter(sp => sp.required)
|
||||
const optionalPoints = mockAgencyBrief.sellingPoints.filter(sp => !sp.required)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="border-accent-indigo/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<File size={18} className="text-accent-indigo" />
|
||||
Brief 文档与要求
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="p-1 hover:bg-bg-elevated rounded"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp size={18} className="text-text-tertiary" />
|
||||
) : (
|
||||
<ChevronDown size={18} className="text-text-tertiary" />
|
||||
)}
|
||||
</button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
{isExpanded && (
|
||||
<CardContent className="space-y-4">
|
||||
{/* Brief文档列表 */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-text-primary mb-2 flex items-center gap-2">
|
||||
<FileText size={14} className="text-accent-indigo" />
|
||||
参考文档
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{mockAgencyBrief.files.map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between p-3 bg-bg-elevated rounded-lg">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="w-8 h-8 rounded bg-accent-indigo/15 flex items-center justify-center flex-shrink-0">
|
||||
<FileText size={16} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">{file.name}</p>
|
||||
<p className="text-xs text-text-tertiary">{file.size}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<Button variant="ghost" size="sm" onClick={() => handlePreview(file)}>
|
||||
<Eye size={14} />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDownload(file)}>
|
||||
<Download size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 卖点要求 */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-text-primary mb-2 flex items-center gap-2">
|
||||
<Target size={14} className="text-accent-green" />
|
||||
卖点要求
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{requiredPoints.length > 0 && (
|
||||
<div className="p-3 bg-accent-coral/10 rounded-lg border border-accent-coral/30">
|
||||
<p className="text-xs text-accent-coral font-medium mb-2">必选卖点(必须提及)</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{requiredPoints.map((sp) => (
|
||||
<span key={sp.id} className="px-2 py-1 text-xs bg-accent-coral/20 text-accent-coral rounded">
|
||||
{sp.content}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{optionalPoints.length > 0 && (
|
||||
<div className="p-3 bg-bg-elevated rounded-lg">
|
||||
<p className="text-xs text-text-tertiary font-medium mb-2">可选卖点</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{optionalPoints.map((sp) => (
|
||||
<span key={sp.id} className="px-2 py-1 text-xs bg-bg-page text-text-secondary rounded">
|
||||
{sp.content}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 违禁词 */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-text-primary mb-2 flex items-center gap-2">
|
||||
<Ban size={14} className="text-accent-coral" />
|
||||
违禁词(请勿使用)
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mockAgencyBrief.blacklistWords.map((bw) => (
|
||||
<span key={bw.id} className="px-2 py-1 text-xs bg-accent-coral/15 text-accent-coral rounded border border-accent-coral/30">
|
||||
「{bw.word}」
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 文件预览弹窗 */}
|
||||
<Modal
|
||||
isOpen={!!previewFile}
|
||||
onClose={() => setPreviewFile(null)}
|
||||
title={previewFile?.name || '文件预览'}
|
||||
size="lg"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="aspect-[4/3] bg-bg-elevated rounded-lg flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<FileText size={48} className="mx-auto text-accent-indigo mb-4" />
|
||||
<p className="text-text-secondary">文件预览区域</p>
|
||||
<p className="text-xs text-text-tertiary mt-1">实际开发中将嵌入文件预览组件</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="secondary" onClick={() => setPreviewFile(null)}>
|
||||
关闭
|
||||
</Button>
|
||||
{previewFile && (
|
||||
<Button onClick={() => handleDownload(previewFile)}>
|
||||
<Download size={16} />
|
||||
下载文件
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function UploadSection({ onUpload }: { onUpload: () => void }) {
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
|
||||
@ -336,6 +532,7 @@ export default function CreatorScriptPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const searchParams = useSearchParams()
|
||||
const toast = useToast()
|
||||
const status = searchParams.get('status') || 'pending_upload'
|
||||
|
||||
const [task, setTask] = useState(getTaskByStatus(status))
|
||||
@ -390,6 +587,9 @@ export default function CreatorScriptPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Brief文档与要求(始终显示) */}
|
||||
<AgencyBriefSection toast={toast} />
|
||||
|
||||
{/* 根据状态显示不同内容 */}
|
||||
{task.scriptStatus === 'pending_upload' && (
|
||||
<UploadSection onUpload={simulateUpload} />
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import '../styles/globals.css'
|
||||
import { AuthProvider } from '@/contexts/AuthContext'
|
||||
import { ToastProvider } from '@/components/ui/Toast'
|
||||
|
||||
export const metadata = {
|
||||
title: '秒思智能审核',
|
||||
@ -14,7 +15,9 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="zh-CN" className="h-full">
|
||||
<body className="h-full bg-bg-page text-text-primary font-sans">
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
<ToastProvider>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</ToastProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
@ -22,6 +22,7 @@ export {
|
||||
getFileCategory,
|
||||
type FileInfo
|
||||
} from './ui/FilePreview';
|
||||
export { ToastProvider, useToast } from './ui/Toast';
|
||||
|
||||
// 导航组件
|
||||
export { BottomNav } from './navigation/BottomNav';
|
||||
|
||||
134
frontend/components/ui/Toast.tsx
Normal file
134
frontend/components/ui/Toast.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useState, useCallback, ReactNode } from 'react'
|
||||
import { CheckCircle, XCircle, AlertTriangle, Info, X } from 'lucide-react'
|
||||
|
||||
// Toast 类型
|
||||
type ToastType = 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
// Toast 项
|
||||
interface ToastItem {
|
||||
id: string
|
||||
type: ToastType
|
||||
message: string
|
||||
duration?: number
|
||||
}
|
||||
|
||||
// Toast Context
|
||||
interface ToastContextType {
|
||||
toast: {
|
||||
success: (message: string, duration?: number) => void
|
||||
error: (message: string, duration?: number) => void
|
||||
warning: (message: string, duration?: number) => void
|
||||
info: (message: string, duration?: number) => void
|
||||
}
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextType | null>(null)
|
||||
|
||||
// Toast 图标配置
|
||||
const toastConfig = {
|
||||
success: {
|
||||
icon: CheckCircle,
|
||||
bgColor: 'bg-accent-green/15',
|
||||
borderColor: 'border-accent-green/30',
|
||||
iconColor: 'text-accent-green',
|
||||
textColor: 'text-accent-green',
|
||||
},
|
||||
error: {
|
||||
icon: XCircle,
|
||||
bgColor: 'bg-accent-coral/15',
|
||||
borderColor: 'border-accent-coral/30',
|
||||
iconColor: 'text-accent-coral',
|
||||
textColor: 'text-accent-coral',
|
||||
},
|
||||
warning: {
|
||||
icon: AlertTriangle,
|
||||
bgColor: 'bg-accent-amber/15',
|
||||
borderColor: 'border-accent-amber/30',
|
||||
iconColor: 'text-accent-amber',
|
||||
textColor: 'text-accent-amber',
|
||||
},
|
||||
info: {
|
||||
icon: Info,
|
||||
bgColor: 'bg-accent-indigo/15',
|
||||
borderColor: 'border-accent-indigo/30',
|
||||
iconColor: 'text-accent-indigo',
|
||||
textColor: 'text-accent-indigo',
|
||||
},
|
||||
}
|
||||
|
||||
// 单个 Toast 组件
|
||||
function ToastItem({ item, onClose }: { item: ToastItem; onClose: (id: string) => void }) {
|
||||
const config = toastConfig[item.type]
|
||||
const Icon = config.icon
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-xl border ${config.bgColor} ${config.borderColor} shadow-lg animate-slide-in min-w-[280px] max-w-[400px]`}
|
||||
>
|
||||
<Icon size={20} className={config.iconColor} />
|
||||
<span className={`flex-1 text-sm font-medium ${config.textColor}`}>{item.message}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onClose(item.id)}
|
||||
className="p-1 rounded-lg hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<X size={16} className="text-text-tertiary" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Toast Provider
|
||||
export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
const [toasts, setToasts] = useState<ToastItem[]>([])
|
||||
|
||||
const removeToast = useCallback((id: string) => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id))
|
||||
}, [])
|
||||
|
||||
const addToast = useCallback((type: ToastType, message: string, duration = 3000) => {
|
||||
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
const newToast: ToastItem = { id, type, message, duration }
|
||||
|
||||
setToasts((prev) => [...prev, newToast])
|
||||
|
||||
// 自动移除
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
removeToast(id)
|
||||
}, duration)
|
||||
}
|
||||
}, [removeToast])
|
||||
|
||||
const toast = {
|
||||
success: (message: string, duration?: number) => addToast('success', message, duration),
|
||||
error: (message: string, duration?: number) => addToast('error', message, duration),
|
||||
warning: (message: string, duration?: number) => addToast('warning', message, duration),
|
||||
info: (message: string, duration?: number) => addToast('info', message, duration),
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ toast }}>
|
||||
{children}
|
||||
{/* Toast 容器 */}
|
||||
<div className="fixed top-4 right-4 z-[9999] flex flex-col gap-2">
|
||||
{toasts.map((item) => (
|
||||
<ToastItem key={item.id} item={item} onClose={removeToast} />
|
||||
))}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// useToast Hook
|
||||
export function useToast() {
|
||||
const context = useContext(ToastContext)
|
||||
if (!context) {
|
||||
throw new Error('useToast must be used within a ToastProvider')
|
||||
}
|
||||
return context.toast
|
||||
}
|
||||
|
||||
export default ToastProvider
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { api } from '@/lib/api'
|
||||
import type {
|
||||
VideoReviewRequest,
|
||||
@ -24,6 +24,7 @@ export function useReview(options: UseReviewOptions = {}) {
|
||||
const [isPolling, setIsPolling] = useState(false)
|
||||
const [task, setTask] = useState<ReviewTask | null>(null)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
/**
|
||||
* 提交审核
|
||||
@ -97,10 +98,22 @@ export function useReview(options: UseReviewOptions = {}) {
|
||||
}
|
||||
}, [task?.createdAt])
|
||||
|
||||
/**
|
||||
* 清除轮询定时器
|
||||
*/
|
||||
const clearPollingInterval = useCallback(() => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* 开始轮询进度
|
||||
*/
|
||||
const startPolling = useCallback((reviewId: string) => {
|
||||
// 清除之前的轮询(如果有)
|
||||
clearPollingInterval()
|
||||
setIsPolling(true)
|
||||
|
||||
const poll = async () => {
|
||||
@ -108,36 +121,45 @@ export function useReview(options: UseReviewOptions = {}) {
|
||||
const progress = await fetchProgress(reviewId)
|
||||
|
||||
if (progress.status === 'completed') {
|
||||
clearPollingInterval()
|
||||
setIsPolling(false)
|
||||
const result = await fetchResult(reviewId)
|
||||
onComplete?.(result)
|
||||
} else if (progress.status === 'failed') {
|
||||
clearPollingInterval()
|
||||
setIsPolling(false)
|
||||
const error = new Error('审核失败')
|
||||
setError(error)
|
||||
onError?.(error)
|
||||
}
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// 继续轮询,忽略单次错误
|
||||
console.error('Polling error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const intervalId = setInterval(poll, pollingInterval)
|
||||
intervalRef.current = setInterval(poll, pollingInterval)
|
||||
poll() // 立即执行一次
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId)
|
||||
clearPollingInterval()
|
||||
setIsPolling(false)
|
||||
}
|
||||
}, [fetchProgress, fetchResult, pollingInterval, onComplete, onError])
|
||||
}, [fetchProgress, fetchResult, pollingInterval, onComplete, onError, clearPollingInterval])
|
||||
|
||||
/**
|
||||
* 停止轮询
|
||||
*/
|
||||
const stopPolling = useCallback(() => {
|
||||
clearPollingInterval()
|
||||
setIsPolling(false)
|
||||
}, [])
|
||||
}, [clearPollingInterval])
|
||||
|
||||
// 组件卸载时清除定时器,防止内存泄漏
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearPollingInterval()
|
||||
}
|
||||
}, [clearPollingInterval])
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
|
||||
@ -41,7 +41,6 @@ class ApiClient {
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const message = error.response?.data?.detail || error.message || '请求失败'
|
||||
console.error('API Error:', message)
|
||||
return Promise.reject(new Error(message))
|
||||
}
|
||||
)
|
||||
|
||||
@ -140,6 +140,22 @@
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Toast 滑入动画 */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
5. Utility Classes (工具类)
|
||||
======================================== */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user