Files
users/lib/mock-users.ts
v0 22e725887a refactor: restructure project into 5 core modules
Organize project by 5 core modules based on requirement docs.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2026-01-31 04:18:24 +00:00

321 lines
6.0 KiB
TypeScript

export type UserStatus = "活跃" | "沉睡" | "流失风险"
export interface User {
id: string
name: string
email: string
phone: string
avatar: string
tags: string[]
status: UserStatus
rfmScore: number
createdAt: string
lastActiveAt: string
}
const familyNames = [
"张",
"李",
"王",
"赵",
"刘",
"陈",
"杨",
"黄",
"周",
"吴",
"徐",
"孙",
"胡",
"朱",
"高",
"林",
"何",
"郭",
"马",
"罗",
]
const givenNames = [
"伟",
"芳",
"娜",
"敏",
"静",
"秀英",
"丽",
"强",
"磊",
"军",
"洋",
"艳",
"勇",
"杰",
"娟",
"涛",
"明",
"超",
"霞",
"平",
"俊",
"凯",
"佳",
"鑫",
"鹏",
"晨",
"倩",
"颖",
"梅",
"慧",
"雪",
"宇",
"涵",
"宁",
"璐",
"龙",
"震",
"航",
"璟",
"钰",
]
const tagPool = [
"高价值",
"近7日活跃",
"新客",
"回流",
"社群达人",
"潜在复购",
"高互动",
"低客单",
"私域粉",
"公众号粉",
]
const statusPool: UserStatus[] = ["活跃", "沉睡", "流失风险"]
const avatars = [
"/user-avatar-zhangsan.png",
"/user-avatar-lisi.png",
"/avatar-wanglei.png",
"/generic-user-avatar.png",
"/wechat-avatar-1.png",
"/wechat-avatar-2.png",
"/wechat-avatar-3.png",
]
const rand = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min
const pick = <T,>(arr: T[]) => arr[rand(0, arr.length - 1)]
function toPinyinLike(name: string) {
const map: Record<string, string> = {
: "zhang",
: "li",
: "wang",
: "zhao",
: "liu",
: "chen",
: "yang",
: "huang",
: "zhou",
: "wu",
: "xu",
: "sun",
: "hu",
: "zhu",
: "gao",
: "lin",
: "he",
: "guo",
: "ma",
: "luo",
: "wei",
: "fang",
: "na",
: "min",
: "jing",
: "xiuying",
: "li",
: "qiang",
: "lei",
: "jun",
: "yang",
: "yan",
: "yong",
: "jie",
: "juan",
: "tao",
: "ming",
: "chao",
: "xia",
: "ping",
: "jun",
: "kai",
: "jia",
: "xin",
: "peng",
: "chen",
: "qian",
: "ying",
: "mei",
: "hui",
: "xue",
: "yu",
: "han",
: "ning",
: "lu",
: "long",
: "zhen",
: "hang",
: "jing",
: "yu",
}
return name
.split("")
.map((c) => map[c] ?? "u")
.join("")
}
function randomPhone() {
const prefixes = [
"139",
"138",
"137",
"136",
"135",
"188",
"187",
"186",
"185",
"184",
"183",
"182",
"159",
"158",
"157",
"156",
"155",
]
return `${pick(prefixes)}${rand(1000, 9999)}${rand(1000, 9999)}`
}
function randomTags() {
const count = rand(2, 4)
const s = new Set<string>()
while (s.size < count) s.add(pick(tagPool))
return Array.from(s)
}
function timeNearNow(daysSpan = 90) {
const now = Date.now()
const offset = rand(0, daysSpan * 86400000)
return new Date(now - offset).toISOString()
}
let cache: User[] | null = null
function seed(n = 120) {
const list: User[] = []
for (let i = 0; i < n; i++) {
const name = `${pick(familyNames)}${pick(givenNames)}${Math.random() < 0.2 ? pick(givenNames) : ""}`
const email = `${toPinyinLike(name)}${rand(1, 99)}@example.com`
const phone = randomPhone()
list.push({
id: `user_${Date.now()}_${i}`,
name,
email,
phone,
avatar: avatars[i % avatars.length],
tags: randomTags(),
status: pick(statusPool),
rfmScore: rand(15, 95),
createdAt: timeNearNow(180),
lastActiveAt: timeNearNow(15),
})
}
return list
}
export function getUsersStore() {
if (!cache) cache = seed()
return cache
}
export interface QueryParams {
q?: string
tags?: string[]
status?: UserStatus[]
rfmMin?: number
rfmMax?: number
page?: number
pageSize?: number
}
export function queryUsers(params: QueryParams) {
const { q, tags, status, rfmMin = 0, rfmMax = 100, page = 1, pageSize = 20 } = params
let list = getUsersStore()
if (q && q.trim()) {
const s = q.trim().toLowerCase()
list = list.filter(
(u) =>
u.name.toLowerCase().includes(s) ||
u.email.toLowerCase().includes(s) ||
u.phone.includes(s) ||
u.tags.some((t) => t.toLowerCase().includes(s)),
)
}
if (tags?.length) {
list = list.filter((u) => tags.every((t) => u.tags.includes(t)))
}
if (status?.length) {
const st = new Set(status)
list = list.filter((u) => st.has(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 data = list.slice(start, end)
// 列表行仅返回必要字段
const thin = data.map((u) => ({
id: u.id,
name: u.name,
email: u.email,
phone: u.phone,
rfmScore: u.rfmScore,
lastActiveAt: u.lastActiveAt,
tags: u.tags,
}))
return { data: thin, pagination: { page, pageSize, total, totalPages: Math.max(1, Math.ceil(total / pageSize)) } }
}
export function addUser(input: Partial<User>) {
const list = getUsersStore()
const now = new Date().toISOString()
const name = input.name ?? `${pick(familyNames)}${pick(givenNames)}`
const email = input.email ?? `${toPinyinLike(name)}@example.com`
const phone = input.phone ?? randomPhone()
const u: User = {
id: `user_${Date.now()}_${rand(1000, 9999)}`,
name,
email,
phone,
avatar: input.avatar ?? avatars[rand(0, avatars.length - 1)],
tags: input.tags ?? randomTags(),
status: input.status ?? pick(statusPool),
rfmScore: input.rfmScore ?? rand(20, 80),
createdAt: now,
lastActiveAt: now,
}
list.unshift(u)
return u
}
export function getUserById(id: string) {
return getUsersStore().find((u) => u.id === id) ?? null
}