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

@@ -1,6 +1,4 @@
// MindsDB连接器 - 实现AI增强的数据查询和分析
import { Client } from "mindsdb-js-sdk"
// MindsDB连接器 - 模拟实现,避免第三方包兼容性问题
export interface MindsDBConfig {
host: string
port: number
@@ -31,26 +29,74 @@ export interface VersionInfo {
author: string
}
// 模拟数据
const mockUsers = [
{
id: "user_001",
type: "user",
username: "张三",
phone: "13800138001",
email: "zhangsan@example.com",
tags: ["高价值用户", "活跃用户"],
rfm_score: 85,
last_active: "2024-01-15T10:30:00Z",
created_at: "2023-06-01T08:00:00Z",
relevance_score: 0.95,
},
{
id: "user_002",
type: "user",
username: "李四",
phone: "13800138002",
email: "lisi@example.com",
tags: ["新用户", "潜在客户"],
rfm_score: 65,
last_active: "2024-01-14T15:20:00Z",
created_at: "2024-01-01T09:00:00Z",
relevance_score: 0.88,
},
]
const mockTrafficKeywords = [
{
id: "keyword_001",
type: "traffic",
keyword: "数据分析",
category: "技术",
search_volume: 12000,
competition: "高",
cpc: 3.5,
trend_data: [100, 120, 110, 130, 125],
last_updated: "2024-01-15T12:00:00Z",
relevance_score: 0.92,
},
{
id: "keyword_002",
type: "traffic",
keyword: "用户画像",
category: "营销",
search_volume: 8500,
competition: "中",
cpc: 2.8,
trend_data: [80, 90, 95, 100, 105],
last_updated: "2024-01-15T11:30:00Z",
relevance_score: 0.87,
},
]
export class MindsDBConnector {
private client: Client
private connected = false
private cache: Map<string, any> = new Map()
constructor(private config: MindsDBConfig) {
this.client = new Client({
host: config.host,
port: config.port,
username: config.username,
password: config.password,
})
}
constructor(private config: MindsDBConfig) {}
// 连接到MindsDB
// 连接到MindsDB(模拟)
async connect(): Promise<void> {
try {
await this.client.connect()
// 模拟连接延迟
await new Promise((resolve) => setTimeout(resolve, 100))
this.connected = true
console.log("MindsDB连接成功")
console.log("MindsDB连接成功(模拟)")
} catch (error) {
console.error("MindsDB连接失败:", error)
throw error
@@ -60,12 +106,11 @@ export class MindsDBConnector {
// 断开连接
async disconnect(): Promise<void> {
if (this.connected) {
await this.client.disconnect()
this.connected = false
}
}
// AI增强查询 - 使用自然语言查询数据
// AI增强查询(模拟)
async aiQuery(request: AIQueryRequest): Promise<any> {
if (!this.connected) {
await this.connect()
@@ -79,17 +124,30 @@ export class MindsDBConnector {
}
try {
// 使用MindsDB的AI模型进行查询
const query = `
SELECT * FROM mindsdb.${request.model || "gpt4"}
WHERE text = '${request.query}'
`
const result = await this.client.query(query)
// 模拟AI查询结果
const result = {
response: `基于查询"${request.query}"的AI分析结果`,
confidence: 0.85,
suggestions: [`${request.query}相关建议1`, `${request.query}相关建议2`, `${request.query}相关建议3`],
insights: [
{
title: "数据洞察1",
description: `关于"${request.query}"的重要发现`,
confidence: 0.9,
},
{
title: "数据洞察2",
description: `"${request.query}"的趋势分析`,
confidence: 0.8,
},
],
intent: this.analyzeIntent(request.query),
filters: this.generateFilters(request.query),
}
// 缓存结果
if (request.useCache) {
this.cache.set(cacheKey, result, 300000) // 5分钟缓存
this.cache.set(cacheKey, result)
}
return result
@@ -99,7 +157,7 @@ export class MindsDBConnector {
}
}
// 智能搜索 - 支持用户数据和流量关键词的快速搜索
// 智能搜索(模拟)
async intelligentSearch(request: SearchRequest): Promise<any> {
if (!this.connected) {
await this.connect()
@@ -113,179 +171,142 @@ export class MindsDBConnector {
}
try {
let searchQuery = ""
let results: any[] = []
switch (request.type) {
case "user":
searchQuery = this.buildUserSearchQuery(request)
results = this.searchUsers(request)
break
case "traffic":
searchQuery = this.buildTrafficSearchQuery(request)
results = this.searchTraffic(request)
break
case "all":
searchQuery = this.buildUnifiedSearchQuery(request)
results = [...this.searchUsers(request), ...this.searchTraffic(request)]
break
}
const result = await this.client.query(searchQuery)
// 应用过滤器
if (request.filters) {
results = this.applyFilters(results, request.filters)
}
// 分页
const start = request.offset || 0
const end = start + (request.limit || 50)
results = results.slice(start, end)
// 缓存结果
this.cache.set(cacheKey, result, 60000) // 1分钟缓存
this.cache.set(cacheKey, results)
return result
return results
} catch (error) {
console.error("智能搜索失败:", error)
throw error
}
}
// 构建用户搜索查询
private buildUserSearchQuery(request: SearchRequest): string {
const { keyword, filters, limit = 100, offset = 0 } = request
// 搜索用户
private searchUsers(request: SearchRequest): any[] {
const keyword = request.keyword.toLowerCase()
return mockUsers.filter(
(user) =>
user.username.toLowerCase().includes(keyword) ||
user.phone.includes(keyword) ||
user.email.toLowerCase().includes(keyword) ||
user.tags.some((tag) => tag.toLowerCase().includes(keyword)),
)
}
let query = `
SELECT
u.user_id,
u.username,
u.phone,
u.email,
u.tags,
u.rfm_score,
u.last_active,
u.created_at,
MATCH(u.username, u.phone, u.email, u.tags) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE) as relevance_score
FROM users u
WHERE MATCH(u.username, u.phone, u.email, u.tags) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE)
`
// 搜索流量关键词
private searchTraffic(request: SearchRequest): any[] {
const keyword = request.keyword.toLowerCase()
return mockTrafficKeywords.filter(
(traffic) => traffic.keyword.toLowerCase().includes(keyword) || traffic.category.toLowerCase().includes(keyword),
)
}
// 添加过滤条件
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
query += ` AND u.${key} = '${value}'`
// 应用过滤器
private applyFilters(results: any[], filters: Record<string, any>): any[] {
return results.filter((item) => {
return Object.entries(filters).every(([key, value]) => {
if (typeof value === "object" && value.$gte) {
return item[key] >= value.$gte
}
return item[key] === value
})
})
}
// 分析查询意图
private analyzeIntent(query: string): string {
const lowerQuery = query.toLowerCase()
if (lowerQuery.includes("高价值") || lowerQuery.includes("vip")) {
return "high_value_users"
}
if (lowerQuery.includes("最近") || lowerQuery.includes("活跃")) {
return "recent_activity"
}
if (lowerQuery.includes("流量") || lowerQuery.includes("关键词")) {
return "traffic_analysis"
}
query += ` ORDER BY relevance_score DESC, u.last_active DESC`
query += ` LIMIT ${limit} OFFSET ${offset}`
return query
return "general_search"
}
// 构建流量关键词搜索查询
private buildTrafficSearchQuery(request: SearchRequest): string {
const { keyword, filters, limit = 100, offset = 0 } = request
// 生成过滤器
private generateFilters(query: string): Record<string, any> {
const filters: Record<string, any> = {}
const lowerQuery = query.toLowerCase()
let query = `
SELECT
t.keyword_id,
t.keyword,
t.category,
t.search_volume,
t.competition,
t.cpc,
t.trend_data,
t.last_updated,
MATCH(t.keyword, t.category) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE) as relevance_score
FROM traffic_keywords t
WHERE MATCH(t.keyword, t.category) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE)
`
// 添加过滤条件
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
query += ` AND t.${key} = '${value}'`
})
if (lowerQuery.includes("高价值")) {
filters.rfm_score = { $gte: 80 }
}
if (lowerQuery.includes("最近")) {
filters.last_active = { $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
}
query += ` ORDER BY relevance_score DESC, t.search_volume DESC`
query += ` LIMIT ${limit} OFFSET ${offset}`
return query
return filters
}
// 构建统一搜索查询
private buildUnifiedSearchQuery(request: SearchRequest): string {
const { keyword, limit = 100, offset = 0 } = request
return `
(
SELECT
'user' as type,
user_id as id,
username as title,
CONCAT(phone, ' | ', email) as description,
tags,
last_active as updated_at,
MATCH(username, phone, email, tags) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE) as relevance_score
FROM users
WHERE MATCH(username, phone, email, tags) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE)
)
UNION ALL
(
SELECT
'traffic' as type,
keyword_id as id,
keyword as title,
CONCAT('搜索量: ', search_volume, ' | 竞争度: ', competition) as description,
category as tags,
last_updated as updated_at,
MATCH(keyword, category) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE) as relevance_score
FROM traffic_keywords
WHERE MATCH(keyword, category) AGAINST('${keyword}' IN NATURAL LANGUAGE MODE)
)
ORDER BY relevance_score DESC
LIMIT ${limit} OFFSET ${offset}
`
}
// 用户数据分析 - 使用AI进行用户行为分析
// 用户数据分析(模拟)
async analyzeUserBehavior(userId: string): Promise<any> {
if (!this.connected) {
await this.connect()
}
try {
const query = `
SELECT
prediction,
confidence,
explanation
FROM mindsdb.user_behavior_predictor
WHERE user_id = '${userId}'
`
return await this.client.query(query)
return {
prediction: "高价值用户",
confidence: 0.85,
explanation: `用户${userId}具有高活跃度和消费潜力`,
}
} catch (error) {
console.error("用户行为分析失败:", error)
throw error
}
}
// 流量预测 - 使用AI预测流量趋势
// 流量预测(模拟)
async predictTrafficTrends(keyword: string, timeframe = "30d"): Promise<any> {
if (!this.connected) {
await this.connect()
}
try {
const query = `
SELECT
predicted_volume,
trend_direction,
confidence_interval,
factors
FROM mindsdb.traffic_predictor
WHERE keyword = '${keyword}' AND timeframe = '${timeframe}'
`
return await this.client.query(query)
return {
predicted_volume: Math.floor(Math.random() * 10000) + 5000,
trend_direction: Math.random() > 0.5 ? "上升" : "下降",
confidence_interval: [0.7, 0.9],
factors: ["季节性变化", "行业趋势", "竞争环境"],
}
} catch (error) {
console.error("流量预测失败:", error)
throw error
}
}
// 版本管理 - 创建数据版本
// 版本管理(模拟)
async createVersion(data: any, author: string, changes: string[]): Promise<VersionInfo> {
const version = `v${Date.now()}`
const timestamp = new Date().toISOString()
@@ -298,13 +319,8 @@ export class MindsDBConnector {
}
try {
// 存储版本信息
const query = `
INSERT INTO data_versions (version, timestamp, data_snapshot, changes, author)
VALUES ('${version}', '${timestamp}', '${JSON.stringify(data)}', '${JSON.stringify(changes)}', '${author}')
`
await this.client.query(query)
// 模拟存储版本信息
console.log("创建版本:", versionInfo)
return versionInfo
} catch (error) {
console.error("创建版本失败:", error)
@@ -312,69 +328,57 @@ export class MindsDBConnector {
}
}
// 获取版本历史
// 获取版本历史(模拟)
async getVersionHistory(limit = 50): Promise<VersionInfo[]> {
if (!this.connected) {
await this.connect()
}
try {
const query = `
SELECT version, timestamp, changes, author
FROM data_versions
ORDER BY timestamp DESC
LIMIT ${limit}
`
const result = await this.client.query(query)
return result.rows || []
// 模拟版本历史数据
return [
{
version: "v1704067200000",
timestamp: "2024-01-01T00:00:00Z",
changes: ["初始版本", "基础功能实现"],
author: "系统管理员",
},
{
version: "v1704153600000",
timestamp: "2024-01-02T00:00:00Z",
changes: ["添加用户搜索功能", "优化界面显示"],
author: "开发团队",
},
]
} catch (error) {
console.error("获取版本历史失败:", error)
throw error
}
}
// 恢复到指定版本
// 恢复到指定版本(模拟)
async restoreVersion(version: string): Promise<any> {
if (!this.connected) {
await this.connect()
}
try {
const query = `
SELECT data_snapshot
FROM data_versions
WHERE version = '${version}'
`
const result = await this.client.query(query)
if (result.rows && result.rows.length > 0) {
return JSON.parse(result.rows[0].data_snapshot)
}
throw new Error(`版本 ${version} 不存在`)
console.log(`恢复到版本: ${version}`)
return { success: true, message: `已恢复到版本 ${version}` }
} catch (error) {
console.error("恢复版本失败:", error)
throw error
}
}
// 实时数据同步
// 实时数据同步(模拟)
async syncRealTimeData(source: string, data: any): Promise<void> {
if (!this.connected) {
await this.connect()
}
try {
const query = `
INSERT INTO real_time_data (source, data, timestamp)
VALUES ('${source}', '${JSON.stringify(data)}', NOW())
ON DUPLICATE KEY UPDATE
data = '${JSON.stringify(data)}',
timestamp = NOW()
`
await this.client.query(query)
console.log(`同步数据源 ${source}:`, data)
} catch (error) {
console.error("实时数据同步失败:", error)
throw error
@@ -386,27 +390,18 @@ export class MindsDBConnector {
this.cache.clear()
}
// 获取系统状态
// 获取系统状态(模拟)
async getSystemStatus(): Promise<any> {
if (!this.connected) {
await this.connect()
}
try {
const queries = [
"SELECT COUNT(*) as user_count FROM users",
"SELECT COUNT(*) as keyword_count FROM traffic_keywords",
"SELECT COUNT(*) as version_count FROM data_versions",
"SELECT AVG(response_time) as avg_response_time FROM query_logs WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)",
]
const results = await Promise.all(queries.map((query) => this.client.query(query)))
return {
userCount: results[0].rows[0].user_count,
keywordCount: results[1].rows[0].keyword_count,
versionCount: results[2].rows[0].version_count,
avgResponseTime: results[3].rows[0].avg_response_time || 0,
userCount: 4000000000, // 40亿用户
keywordCount: 150000,
versionCount: 25,
avgResponseTime: 120,
cacheSize: this.cache.size,
connected: this.connected,
}
@@ -426,7 +421,15 @@ export function getMindsDBConnector(config?: MindsDBConfig): MindsDBConnector {
}
if (!mindsDBInstance) {
throw new Error("MindsDB连接器未初始化请提供配置信息")
// 提供默认配置
const defaultConfig: MindsDBConfig = {
host: "localhost",
port: 47334,
username: "mindsdb",
password: "",
database: "mindsdb",
}
mindsDBInstance = new MindsDBConnector(defaultConfig)
}
return mindsDBInstance

206
lib/mock-users.ts Normal file
View File

@@ -0,0 +1,206 @@
export type Status = "活跃" | "沉睡" | "已封禁"
export type UserBase = {
id: string
name: string
phone: string
email: string
tags: string[]
rfmScore: number
lastActivity: string
status: Status
}
export type UserDetail = UserBase & {
avatar?: string
company?: string
position?: string
recency: number
frequency: number
monetary: number
interactions: { id: string; type: string; time: string; note?: string }[]
purchaseHistory: { id: string; amount: number; time: string; item: string }[]
wechatAccounts: { id: string; nickname: string; avatar?: string }[]
}
/* helpers */
const NOW = Date.now()
const rand = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min
const maskPhone = (p: string) => p.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2")
const pick = <T,>(arr: T[]) => arr[rand(0, arr.length - 1)]
const TAGS = [
"高价值用户", "活跃用户", "潜在客户", "价格敏感", "科技爱好者",
"内容创作者", "一线城市", "二线城市", "iPhone", "Android",
"社群成员", "低活跃", "沉睡风险", "新用户", "忠诚用户",
]
const COMPANIES = ["合星科技", "云杉数智", "万像互动", "星远数科", "数研云", "青瓦科技"]
const POSITIONS = ["产品经理", "运营经理", "市场总监", "技术负责人", "销售", "数据分析师"]
const AVATARS = [
"/user-avatar-zhangsan.png",
"/user-avatar-lisi.png",
"/wechat-avatar-1.png",
"/wechat-avatar-2.png",
"/wechat-avatar-3.png",
]
/* seed users */
const baseNames = [
"王磊","刘婷","张三","李四","赵六","钱七","周敏","孙悦","吴迪","郑航",
"冯晨","褚野","卫国","蒋楠","沈静","韩睿","唐奕","曹越","彭博","鲁洋",
"韦东","昌华","顾诚","孟辉","尹雪","谭清","严杰","霍宇","龚一","程远",
]
const USERS: UserDetail[] = baseNames.slice(0, 24).map((name, idx) => {
const n = idx + 1
const rawPhone = `1${rand(3,9)}${rand(0,9)}${rand(0,9)}${rand(10000000, 99999999)}`
const email = `${pinyinLike(name)}${n}@example.com`.toLowerCase()
const tagCount = rand(2, 5)
const tags = Array.from(new Set(Array.from({ length: tagCount }, () => pick(TAGS))))
const status: Status = ["活跃","活跃","活跃","沉睡","已封禁"][rand(0,4)]
const rfm = rand(45, 95)
const lastActivity = new Date(NOW - rand(0, 7) * 86400_000 - rand(0, 12) * 3600_000).toISOString()
const interactions = Array.from({ length: rand(1, 4) }).map((_, i) => ({
id: `i_${n}_${i}`,
type: pick(["咨询", "浏览", "下载白皮书", "提交表单", "聊天"]),
time: new Date(NOW - rand(0, 14) * 86400_000 - rand(0, 20) * 3600_000).toISOString(),
note: pick(["", "询价", "对比竞品", "需要发票", "待回访"]),
}))
const purchaseHistory = rand(0, 1)
? [{ id: `o_${n}_1`, amount: rand(299, 9999), time: new Date(NOW - rand(0, 30) * 86400_000).toISOString(), item: pick(["标准版SaaS","高级版SaaS","增值模块"]) }]
: []
const wechatAccounts = Array.from({ length: rand(1, 2) }).map((_, i) => ({
id: `wx_${n}_${i}`,
nickname: `${name}-微信${i+1}`,
avatar: pick(AVATARS),
}))
return {
id: `user_${1000 + n}`,
name,
phone: maskPhone(rawPhone),
email,
tags,
rfmScore: rfm,
lastActivity,
status,
avatar: pick(AVATARS),
company: pick(COMPANIES),
position: pick(POSITIONS),
recency: rand(1, 10),
frequency: rand(1, 30),
monetary: rand(0, 20000),
interactions,
purchaseHistory,
wechatAccounts,
}
})
function pinyinLike(name: string) {
// super simple fake pinyin-ish
const map: Record<string, string> = {
"王":"wang","张":"zhang","李":"li","刘":"liu","赵":"zhao","钱":"qian","孙":"sun","周":"zhou",
"吴":"wu","郑":"zheng","冯":"feng","褚":"chu","卫":"wei","蒋":"jiang","沈":"shen","韩":"han",
"唐":"tang","曹":"cao","彭":"peng","鲁":"lu","韦":"wei","昌":"chang","顾":"gu","孟":"meng",
"尹":"yin","谭":"tan","严":"yan","霍":"huo","龚":"gong","程":"cheng",
}
const first = map[name[0]] || "user"
const rest = "abcxyz"
return `${first}${rest[Math.floor(Math.random()*rest.length)]}${rest[Math.floor(Math.random()*rest.length)]}`
}
/* public APIs */
export type FilterOptions = {
q?: string
tags?: string[]
status?: Status[]
rfmMin?: number
rfmMax?: number
page?: number
pageSize?: number
}
export function getUsers(): UserBase[] {
return USERS.map(({ interactions, purchaseHistory, wechatAccounts, recency, frequency, monetary, company, position, avatar, ...u }) => u)
}
export function getUserDetail(id: string): UserDetail | null {
return USERS.find((u) => u.id === id) ?? null
}
export function getDistinctTags(): string[] {
const s = new Set<string>()
USERS.forEach((u) => u.tags.forEach((t) => s.add(t)))
return Array.from(s)
}
export function filterUsers(opts: FilterOptions) {
const {
q = "",
tags = [],
status = [],
rfmMin = 0,
rfmMax = 100,
page = 1,
pageSize = 20,
} = opts
let list = getUsers()
if (q) {
const ql = q.toLowerCase()
list = list.filter(
(u) =>
u.name.toLowerCase().includes(ql) ||
u.phone.includes(q) ||
u.email.toLowerCase().includes(ql) ||
u.tags.some((t) => t.toLowerCase().includes(ql)),
)
}
if (tags.length) {
list = list.filter((u) => tags.some((t) => u.tags.includes(t)))
}
if (status.length) {
list = list.filter((u) => status.includes(u.status))
}
list = list.filter((u) => u.rfmScore >= rfmMin && u.rfmScore <= rfmMax)
const total = list.length
const start = (page - 1) * pageSize
const end = start + pageSize
const items = list.slice(start, end)
return { items, total, page, pageSize }
}
export function addUser(payload: { name: string; phone: string; email: string; tags?: string[] }): UserDetail {
const n = USERS.length + 1000
const u: UserDetail = {
id: `user_${n}`,
name: payload.name,
phone: maskPhone(payload.phone),
email: payload.email,
tags: payload.tags ?? [],
rfmScore: 60 + (n % 40),
lastActivity: new Date().toISOString(),
status: "活跃",
avatar: pick(AVATARS),
company: pick(COMPANIES),
position: pick(POSITIONS),
recency: rand(1, 5),
frequency: rand(1, 10),
monetary: rand(0, 5000),
interactions: [],
purchaseHistory: [],
wechatAccounts: [],
}
USERS.unshift(u)
return u
}

22
lib/text-sanitize.ts Normal file
View File

@@ -0,0 +1,22 @@
/**
* 文本清洗工具:去除常见的 JSON 残留与转义符,避免卡片描述中出现 \\n、"]]} 等。
* - 保持幂等:多次调用不会破坏原文本语义
* - 安全:不做危险字符拼接或 HTML 注入
*/
export function sanitizeText(input?: string): string {
if (!input) return ""
let s = String(input)
// 1) 常见序列清洗
s = s.replace(/\\n/g, " ") // 去除转义换行
s = s.replace(/\s+/g, " ").trim()
// 2) 去掉串首尾常见 JSON 残留符号(宽松处理)
s = s.replace(/^[\s\["'{(\\]+/g, "")
s = s.replace(/["'\]\})\\\s]+$/g, "")
// 3) 反转义常见字符
s = s.replace(/\\"/g, '"').replace(/\\'/g, "'")
return s.trim()
}