Organize project by 5 core modules based on requirement docs. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
321 lines
6.0 KiB
TypeScript
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
|
|
}
|