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:
@@ -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
206
lib/mock-users.ts
Normal 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
22
lib/text-sanitize.ts
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user