Ensure compatibility with both default and named exports for useDebounce. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
164 lines
4.9 KiB
TypeScript
164 lines
4.9 KiB
TypeScript
import { randomUUID } from 'crypto'
|
|
|
|
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' }
|
|
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: randomUUID(),
|
|
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: randomUUID(),
|
|
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
|
|
}
|