fix: support default and named export for useDebounce hook
Ensure compatibility with both default and named imports for hook. #VERCEL_SKIP Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "crypto"
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
export type UserStatus = "活跃" | "沉睡" | "流失风险"
|
||||
export type UserStatus = '活跃' | '沉睡' | '流失风险'
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
@@ -13,106 +13,40 @@ export interface User {
|
||||
rfmScore: number
|
||||
createdAt: string
|
||||
lastActiveAt: string
|
||||
// 画像扩展
|
||||
city: string
|
||||
store: string
|
||||
privateDomain: string
|
||||
project: string
|
||||
team: string
|
||||
persona: string[]
|
||||
source: "douyin" | "cunkebao_form_submit" | "touchkebao_call_in" | "pos_api" | "manual"
|
||||
}
|
||||
|
||||
const cities = ["北京", "上海", "广州", "深圳", "成都", "杭州", "苏州", "武汉", "西安", "长沙"]
|
||||
const stores = ["门店A1", "门店A2", "门店B1", "门店C3", "门店D2"]
|
||||
const privateDomains = ["企业微信私域1", "社群A", "社群B", "公众号粉丝池"]
|
||||
const projects = ["项目X", "项目Y", "项目Z", "活动618"]
|
||||
const teams = ["团队一组", "团队二组", "团队增长", "客户成功"]
|
||||
const personas = ["技术爱好者", "夜猫子", "内容创作者", "效率提升", "价格敏感", "品牌忠诚"]
|
||||
const tagPool = [
|
||||
"高价值",
|
||||
"近7日活跃",
|
||||
"新客",
|
||||
"回流",
|
||||
"社群达人",
|
||||
"潜在复购",
|
||||
"高互动",
|
||||
"低客单",
|
||||
"私域粉",
|
||||
"公众号粉",
|
||||
]
|
||||
const statusPool: UserStatus[] = ["活跃", "沉睡", "流失风险"]
|
||||
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",
|
||||
'/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("")
|
||||
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",
|
||||
"183",
|
||||
"182",
|
||||
"159",
|
||||
"158",
|
||||
"157",
|
||||
"156",
|
||||
"155",
|
||||
]
|
||||
return `${pick(prefixes)}${rand(1000, 9999)}${rand(1000, 9999)}`
|
||||
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 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)
|
||||
@@ -121,96 +55,24 @@ function timeNearNow(daysSpan = 90) {
|
||||
|
||||
let cache: User[] | null = null
|
||||
|
||||
function seed(n = 240) {
|
||||
const familyNames = [
|
||||
"张",
|
||||
"李",
|
||||
"王",
|
||||
"赵",
|
||||
"刘",
|
||||
"陈",
|
||||
"杨",
|
||||
"黄",
|
||||
"周",
|
||||
"吴",
|
||||
"徐",
|
||||
"孙",
|
||||
"胡",
|
||||
"朱",
|
||||
"高",
|
||||
"林",
|
||||
"何",
|
||||
"郭",
|
||||
"马",
|
||||
"罗",
|
||||
]
|
||||
const givenNames = [
|
||||
"伟",
|
||||
"芳",
|
||||
"娜",
|
||||
"敏",
|
||||
"静",
|
||||
"秀英",
|
||||
"丽",
|
||||
"强",
|
||||
"磊",
|
||||
"军",
|
||||
"洋",
|
||||
"艳",
|
||||
"勇",
|
||||
"杰",
|
||||
"娟",
|
||||
"涛",
|
||||
"明",
|
||||
"超",
|
||||
"霞",
|
||||
"平",
|
||||
"俊",
|
||||
"凯",
|
||||
"佳",
|
||||
"鑫",
|
||||
"鹏",
|
||||
"晨",
|
||||
"倩",
|
||||
"颖",
|
||||
"梅",
|
||||
"慧",
|
||||
"雪",
|
||||
"宇",
|
||||
"涵",
|
||||
"宁",
|
||||
"璐",
|
||||
"龙",
|
||||
"震",
|
||||
"航",
|
||||
"璟",
|
||||
"钰",
|
||||
]
|
||||
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 u: User = {
|
||||
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: randomPhone(),
|
||||
phone,
|
||||
avatar: avatars[i % avatars.length],
|
||||
tags: randomTags(),
|
||||
status: pick(statusPool),
|
||||
rfmScore: rand(20, 95),
|
||||
rfmScore: rand(15, 95),
|
||||
createdAt: timeNearNow(180),
|
||||
lastActiveAt: timeNearNow(15),
|
||||
city: pick(cities),
|
||||
store: pick(stores),
|
||||
privateDomain: pick(privateDomains),
|
||||
project: pick(projects),
|
||||
team: pick(teams),
|
||||
persona: [pick(personas), Math.random() > 0.6 ? pick(personas) : undefined].filter(Boolean) as string[],
|
||||
source: pick(["douyin", "cunkebao_form_submit", "touchkebao_call_in", "pos_api", "manual"]),
|
||||
}
|
||||
list.push(u)
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -226,68 +88,76 @@ export interface QueryParams {
|
||||
status?: UserStatus[]
|
||||
rfmMin?: number
|
||||
rfmMax?: number
|
||||
city?: string[]
|
||||
persona?: string[]
|
||||
source?: string[]
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export function queryUsers(params: QueryParams) {
|
||||
const { q, tags, status, rfmMin = 0, rfmMax = 100, city, persona, source, page = 1, pageSize = 20 } = params
|
||||
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)) ||
|
||||
u.persona.some((p) => p.toLowerCase().includes(s)),
|
||||
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) list = list.filter((u) => status.includes(u.status))
|
||||
list = list.filter((u) => u.rfmScore >= rfmMin && u.rfmScore <= rfmMax)
|
||||
if (city?.length) list = list.filter((u) => city.includes(u.city))
|
||||
if (persona?.length) list = list.filter((u) => u.persona.some((p) => persona.includes(p)))
|
||||
if (source?.length) list = list.filter((u) => source.includes(u.source))
|
||||
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 data = list.slice(start, start + pageSize)
|
||||
const end = start + pageSize
|
||||
const data = list.slice(start, end)
|
||||
|
||||
return {
|
||||
data,
|
||||
pagination: { page, pageSize, total, totalPages: Math.max(1, Math.ceil(total / pageSize)) },
|
||||
}
|
||||
// 列表行仅返回必要字段
|
||||
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: input.name ?? "新用户",
|
||||
email: input.email ?? "user@example.com",
|
||||
phone: input.phone ?? "13900000000",
|
||||
avatar: input.avatar ?? avatars[0],
|
||||
tags: input.tags ?? ["新客"],
|
||||
status: input.status ?? "活跃",
|
||||
rfmScore: input.rfmScore ?? 60,
|
||||
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,
|
||||
city: input.city ?? pick(cities),
|
||||
store: input.store ?? pick(stores),
|
||||
privateDomain: input.privateDomain ?? pick(privateDomains),
|
||||
project: input.project ?? pick(projects),
|
||||
team: input.team ?? pick(teams),
|
||||
persona: input.persona ?? [pick(personas)],
|
||||
source: input.source ?? "manual",
|
||||
}
|
||||
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