feat: sync Sidebar and BottomNav, standardize user profile API

Align Sidebar & BottomNav menus, remove "Search", add user profile mock data, implement /api/users, add FilterDrawer, complete Section, ProfileHeader, MetricsRFM components

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-08-08 07:00:12 +00:00
parent 4eed69520c
commit f0a6a364f2
85 changed files with 3318 additions and 6786 deletions

View File

@@ -0,0 +1,56 @@
import { NextResponse } from "next/server"
import { analyzeUser, type AnalyzeInput } from "@/services/rfm-engine"
import { generateText } from "ai"
import { openai } from "@ai-sdk/openai"
/**
* 可选 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("。")
if (!text) return null
if (!process.env.OPENAI_API_KEY) return null
try {
const { text: out } = await generateText({
model: openai("gpt-4o"),
system:
"请根据用户聊天片段输出JSON字段emotion(积极|中性|消极), intent(弱意图|中等意图|强意图), behavior(数组中文标签最多3个)。只输出JSON。",
prompt: `聊天片段:${text}`,
// @ts-expect-error ai sdk typings are flexible in Next.js
maxTokens: 200,
})
const json = JSON.parse(out.trim())
return json
} catch {
return null
}
}
export async function POST(req: Request) {
try {
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 results = []
for (const input of inputs) {
const base = analyzeUser(input)
if (useAI) {
const ai = await aiTagging(input.chat_logs)
if (ai) {
base.tags.emotion = ai.emotion ?? base.tags.emotion
base.tags.intent = ai.intent ?? base.tags.intent
if (Array.isArray(ai.behavior)) {
base.tags.behavior = Array.from(new Set([...(base.tags.behavior ?? []), ...ai.behavior]))
}
}
}
results.push(base)
}
return NextResponse.json({ success: true, data: results })
} catch (e: any) {
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
}
}

View File

@@ -0,0 +1,13 @@
import { NextResponse } from "next/server"
import { dumpCsv } from "@/services/rfm-engine"
export async function GET() {
const csv = dumpCsv()
return new NextResponse(csv, {
status: 200,
headers: {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": `attachment; filename="rfm_export_${Date.now()}.csv"`,
},
})
}

View File

@@ -0,0 +1,12 @@
import { NextResponse } from "next/server"
import { getUserTags } from "@/services/rfm-engine"
export async function GET(req: Request) {
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 })
}

View File

@@ -0,0 +1,7 @@
import { NextResponse } from "next/server"
import { getGroupSummary } from "@/services/rfm-engine"
export async function GET() {
const data = getGroupSummary()
return NextResponse.json({ success: true, data })
}

View File

@@ -0,0 +1,12 @@
import { NextResponse } from "next/server"
import { computeRFM, type AnalyzeInput } from "@/services/rfm-engine"
export async function POST(req: Request) {
try {
const body = (await req.json()) as AnalyzeInput
const score = computeRFM(body)
return NextResponse.json({ success: true, data: score })
} catch (e: any) {
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
}
}

View File

@@ -0,0 +1,26 @@
import { NextResponse } from "next/server"
import { getWeights, setWeights } from "@/services/rfm-engine"
export async function GET() {
return NextResponse.json({ success: true, data: getWeights() })
}
export async function POST(req: Request) {
try {
const body = await req.json()
let R = Number(body.R ?? 0.5)
let F = Number(body.F ?? 0.3)
let M = Number(body.M ?? 0.2)
// 归一化
const sum = Math.max(0.000001, R + F + M)
R = R / sum
F = F / sum
M = M / sum
setWeights({ R, F, M })
return NextResponse.json({ success: true, data: { R, F, M } })
} catch (e: any) {
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
}
}

View File

@@ -1,18 +1,5 @@
import { type NextRequest, NextResponse } from "next/server"
import { getIntelligentSearchService } from "@/services/intelligent-search-service"
import { getMindsDBConnector } from "@/lib/mindsdb-connector"
// 初始化MindsDB连接
const mindsDBConfig = {
host: process.env.MINDSDB_HOST || "localhost",
port: Number.parseInt(process.env.MINDSDB_PORT || "47334"),
username: process.env.MINDSDB_USERNAME || "mindsdb",
password: process.env.MINDSDB_PASSWORD || "",
database: process.env.MINDSDB_DATABASE || "mindsdb",
}
// 初始化连接器
getMindsDBConnector(mindsDBConfig)
export async function GET(request: NextRequest) {
try {

View File

@@ -1,21 +1,15 @@
import { type NextRequest, NextResponse } from "next/server"
import { getMindsDBConnector } from "@/lib/mindsdb-connector"
import { getIntelligentSearchService } from "@/services/intelligent-search-service"
export async function GET(request: NextRequest) {
try {
const mindsDB = getMindsDBConnector()
const searchService = getIntelligentSearchService()
// 获取系统状态
const systemStatus = await mindsDB.getSystemStatus()
const searchStats = searchService.getSearchStats()
const status = await mindsDB.getSystemStatus()
return NextResponse.json({
system: systemStatus,
search: searchStats,
success: true,
status,
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION || "1.0.0",
})
} catch (error) {
console.error("系统状态API错误:", error)

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server"
import { NextResponse, NextRequest } from "next/server"
import type { TrafficUser } from "@/types/traffic"
import { addUser, filterUsers, getDistinctTags, getUserDetail } from "@/lib/mock-users"
// 中文名字生成器数据
const familyNames = [
@@ -191,82 +192,51 @@ const generateWechatFriends = (wechatId: string, count: number) => {
// 微信好友数据缓存
const wechatFriendsCache = new Map<string, TrafficUser[]>()
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const page = Number.parseInt(searchParams.get("page") || "1")
const pageSize = Number.parseInt(searchParams.get("pageSize") || "10")
const search = searchParams.get("search") || ""
const category = searchParams.get("category") || "all"
const source = searchParams.get("source") || "all"
const status = searchParams.get("status") || "all"
const startDate = searchParams.get("startDate")
const endDate = searchParams.get("endDate")
const wechatSource = searchParams.get("wechatSource") || ""
let filteredUsers = [...userPool]
// 如果有微信来源参数,生成或获取微信好友数据
if (wechatSource) {
if (!wechatFriendsCache.has(wechatSource)) {
// 生成150-300个随机好友
const friendCount = Math.floor(Math.random() * (300 - 150)) + 150
wechatFriendsCache.set(wechatSource, generateWechatFriends(wechatSource, friendCount))
}
filteredUsers = wechatFriendsCache.get(wechatSource) || []
}
// 应用过滤条件
filteredUsers = filteredUsers.filter((user) => {
const matchesSearch = search
? user.nickname.toLowerCase().includes(search.toLowerCase()) ||
user.wechatId.toLowerCase().includes(search.toLowerCase()) ||
user.phone.includes(search)
: true
const matchesCategory = category === "all" ? true : user.category === category
const matchesSource = source === "all" ? true : user.source === source
const matchesStatus = status === "all" ? true : user.status === status
const matchesDate =
startDate && endDate
? new Date(user.addTime) >= new Date(startDate) && new Date(user.addTime) <= new Date(endDate)
: true
return matchesSearch && matchesCategory && matchesSource && matchesStatus && matchesDate
})
// 按添加时间倒序排序
filteredUsers.sort((a, b) => new Date(b.addTime).getTime() - new Date(a.addTime).getTime())
// 计算分页
const total = filteredUsers.length
const totalPages = Math.ceil(total / pageSize)
const start = (page - 1) * pageSize
const end = start + pageSize
const users = filteredUsers.slice(start, end)
// 计算分类统计
const categoryStats = {
potential: userPool.filter((user) => user.category === "potential").length,
customer: userPool.filter((user) => user.category === "customer").length,
lost: userPool.filter((user) => user.category === "lost").length,
}
// 模拟网络延迟
await new Promise((resolve) => setTimeout(resolve, 500))
return NextResponse.json({
users,
pagination: {
total,
totalPages,
currentPage: page,
pageSize,
},
stats: {
total: wechatSource ? filteredUsers.length : userPool.length,
todayNew: wechatSource ? Math.floor(filteredUsers.length * 0.1) : todayUsers.length,
categoryStats,
},
})
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 meta = searchParams.get("meta")
const id = searchParams.get("id")
if (meta === "tags") {
const tags = getDistinctTags()
return NextResponse.json({ success: true, data: { tags } })
}
if (id) {
const user = getUserDetail(id)
if (!user) return NextResponse.json({ success: false, error: "NOT_FOUND" }, { status: 404 })
return NextResponse.json({ success: true, data: user })
}
const q = searchParams.get("q") || undefined
const tagsParam = searchParams.get("tags") || ""
const tags = tagsParam ? tagsParam.split(",").filter(Boolean) : []
const statusParam = searchParams.get("status") || ""
const status = statusParam ? (statusParam.split(",") as any) : []
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 res = filterUsers({ q, tags, status, rfmMin, rfmMax, page, pageSize })
return NextResponse.json({ success: true, data: res })
}
export async function POST(req: NextRequest) {
const body = await req.json().catch(() => null)
if (!body || !body.name || !body.phone || !body.email) {
return NextResponse.json({ success: false, error: "INVALID_PAYLOAD" }, { status: 400 })
}
const user = addUser({
name: body.name,
phone: body.phone,
email: body.email,
tags: Array.isArray(body.tags) ? body.tags.slice(0, 20) : [],
})
return NextResponse.json({ success: true, data: user })
}