feat: optimize interface and database connection

Adjust MongoDB mock connector and update database structure.
Enhance sidebar, data platform, and AI analysis tools.
Clean up unnecessary code and update development docs.

#VERCEL_SKIP

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-07-21 00:11:52 +00:00
parent 892c5b4855
commit ecd8a48863
26 changed files with 1821 additions and 1024 deletions

View File

@@ -26,9 +26,19 @@ import {
Copy,
Check,
Zap,
Plus,
Download,
} from "lucide-react"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog"
import { useToast } from "@/components/ui/use-toast"
import { Textarea } from "@/components/ui/textarea"
interface AIAnalysisTask {
id: string
@@ -297,8 +307,8 @@ export function AIAnalysisTools() {
description: "为特定用户分群生成营销策略",
type: "strategy-generation",
parameters: {
targetSegment: "所有用户",
campaignGoal: "提高转化率",
targetSegment: "流失风险用户",
campaignGoal: "提高留存率",
budgetConstraint: "中等",
channelPreference: ["短信", "应用内推送", "邮件"],
},
@@ -664,9 +674,7 @@ export function AIAnalysisTools() {
<Plus className="h-6 w-6 text-muted-foreground" />
</div>
<h3 className="text-lg font-medium mb-2"></h3>
<p className="text-sm text-muted-foreground text-center mb-4">
</p>
<p className="text-sm text-muted-foreground text-center mb-4"></p>
<Button variant="outline"></Button>
</CardContent>
</Card>
@@ -719,9 +727,7 @@ export function AIAnalysisTools() {
<SelectItem value="10">10</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
AI分析任务数量
</p>
<p className="text-sm text-muted-foreground">AI分析任务数量</p>
</div>
<Separator />
@@ -920,4 +926,231 @@ export function AIAnalysisTools() {
</TableHeader>
<TableBody>
{selectedTask.result.clusters.map((cluster: any, index: number) => (
<TableRow key={
<TableRow key={index}>
<TableCell className="font-medium">{cluster.name}</TableCell>
<TableCell>{cluster.size.toLocaleString()}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{cluster.characteristics.map((char: string, charIndex: number) => (
<Badge key={charIndex} variant="outline">
{char}
</Badge>
))}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
{selectedTask.result.tagCategories && (
<div className="space-y-2">
<h4 className="text-sm font-medium"></h4>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{selectedTask.result.tagCategories.map((category: any, index: number) => (
<TableRow key={index}>
<TableCell className="font-medium">{category.name}</TableCell>
<TableCell>{category.count}</TableCell>
<TableCell>{category.coverage}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{category.examples.map((example: string, exampleIndex: number) => (
<Badge key={exampleIndex} variant="outline">
{example}
</Badge>
))}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</div>
)}
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setSelectedTaskId(null)}>
</Button>
{selectedTask?.status === "completed" && (
<Button>
<Download className="mr-2 h-4 w-4" />
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
{/* 添加模型对话框 */}
<Dialog open={isAddingModel} onOpenChange={setIsAddingModel}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>AI模型</DialogTitle>
<DialogDescription>AI模型连接</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="model-name" className="text-right">
</Label>
<Input id="model-name" className="col-span-3" placeholder="例如GPT-4" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="model-provider" className="text-right">
</Label>
<Input id="model-provider" className="col-span-3" placeholder="例如OpenAI" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="model-type" className="text-right">
</Label>
<Select>
<SelectTrigger id="model-type" className="col-span-3">
<SelectValue placeholder="选择模型类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="llm"></SelectItem>
<SelectItem value="image-gen"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="api-endpoint" className="text-right">
API端点
</Label>
<Input id="api-endpoint" className="col-span-3" placeholder="例如https://api.openai.com/v1/chat" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="api-key" className="text-right">
API密钥
</Label>
<Input id="api-key" type="password" className="col-span-3" placeholder="输入API密钥" />
</div>
<div className="grid grid-cols-4 items-start gap-4">
<Label htmlFor="capabilities" className="text-right pt-2">
</Label>
<Textarea id="capabilities" className="col-span-3" placeholder="例如:文本生成, 内容分析" rows={3} />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<div className="text-right">
<Label htmlFor="model-status"></Label>
</div>
<div className="flex items-center space-x-2 col-span-3">
<Switch id="model-status" defaultChecked />
<Label htmlFor="model-status"></Label>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddingModel(false)}>
</Button>
<Button onClick={() => setIsAddingModel(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 创建分析任务对话框 */}
<Dialog open={isCreatingTask} onOpenChange={setIsCreatingTask}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="task-name" className="text-right">
</Label>
<Input id="task-name" className="col-span-3" placeholder="例如:高价值用户流失预测" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="task-type" className="text-right">
</Label>
<Select>
<SelectTrigger id="task-type" className="col-span-3">
<SelectValue placeholder="选择分析类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="behavior-analysis"></SelectItem>
<SelectItem value="segment-discovery"></SelectItem>
<SelectItem value="churn-prediction"></SelectItem>
<SelectItem value="strategy-generation"></SelectItem>
<SelectItem value="tag-generation"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="task-datasource" className="text-right">
</Label>
<Select>
<SelectTrigger id="task-datasource" className="col-span-3">
<SelectValue placeholder="选择数据源" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="user-db"></SelectItem>
<SelectItem value="transaction"></SelectItem>
<SelectItem value="behavior"></SelectItem>
<SelectItem value="user-value"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-start gap-4">
<Label htmlFor="task-parameters" className="text-right pt-2">
</Label>
<Textarea
id="task-parameters"
className="col-span-3"
placeholder="输入JSON格式的分析参数例如{ 'userSegment': '高价值用户', 'timeRange': '近30天' }"
rows={5}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<div className="text-right">
<Label htmlFor="schedule-task"></Label>
</div>
<div className="flex items-center space-x-2 col-span-3">
<Switch id="schedule-task" />
<Label htmlFor="schedule-task"></Label>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCreatingTask(false)}>
</Button>
<Button onClick={() => setIsCreatingTask(false)}>
<Play className="mr-2 h-4 w-4" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -1,18 +1,18 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Plus, Trash2 } from "lucide-react"
import { Trash2, Plus, Save } from "lucide-react"
interface RuleCondition {
field: string
operator: string
value: string
logicalOperator?: "AND" | "OR"
}
interface TagRule {
@@ -24,151 +24,263 @@ interface TagRule {
addTags: string[]
removeTags: string[]
}
enabled: boolean
}
/**
* 简易标签规则编辑器
* 只实现基本字段编辑与本地状态保存,后端持久化可在后续迭代中接入
*/
export function RuleEditor({ ruleId }: { ruleId: string | null }) {
const [rule, setRule] = useState<TagRule | null>(null)
interface RuleEditorProps {
rule?: TagRule
onSave: (rule: TagRule) => void
onCancel: () => void
}
// ❗️此处仅示例:真实项目应替换为 API 请求
useEffect(() => {
if (!ruleId) {
setRule(null)
return
}
// 模拟异步加载
setTimeout(() => {
setRule({
id: ruleId,
name: "示例规则",
description: "这是一个示例规则,用于演示编辑器",
conditions: [
{ field: "orderAmount", operator: ">", value: "500", logicalOperator: "AND" },
{ field: "loginDays", operator: "<", value: "30" },
],
actions: { addTags: ["高价值用户"], removeTags: [] },
export function RuleEditor({ rule, onSave, onCancel }: RuleEditorProps) {
const [formData, setFormData] = useState<TagRule>(
rule || {
id: "",
name: "",
description: "",
conditions: [{ field: "", operator: "", value: "" }],
actions: {
addTags: [],
removeTags: [],
},
enabled: true,
},
)
const [newAddTag, setNewAddTag] = useState("")
const [newRemoveTag, setNewRemoveTag] = useState("")
const fieldOptions = [
{ value: "imei", label: "IMEI" },
{ value: "phone", label: "手机号" },
{ value: "device_brand", label: "设备品牌" },
{ value: "device_model", label: "设备型号" },
{ value: "os_version", label: "系统版本" },
{ value: "app_version", label: "应用版本" },
{ value: "location", label: "地理位置" },
{ value: "user_behavior", label: "用户行为" },
{ value: "consumption_amount", label: "消费金额" },
{ value: "activity_frequency", label: "活跃频率" },
]
const operatorOptions = [
{ value: "equals", label: "等于" },
{ value: "not_equals", label: "不等于" },
{ value: "contains", label: "包含" },
{ value: "not_contains", label: "不包含" },
{ value: "starts_with", label: "开始于" },
{ value: "ends_with", label: "结束于" },
{ value: "greater_than", label: "大于" },
{ value: "less_than", label: "小于" },
{ value: "in_range", label: "在范围内" },
{ value: "regex", label: "正则匹配" },
]
const addCondition = () => {
setFormData({
...formData,
conditions: [...formData.conditions, { field: "", operator: "", value: "" }],
})
}
const removeCondition = (index: number) => {
setFormData({
...formData,
conditions: formData.conditions.filter((_, i) => i !== index),
})
}
const updateCondition = (index: number, field: keyof RuleCondition, value: string) => {
const newConditions = [...formData.conditions]
newConditions[index] = { ...newConditions[index], [field]: value }
setFormData({ ...formData, conditions: newConditions })
}
const addTag = (type: "addTags" | "removeTags") => {
const tagValue = type === "addTags" ? newAddTag : newRemoveTag
if (tagValue.trim()) {
setFormData({
...formData,
actions: {
...formData.actions,
[type]: [...formData.actions[type], tagValue.trim()],
},
})
}, 300)
}, [ruleId])
if (!ruleId) {
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
</Card>
)
if (type === "addTags") {
setNewAddTag("")
} else {
setNewRemoveTag("")
}
}
}
if (!rule) {
return (
<Card>
<CardHeader>
<CardTitle>...</CardTitle>
</CardHeader>
</Card>
)
const removeTag = (type: "addTags" | "removeTags", index: number) => {
setFormData({
...formData,
actions: {
...formData.actions,
[type]: formData.actions[type].filter((_, i) => i !== index),
},
})
}
// === 内部更新函数 ===
const updateRule = (partial: Partial<TagRule>) => setRule((prev) => (prev ? { ...prev, ...partial } : prev))
const addCondition = () =>
updateRule({
conditions: [...rule.conditions, { field: "", operator: "=", value: "" }],
})
const removeCondition = (index: number) =>
updateRule({
conditions: rule.conditions.filter((_, i) => i !== index),
})
const saveRule = () => {
// TODO: 调用后端保存
console.log("保存规则:", rule)
const handleSave = () => {
if (formData.name && formData.conditions.length > 0) {
onSave(formData)
}
}
return (
<Card>
<CardHeader>
<CardTitle>{rule.name}</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* 基本信息 */}
<div className="space-y-2">
<label className="block text-sm font-medium"></label>
<Input value={rule.name} onChange={(e) => updateRule({ name: e.target.value })} />
</div>
<div className="space-y-2">
<label className="block text-sm font-medium"></label>
<Textarea value={rule.description} onChange={(e) => updateRule({ description: e.target.value })} />
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="rule-name"></Label>
<Input
id="rule-name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="输入规则名称"
/>
</div>
<div>
<Label htmlFor="rule-description"></Label>
<Input
id="rule-description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="输入规则描述"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{formData.conditions.map((condition, index) => (
<div key={index} className="flex items-center space-x-2 p-3 border rounded-lg">
<Select value={condition.field} onValueChange={(value) => updateCondition(index, "field", value)}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="选择字段" />
</SelectTrigger>
<SelectContent>
{fieldOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={condition.operator} onValueChange={(value) => updateCondition(index, "operator", value)}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="选择操作符" />
</SelectTrigger>
<SelectContent>
{operatorOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 条件编辑 */}
<div className="space-y-2">
<label className="block text-sm font-medium"></label>
{rule.conditions.map((c, idx) => (
<div key={idx} className="flex items-center gap-2">
<Input
placeholder="字段"
value={c.field}
onChange={(e) => {
const next = [...rule.conditions]
next[idx].field = e.target.value
updateRule({ conditions: next })
}}
className="w-1/3"
value={condition.value}
onChange={(e) => updateCondition(index, "value", e.target.value)}
placeholder="输入值"
className="flex-1"
/>
<Input
placeholder="运算符"
value={c.operator}
onChange={(e) => {
const next = [...rule.conditions]
next[idx].operator = e.target.value
updateRule({ conditions: next })
}}
className="w-1/6"
/>
<Input
placeholder="值"
value={c.value}
onChange={(e) => {
const next = [...rule.conditions]
next[idx].value = e.target.value
updateRule({ conditions: next })
}}
className="w-1/3"
/>
<Button variant="ghost" size="icon" onClick={() => removeCondition(idx)} aria-label="删除条件">
<Button
variant="outline"
size="icon"
onClick={() => removeCondition(index)}
disabled={formData.conditions.length === 1}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
<Button variant="outline" onClick={addCondition}>
<Plus className="mr-2 h-4 w-4" />
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
{/* 动作(标签) */}
<div className="space-y-2">
<label className="block text-sm font-medium"></label>
<div className="flex flex-wrap gap-2">
{rule.actions.addTags.map((tag, i) => (
<Badge key={i}>{tag}</Badge>
))}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div>
<Label></Label>
<div className="flex space-x-2 mt-2">
<Input
value={newAddTag}
onChange={(e) => setNewAddTag(e.target.value)}
placeholder="输入要添加的标签"
onKeyPress={(e) => e.key === "Enter" && addTag("addTags")}
/>
<Button onClick={() => addTag("addTags")}></Button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{formData.actions.addTags.map((tag, index) => (
<Badge key={index} variant="secondary" className="flex items-center gap-1">
{tag}
<button onClick={() => removeTag("addTags", index)}>
<Trash2 className="h-3 w-3" />
</button>
</Badge>
))}
</div>
</div>
</div>
{/* 保存 */}
<div className="text-right">
<Button onClick={saveRule}></Button>
</div>
</CardContent>
</Card>
<div>
<Label></Label>
<div className="flex space-x-2 mt-2">
<Input
value={newRemoveTag}
onChange={(e) => setNewRemoveTag(e.target.value)}
placeholder="输入要移除的标签"
onKeyPress={(e) => e.key === "Enter" && addTag("removeTags")}
/>
<Button onClick={() => addTag("removeTags")}></Button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{formData.actions.removeTags.map((tag, index) => (
<Badge key={index} variant="destructive" className="flex items-center gap-1">
{tag}
<button onClick={() => removeTag("removeTags", index)}>
<Trash2 className="h-3 w-3" />
</button>
</Badge>
))}
</div>
</div>
</CardContent>
</Card>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={onCancel}>
</Button>
<Button onClick={handleSave}>
<Save className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
)
}