-
AI智能助手
-
数据库分析与智能报告生成
+
+ {/* Header */}
+
+
+
+
+
+
+
AI智能助手
+
数据分析、报告生成与智能预测
+
+
+
+
+
+ {/* AI状态 */}
+
+
+
+
+ AI运行状态
+
+
+
+
+
+
+ 模型版本
+ v3.5
+
+
+ 响应速度
+ 0.8s
+
+
+ 准确率
+ 94.2%
+
+
+
+
+
+ {/* AI功能卡片 */}
+
+ {aiFeatures.map((feature, index) => (
+
+
+
+
+
+ {feature.icon}
+
+
+
+
{feature.description}
+
+
+
+ {Object.entries(feature.stats).map(([key, value], i) => (
+
+
{key === 'reports' ? '报告数' : key === 'insights' ? '洞察数' : key === 'accuracy' ? '准确率' : key === 'predictions' ? '预测数' : key === 'profiles' ? '画像数' : key === 'segments' ? '分组数' : key === 'campaigns' ? '活动数' : '转化率'}
+
+ {typeof value === 'number' && value % 1 !== 0 ? `${value}%` : value}
+
+
+ ))}
+
+
+
+
+ ))}
-
-
-
-
- 数据分析
-
-
-
- 数据库
-
-
-
- 报告模板
-
-
-
-
-
-
数据分析任务
-
-
-
-
-
-
-
-
-
-
-
-
-
- 数据库
-
-
-
- {databases.map((db) => (
-
-
- {db.name}
-
- {db.type} · {db.tables} 表
-
-
- 最近更新 {new Date(db.lastUpdated).toLocaleString("zh-CN")}
-
-
-
+ {/* 对话区域 */}
+
+
+
+
+
+ AI对话
+
+
+
+ {/* 聊天历史 */}
+
+ {chatHistory.map((chat, index) => (
+
+
+
{chat.content}
+
{chat.timestamp}
+
+
))}
-
-
-
+
-
-
-
-
-
- 报告模板
-
-
-
- {templates.map((t) => (
-
-
- {t.name}
- {t.category}
- 字段: {t.fields.join(" / ")}
-
-
+ {/* 快捷问题 */}
+
+ {quickQuestions.map((question, index) => (
+
))}
-
-
-
-
+
+
+ {/* 输入框 */}
+
+
+
+
+
)
diff --git a/app/ai-assistant/recommendation/page.tsx b/app/ai-assistant/recommendation/page.tsx
new file mode 100644
index 0000000..a690485
--- /dev/null
+++ b/app/ai-assistant/recommendation/page.tsx
@@ -0,0 +1,313 @@
+"use client"
+
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Target, Users, TrendingUp, Zap, Download, RefreshCw, ArrowRight } from 'lucide-react'
+
+export default function Recommendation() {
+ const recommendedCampaigns = [
+ {
+ id: 1,
+ title: '美妆新品推广',
+ targetGroup: '高净值女性用户',
+ userCount: 856780,
+ expectedRevenue: 12850000,
+ probability: 82,
+ roi: 3.5,
+ timeframe: '未来7天',
+ actions: ['朋友圈广告', '私聊推送', '限时优惠'],
+ priority: 'high'
+ },
+ {
+ id: 2,
+ title: '会员升级计划',
+ targetGroup: '活跃普通会员',
+ userCount: 1285670,
+ expectedRevenue: 8560000,
+ probability: 75,
+ roi: 4.2,
+ timeframe: '未来15天',
+ actions: ['会员权益推广', '专属优惠', 'VIP体验券'],
+ priority: 'high'
+ },
+ {
+ id: 3,
+ title: '沉默用户唤醒',
+ targetGroup: '30天未活跃用户',
+ userCount: 2856780,
+ expectedRevenue: 5680000,
+ probability: 58,
+ roi: 2.8,
+ timeframe: '未来30天',
+ actions: ['优惠券发放', '新品预告', '个性化推荐'],
+ priority: 'medium'
+ },
+ {
+ id: 4,
+ title: '交叉销售',
+ targetGroup: '单品类购买用户',
+ userCount: 1714283,
+ expectedRevenue: 9280000,
+ probability: 68,
+ roi: 3.1,
+ timeframe: '未来14天',
+ actions: ['关联商品推荐', '搭配优惠', '场景营销'],
+ priority: 'medium'
+ },
+ ]
+
+ const recommendedSegments = [
+ {
+ name: '高转化潜力群体',
+ count: 428567,
+ conversion: 35.8,
+ value: 580,
+ characteristics: ['近期浏览频繁', '价格接受度高', '品牌认知强']
+ },
+ {
+ name: '复购价值用户',
+ count: 856780,
+ conversion: 28.5,
+ value: 420,
+ characteristics: ['首购满意度高', '客单价适中', '活跃度稳定']
+ },
+ {
+ name: '裂变传播节点',
+ count: 285670,
+ conversion: 22.3,
+ value: 320,
+ characteristics: ['社交影响力大', '分享意愿强', '粉丝基数高']
+ },
+ ]
+
+ const contentRecommendations = [
+ {
+ type: '朋友圈内容',
+ theme: '新品上市+限时优惠',
+ targetUsers: 2856780,
+ expectedEngagement: 18.5,
+ bestTime: '20:00-22:00'
+ },
+ {
+ type: '私聊话术',
+ theme: '专属VIP权益介绍',
+ targetUsers: 856780,
+ expectedEngagement: 42.8,
+ bestTime: '10:00-12:00'
+ },
+ {
+ type: '社群活动',
+ theme: '用户互动+抽奖',
+ targetUsers: 1285670,
+ expectedEngagement: 32.5,
+ bestTime: '周末 15:00-17:00'
+ },
+ ]
+
+ const getPriorityColor = (priority: string) => {
+ return priority === 'high' ? 'border-red-500 text-red-600 bg-red-50' : 'border-yellow-500 text-yellow-600 bg-yellow-50'
+ }
+
+ return (
+
+
+
+
精准推荐
+
AI智能推荐营销策略、目标用户群与内容方案
+
+
+
+
+
+
+
+ {/* 核心指标 */}
+
+
+
+ 推荐方案数
+
+
+ 586
+ 本月生成
+
+
+
+
+ 平均转化率
+
+
+ 18.5%
+ +3.2%
+
+
+
+
+ 预计收益
+
+
+ ¥36.4M
+ 近期方案
+
+
+
+
+ 平均ROI
+
+
+ 3.4x
+ 投入产出比
+
+
+
+
+ {/* 推荐营销方案 */}
+
+
+
+
+ 推荐营销方案
+
+
+
+
+ {recommendedCampaigns.map((campaign) => (
+
+
+
+
+
{campaign.title}
+
+ {campaign.priority === 'high' ? '高优先级' : '中优先级'}
+
+
+ {campaign.timeframe}
+
+
+
目标群体: {campaign.targetGroup}
+
+
+
+
+
+
+
目标用户数
+
{campaign.userCount.toLocaleString()}
+
+
+
预期收益
+
¥{(campaign.expectedRevenue / 10000).toFixed(1)}万
+
+
+
成功概率
+
{campaign.probability}%
+
+
+
预期ROI
+
{campaign.roi}x
+
+
+
+
+
推荐动作
+
+ {campaign.actions.map((action, index) => (
+
+
+ {action}
+
+ ))}
+
+
+
+ ))}
+
+
+
+
+ {/* 推荐目标群体 */}
+
+ {recommendedSegments.map((segment, index) => (
+
+
+ {segment.name}
+
+
+
+
+ 用户数
+ {segment.count.toLocaleString()}
+
+
+ 预期转化率
+ {segment.conversion}%
+
+
+ 人均价值
+ ¥{segment.value}
+
+
+
+
群体特征
+
+ {segment.characteristics.map((char, i) => (
+
+ {char}
+
+ ))}
+
+
+
+
+ ))}
+
+
+ {/* 内容推荐 */}
+
+
+
+
+ 内容推荐
+
+
+
+
+ {contentRecommendations.map((content, index) => (
+
+
+
+
{content.type}
+
{content.theme}
+
+
+ 最佳时间: {content.bestTime}
+
+
+
+
+ 目标触达:
+ {content.targetUsers.toLocaleString()} 人
+
+
+ 预期互动率:
+ {content.expectedEngagement}%
+
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/app/ai-assistant/trend-prediction/page.tsx b/app/ai-assistant/trend-prediction/page.tsx
new file mode 100644
index 0000000..7258dfb
--- /dev/null
+++ b/app/ai-assistant/trend-prediction/page.tsx
@@ -0,0 +1,222 @@
+"use client"
+
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { TrendingUp, TrendingDown, AlertCircle, Download, RefreshCw } from 'lucide-react'
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts'
+
+export default function TrendPrediction() {
+ const userGrowthPrediction = [
+ { date: '2024-06', actual: 428, predicted: null },
+ { date: '2024-07', actual: null, predicted: 445 },
+ { date: '2024-08', actual: null, predicted: 468 },
+ { date: '2024-09', actual: null, predicted: 495 },
+ { date: '2024-10', actual: null, predicted: 528 },
+ { date: '2024-11', actual: null, predicted: 565 },
+ { date: '2024-12', actual: null, predicted: 608 },
+ ]
+
+ const valuePrediction = [
+ { date: '2024-06', actual: 125.8, predicted: null },
+ { date: '2024-07', actual: null, predicted: 132.5 },
+ { date: '2024-08', actual: null, predicted: 141.2 },
+ { date: '2024-09', actual: null, predicted: 151.8 },
+ { date: '2024-10', actual: null, predicted: 164.5 },
+ { date: '2024-11', actual: null, predicted: 179.2 },
+ { date: '2024-12', actual: null, predicted: 196.5 },
+ ]
+
+ const churnRiskData = [
+ { segment: '高价值用户', risk: 8.5, count: 1285, trend: 'down' },
+ { segment: 'VIP用户', risk: 12.3, count: 856, trend: 'stable' },
+ { segment: '活跃用户', risk: 18.6, count: 12856, trend: 'up' },
+ { segment: '沉默用户', risk: 45.8, count: 28560, trend: 'up' },
+ ]
+
+ const opportunityData = [
+ { name: '新品推广机会', potential: '¥85.6M', probability: 78, timeframe: '未来30天' },
+ { name: '复购唤醒', potential: '¥56.2M', probability: 65, timeframe: '未来15天' },
+ { name: '会员升级', potential: '¥38.5M', probability: 82, timeframe: '未来7天' },
+ { name: '交叉销售', potential: '¥92.8M', probability: 58, timeframe: '未来45天' },
+ ]
+
+ return (
+
+
+
+
趋势预测
+
用户增长预测、价值预测与流失预警
+
+
+
+
+
+
+
+ {/* 预测指标 */}
+
+
+
+ 下月预测用户数
+
+
+ 445M
+ +3.9% 增长
+
+
+
+
+ 下月预测价值
+
+
+ ¥132.5亿
+ +5.3% 增长
+
+
+
+
+ 流失风险用户
+
+
+ 42.5K
+ 需重点关注
+
+
+
+
+ 预测准确率
+
+
+ 94.2%
+ 基于历史数据
+
+
+
+
+ {/* 趋势图表 */}
+
+
+
+
+
+ 用户增长预测
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 资产价值预测
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* 流失风险预警 */}
+
+
+
+
+ 流失风险预警
+
+
+
+
+ {churnRiskData.map((item, index) => (
+
+
+
+
{item.segment}
+
{item.count.toLocaleString()} 用户
+
+
+
+
+ {item.trend === 'down' ? (
+
+ ) : item.trend === 'up' ? (
+
+ ) : (
+
+ )}
+
+
+ ))}
+
+
+
+
+ {/* 商机预测 */}
+
+
+
+
+ 商机预测
+
+
+
+
+ {opportunityData.map((item, index) => (
+
+
+
{item.name}
+
+ {item.timeframe}
+
+
+
+
+
预测收益
+
{item.potential}
+
+
+
成功概率
+
{item.probability}%
+
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/app/ai-assistant/user-profiling/page.tsx b/app/ai-assistant/user-profiling/page.tsx
new file mode 100644
index 0000000..5e50949
--- /dev/null
+++ b/app/ai-assistant/user-profiling/page.tsx
@@ -0,0 +1,239 @@
+"use client"
+
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Users, Tag, Download, RefreshCw, TrendingUp } from 'lucide-react'
+import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar } from 'recharts'
+
+export default function UserProfiling() {
+ const ageDistribution = [
+ { range: '18-25', count: 856780, percentage: 20 },
+ { range: '26-35', count: 1500000, percentage: 35 },
+ { range: '36-45', count: 1285670, percentage: 30 },
+ { range: '46-55', count: 428567, percentage: 10 },
+ { range: '55+', count: 214284, percentage: 5 },
+ ]
+
+ const behaviorProfile = [
+ { aspect: '消费能力', value: 85 },
+ { aspect: '活跃度', value: 72 },
+ { aspect: '互动频率', value: 68 },
+ { aspect: '忠诚度', value: 78 },
+ { aspect: '推荐意愿', value: 62 },
+ { aspect: '复购率', value: 75 },
+ ]
+
+ const segments = [
+ {
+ name: '高净值群体',
+ count: 1284567,
+ characteristics: ['消费能力强', '忠诚度高', '品牌认知强'],
+ value: 580,
+ color: 'purple'
+ },
+ {
+ name: '年轻潮流族',
+ count: 2856780,
+ characteristics: ['追求时尚', '社交活跃', '冲动消费'],
+ value: 280,
+ color: 'blue'
+ },
+ {
+ name: '理性消费者',
+ count: 4285670,
+ characteristics: ['价格敏感', '重视品质', '对比研究'],
+ value: 180,
+ color: 'green'
+ },
+ {
+ name: '价格敏感型',
+ count: 1714283,
+ characteristics: ['促销驱动', '低频复购', '优惠敏感'],
+ value: 85,
+ color: 'yellow'
+ },
+ ]
+
+ const interestTags = [
+ { tag: '美妆护肤', count: 2856780, heat: 95 },
+ { tag: '时尚穿搭', count: 2145680, heat: 88 },
+ { tag: '健康养生', count: 1856780, heat: 76 },
+ { tag: '数码科技', count: 1428567, heat: 72 },
+ { tag: '母婴育儿', count: 1285670, heat: 68 },
+ { tag: '美食烹饪', count: 985670, heat: 58 },
+ { tag: '运动健身', count: 856780, heat: 52 },
+ { tag: '旅游出行', count: 714283, heat: 45 },
+ ]
+
+ return (
+
+
+
+
用户画像
+
智能分析用户群体特征与行为偏好
+
+
+
+
+
+
+
+ {/* 核心指标 */}
+
+
+
+ 用户群体数
+
+
+ 128
+ 覆盖42.8亿用户
+
+
+
+
+ 标签覆盖率
+
+
+ 87.5%
+ 优秀
+
+
+
+
+ 画像完整度
+
+
+ 92.3%
+ 优秀
+
+
+
+
+ 更新频率
+
+
+ 实时
+ 每小时更新
+
+
+
+
+ {/* 年龄分布与行为画像 */}
+
+
+
+
+
+ 年龄分布
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 行为画像
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* 用户群体细分 */}
+
+
+
+
+ 用户群体细分
+
+
+
+
+ {segments.map((segment, index) => (
+
+
+
{segment.name}
+
+ {segment.count.toLocaleString()} 人
+
+
+
+ {segment.characteristics.map((char, i) => (
+
+ {char}
+
+ ))}
+
+
+ 人均价值: ¥{segment.value}
+
+
+ ))}
+
+
+
+
+ {/* 兴趣标签热度 */}
+
+
+
+
+ 兴趣标签热度
+
+
+
+
+ {interestTags.map((item, index) => (
+
+
+
+
{item.tag}
+
+ {item.count.toLocaleString()} 人
+ 热度 {item.heat}
+
+
+
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts
deleted file mode 100644
index 8f3de71..0000000
--- a/app/api/auth/login/route.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { NextRequest, NextResponse } from "next/server"
-
-/**
- * 本地登录 API - 支持邮箱/手机号 + 密码
- * 当未配置 NEXT_PUBLIC_API_BASE_URL 时使用
- * 开发账号: zhiqun@qq.com / Zhiqun1984
- */
-const MOCK_USERS: Record
= {
- "zhiqun@qq.com": { password: "Zhiqun1984" },
-}
-
-function isEmail(value: string): boolean {
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
-}
-
-export async function POST(request: NextRequest) {
- try {
- const formData = await request.formData()
- const account = (formData.get("email") || formData.get("phone") || "").toString().trim()
- const password = (formData.get("password") || "").toString()
- const verificationCode = formData.get("verificationCode")?.toString()
-
- if (!account) {
- return NextResponse.json(
- { code: 40001, message: "请输入邮箱或手机号" },
- { status: 200 }
- )
- }
-
- // 验证码登录:开发环境下任意6位验证码通过
- if (verificationCode) {
- if (verificationCode.length >= 4) {
- const token = `mock_token_${Date.now()}_${account}`
- return NextResponse.json({
- code: 10000,
- message: "登录成功",
- data: { token },
- })
- }
- return NextResponse.json(
- { code: 40002, message: "验证码错误" },
- { status: 200 }
- )
- }
-
- // 密码登录
- if (!password) {
- return NextResponse.json(
- { code: 40003, message: "请输入密码" },
- { status: 200 }
- )
- }
-
- const key = isEmail(account) ? account : account
- const user = MOCK_USERS[key]
-
- if (user && user.password === password) {
- const token = `mock_token_${Date.now()}_${account}`
- return NextResponse.json({
- code: 10000,
- message: "登录成功",
- data: { token },
- })
- }
-
- return NextResponse.json(
- { code: 40004, message: "邮箱/手机号或密码错误" },
- { status: 200 }
- )
- } catch (error) {
- console.error("[auth/login]", error)
- return NextResponse.json(
- { code: 50000, message: "服务器错误" },
- { status: 500 }
- )
- }
-}
diff --git a/app/api/auth/send-code/route.ts b/app/api/auth/send-code/route.ts
deleted file mode 100644
index d06b6f4..0000000
--- a/app/api/auth/send-code/route.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { NextRequest, NextResponse } from "next/server"
-
-/**
- * 本地验证码发送 API (mock)
- * 开发环境下直接返回成功,验证码可为任意4位以上
- */
-export async function POST(request: NextRequest) {
- try {
- const formData = await request.formData()
- const phone = (formData.get("phone") || "").toString().trim()
-
- if (!phone) {
- return NextResponse.json(
- { code: 40001, message: "请输入手机号" },
- { status: 200 }
- )
- }
-
- // Mock: 模拟发送成功,开发时可用 123456 等作为验证码
- return NextResponse.json({
- code: 10000,
- message: "验证码已发送(开发模式:可使用任意4位以上数字)",
- })
- } catch (error) {
- console.error("[auth/send-code]", error)
- return NextResponse.json(
- { code: 50000, message: "服务器错误" },
- { status: 500 }
- )
- }
-}
diff --git a/app/api/rfm/analyze/route.ts b/app/api/rfm/analyze/route.ts
index 7d72064..8c0b91b 100644
--- a/app/api/rfm/analyze/route.ts
+++ b/app/api/rfm/analyze/route.ts
@@ -1,16 +1,12 @@
-/**
- * RFM 分析 API
- * 支持 MongoDB 真实数据 + 内存计算
- */
-
import { NextResponse } from "next/server"
import { analyzeUser, type AnalyzeInput } from "@/services/rfm-engine"
-import { analyzeUserRFM } from "@/services/rfm-mongodb-service"
import { generateText } from "ai"
import { openai } from "@ai-sdk/openai"
/**
- * 可选 AI 标签增强
+ * 可选 AI 标签增强:
+ * - 使用 AI SDK (generateText + openai("gpt-4o")),符合统一标准 [^1]
+ * - 无 OPENAI_API_KEY 时自动跳过,保持稳定
*/
async function aiTagging(chat_logs?: string[]) {
const text = (chat_logs ?? []).slice(0, 8).join("。")
@@ -37,38 +33,13 @@ export async function POST(req: Request) {
const body = await req.json()
const inputs: AnalyzeInput[] = Array.isArray(body) ? body : [body]
const useAI = (Array.isArray(body) ? (body as any).useAI : (body as any)?.useAI) ?? false
- const useMongoData = (body as any)?.useMongoData ?? true
const results = []
-
for (const input of inputs) {
- let base: any
- let source = 'memory'
-
- // 尝试从 MongoDB 获取真实数据
- if (useMongoData && input.user_id) {
- // 如果 user_id 是手机号格式,尝试从 MongoDB 查询
- const phone = input.user_id.replace(/\D/g, '')
- if (/^1[3-9]\d{9}$/.test(phone)) {
- const mongoResult = await analyzeUserRFM(phone)
- if (mongoResult.found) {
- base = mongoResult.data
- source = 'mongodb'
- }
- }
- }
-
- // 如果 MongoDB 没有数据,使用内存计算
- if (!base) {
- base = analyzeUser(input)
- source = 'memory'
- }
-
- // AI 标签增强
+ const base = analyzeUser(input)
if (useAI) {
const ai = await aiTagging(input.chat_logs)
if (ai) {
- base.tags = base.tags || {}
base.tags.emotion = ai.emotion ?? base.tags.emotion
base.tags.intent = ai.intent ?? base.tags.intent
if (Array.isArray(ai.behavior)) {
@@ -76,55 +47,10 @@ export async function POST(req: Request) {
}
}
}
-
- results.push({ ...base, source })
+ results.push(base)
}
-
return NextResponse.json({ success: true, data: results })
-
} catch (e: any) {
- console.error('RFM analyze error:', e)
- return NextResponse.json({
- success: false,
- error: e?.message || "Invalid input"
- }, { status: 400 })
- }
-}
-
-/**
- * GET 方法:按手机号查询用户 RFM
- */
-export async function GET(req: Request) {
- try {
- const { searchParams } = new URL(req.url)
- const phone = searchParams.get('phone')
-
- if (!phone) {
- return NextResponse.json({
- success: false,
- error: '请提供手机号参数'
- }, { status: 400 })
- }
-
- const result = await analyzeUserRFM(phone)
-
- if (!result.found) {
- return NextResponse.json({
- success: false,
- error: '未找到该用户'
- }, { status: 404 })
- }
-
- return NextResponse.json({
- success: true,
- data: result.data,
- source: 'mongodb'
- })
-
- } catch (e: any) {
- return NextResponse.json({
- success: false,
- error: e?.message || "查询失败"
- }, { status: 500 })
+ return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
}
}
diff --git a/app/api/rfm/get_tags/route.ts b/app/api/rfm/get_tags/route.ts
index 452404c..d0c978c 100644
--- a/app/api/rfm/get_tags/route.ts
+++ b/app/api/rfm/get_tags/route.ts
@@ -1,51 +1,12 @@
-/**
- * RFM 标签获取 API
- * 对接 MongoDB 真实数据
- */
-
import { NextResponse } from "next/server"
-import { getRFMTagsDistribution } from "@/services/rfm-mongodb-service"
import { getUserTags } from "@/services/rfm-engine"
-/**
- * GET /api/rfm/get_tags
- * 获取标签分布或指定用户的标签
- */
export async function GET(req: Request) {
- try {
- const { searchParams } = new URL(req.url)
- const userId = searchParams.get('user_id')
-
- // 如果指定用户,返回用户标签
- if (userId) {
- const userTags = getUserTags(userId)
- if (userTags) {
- return NextResponse.json({
- success: true,
- data: userTags.tags,
- source: 'memory'
- })
- }
- return NextResponse.json({
- success: false,
- error: '未找到该用户标签'
- }, { status: 404 })
- }
-
- // 否则返回标签分布
- const distribution = await getRFMTagsDistribution()
-
- return NextResponse.json({
- success: true,
- data: distribution,
- source: 'mongodb'
- })
-
- } catch (error) {
- console.error('Get tags error:', error)
- return NextResponse.json({
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error'
- }, { status: 500 })
+ const url = new URL(req.url)
+ const userId = url.searchParams.get("user_id")
+ if (!userId) {
+ return NextResponse.json({ success: false, error: "missing user_id" }, { status: 400 })
}
+ const data = getUserTags(userId)
+ return NextResponse.json({ success: true, data })
}
diff --git a/app/api/rfm/group_summary/route.ts b/app/api/rfm/group_summary/route.ts
index dd6279f..149e5e7 100644
--- a/app/api/rfm/group_summary/route.ts
+++ b/app/api/rfm/group_summary/route.ts
@@ -1,54 +1,7 @@
-/**
- * RFM 分组统计 API
- * 对接 MongoDB 真实数据
- */
-
import { NextResponse } from "next/server"
-import { getMongoRFMGroupSummary } from "@/services/rfm-mongodb-service"
import { getGroupSummary } from "@/services/rfm-engine"
export async function GET() {
- try {
- // 尝试从 MongoDB 获取真实数据
- const mongoData = await getMongoRFMGroupSummary()
-
- if (mongoData.totalUsers > 0) {
- return NextResponse.json({
- success: true,
- data: {
- gradeCount: mongoData.gradeCount,
- valueCount: mongoData.valueCount,
- lifecycleCount: {}, // MongoDB 暂无此字段
- totalUsers: mongoData.totalUsers,
- avgScore: mongoData.avgScore
- },
- source: 'mongodb'
- })
- }
-
- // 回退到内存数据
- const memData = getGroupSummary()
- return NextResponse.json({
- success: true,
- data: memData,
- source: 'memory'
- })
-
- } catch (error) {
- console.error('RFM group summary error:', error)
-
- // 返回默认数据
- return NextResponse.json({
- success: true,
- data: {
- gradeCount: { S: 0, A: 0, B: 0, C: 0, D: 0 },
- valueCount: { '高': 0, '中': 0, '低': 0 },
- lifecycleCount: {},
- totalUsers: 0,
- avgScore: 0
- },
- source: 'fallback',
- error: error instanceof Error ? error.message : 'Unknown error'
- })
- }
+ const data = getGroupSummary()
+ return NextResponse.json({ success: true, data })
}
diff --git a/app/api/search/route.ts b/app/api/search/route.ts
index 411fcf4..c004650 100644
--- a/app/api/search/route.ts
+++ b/app/api/search/route.ts
@@ -1,177 +1,52 @@
-/**
- * 智能搜索 API 路由
- * 对接神射手 MongoDB - 跨库查询
- */
-
import { type NextRequest, NextResponse } from "next/server"
-import {
- intelligentSearch,
- queryFullProfile,
- queryPhoneByQQ,
- UserValuationDoc
-} from "@/lib/mongodb"
+import { getIntelligentSearchService } from "@/services/intelligent-search-service"
-/**
- * 脱敏手机号
- */
-function maskPhone(phone: string | undefined): string {
- if (!phone) return ''
- if (phone.length !== 11) return phone
- return `${phone.slice(0, 3)}****${phone.slice(-4)}`
-}
-
-/**
- * 转换搜索结果
- */
-function transformSearchResult(doc: UserValuationDoc, queryType: string): any {
- const name = doc.name || '未知用户'
- return {
- id: doc._id?.toString(),
- type: 'user',
- title: name,
- subtitle: doc.phone_masked || maskPhone(doc.phone),
- description: `${doc.province || ''}${doc.city || ''} | ${doc.user_level || '未分级'} | RFM: ${doc.rfm_composite_score?.toFixed(2) || 'N/A'}`,
- data: {
- phone: doc.phone,
- phone_masked: doc.phone_masked || maskPhone(doc.phone),
- name: doc.name,
- province: doc.province,
- city: doc.city,
- userLevel: doc.user_level,
- rfmScore: doc.rfm_composite_score,
- tags: doc.tags || [],
- email: doc.email
- },
- matchedBy: queryType,
- relevanceScore: doc.rfm_composite_score || 0
- }
-}
-
-/**
- * GET /api/search
- * 智能搜索
- */
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const query = searchParams.get("q") || ""
+ const type = (searchParams.get("type") as "user" | "traffic" | "all") || "all"
const limit = Number.parseInt(searchParams.get("limit") || "50")
const offset = Number.parseInt(searchParams.get("offset") || "0")
+ const useAI = searchParams.get("ai") === "true"
+ const includeInsights = searchParams.get("insights") === "true"
if (!query.trim()) {
- return NextResponse.json({
- error: "搜索查询不能为空"
- }, { status: 400 })
+ return NextResponse.json({ error: "搜索查询不能为空" }, { status: 400 })
}
- // 执行智能搜索
- const result = await intelligentSearch(query, { limit, offset })
-
- // 转换结果
- const items = result.users.map(doc => transformSearchResult(doc, result.queryType))
-
- // 如果是 QQ 查询,补充 QQ 信息
- if (result.queryType === 'qq' && result.users.length > 0) {
- const qqInfo = await queryPhoneByQQ(query.trim())
- if (qqInfo) {
- items[0].data.qq = qqInfo.qq
- items[0].data.qqScore = qqInfo.QQ号评分
- items[0].data.carrier = qqInfo.运营商
- }
- }
+ const searchService = getIntelligentSearchService()
- return NextResponse.json({
- query,
- queryType: result.queryType,
- total: result.total,
- items,
- pagination: {
- limit,
- offset,
- hasMore: offset + items.length < result.total
- }
+ const results = await searchService.search(query, type, {
+ limit,
+ offset,
+ useAI,
+ includeInsights,
+ filters: {},
})
-
+
+ return NextResponse.json(results)
} catch (error) {
console.error("搜索API错误:", error)
- return NextResponse.json({
- error: "搜索失败,请稍后重试",
- details: error instanceof Error ? error.message : 'Unknown error'
- }, { status: 500 })
+ return NextResponse.json({ error: "搜索失败,请稍后重试" }, { status: 500 })
}
}
-/**
- * POST /api/search
- * 高级搜索(支持更多参数)
- * 返回格式适配前端 SearchResponse 接口
- */
export async function POST(request: NextRequest) {
try {
- const startTime = Date.now()
const body = await request.json()
const { query, type = "all", options = {} } = body
if (!query || !query.trim()) {
- return NextResponse.json({
- error: "搜索查询不能为空"
- }, { status: 400 })
+ return NextResponse.json({ error: "搜索查询不能为空" }, { status: 400 })
}
- const limit = options.limit || 50
- const offset = options.offset || 0
-
- const result = await intelligentSearch(query, { limit, offset })
-
- // 转换为前端期望的格式
- const results = result.users.map(doc => {
- const name = doc.name || '未知用户'
- return {
- id: doc._id?.toString() || '',
- type: 'user' as const,
- title: name,
- description: `${doc.province || ''}${doc.city || ''} | 估值: ${doc.user_evaluation_score || 'N/A'}`,
- tags: doc.tags || [],
- relevanceScore: doc.user_evaluation_score || 0,
- updatedAt: doc.computed_at?.toISOString() || new Date().toISOString(),
- metadata: {
- phone: doc.phone,
- phone_masked: doc.phone_masked || maskPhone(doc.phone),
- province: doc.province,
- city: doc.city,
- gender: doc.gender,
- age_range: doc.age_range,
- userLevel: doc.user_level,
- rfmScore: doc.rfm_composite_score,
- evaluationScore: doc.user_evaluation_score,
- dataQuality: doc.data_quality
- }
- }
- })
+ const searchService = getIntelligentSearchService()
+ const results = await searchService.search(query, type, options)
- const queryTime = Date.now() - startTime
-
- // 返回前端期望的 SearchResponse 格式
- return NextResponse.json({
- results,
- stats: {
- totalResults: result.total,
- queryTime,
- suggestions: [],
- filters: {
- queryType: result.queryType
- }
- },
- hasMore: offset + results.length < result.total
- })
-
+ return NextResponse.json(results)
} catch (error) {
console.error("搜索API错误:", error)
- return NextResponse.json({
- error: "搜索失败,请稍后重试",
- results: [],
- stats: { totalResults: 0, queryTime: 0, suggestions: [], filters: {} },
- hasMore: false
- }, { status: 500 })
+ return NextResponse.json({ error: "搜索失败,请稍后重试" }, { status: 500 })
}
}
diff --git a/app/api/system-status/route.ts b/app/api/system-status/route.ts
index 57105c1..78963ee 100644
--- a/app/api/system-status/route.ts
+++ b/app/api/system-status/route.ts
@@ -1,78 +1,18 @@
-/**
- * 系统状态 API 路由
- * 返回 MongoDB 数据库真实状态
- */
+import { type NextRequest, NextResponse } from "next/server"
+import { getMindsDBConnector } from "@/lib/mindsdb-connector"
-import { NextResponse } from "next/server"
-import { getDatabaseStats, healthCheck } from "@/lib/mongodb"
-
-/**
- * GET /api/system-status
- * 获取系统状态
- */
-export async function GET() {
+export async function GET(request: NextRequest) {
try {
- // 健康检查
- const health = await healthCheck()
-
- if (!health.mongodb) {
- return NextResponse.json({
- status: 'error',
- connected: false,
- latencyMs: health.latencyMs,
- error: health.error || 'MongoDB 连接失败',
- databases: [],
- totalDocuments: 0,
- totalSizeGB: 0,
- lastCheck: new Date().toISOString()
- }, { status: 503 })
- }
-
- // 获取数据库统计
- const stats = await getDatabaseStats()
-
- return NextResponse.json({
- status: 'healthy',
- connected: stats.connected,
- latencyMs: health.latencyMs,
- databases: stats.databases,
- totalDocuments: stats.totalDocuments,
- totalSizeGB: stats.totalSizeGB,
- lastCheck: new Date().toISOString(),
- // 格式化显示
- summary: {
- userCount: formatNumber(stats.totalDocuments),
- dataSize: `${stats.totalSizeGB} GB`,
- dbCount: stats.databases.length,
- responseTime: `${health.latencyMs}ms`
- }
- }, {
- headers: { 'Cache-Control': 'no-store, max-age=0' }
- })
-
- } catch (error) {
- console.error('System status error:', error)
- return NextResponse.json({
- status: 'error',
- connected: false,
- error: error instanceof Error ? error.message : 'Unknown error',
- lastCheck: new Date().toISOString()
- }, { status: 500 })
- }
-}
+ const mindsDB = getMindsDBConnector()
+ const status = await mindsDB.getSystemStatus()
-/**
- * 格式化数字显示
- */
-function formatNumber(num: number): string {
- if (num >= 1000000000) {
- return `${(num / 1000000000).toFixed(2)}B`
+ return NextResponse.json({
+ success: true,
+ status,
+ timestamp: new Date().toISOString(),
+ })
+ } catch (error) {
+ console.error("系统状态API错误:", error)
+ return NextResponse.json({ error: "获取系统状态失败" }, { status: 500 })
}
- if (num >= 1000000) {
- return `${(num / 1000000).toFixed(1)}M`
- }
- if (num >= 1000) {
- return `${(num / 1000).toFixed(1)}K`
- }
- return num.toString()
}
diff --git a/app/api/users/route.ts b/app/api/users/route.ts
index e2d06da..49ff84b 100644
--- a/app/api/users/route.ts
+++ b/app/api/users/route.ts
@@ -1,186 +1,233 @@
-/**
- * 用户 API 路由
- * 对接神射手 MongoDB 数据库 - KR.用户估值
- */
+import { NextResponse, type NextRequest } from "next/server"
+import type { TrafficUser } from "@/types/traffic"
+import { addUser, getUserById, queryUsers, type UserStatus } from "@/lib/mock-users"
-import { NextResponse, NextRequest } from "next/server"
-import {
- queryUserList,
- queryUserByPhone,
- queryFullProfile,
- UserValuationDoc
-} from "@/lib/mongodb"
+// 中文名字生成器数据
+const familyNames = [
+ "张",
+ "王",
+ "李",
+ "赵",
+ "陈",
+ "刘",
+ "杨",
+ "黄",
+ "周",
+ "吴",
+ "朱",
+ "孙",
+ "马",
+ "胡",
+ "郭",
+ "林",
+ "何",
+ "高",
+ "梁",
+ "郑",
+ "罗",
+ "宋",
+ "谢",
+ "唐",
+ "韩",
+ "曹",
+ "许",
+ "邓",
+ "萧",
+ "冯",
+]
+const givenNames1 = [
+ "志",
+ "建",
+ "文",
+ "明",
+ "永",
+ "春",
+ "秀",
+ "金",
+ "水",
+ "玉",
+ "国",
+ "立",
+ "德",
+ "海",
+ "和",
+ "荣",
+ "伟",
+ "新",
+ "英",
+ "佳",
+]
+const givenNames2 = [
+ "华",
+ "平",
+ "军",
+ "强",
+ "辉",
+ "敏",
+ "峰",
+ "磊",
+ "超",
+ "艳",
+ "娜",
+ "霞",
+ "燕",
+ "娟",
+ "静",
+ "丽",
+ "涛",
+ "洋",
+ "勇",
+ "龙",
+]
-/**
- * 脱敏手机号
- */
-function maskPhone(phone: string | undefined): string {
- if (!phone) return ''
- if (phone.length !== 11) return phone
- return `${phone.slice(0, 3)}****${phone.slice(-4)}`
-}
+// 生成固定的用户数据池
+const userPool: TrafficUser[] = Array.from({ length: 1610 }, (_, i) => {
+ const familyName = familyNames[Math.floor(Math.random() * familyNames.length)]
+ const givenName1 = givenNames1[Math.floor(Math.random() * givenNames1.length)]
+ const givenName2 = givenNames2[Math.floor(Math.random() * givenNames2.length)]
+ const fullName = Math.random() > 0.5 ? familyName + givenName1 + givenName2 : familyName + givenName1
+
+ // 生成随机时间(在过去7天内)
+ const date = new Date()
+ date.setDate(date.getDate() - Math.floor(Math.random() * 7))
-/**
- * 转换用户数据格式(适配前端)
- */
-function transformUser(doc: UserValuationDoc, index: number = 0): any {
- const name = doc.name || '未知用户'
return {
- id: doc._id?.toString() || `user-${index}`,
- avatar: `/placeholder.svg?height=40&width=40&text=${name[0] || 'U'}`,
- nickname: name,
- wechatId: doc.phone ? `wxid_${doc.phone.slice(-8)}` : '',
- phone: doc.phone || '',
- phone_masked: doc.phone_masked || maskPhone(doc.phone),
- region: doc.province && doc.city ? `${doc.province}${doc.city}` : (doc.province || '未知'),
- note: '',
- status: 'added' as const,
- addTime: doc.created_at?.toISOString() || new Date().toISOString(),
- source: (doc.source_channels && doc.source_channels[0]) || '神射手',
- assignedTo: '',
- category: 'customer' as const,
- tags: doc.tags || [],
- // RFM 数据
- userLevel: doc.user_level || 'D',
- rfmScore: doc.rfm_composite_score || 0,
- rfmR: doc.rfm_r_score,
- rfmF: doc.rfm_f_score,
- rfmM: doc.rfm_m_score,
- totalAmount: doc.total_amount || 0,
- orderCount: doc.order_count || 0,
- // 额外信息
- email: doc.email,
- address: doc.address,
- province: doc.province,
- city: doc.city,
+ id: `${Date.now()}-${i}`,
+ avatar: `/placeholder.svg?height=40&width=40&text=${fullName[0]}`,
+ nickname: fullName,
+ wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
+ phone: `1${["3", "5", "7", "8", "9"][Math.floor(Math.random() * 5)]}${Array.from({ length: 9 }, () => Math.floor(Math.random() * 10)).join("")}`,
+ region: [
+ "广东深圳",
+ "浙江杭州",
+ "江苏苏州",
+ "北京",
+ "上海",
+ "四川成都",
+ "湖北武汉",
+ "福建厦门",
+ "山东青岛",
+ "河南郑州",
+ ][Math.floor(Math.random() * 10)],
+ note: [
+ "咨询产品价格",
+ "对产品很感兴趣",
+ "准备购买",
+ "需要更多信息",
+ "想了解优惠活动",
+ "询问产品规格",
+ "要求产品demo",
+ "索要产品目录",
+ "询问售后服务",
+ "要求上门演示",
+ ][Math.floor(Math.random() * 10)],
+ status: ["pending", "added", "failed"][Math.floor(Math.random() * 3)] as TrafficUser["status"],
+ addTime: date.toISOString(),
+ source: ["抖音直播", "小红书", "微信朋友圈", "视频号", "公众号", "个人主页"][Math.floor(Math.random() * 6)],
+ assignedTo: "",
+ category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as TrafficUser["category"],
+ tags: [],
}
-}
+})
-/**
- * GET /api/users
- * 查询用户列表或单个用户详情
- */
-export async function GET(req: NextRequest) {
- try {
- const { searchParams } = new URL(req.url)
-
- // 单用户详情查询(按ID或手机号)
- const id = searchParams.get('id')
- const phone = searchParams.get('phone')
-
- if (id || phone) {
- // 如果是手机号格式,按手机号查询
- const queryPhone = phone || (id && /^1[3-9]\d{9}$/.test(id) ? id : null)
-
- if (queryPhone) {
- // 完整画像查询(跨库)
- const profile = await queryFullProfile(queryPhone)
-
- if (profile.valuation) {
- const user = transformUser(profile.valuation)
-
- // 补充 QQ 信息
- if (profile.qq) {
- user.qq = profile.qq.qq
- user.qqScore = profile.qq.QQ号评分
- user.phoneScore = profile.qq.手机号评分
- }
-
- // 补充存客宝信息
- if (profile.ckb) {
- user.wechat = profile.ckb.social_accounts?.wechat
- user.trafficPool = profile.ckb.traffic_pool?.pool_name
- }
-
- return NextResponse.json({
- data: user,
- sources: {
- valuation: !!profile.valuation,
- qq: !!profile.qq,
- ckb: !!profile.ckb
- }
- }, { headers: { 'Cache-Control': 'no-store' } })
- }
-
- return NextResponse.json({
- data: null,
- error: '未找到该用户'
- }, { status: 404 })
- }
-
- return NextResponse.json({
- data: null,
- error: '无效的查询参数'
- }, { status: 400 })
+// 计算今日新增数量
+const todayStart = new Date()
+todayStart.setHours(0, 0, 0, 0)
+const todayUsers = userPool.filter((user) => new Date(user.addTime) >= todayStart)
+
+// 生成微信好友数据池
+const generateWechatFriends = (wechatId: string, count: number) => {
+ return Array.from({ length: count }, (_, i) => {
+ const familyName = familyNames[Math.floor(Math.random() * familyNames.length)]
+ const givenName1 = givenNames1[Math.floor(Math.random() * givenNames1.length)]
+ const givenName2 = givenNames2[Math.floor(Math.random() * givenNames2.length)]
+ const fullName = Math.random() > 0.5 ? familyName + givenName1 + givenName2 : familyName + givenName1
+
+ // 生成随机时间(在过去30天内)
+ const date = new Date()
+ date.setDate(date.getDate() - Math.floor(Math.random() * 30))
+
+ return {
+ id: `wechat-${wechatId}-${i}`,
+ avatar: `/placeholder.svg?height=40&width=40&text=${fullName[0]}`,
+ nickname: fullName,
+ wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
+ phone: `1${["3", "5", "7", "8", "9"][Math.floor(Math.random() * 5)]}${Array.from({ length: 9 }, () => Math.floor(Math.random() * 10)).join("")}`,
+ region: [
+ "广东深圳",
+ "浙江杭州",
+ "江苏苏州",
+ "北京",
+ "上海",
+ "四川成都",
+ "湖北武汉",
+ "福建厦门",
+ "山东青岛",
+ "河南郑州",
+ ][Math.floor(Math.random() * 10)],
+ note: [
+ "咨询产品价格",
+ "对产品很感兴趣",
+ "准备购买",
+ "需要更多信息",
+ "想了解优惠活动",
+ "询问产品规格",
+ "要求产品demo",
+ "索要产品目录",
+ "询问售后服务",
+ "要求上门演示",
+ ][Math.floor(Math.random() * 10)],
+ status: ["pending", "added", "failed"][Math.floor(Math.random() * 3)] as TrafficUser["status"],
+ addTime: date.toISOString(),
+ source: ["抖音直播", "小红书", "微信朋友圈", "视频号", "公众号", "个人主页", "微信好友"][
+ Math.floor(Math.random() * 7)
+ ],
+ assignedTo: "",
+ category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as TrafficUser["category"],
+ tags: [],
}
-
- // 列表查询
- const q = searchParams.get('q') || undefined
- const tagsStr = searchParams.get('tags') || ''
- const userLevel = searchParams.get('userLevel') || searchParams.get('status') || undefined
- const rfmMin = searchParams.get('rfmMin') ? Number(searchParams.get('rfmMin')) : undefined
- const rfmMax = searchParams.get('rfmMax') ? Number(searchParams.get('rfmMax')) : undefined
- const page = Number(searchParams.get('page') ?? 1)
- const pageSize = Number(searchParams.get('pageSize') ?? 20)
-
- const tags = tagsStr ? tagsStr.split(',').filter(Boolean) : undefined
-
- const result = await queryUserList({
- page,
- pageSize,
- userLevel,
- minRfm: rfmMin,
- maxRfm: rfmMax,
- search: q,
- tags
- })
-
- const transformedData = result.data.map((doc, i) => transformUser(doc, i))
-
- return NextResponse.json({
- data: transformedData,
- total: result.total,
- page,
- pageSize,
- totalPages: Math.ceil(result.total / pageSize)
- }, { headers: { 'Cache-Control': 'no-store' } })
-
- } catch (error) {
- console.error('Users API error:', error)
-
- // 数据库连接失败时返回模拟数据
- return NextResponse.json({
- data: [],
- total: 0,
- page: 1,
- pageSize: 20,
- totalPages: 0,
- error: error instanceof Error ? error.message : '查询失败',
- fallback: true
- }, {
- status: 500,
- headers: { 'Cache-Control': 'no-store' }
- })
- }
+ })
}
-/**
- * POST /api/users
- * 创建用户(预留接口)
- */
-export async function POST(req: NextRequest) {
- try {
- const body = await req.json().catch(() => ({}))
-
- // TODO: 实现用户创建逻辑
- return NextResponse.json({
- success: false,
- error: '用户创建功能暂未开放'
- }, { status: 501 })
-
- } catch (error) {
- return NextResponse.json({
- error: error instanceof Error ? error.message : '创建失败'
- }, { status: 500 })
- }
+// 微信好友数据缓存
+const wechatFriendsCache = new Map()
+
+function parseArrayParam(v: string | null) {
+ if (!v) return []
+ return v
+ .split(",")
+ .map((s) => s.trim())
+ .filter(Boolean)
+}
+
+export async function GET(req: NextRequest) {
+ const { searchParams } = new URL(req.url)
+
+ // 详情优先
+ const id = searchParams.get("id")
+ if (id) {
+ const detail = getUserById(id)
+ return NextResponse.json({ data: detail }, { headers: { "Cache-Control": "no-store" } })
+ }
+
+ // 列表
+ const q = searchParams.get("q") ?? undefined
+ const tagsStr = searchParams.get("tags") ?? ""
+ const statusStr = searchParams.get("status") ?? ""
+ const rfmMin = Number(searchParams.get("rfmMin") ?? 0)
+ const rfmMax = Number(searchParams.get("rfmMax") ?? 100)
+ const page = Number(searchParams.get("page") ?? 1)
+ const pageSize = Number(searchParams.get("pageSize") ?? 20)
+
+ const tags = tagsStr ? tagsStr.split(",").filter(Boolean) : undefined
+ const status = statusStr ? (statusStr.split(",").filter(Boolean) as UserStatus[]) : undefined
+
+ const result = queryUsers({ q, tags, status, rfmMin, rfmMax, page, pageSize })
+ return NextResponse.json(result, { headers: { "Cache-Control": "no-store" } })
+}
+
+export async function POST(req: NextRequest) {
+ const body = await req.json().catch(() => ({}))
+ const created = addUser(body ?? {})
+ return NextResponse.json({ data: created }, { status: 201 })
}
diff --git a/app/data-platform/page.tsx b/app/data-platform/page.tsx
index c77c733..8269a6c 100644
--- a/app/data-platform/page.tsx
+++ b/app/data-platform/page.tsx
@@ -1,681 +1,408 @@
"use client"
import { useState } from "react"
-import { Database, Plus, Settings, Play, Pause, RotateCcw, Brain, Zap, CheckCircle, AlertCircle } from "lucide-react"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
+import Link from "next/link"
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
-import { Progress } from "@/components/ui/progress"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
+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 { Textarea } from "@/components/ui/textarea"
-import { Switch } from "@/components/ui/switch"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { Database, RefreshCw, CheckCircle2, AlertTriangle, Search, FileText, Upload, Download, Filter, TrendingUp, Zap, Tag, Plus, Plug, Shield, ArrowRight } from 'lucide-react'
+import { Progress } from "@/components/ui/progress"
-interface DataSource {
- id: string
- name: string
- type: string
- status: "connected" | "disconnected" | "syncing"
- lastSync: string
- recordCount: number
- description: string
-}
+export default function DataManagement() {
+ const [searchQuery, setSearchQuery] = useState("")
-interface AIModel {
- id: string
- name: string
- type: string
- status: "training" | "ready" | "error"
- accuracy: number
- lastTrained: string
- parameters: Record
-}
-
-export default function DataPlatformPage() {
- const [dataSources, setDataSources] = useState([
+ // 数据源管理
+ const dataSources = [
{
- id: "ds_001",
- name: "微信用户数据库",
- type: "MySQL",
- status: "connected",
- lastSync: "2024-01-15T10:30:00Z",
- recordCount: 2500000000,
- description: "存储微信用户基础信息和行为数据",
+ id: 1,
+ name: "微信好友数据",
+ type: "微信生态",
+ status: "active",
+ lastSync: "2分钟前",
+ users: 1234567,
+ syncRate: 98.5,
+ dataSize: "85GB",
},
{
- id: "ds_002",
- name: "流量关键词库",
- type: "PostgreSQL",
- status: "connected",
- lastSync: "2024-01-15T09:45:00Z",
- recordCount: 150000,
- description: "搜索引擎关键词和流量数据",
+ id: 2,
+ name: "抖音粉丝数据",
+ type: "抖音平台",
+ status: "active",
+ lastSync: "5分钟前",
+ users: 856432,
+ syncRate: 95.2,
+ dataSize: "21GB",
},
{
- id: "ds_003",
- name: "用户行为日志",
- type: "MongoDB",
- status: "syncing",
- lastSync: "2024-01-15T11:00:00Z",
- recordCount: 1500000000,
- description: "用户操作行为和交互记录",
- },
- ])
-
- const [aiModels, setAiModels] = useState([
- {
- id: "model_001",
- name: "用户价值预测模型",
- type: "Classification",
- status: "ready",
- accuracy: 0.92,
- lastTrained: "2024-01-14T15:30:00Z",
- parameters: {
- algorithm: "RandomForest",
- features: 25,
- epochs: 100,
- learningRate: 0.01,
- },
+ id: 3,
+ name: "小红书数据",
+ type: "小红书",
+ status: "pending",
+ lastSync: "待配置",
+ users: 0,
+ syncRate: 0,
+ dataSize: "0",
},
{
- id: "model_002",
- name: "流量趋势分析模型",
- type: "Regression",
- status: "training",
- accuracy: 0.87,
- lastTrained: "2024-01-15T08:00:00Z",
- parameters: {
- algorithm: "LSTM",
- features: 15,
- epochs: 200,
- learningRate: 0.001,
- },
+ id: 4,
+ name: "企业CRM数据",
+ type: "数据库直连",
+ status: "active",
+ lastSync: "1分钟前",
+ users: 2456789,
+ syncRate: 99.1,
+ dataSize: "156GB",
},
{
- id: "model_003",
- name: "用户聚类模型",
- type: "Clustering",
- status: "ready",
- accuracy: 0.89,
- lastTrained: "2024-01-13T12:00:00Z",
- parameters: {
- algorithm: "KMeans",
- clusters: 8,
- features: 20,
- iterations: 300,
- },
+ id: 5,
+ name: "淘宝店铺数据",
+ type: "API接入",
+ status: "error",
+ lastSync: "授权过期",
+ users: 345678,
+ syncRate: 0,
+ dataSize: "18GB",
},
- ])
+ ]
- const [isAddingDataSource, setIsAddingDataSource] = useState(false)
- const [isTrainingModel, setIsTrainingModel] = useState(false)
- const [newDataSource, setNewDataSource] = useState({
- name: "",
- type: "MySQL",
- host: "",
- port: "",
- database: "",
- username: "",
- password: "",
- description: "",
- })
+ // 数据清洗进度
+ const cleaningTasks = [
+ { name: "手机号格式校验", total: 4285670, processed: 4200000, progress: 98.0, errors: 1250 },
+ { name: "重复数据去除", total: 4285670, processed: 3850000, progress: 89.8, errors: 85000 },
+ { name: "缺失字段补全", total: 4285670, processed: 3250000, progress: 75.8, errors: 125000 },
+ { name: "异常值检测", total: 4285670, processed: 4100000, progress: 95.7, errors: 8500 },
+ ]
- const [modelTrainingConfig, setModelTrainingConfig] = useState({
- modelId: "",
- algorithm: "RandomForest",
- features: 25,
- epochs: 100,
- learningRate: 0.01,
- validationSplit: 0.2,
- autoTune: true,
- })
+ // 标签分类
+ const tagCategories = [
+ { name: "用户价值", count: 5, color: "bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400" },
+ { name: "活跃度", count: 4, color: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400" },
+ { name: "消费行为", count: 6, color: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" },
+ { name: "兴趣偏好", count: 12, color: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400" },
+ { name: "生命周期", count: 5, color: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" },
+ { name: "地域分布", count: 8, color: "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-400" },
+ ]
- // 添加数据源
- const handleAddDataSource = async () => {
- try {
- const newSource: DataSource = {
- id: `ds_${Date.now()}`,
- name: newDataSource.name,
- type: newDataSource.type,
- status: "connected",
- lastSync: new Date().toISOString(),
- recordCount: 0,
- description: newDataSource.description,
- }
-
- setDataSources((prev) => [...prev, newSource])
- setIsAddingDataSource(false)
- setNewDataSource({
- name: "",
- type: "MySQL",
- host: "",
- port: "",
- database: "",
- username: "",
- password: "",
- description: "",
- })
-
- // 模拟数据导入
- setTimeout(() => {
- setDataSources((prev) =>
- prev.map((ds) =>
- ds.id === newSource.id
- ? { ...ds, recordCount: Math.floor(Math.random() * 1000000) + 10000, status: "connected" as const }
- : ds,
- ),
- )
- }, 2000)
- } catch (error) {
- console.error("添加数据源失败:", error)
- }
+ // 标签系统统计
+ const tagStats = {
+ totalTags: 40,
+ systemTags: 25,
+ customTags: 15,
+ activeRules: 28,
+ todayTagged: 28450,
}
- // 同步数据源
- const handleSyncDataSource = (id: string) => {
- setDataSources((prev) =>
- prev.map((ds) => (ds.id === id ? { ...ds, status: "syncing" as const, lastSync: new Date().toISOString() } : ds)),
- )
+ // 数据质量检测
+ const qualityChecks = [
+ { type: "重复数据", count: 85420, percentage: 2.0, severity: "warning" },
+ { type: "异常数据", count: 12850, percentage: 0.3, severity: "error" },
+ { type: "缺失字段", count: 325680, percentage: 7.6, severity: "warning" },
+ { type: "格式错误", count: 5280, percentage: 0.12, severity: "error" },
+ ]
- // 模拟同步完成
- setTimeout(() => {
- setDataSources((prev) =>
- prev.map((ds) =>
- ds.id === id
- ? {
- ...ds,
- status: "connected" as const,
- recordCount: ds.recordCount + Math.floor(Math.random() * 10000),
- lastSync: new Date().toISOString(),
- }
- : ds,
- ),
- )
- }, 3000)
- }
-
- // 训练AI模型
- const handleTrainModel = async () => {
- if (!modelTrainingConfig.modelId) return
-
- setIsTrainingModel(true)
-
- // 更新模型状态为训练中
- setAiModels((prev) =>
- prev.map((model) =>
- model.id === modelTrainingConfig.modelId ? { ...model, status: "training" as const } : model,
- ),
- )
-
- // 模拟训练过程
- setTimeout(() => {
- setAiModels((prev) =>
- prev.map((model) =>
- model.id === modelTrainingConfig.modelId
- ? {
- ...model,
- status: "ready" as const,
- accuracy: Math.random() * 0.1 + 0.85,
- lastTrained: new Date().toISOString(),
- parameters: {
- algorithm: modelTrainingConfig.algorithm,
- features: modelTrainingConfig.features,
- epochs: modelTrainingConfig.epochs,
- learningRate: modelTrainingConfig.learningRate,
- },
- }
- : model,
- ),
- )
- setIsTrainingModel(false)
- }, 5000)
- }
-
- // 格式化数字
const formatNumber = (num: number): string => {
- if (num >= 1000000000) {
- return `${(num / 1000000000).toFixed(1)}B`
- }
- if (num >= 1000000) {
- return `${(num / 1000000).toFixed(1)}M`
- }
- if (num >= 1000) {
- return `${(num / 1000).toFixed(1)}K`
- }
+ if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
+ if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
return num.toString()
}
- // 获取状态颜色
- const getStatusColor = (status: string) => {
- switch (status) {
- case "connected":
- case "ready":
- return "text-green-600 bg-green-50 border-green-200"
- case "syncing":
- case "training":
- return "text-yellow-600 bg-yellow-50 border-yellow-200"
- case "disconnected":
- case "error":
- return "text-red-600 bg-red-50 border-red-200"
- default:
- return "text-gray-600 bg-gray-50 border-gray-200"
- }
- }
-
- // 获取状态图标
- const getStatusIcon = (status: string) => {
- switch (status) {
- case "connected":
- case "ready":
- return
- case "syncing":
- case "training":
- return
- case "disconnected":
- case "error":
- return
- default:
- return
- }
- }
-
return (
-
-
- {/* 页面标题 */}
-
-
数据中台
-
数据源管理与AI模型训练平台
+
+ {/* Header */}
+
+
+
数据中台
+
管理数据源、清洗规则和标签体系
+
+
+
+
-
-
-
-
-
- 数据源管理
-
-
-
- AI模型
-
-
-
- {/* 数据源管理 */}
-
-
-
数据源管理
-
-
-
-
- {dataSources.map((source) => (
-
-
-
- {source.name}
-
- {getStatusIcon(source.status)}
- {source.status}
-
-
- {source.description}
-
-
-
-
- 类型:
- {source.type}
-
-
- 记录数:
- {formatNumber(source.recordCount)}
-
-
-
-
- 最后同步:
- {new Date(source.lastSync).toLocaleString()}
-
-
-
-
-
-
-
-
- ))}
-
-
-
- {/* AI模型管理 */}
-
-
-
AI模型管理
-
-
-
-
- {aiModels.map((model) => (
-
-
-
- {model.name}
-
- {getStatusIcon(model.status)}
- {model.status}
-
-
- {model.type} 模型
-
-
-
-
- 准确率
- {(model.accuracy * 100).toFixed(1)}%
-
-
-
-
-
-
- 算法:
- {model.parameters.algorithm}
-
-
- 特征数:
- {model.parameters.features}
-
-
-
-
- 最后训练:
- {new Date(model.lastTrained).toLocaleString()}
-
-
-
-
-
-
-
-
- ))}
-
-
-
+
+ {/* 子功能导航卡片 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 数据源
+ 数据清洗
+ 标签体系
+ 质量检测
+
+
+ {/* 数据源管理 */}
+
+ {/* 搜索栏 */}
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+
+ {/* 数据源列表 */}
+
+
+ 已接入数据源
+ 管理所有已配置的数据源连接
+
+
+
+ {dataSources.map((source) => (
+
+
+
+
+
+
+
{source.name}
+
+ {source.type} · {formatNumber(source.users)} 条记录
+
+
+
+
+
+
+ {source.status === "active" && }
+ {source.status === "pending" && }
+ {source.status === "error" && }
+ {source.status === "active" ? "已连接" : source.status === "pending" ? "待配置" : "错误"}
+
+
{source.lastSync}
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* 数据清洗 */}
+
+
+
+ 清洗任务
+ 数据清洗与标准化任务进度
+
+
+
+ {cleaningTasks.map((task, index) => (
+
+
+
+
+
{task.name}
+
{formatNumber(task.processed)}
+
+
0 ? "secondary" : "outline"}
+ >
+ {task.progress === 100 ? "已完成" : task.progress > 0 ? "运行中" : "等待中"}
+
+
+
+
+ {task.progress}% 完成
+ {formatNumber(task.errors)} 异常
+
+
+ ))}
+
+
+
+
+
+ {/* 标签系统 */}
+
+ {/* 统计卡片 */}
+
+
+
+
+ {tagStats.totalTags}
+ 标签总数
+
+
+
+
+
+ {tagStats.systemTags}
+ 系统标签
+
+
+
+
+
+ {tagStats.customTags}
+ 自定义标签
+
+
+
+
+
+ {tagStats.activeRules}
+ 活跃规则
+
+
+
+
+
+ {formatNumber(tagStats.todayTagged)}
+ 今日打标
+
+
+
+
+ {/* 标签分类 */}
+
+
+ 标签分类
+ 用户标签体系管理
+
+
+
+ {tagCategories.map((category) => (
+
+
+
+
+
+
{category.name}
+
{category.count} 个标签
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* 质量检测 */}
+
+
+
+ 数据质量报告
+ 检测并修复数据质量问题
+
+
+
+ {qualityChecks.map((check, index) => (
+
+
+
+
+ {check.severity === "error" ? "严重" : "警告"}
+
+
+
+
+
{formatNumber(check.count)}
+
问题数量
+
+
+
{check.percentage}%
+
占比
+
+
+
+ ))}
+
+
+
+
+
)
}
diff --git a/app/globals.css b/app/globals.css
index 93849a9..4004786 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -4,14 +4,14 @@
@layer base {
:root {
- --background: 240 10% 98%;
+ --background: 0 0% 100%;
--foreground: 240 10% 3.9%;
- --card: 240 10% 100%;
+ --card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
- --popover: 240 10% 100%;
+ --popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
- --primary-foreground: 240 5.9% 98%;
+ --primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
@@ -23,26 +23,26 @@
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
- --radius: 0.75rem;
+ --radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
- --foreground: 240 5.9% 98%;
+ --foreground: 0 0% 98%;
--card: 240 10% 3.9%;
- --card-foreground: 240 5.9% 98%;
+ --card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
- --popover-foreground: 240 5.9% 98%;
- --primary: 240 5.9% 98%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
- --secondary-foreground: 240 5.9% 98%;
+ --secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
- --accent-foreground: 240 5.9% 98%;
+ --accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
- --destructive-foreground: 240 5.9% 98%;
+ --destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
@@ -53,140 +53,7 @@
* {
@apply border-border;
}
-
- html {
- @apply scroll-smooth;
- }
-
body {
- @apply bg-gradient-to-br from-blue-50 via-white to-purple-50 text-foreground min-h-screen;
- background-attachment: fixed;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
-
- .dark body {
- @apply bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900;
- }
-
- @media (max-width: 768px) {
- body {
- background-attachment: scroll;
- }
- }
-}
-
-@layer components {
- .glass {
- @apply backdrop-blur-md border border-white/20;
- background-color: rgba(255, 255, 255, 0.1);
- box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
- }
-
- .glass-light {
- @apply backdrop-blur-sm border border-white/30;
- background-color: rgba(255, 255, 255, 0.2);
- box-shadow: 0 4px 16px 0 rgba(31, 38, 135, 0.2);
- }
-
- .glass-heavy {
- @apply backdrop-blur-xl border border-white/40;
- background-color: rgba(255, 255, 255, 0.3);
- box-shadow: 0 16px 64px 0 rgba(31, 38, 135, 0.5);
- }
-
- .glass-dark {
- @apply backdrop-blur-md border border-white/10;
- background-color: rgba(0, 0, 0, 0.1);
- box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
- }
-
- @media (max-width: 768px) {
- .glass {
- @apply backdrop-blur-sm;
- background-color: rgba(255, 255, 255, 0.15);
- }
-
- .glass-light {
- @apply backdrop-blur-sm;
- background-color: rgba(255, 255, 255, 0.25);
- }
-
- .glass-heavy {
- @apply backdrop-blur-md;
- background-color: rgba(255, 255, 255, 0.35);
- }
- }
-
- .glass-card {
- @apply glass rounded-2xl p-6 transition-all duration-300;
- }
-
- .glass-card:hover {
- background-color: rgba(255, 255, 255, 0.2);
- }
-
- @media (max-width: 768px) {
- .glass-card {
- @apply p-4 rounded-xl;
- }
- }
-
- .glass-nav {
- @apply glass-light rounded-2xl transition-all duration-300;
- }
-
- .glass-button {
- @apply glass-light rounded-xl px-4 py-2 transition-all duration-300;
- }
-
- .glass-button:hover {
- background-color: rgba(255, 255, 255, 0.3);
- transform: scale(1.05);
- }
-
- @media (max-width: 768px) {
- .glass-button {
- @apply px-6 py-3 text-base;
- min-height: 44px;
- }
- }
-
- .glass-input {
- @apply glass-light rounded-xl px-4 py-2 transition-all duration-300;
- }
-
- .glass-input:focus {
- background-color: rgba(255, 255, 255, 0.3);
- @apply ring-2 ring-white/50;
- }
-
- @media (max-width: 768px) {
- .glass-input {
- @apply px-4 py-3 text-base;
- min-height: 44px;
- }
- }
-
- .safe-area-top {
- padding-top: env(safe-area-inset-top);
- }
-
- .safe-area-bottom {
- padding-bottom: env(safe-area-inset-bottom);
- }
-
- .safe-area-left {
- padding-left: env(safe-area-inset-left);
- }
-
- .safe-area-right {
- padding-right: env(safe-area-inset-right);
- }
-}
-
-@media (max-width: 768px) {
- .overflow-scroll {
- -webkit-overflow-scrolling: touch;
+ @apply bg-background text-foreground;
}
}
diff --git a/app/layout.tsx b/app/layout.tsx
index 17f5e9d..a8a6682 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -2,6 +2,7 @@ import type React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import ClientLayout from "./ClientLayout"
+import "./globals.css"
const inter = Inter({ subsets: ["latin"] })
@@ -18,6 +19,3 @@ export default function RootLayout({
}) {
return
{children}
}
-
-
-import './globals.css'
\ No newline at end of file
diff --git a/app/login/page.tsx b/app/login/page.tsx
index 9919308..76c0403 100644
--- a/app/login/page.tsx
+++ b/app/login/page.tsx
@@ -2,7 +2,7 @@
import type React from "react"
import { useState, useEffect } from "react"
-import { Eye, EyeOff, Mail, Phone } from "lucide-react"
+import { Eye, EyeOff, Phone } from "lucide-react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
@@ -12,8 +12,8 @@ import { WeChatIcon } from "@/components/icons/wechat-icon"
import { AppleIcon } from "@/components/icons/apple-icon"
import { useToast } from "@/components/ui/use-toast"
-// 使用环境变量:不配置则用本地 API(支持 zhiqun@qq.com / Zhiqun1984)
-const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ""
+// 使用环境变量获取API域名
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.example.com"
// 定义登录响应类型
interface LoginResponse {
@@ -25,7 +25,7 @@ interface LoginResponse {
}
interface LoginForm {
- account: string // 邮箱或手机号
+ phone: string
password: string
verificationCode: string
agreeToTerms: boolean
@@ -36,7 +36,7 @@ export default function LoginPage() {
const [activeTab, setActiveTab] = useState<"password" | "verification">("password")
const [isLoading, setIsLoading] = useState(false)
const [form, setForm] = useState
({
- account: "",
+ phone: "",
password: "",
verificationCode: "",
agreeToTerms: false,
@@ -55,11 +55,11 @@ export default function LoginPage() {
}
const validateForm = () => {
- if (!form.account.trim()) {
+ if (!form.phone) {
toast({
variant: "destructive",
- title: "请输入账号",
- description: "请输入邮箱或手机号",
+ title: "请输入手机号",
+ description: "手机号不能为空",
})
return false
}
@@ -101,9 +101,9 @@ export default function LoginPage() {
setIsLoading(true)
try {
+ // 创建FormData对象
const formData = new FormData()
- const isEmail = form.account.includes("@")
- formData.append(isEmail ? "email" : "phone", form.account)
+ formData.append("phone", form.phone)
if (activeTab === "password") {
formData.append("password", form.password)
@@ -111,8 +111,8 @@ export default function LoginPage() {
formData.append("verificationCode", form.verificationCode)
}
- const apiUrl = API_BASE_URL ? `${API_BASE_URL}/auth/login` : "/api/auth/login"
- const response = await fetch(apiUrl, {
+ // 发送登录请求
+ const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: "POST",
body: formData,
// 不需要设置Content-Type,浏览器会自动设置为multipart/form-data并添加boundary
@@ -125,7 +125,7 @@ export default function LoginPage() {
localStorage.setItem("token", result.data.token)
// 成功后跳转
- router.push("/")
+ router.push("/profile")
toast({
title: "登录成功",
@@ -146,7 +146,7 @@ export default function LoginPage() {
}
const handleSendVerificationCode = async () => {
- if (!form.account.trim()) {
+ if (!form.phone) {
toast({
variant: "destructive",
title: "请输入手机号",
@@ -157,11 +157,12 @@ export default function LoginPage() {
setIsLoading(true)
try {
+ // 创建FormData对象
const formData = new FormData()
- formData.append("phone", form.account)
+ formData.append("phone", form.phone)
- const apiUrl = API_BASE_URL ? `${API_BASE_URL}/auth/send-code` : "/api/auth/send-code"
- const response = await fetch(apiUrl, {
+ // 发送验证码请求
+ const response = await fetch(`${API_BASE_URL}/auth/send-code`, {
method: "POST",
body: formData,
})
@@ -219,25 +220,22 @@ export default function LoginPage() {
-
支持 邮箱 / 手机号 / 微信 / Apple 登录
+
你所在地区仅支持 手机号 / 微信 / Apple 登录