463 lines
13 KiB
TypeScript
463 lines
13 KiB
TypeScript
// 数据库操作Hooks
|
|
"use client"
|
|
|
|
import { useState, useEffect, useCallback } from "react"
|
|
import { db } from "./schema"
|
|
import { seedDatabase } from "./seed"
|
|
import type {
|
|
User,
|
|
Streamer,
|
|
Star,
|
|
Guild,
|
|
Companion,
|
|
EsportsVenue,
|
|
Product,
|
|
Transaction,
|
|
Moment,
|
|
Message,
|
|
Notification,
|
|
} from "./schema"
|
|
|
|
// 初始化数据库
|
|
export function useInitDatabase() {
|
|
const [isReady, setIsReady] = useState(false)
|
|
const [error, setError] = useState<Error | null>(null)
|
|
|
|
useEffect(() => {
|
|
const init = async () => {
|
|
try {
|
|
await db.open()
|
|
await seedDatabase()
|
|
setIsReady(true)
|
|
} catch (err) {
|
|
console.error("[v0] Database init error:", err)
|
|
setError(err as Error)
|
|
}
|
|
}
|
|
init()
|
|
}, [])
|
|
|
|
return { isReady, error }
|
|
}
|
|
|
|
// 用户相关
|
|
export function useCurrentUser() {
|
|
const [user, setUser] = useState<User | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const loadUser = async () => {
|
|
try {
|
|
const currentUser = await db.users.get(1) // 默认用户ID为1
|
|
setUser(currentUser || null)
|
|
} catch (err) {
|
|
console.error("[v0] Load user error:", err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
loadUser()
|
|
}, [])
|
|
|
|
const updateUser = useCallback(
|
|
async (updates: Partial<User>) => {
|
|
if (!user?.id) return
|
|
await db.users.update(user.id, { ...updates, updatedAt: new Date().toISOString() })
|
|
setUser((prev) => (prev ? { ...prev, ...updates } : null))
|
|
},
|
|
[user?.id],
|
|
)
|
|
|
|
const updateBalance = useCallback(
|
|
async (amount: number, type: "add" | "subtract") => {
|
|
if (!user?.id) return false
|
|
const newBalance = type === "add" ? user.balance + amount : user.balance - amount
|
|
if (newBalance < 0) return false
|
|
await db.users.update(user.id, { balance: newBalance, updatedAt: new Date().toISOString() })
|
|
setUser((prev) => (prev ? { ...prev, balance: newBalance } : null))
|
|
return true
|
|
},
|
|
[user],
|
|
)
|
|
|
|
return { user, loading, updateUser, updateBalance }
|
|
}
|
|
|
|
// 主播列表
|
|
export function useStreamers(options?: { game?: string; isLive?: boolean; limit?: number }) {
|
|
const [streamers, setStreamers] = useState<Streamer[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
let query = db.streamers.where("status").equals("approved")
|
|
if (options?.isLive !== undefined) {
|
|
query = db.streamers.where("isLive").equals(options.isLive ? 1 : 0)
|
|
}
|
|
let results = await query.toArray()
|
|
if (options?.game) {
|
|
results = results.filter((s) => s.game === options.game)
|
|
}
|
|
if (options?.limit) {
|
|
results = results.slice(0, options.limit)
|
|
}
|
|
setStreamers(results)
|
|
} catch (err) {
|
|
console.error("[v0] Load streamers error:", err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [options?.game, options?.isLive, options?.limit])
|
|
|
|
return { streamers, loading }
|
|
}
|
|
|
|
// 明星列表
|
|
export function useStars(options?: { game?: string; limit?: number }) {
|
|
const [stars, setStars] = useState<Star[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
let results = await db.stars.where("status").equals("active").toArray()
|
|
if (options?.game) {
|
|
results = results.filter((s) => s.game === options.game)
|
|
}
|
|
if (options?.limit) {
|
|
results = results.slice(0, options.limit)
|
|
}
|
|
setStars(results)
|
|
} catch (err) {
|
|
console.error("[v0] Load stars error:", err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [options?.game, options?.limit])
|
|
|
|
return { stars, loading }
|
|
}
|
|
|
|
// 公会列表
|
|
export function useGuilds(options?: { limit?: number }) {
|
|
const [guilds, setGuilds] = useState<Guild[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
let results = await db.guilds.where("status").equals("active").toArray()
|
|
if (options?.limit) {
|
|
results = results.slice(0, options.limit)
|
|
}
|
|
setGuilds(results)
|
|
} catch (err) {
|
|
console.error("[v0] Load guilds error:", err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [options?.limit])
|
|
|
|
return { guilds, loading }
|
|
}
|
|
|
|
// CP/陪练列表
|
|
export function useCompanions(options?: { category?: "cp" | "coach"; gender?: string; limit?: number }) {
|
|
const [companions, setCompanions] = useState<Companion[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
let results = await db.companions.where("status").equals("active").toArray()
|
|
if (options?.category) {
|
|
results = results.filter((c) => c.category === options.category)
|
|
}
|
|
if (options?.gender) {
|
|
results = results.filter((c) => c.gender === options.gender)
|
|
}
|
|
if (options?.limit) {
|
|
results = results.slice(0, options.limit)
|
|
}
|
|
setCompanions(results)
|
|
} catch (err) {
|
|
console.error("[v0] Load companions error:", err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [options?.category, options?.gender, options?.limit])
|
|
|
|
return { companions, loading }
|
|
}
|
|
|
|
// 电竞酒店/网咖
|
|
export function useVenues(options?: { type?: "hotel" | "cafe"; city?: string; limit?: number }) {
|
|
const [venues, setVenues] = useState<EsportsVenue[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
let results = await db.esportsVenues.where("status").equals("active").toArray()
|
|
if (options?.type) {
|
|
results = results.filter((v) => v.type === options.type)
|
|
}
|
|
if (options?.city) {
|
|
results = results.filter((v) => v.city === options.city)
|
|
}
|
|
if (options?.limit) {
|
|
results = results.slice(0, options.limit)
|
|
}
|
|
setVenues(results)
|
|
} catch (err) {
|
|
console.error("[v0] Load venues error:", err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [options?.type, options?.city, options?.limit])
|
|
|
|
return { venues, loading }
|
|
}
|
|
|
|
// 商品列表
|
|
export function useProducts(options?: { category?: string; game?: string; limit?: number }) {
|
|
const [products, setProducts] = useState<Product[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
let results = await db.products.where("status").equals("active").toArray()
|
|
if (options?.category) {
|
|
results = results.filter((p) => p.category === options.category)
|
|
}
|
|
if (options?.game) {
|
|
results = results.filter((p) => p.game === options.game)
|
|
}
|
|
if (options?.limit) {
|
|
results = results.slice(0, options.limit)
|
|
}
|
|
setProducts(results)
|
|
} catch (err) {
|
|
console.error("[v0] Load products error:", err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [options?.category, options?.game, options?.limit])
|
|
|
|
return { products, loading }
|
|
}
|
|
|
|
// 交易记录
|
|
export function useTransactions(userId: number, options?: { type?: string; limit?: number }) {
|
|
const [transactions, setTransactions] = useState<Transaction[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
let results = await db.transactions.where("userId").equals(userId).toArray()
|
|
if (options?.type) {
|
|
results = results.filter((t) => t.type === options.type)
|
|
}
|
|
results = results.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
if (options?.limit) {
|
|
results = results.slice(0, options.limit)
|
|
}
|
|
setTransactions(results)
|
|
} catch (err) {
|
|
console.error("[v0] Load transactions error:", err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [userId, options?.type, options?.limit])
|
|
|
|
return { transactions, loading }
|
|
}
|
|
|
|
// 动态列表
|
|
export function useMoments(options?: { userId?: number; limit?: number }) {
|
|
const [moments, setMoments] = useState<Moment[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
const loadMoments = useCallback(async () => {
|
|
try {
|
|
let results = await db.moments.where("status").equals("active").toArray()
|
|
if (options?.userId) {
|
|
results = results.filter((m) => m.userId === options.userId)
|
|
}
|
|
results = results.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
if (options?.limit) {
|
|
results = results.slice(0, options.limit)
|
|
}
|
|
setMoments(results)
|
|
} catch (err) {
|
|
console.error("[v0] Load moments error:", err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [options?.userId, options?.limit])
|
|
|
|
useEffect(() => {
|
|
loadMoments()
|
|
}, [loadMoments])
|
|
|
|
const likeMoment = useCallback(
|
|
async (momentId: number) => {
|
|
const moment = await db.moments.get(momentId)
|
|
if (moment) {
|
|
await db.moments.update(momentId, {
|
|
likes: moment.likes + 1,
|
|
isLiked: true,
|
|
updatedAt: new Date().toISOString(),
|
|
})
|
|
await loadMoments()
|
|
}
|
|
},
|
|
[loadMoments],
|
|
)
|
|
|
|
return { moments, loading, likeMoment, refresh: loadMoments }
|
|
}
|
|
|
|
// 消息列表
|
|
export function useMessages(conversationId: number) {
|
|
const [messages, setMessages] = useState<Message[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
const results = await db.messages.where("conversationId").equals(conversationId).toArray()
|
|
setMessages(results.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()))
|
|
} catch (err) {
|
|
console.error("[v0] Load messages error:", err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [conversationId])
|
|
|
|
const sendMessage = useCallback(
|
|
async (content: string, type: "text" | "image" | "voice" = "text") => {
|
|
const newMessage: Omit<Message, "id"> = {
|
|
conversationId,
|
|
senderId: 1, // 当前用户
|
|
receiverId: 0, // 需要从conversation获取
|
|
type,
|
|
content,
|
|
isRead: false,
|
|
createdAt: new Date().toISOString(),
|
|
}
|
|
const id = await db.messages.add(newMessage as Message)
|
|
setMessages((prev) => [...prev, { ...newMessage, id } as Message])
|
|
},
|
|
[conversationId],
|
|
)
|
|
|
|
return { messages, loading, sendMessage }
|
|
}
|
|
|
|
// 通知列表
|
|
export function useNotifications(userId: number) {
|
|
const [notifications, setNotifications] = useState<Notification[]>([])
|
|
const [unreadCount, setUnreadCount] = useState(0)
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
const results = await db.notifications.where("userId").equals(userId).toArray()
|
|
const sorted = results.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
setNotifications(sorted)
|
|
setUnreadCount(sorted.filter((n) => !n.isRead).length)
|
|
} catch (err) {
|
|
console.error("[v0] Load notifications error:", err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [userId])
|
|
|
|
const markAsRead = useCallback(async (notificationId: number) => {
|
|
await db.notifications.update(notificationId, { isRead: true })
|
|
setNotifications((prev) => prev.map((n) => (n.id === notificationId ? { ...n, isRead: true } : n)))
|
|
setUnreadCount((prev) => Math.max(0, prev - 1))
|
|
}, [])
|
|
|
|
const markAllAsRead = useCallback(async () => {
|
|
const unread = notifications.filter((n) => !n.isRead)
|
|
await Promise.all(unread.map((n) => db.notifications.update(n.id!, { isRead: true })))
|
|
setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true })))
|
|
setUnreadCount(0)
|
|
}, [notifications])
|
|
|
|
return { notifications, unreadCount, loading, markAsRead, markAllAsRead }
|
|
}
|
|
|
|
// 创建订单
|
|
export function useCreateOrder() {
|
|
const createTransaction = useCallback(
|
|
async (
|
|
userId: number,
|
|
type: Transaction["type"],
|
|
category: string,
|
|
amount: number,
|
|
description: string,
|
|
relatedId?: number,
|
|
relatedType?: string,
|
|
) => {
|
|
const user = await db.users.get(userId)
|
|
if (!user) throw new Error("User not found")
|
|
|
|
const balanceBefore = user.balance
|
|
const balanceAfter =
|
|
type === "recharge" || type === "income" || type === "refund" ? balanceBefore + amount : balanceBefore - amount
|
|
|
|
if (balanceAfter < 0) {
|
|
throw new Error("Insufficient balance")
|
|
}
|
|
|
|
const transaction: Omit<Transaction, "id"> = {
|
|
userId,
|
|
orderNo: `TXN${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
|
|
type,
|
|
category,
|
|
amount,
|
|
balanceBefore,
|
|
balanceAfter,
|
|
relatedId,
|
|
relatedType,
|
|
description,
|
|
status: "completed",
|
|
createdAt: new Date().toISOString(),
|
|
}
|
|
|
|
await db.transactions.add(transaction as Transaction)
|
|
await db.users.update(userId, { balance: balanceAfter, updatedAt: new Date().toISOString() })
|
|
|
|
return transaction
|
|
},
|
|
[],
|
|
)
|
|
|
|
return { createTransaction }
|
|
}
|