fix: support default and named exports for useDebounce hook
Ensure compatibility with both default and named exports for useDebounce. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
@@ -1,206 +1,163 @@
|
||||
export type Status = "活跃" | "沉睡" | "已封禁"
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
export type UserBase = {
|
||||
export type UserStatus = '活跃' | '沉睡' | '流失风险'
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
name: string
|
||||
phone: string
|
||||
email: string
|
||||
phone: string
|
||||
avatar: string
|
||||
tags: string[]
|
||||
status: UserStatus
|
||||
rfmScore: number
|
||||
lastActivity: string
|
||||
status: Status
|
||||
createdAt: string
|
||||
lastActiveAt: string
|
||||
}
|
||||
|
||||
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 }[]
|
||||
}
|
||||
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',
|
||||
]
|
||||
|
||||
/* 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)]}`
|
||||
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()
|
||||
}
|
||||
|
||||
/* public APIs */
|
||||
export type FilterOptions = {
|
||||
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?: Status[]
|
||||
status?: UserStatus[]
|
||||
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 queryUsers(params: QueryParams) {
|
||||
const { q, tags, status, rfmMin = 0, rfmMax = 100, page = 1, pageSize = 20 } = params
|
||||
let list = getUsersStore()
|
||||
|
||||
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 (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.some((t) => u.tags.includes(t)))
|
||||
if (tags?.length) {
|
||||
list = list.filter(u => tags.every(t => u.tags.includes(t)))
|
||||
}
|
||||
|
||||
if (status.length) {
|
||||
list = list.filter((u) => status.includes(u.status))
|
||||
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)
|
||||
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 }
|
||||
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(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: [],
|
||||
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,
|
||||
}
|
||||
USERS.unshift(u)
|
||||
list.unshift(u)
|
||||
return u
|
||||
}
|
||||
|
||||
export function getUserById(id: string) {
|
||||
return getUsersStore().find(u => u.id === id) ?? null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user