710 lines
26 KiB
TypeScript
710 lines
26 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from "react"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog"
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Textarea } from "@/components/ui/textarea"
|
||
import {
|
||
FileText,
|
||
Plus,
|
||
Download,
|
||
RefreshCw,
|
||
Eye,
|
||
Calendar,
|
||
Users,
|
||
TrendingUp,
|
||
PieChart,
|
||
BarChart3,
|
||
Clock,
|
||
CheckCircle2,
|
||
Loader2,
|
||
Send,
|
||
Mail,
|
||
MessageSquare,
|
||
Play,
|
||
Sparkles,
|
||
} from "lucide-react"
|
||
|
||
// 报告规则接口
|
||
interface ReportRule {
|
||
id: string
|
||
name: string
|
||
templateId: string
|
||
schedule: string
|
||
enabled: boolean
|
||
isDefault: boolean
|
||
config: {
|
||
dateRange: string
|
||
includeCharts: boolean
|
||
sendTo: string[]
|
||
format: 'pdf' | 'html' | 'markdown'
|
||
}
|
||
promptTemplate: string
|
||
}
|
||
|
||
// 报告模板
|
||
interface ReportTemplate {
|
||
id: string
|
||
name: string
|
||
description: string
|
||
type: 'daily' | 'weekly' | 'monthly' | 'custom'
|
||
sections: string[]
|
||
icon: string
|
||
}
|
||
|
||
// 默认报告规则
|
||
const DEFAULT_RULES: ReportRule[] = [
|
||
{
|
||
id: 'rule_daily',
|
||
name: '每日运营日报规则',
|
||
templateId: 'tpl_daily',
|
||
schedule: '每日 23:59',
|
||
enabled: true,
|
||
isDefault: true,
|
||
config: {
|
||
dateRange: 'today',
|
||
includeCharts: true,
|
||
sendTo: ['admin@company.com'],
|
||
format: 'pdf',
|
||
},
|
||
promptTemplate: `生成每日运营日报,包含以下内容:
|
||
|
||
1. 核心指标
|
||
- 总用户数及日增量
|
||
- 活跃用户数及活跃率
|
||
- 新增用户数
|
||
|
||
2. 用户活跃
|
||
- 各等级用户活跃情况
|
||
- 活跃时段分布
|
||
|
||
3. 渠道消息
|
||
- 各渠道消息量统计
|
||
- 响应时间分析
|
||
|
||
4. AI查询统计
|
||
- 查询次数
|
||
- 平均响应时间
|
||
- 热门查询类型
|
||
|
||
输出格式:结构化报告,包含数据表格和趋势描述`,
|
||
},
|
||
{
|
||
id: 'rule_weekly',
|
||
name: '周度数据质量规则',
|
||
templateId: 'tpl_weekly',
|
||
schedule: '每周日 00:00',
|
||
enabled: true,
|
||
isDefault: true,
|
||
config: {
|
||
dateRange: 'week',
|
||
includeCharts: true,
|
||
sendTo: ['data@company.com'],
|
||
format: 'pdf',
|
||
},
|
||
promptTemplate: `生成周度数据质量报告:
|
||
|
||
1. 质量概览
|
||
- 数据完整度评分
|
||
- 数据准确度评分
|
||
- 本周质量趋势
|
||
|
||
2. 清洗统计
|
||
- 清洗记录数
|
||
- 格式化字段数
|
||
- 异常数据处理
|
||
|
||
3. 去重效果
|
||
- 去重前后对比
|
||
- 重复率统计
|
||
|
||
4. 数据完整度
|
||
- 各字段填充率
|
||
- 缺失字段分析`,
|
||
},
|
||
{
|
||
id: 'rule_monthly',
|
||
name: '月度资产报告规则',
|
||
templateId: 'tpl_monthly',
|
||
schedule: '每月1日 08:00',
|
||
enabled: true,
|
||
isDefault: true,
|
||
config: {
|
||
dateRange: 'month',
|
||
includeCharts: true,
|
||
sendTo: ['admin@company.com', 'ceo@company.com'],
|
||
format: 'pdf',
|
||
},
|
||
promptTemplate: `生成月度用户资产报告:
|
||
|
||
1. 资产总览
|
||
- 用户总资产价值
|
||
- 月度变化趋势
|
||
- 核心指标汇总
|
||
|
||
2. 用户增长
|
||
- 新增用户分析
|
||
- 流失用户分析
|
||
- 净增长情况
|
||
|
||
3. RFM分析
|
||
- RFM评分分布
|
||
- 各等级用户占比
|
||
- 价值迁移分析
|
||
|
||
4. 流量池分布
|
||
- 各流量池规模
|
||
- 月度变化
|
||
- 转化分析
|
||
|
||
5. 标签覆盖
|
||
- 标签覆盖率
|
||
- 热门标签TOP10`,
|
||
},
|
||
]
|
||
|
||
// 预定义报告模板
|
||
const REPORT_TEMPLATES: ReportTemplate[] = [
|
||
{
|
||
id: 'tpl_daily',
|
||
name: '每日运营日报',
|
||
description: '包含当日用户活跃、渠道消息、AI查询等实时数据汇总',
|
||
type: 'daily',
|
||
sections: ['核心指标', '用户活跃', '渠道消息', 'AI查询统计'],
|
||
icon: '📊',
|
||
},
|
||
{
|
||
id: 'tpl_weekly',
|
||
name: '周度数据质量报告',
|
||
description: '数据清洗、去重、补全等质量指标周度汇总',
|
||
type: 'weekly',
|
||
sections: ['质量概览', '清洗统计', '去重效果', '数据完整度'],
|
||
icon: '📈',
|
||
},
|
||
{
|
||
id: 'tpl_monthly',
|
||
name: '月度用户资产报告',
|
||
description: '用户增长、RFM分布、流量池变化等核心资产指标',
|
||
type: 'monthly',
|
||
sections: ['资产总览', '用户增长', 'RFM分析', '流量池分布', '标签覆盖'],
|
||
icon: '📑',
|
||
},
|
||
{
|
||
id: 'tpl_rfm',
|
||
name: 'RFM分析报告',
|
||
description: '详细的RFM评分分布和用户价值分层分析',
|
||
type: 'custom',
|
||
sections: ['RFM概览', '评分分布', '等级分层', '价值趋势'],
|
||
icon: '💎',
|
||
},
|
||
{
|
||
id: 'tpl_portrait',
|
||
name: '用户画像报告',
|
||
description: '用户画像特征分析,包含地域、年龄、偏好等维度',
|
||
type: 'custom',
|
||
sections: ['画像概览', '地域分布', '行为特征', '偏好分析'],
|
||
icon: '👤',
|
||
},
|
||
{
|
||
id: 'tpl_traffic',
|
||
name: '流量池分析报告',
|
||
description: '各流量池用户规模、转化率、价值贡献分析',
|
||
type: 'custom',
|
||
sections: ['池规模', '转化漏斗', '价值贡献', '增长趋势'],
|
||
icon: '🏊',
|
||
},
|
||
]
|
||
|
||
// 生成的报告
|
||
interface GeneratedReport {
|
||
id: string
|
||
templateId: string
|
||
title: string
|
||
generatedAt: string
|
||
status: 'ready' | 'generating'
|
||
content?: string
|
||
metrics: {
|
||
totalUsers: number
|
||
activeUsers: number
|
||
newUsers: number
|
||
rfmAvg: number
|
||
}
|
||
}
|
||
|
||
const GENERATED_REPORTS: GeneratedReport[] = [
|
||
{
|
||
id: 'rpt_1',
|
||
templateId: 'tpl_monthly',
|
||
title: '2024年1月用户资产月报',
|
||
generatedAt: '2024-02-01 08:00',
|
||
status: 'ready',
|
||
metrics: { totalUsers: 2013000000, activeUsers: 450000000, newUsers: 15000000, rfmAvg: 52.3 },
|
||
},
|
||
{
|
||
id: 'rpt_2',
|
||
templateId: 'tpl_weekly',
|
||
title: '本周数据质量报告',
|
||
generatedAt: '2024-01-28 00:00',
|
||
status: 'ready',
|
||
metrics: { totalUsers: 2013000000, activeUsers: 320000000, newUsers: 2500000, rfmAvg: 51.8 },
|
||
},
|
||
{
|
||
id: 'rpt_3',
|
||
templateId: 'tpl_daily',
|
||
title: '今日运营日报',
|
||
generatedAt: '2024-01-30 23:59',
|
||
status: 'ready',
|
||
metrics: { totalUsers: 2013000000, activeUsers: 45000000, newUsers: 350000, rfmAvg: 52.5 },
|
||
},
|
||
]
|
||
|
||
export default function ReportPage() {
|
||
const [reports, setReports] = useState<GeneratedReport[]>(GENERATED_REPORTS)
|
||
const [rules, setRules] = useState<ReportRule[]>(DEFAULT_RULES)
|
||
const [activeTab, setActiveTab] = useState("templates")
|
||
const [generating, setGenerating] = useState<string | null>(null)
|
||
const [showGenerateDialog, setShowGenerateDialog] = useState(false)
|
||
const [selectedTemplate, setSelectedTemplate] = useState<ReportTemplate | null>(null)
|
||
const [showPreviewDialog, setShowPreviewDialog] = useState(false)
|
||
const [previewReport, setPreviewReport] = useState<GeneratedReport | null>(null)
|
||
const [showRuleDialog, setShowRuleDialog] = useState(false)
|
||
const [selectedRule, setSelectedRule] = useState<ReportRule | null>(null)
|
||
const [editingPrompt, setEditingPrompt] = useState('')
|
||
|
||
const formatNumber = (num: number): string => {
|
||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`
|
||
return num.toLocaleString()
|
||
}
|
||
|
||
// 通过Skill生成报告
|
||
const generateReport = async (template: ReportTemplate) => {
|
||
setGenerating(template.id)
|
||
setShowGenerateDialog(false)
|
||
|
||
try {
|
||
// 调用AI Chat API执行报告生成
|
||
const response = await fetch('/api/ai-chat', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
message: `生成${template.name},包含以下章节:${template.sections.join('、')}`
|
||
})
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
// 添加新报告到列表
|
||
const newReport: GeneratedReport = {
|
||
id: `rpt_${Date.now()}`,
|
||
templateId: template.id,
|
||
title: `${template.name} - ${new Date().toLocaleDateString()}`,
|
||
generatedAt: new Date().toLocaleString(),
|
||
status: 'ready',
|
||
content: data.success ? data.response?.content : '报告生成失败',
|
||
metrics: {
|
||
totalUsers: 2013000000,
|
||
activeUsers: 450000000,
|
||
newUsers: 15000000,
|
||
rfmAvg: 52.3,
|
||
},
|
||
}
|
||
|
||
setReports([newReport, ...reports])
|
||
} catch (error) {
|
||
console.error('报告生成失败:', error)
|
||
} finally {
|
||
setGenerating(null)
|
||
}
|
||
}
|
||
|
||
const openPreview = (report: GeneratedReport) => {
|
||
setPreviewReport(report)
|
||
setShowPreviewDialog(true)
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||
<div className="p-6 space-y-6">
|
||
{/* 顶部标题 */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">智能报告</h1>
|
||
<p className="text-sm text-gray-500 mt-1">AI自动生成数据分析报告,支持多种报告模板</p>
|
||
</div>
|
||
<Button onClick={() => setShowGenerateDialog(true)}>
|
||
<Plus className="h-4 w-4 mr-2" />
|
||
生成报告
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 标签页 */}
|
||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||
<TabsList className="bg-white/80 backdrop-blur">
|
||
<TabsTrigger value="templates">报告模板</TabsTrigger>
|
||
<TabsTrigger value="rules">规则配置</TabsTrigger>
|
||
<TabsTrigger value="history">历史报告</TabsTrigger>
|
||
</TabsList>
|
||
|
||
{/* 报告模板 */}
|
||
<TabsContent value="templates" className="mt-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{REPORT_TEMPLATES.map(template => (
|
||
<Card
|
||
key={template.id}
|
||
className="border-0 shadow-sm bg-white/80 hover:shadow-lg transition-all cursor-pointer group"
|
||
>
|
||
<CardContent className="p-5">
|
||
<div className="flex items-start justify-between mb-3">
|
||
<span className="text-3xl">{template.icon}</span>
|
||
<Badge variant="outline">{
|
||
template.type === 'daily' ? '日报' :
|
||
template.type === 'weekly' ? '周报' :
|
||
template.type === 'monthly' ? '月报' : '自定义'
|
||
}</Badge>
|
||
</div>
|
||
<h3 className="font-bold text-gray-900 text-lg mb-1">{template.name}</h3>
|
||
<p className="text-sm text-gray-500 mb-3">{template.description}</p>
|
||
<div className="flex flex-wrap gap-1 mb-4">
|
||
{template.sections.map((section, i) => (
|
||
<Badge key={i} variant="secondary" className="text-xs">{section}</Badge>
|
||
))}
|
||
</div>
|
||
<Button
|
||
className="w-full"
|
||
onClick={() => generateReport(template)}
|
||
disabled={generating === template.id}
|
||
>
|
||
{generating === template.id ? (
|
||
<><Loader2 className="h-4 w-4 animate-spin mr-2" />生成中...</>
|
||
) : (
|
||
<><Play className="h-4 w-4 mr-2" />生成报告</>
|
||
)}
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</TabsContent>
|
||
|
||
{/* 规则配置 */}
|
||
<TabsContent value="rules" className="mt-4 space-y-3">
|
||
{rules.map(rule => {
|
||
const template = REPORT_TEMPLATES.find(t => t.id === rule.templateId)
|
||
return (
|
||
<Card key={rule.id} className="border-0 shadow-sm bg-white/80 backdrop-blur">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="p-2.5 rounded-xl bg-gradient-to-r from-purple-500 to-blue-500 text-2xl">
|
||
{template?.icon || '📄'}
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<h3 className="font-semibold text-gray-900">{rule.name}</h3>
|
||
{rule.isDefault && <Badge variant="outline" className="text-xs">默认规则</Badge>}
|
||
{rule.enabled ? (
|
||
<Badge className="bg-green-100 text-green-700">已启用</Badge>
|
||
) : (
|
||
<Badge className="bg-gray-100 text-gray-700">已禁用</Badge>
|
||
)}
|
||
</div>
|
||
<p className="text-sm text-gray-500">{template?.name}</p>
|
||
<div className="flex items-center gap-4 text-xs text-gray-400 mt-1">
|
||
<span className="flex items-center gap-1">
|
||
<Clock className="h-3 w-3" />
|
||
{rule.schedule}
|
||
</span>
|
||
<span>格式: {rule.config.format.toUpperCase()}</span>
|
||
<span>发送: {rule.config.sendTo.join(', ')}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => {
|
||
setSelectedRule(rule)
|
||
setEditingPrompt(rule.promptTemplate)
|
||
setShowRuleDialog(true)
|
||
}}
|
||
>
|
||
编辑规则
|
||
</Button>
|
||
<Button
|
||
variant={rule.enabled ? "outline" : "default"}
|
||
size="sm"
|
||
onClick={() => setRules(rules.map(r => r.id === rule.id ? { ...r, enabled: !r.enabled } : r))}
|
||
>
|
||
{rule.enabled ? '禁用' : '启用'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
})}
|
||
</TabsContent>
|
||
|
||
{/* 历史报告 */}
|
||
<TabsContent value="history" className="mt-4">
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardContent className="p-4">
|
||
<div className="space-y-3">
|
||
{reports.map(report => {
|
||
const template = REPORT_TEMPLATES.find(t => t.id === report.templateId)
|
||
|
||
return (
|
||
<div key={report.id} className="flex items-center justify-between p-4 rounded-xl bg-gray-50 hover:bg-gray-100 transition-colors">
|
||
<div className="flex items-center gap-4">
|
||
<div className="p-2.5 rounded-xl bg-white text-2xl">
|
||
{template?.icon || '📄'}
|
||
</div>
|
||
<div>
|
||
<h3 className="font-semibold text-gray-900">{report.title}</h3>
|
||
<div className="flex items-center gap-4 mt-1 text-xs text-gray-500">
|
||
<span className="flex items-center gap-1">
|
||
<Clock className="h-3 w-3" />
|
||
{report.generatedAt}
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<Users className="h-3 w-3" />
|
||
{formatNumber(report.metrics.totalUsers)} 用户
|
||
</span>
|
||
<span>RFM均值: {report.metrics.rfmAvg}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Button variant="outline" size="sm" onClick={() => openPreview(report)}>
|
||
<Eye className="h-4 w-4" />
|
||
</Button>
|
||
<Button variant="outline" size="sm">
|
||
<Download className="h-4 w-4" />
|
||
</Button>
|
||
<Button variant="outline" size="sm">
|
||
<Send className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
{/* 生成报告弹窗 */}
|
||
<Dialog open={showGenerateDialog} onOpenChange={setShowGenerateDialog}>
|
||
<DialogContent className="max-w-lg">
|
||
<DialogHeader>
|
||
<DialogTitle>生成智能报告</DialogTitle>
|
||
<DialogDescription>选择报告模板,AI将自动生成分析报告</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-4 py-4">
|
||
<div className="space-y-2">
|
||
<Label>报告模板</Label>
|
||
<Select onValueChange={(v) => setSelectedTemplate(REPORT_TEMPLATES.find(t => t.id === v) || null)}>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="选择报告模板" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{REPORT_TEMPLATES.map(template => (
|
||
<SelectItem key={template.id} value={template.id}>
|
||
{template.icon} {template.name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
{selectedTemplate && (
|
||
<div className="p-3 rounded-lg bg-purple-50">
|
||
<p className="text-sm text-gray-600 mb-2">{selectedTemplate.description}</p>
|
||
<div className="flex flex-wrap gap-1">
|
||
{selectedTemplate.sections.map((section, i) => (
|
||
<Badge key={i} variant="secondary" className="text-xs">{section}</Badge>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div className="space-y-2">
|
||
<Label>数据范围</Label>
|
||
<Select defaultValue="all">
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">全部数据</SelectItem>
|
||
<SelectItem value="today">今日</SelectItem>
|
||
<SelectItem value="week">本周</SelectItem>
|
||
<SelectItem value="month">本月</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setShowGenerateDialog(false)}>取消</Button>
|
||
<Button
|
||
onClick={() => selectedTemplate && generateReport(selectedTemplate)}
|
||
disabled={!selectedTemplate}
|
||
>
|
||
<Sparkles className="h-4 w-4 mr-2" />
|
||
开始生成
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* 报告预览弹窗 */}
|
||
<Dialog open={showPreviewDialog} onOpenChange={setShowPreviewDialog}>
|
||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>{previewReport?.title}</DialogTitle>
|
||
<DialogDescription>生成时间: {previewReport?.generatedAt}</DialogDescription>
|
||
</DialogHeader>
|
||
{previewReport && (
|
||
<div className="space-y-4 py-4">
|
||
{/* 核心指标 */}
|
||
<div className="grid grid-cols-4 gap-4">
|
||
<Card className="border bg-blue-50">
|
||
<CardContent className="p-3 text-center">
|
||
<div className="text-2xl font-bold text-blue-600">{formatNumber(previewReport.metrics.totalUsers)}</div>
|
||
<div className="text-xs text-gray-500">总用户</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border bg-green-50">
|
||
<CardContent className="p-3 text-center">
|
||
<div className="text-2xl font-bold text-green-600">{formatNumber(previewReport.metrics.activeUsers)}</div>
|
||
<div className="text-xs text-gray-500">活跃用户</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border bg-orange-50">
|
||
<CardContent className="p-3 text-center">
|
||
<div className="text-2xl font-bold text-orange-600">{formatNumber(previewReport.metrics.newUsers)}</div>
|
||
<div className="text-xs text-gray-500">新增用户</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border bg-purple-50">
|
||
<CardContent className="p-3 text-center">
|
||
<div className="text-2xl font-bold text-purple-600">{previewReport.metrics.rfmAvg}</div>
|
||
<div className="text-xs text-gray-500">RFM均值</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* 报告内容 */}
|
||
<Card className="border">
|
||
<CardContent className="p-4">
|
||
<h4 className="font-medium text-gray-900 mb-3">报告摘要</h4>
|
||
<div className="prose prose-sm text-gray-600">
|
||
{previewReport.content || (
|
||
<div className="space-y-2">
|
||
<p>本报告基于神射手数据中台生成,涵盖用户资产数字化核心指标。</p>
|
||
<p><strong>用户规模:</strong> 总用户{formatNumber(previewReport.metrics.totalUsers)},其中活跃用户占比{((previewReport.metrics.activeUsers / previewReport.metrics.totalUsers) * 100).toFixed(1)}%</p>
|
||
<p><strong>用户价值:</strong> 平均RFM评分{previewReport.metrics.rfmAvg},较上期提升0.5分</p>
|
||
<p><strong>增长趋势:</strong> 新增用户{formatNumber(previewReport.metrics.newUsers)},环比增长8.2%</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setShowPreviewDialog(false)}>关闭</Button>
|
||
<Button variant="outline">
|
||
<Download className="h-4 w-4 mr-2" />
|
||
下载PDF
|
||
</Button>
|
||
<Button>
|
||
<Send className="h-4 w-4 mr-2" />
|
||
发送报告
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* 规则编辑弹窗 */}
|
||
<Dialog open={showRuleDialog} onOpenChange={setShowRuleDialog}>
|
||
<DialogContent className="max-w-2xl">
|
||
<DialogHeader>
|
||
<DialogTitle>编辑报告规则</DialogTitle>
|
||
<DialogDescription>{selectedRule?.name}</DialogDescription>
|
||
</DialogHeader>
|
||
{selectedRule && (
|
||
<div className="space-y-4 py-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>执行时间</Label>
|
||
<Input value={selectedRule.schedule} readOnly className="bg-gray-50" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>输出格式</Label>
|
||
<Select defaultValue={selectedRule.config.format}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="pdf">PDF</SelectItem>
|
||
<SelectItem value="html">HTML</SelectItem>
|
||
<SelectItem value="markdown">Markdown</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>发送目标</Label>
|
||
<Input defaultValue={selectedRule.config.sendTo.join(', ')} placeholder="多个邮箱用逗号分隔" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>报告生成提示词 <span className="text-gray-400 text-xs">(AI根据此提示词生成报告内容)</span></Label>
|
||
<Textarea
|
||
value={editingPrompt}
|
||
onChange={(e) => setEditingPrompt(e.target.value)}
|
||
rows={12}
|
||
className="font-mono text-sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setShowRuleDialog(false)}>取消</Button>
|
||
<Button onClick={() => {
|
||
if (selectedRule) {
|
||
setRules(rules.map(r => r.id === selectedRule.id ? { ...r, promptTemplate: editingPrompt } : r))
|
||
}
|
||
setShowRuleDialog(false)
|
||
}}>
|
||
<CheckCircle2 className="h-4 w-4 mr-2" />
|
||
保存规则
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|