Files
soul/lib/store.ts
卡若 b60edb3d47 feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API
主要更新:
1. 按H5网页端完全重构匹配功能(match页面)
   - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募
   - 资源对接等类型弹出手机号/微信号输入框
   - 去掉重新匹配按钮,改为返回按钮

2. 修复所有卡片对齐和宽度问题
   - 目录页附录卡片居中
   - 首页阅读进度卡片满宽度
   - 我的页面菜单卡片对齐
   - 推广中心分享卡片统一宽度

3. 修复目录页图标和文字对齐
   - section-icon固定40rpx宽高
   - section-title与图标垂直居中

4. 更新真实完整文章标题(62篇)
   - 从book目录读取真实markdown文件名
   - 替换之前的简化标题

5. 新增文章数据API
   - /api/db/chapters - 获取完整书籍结构
   - 支持按ID获取单篇文章内容
2026-01-21 15:49:12 +08:00

727 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Zustand 状态管理
* 开发: 卡若
* 技术支持: 存客宝
*/
"use client"
import { create } from "zustand"
import { persist } from "zustand/middleware"
import { getFullBookPrice } from "./book-data"
export interface User {
id: string
phone: string
nickname: string
isAdmin: boolean
purchasedSections: string[]
hasFullBook: boolean
referralCode: string
referredBy?: string
earnings: number
pendingEarnings: number
withdrawnEarnings: number
referralCount: number
createdAt: string
wechat?: string
alipay?: string
}
export interface Withdrawal {
id: string
userId: string
amount: number
method: "wechat" | "alipay"
account: string
name: string
status: "pending" | "completed" | "rejected"
createdAt: string
completedAt?: string
}
export interface Purchase {
id: string
userId: string
userPhone?: string
userNickname?: string
type: "section" | "fullbook"
sectionId?: string
sectionTitle?: string
amount: number
paymentMethod?: string
referralCode?: string
referrerEarnings?: number
status: "pending" | "completed" | "refunded"
createdAt: string
}
export interface LiveQRCodeConfig {
id: string
name: string
urls: string[] // 多个URL随机跳转
imageUrl: string
redirectType: "random" | "sequential" | "weighted"
weights?: number[]
clickCount: number
enabled: boolean
}
export interface QRCodeConfig {
id: string
name: string
url: string
imageUrl: string
weight: number
enabled: boolean
}
export interface PaymentAccountConfig {
wechat: {
enabled: boolean
qrCode: string
account: string
websiteAppId: string
websiteAppSecret: string
serviceAppId: string
serviceAppSecret: string
mpVerifyCode: string
merchantId: string
apiKey: string
groupQrCode?: string // 微信群二维码链接
}
alipay: {
enabled: boolean
qrCode: string
account: string
partnerId: string // PID 合作者身份
securityKey: string // 安全校验码 Key
mobilePayEnabled: boolean
paymentInterface: string // 支付接口类型
}
usdt: {
enabled: boolean
network: "TRC20" | "ERC20" | "BEP20"
address: string
exchangeRate: number
}
paypal: {
enabled: boolean
email: string
exchangeRate: number
}
}
export interface FeishuSyncConfig {
enabled: boolean
docUrl: string
lastSyncAt?: string
autoSync: boolean
syncInterval: number // 分钟
}
export interface SiteConfig {
siteName: string
siteTitle: string
siteDescription: string
logo: string
favicon: string
primaryColor: string
}
export interface MenuConfig {
home: { enabled: boolean; label: string }
chapters: { enabled: boolean; label: string }
match: { enabled: boolean; label: string }
my: { enabled: boolean; label: string }
}
export interface PageConfig {
homeTitle: string
homeSubtitle: string
chaptersTitle: string
matchTitle: string
myTitle: string
aboutTitle: string
}
export interface Settings {
distributorShare: number
authorShare: number
paymentMethods: PaymentAccountConfig
sectionPrice: number
baseBookPrice: number
pricePerSection: number
qrCodes: QRCodeConfig[]
liveQRCodes: LiveQRCodeConfig[]
feishuSync: FeishuSyncConfig
authorInfo: {
name: string
description: string
liveTime: string
platform: string
}
siteConfig: SiteConfig
menuConfig: MenuConfig
pageConfig: PageConfig
}
interface StoreState {
user: User | null
isLoggedIn: boolean
purchases: Purchase[]
withdrawals: Withdrawal[]
settings: Settings
login: (phone: string, code: string) => Promise<boolean>
logout: () => void
register: (phone: string, nickname: string, referralCode?: string) => Promise<boolean>
purchaseSection: (sectionId: string, sectionTitle?: string, paymentMethod?: string) => Promise<boolean>
purchaseFullBook: (paymentMethod?: string) => Promise<boolean>
hasPurchased: (sectionId: string) => boolean
adminLogin: (username: string, password: string) => boolean
updateSettings: (newSettings: Partial<Settings>) => void
getAllUsers: () => User[]
getAllPurchases: () => Purchase[]
addUser: (user: Partial<User>) => User
updateUser: (userId: string, updates: Partial<User>) => void
deleteUser: (userId: string) => void
addPurchase: (purchase: Omit<Purchase, "id" | "createdAt">) => void
requestWithdrawal: (amount: number, method: "wechat" | "alipay", account: string, name: string) => void
completeWithdrawal: (id: string) => void
updateLiveQRCode: (config: Partial<LiveQRCodeConfig>) => void
getRandomQRCode: () => QRCodeConfig | null
getLiveQRCodeUrl: (qrId: string) => string | null
exportData: () => string
fetchSettings: () => Promise<void>
}
const initialSettings: Settings = {
distributorShare: 90,
authorShare: 10,
paymentMethods: {
alipay: {
enabled: false, // 已禁用支付宝
qrCode: "/images/alipay.png",
account: "卡若",
partnerId: "2088511801157159",
securityKey: "lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp",
mobilePayEnabled: true,
paymentInterface: "official_instant",
},
wechat: {
enabled: true,
qrCode: "/images/wechat-pay.png",
account: "卡若",
websiteAppId: "wx432c93e275548671",
websiteAppSecret: "25b7e7fdb7998e5107e242ebb6ddabd0",
serviceAppId: "wx7c0dbf34ddba300d",
serviceAppSecret: "f865ef18c43dfea6cbe3b1f1aebdb82e",
mpVerifyCode: "SP8AfZJyAvprRORT",
merchantId: "1318592501",
apiKey: "wx3e31b068be59ddc131b068be59ddc2",
groupQrCode: "/images/party-group-qr.png",
},
usdt: {
enabled: false,
network: "TRC20",
address: "",
exchangeRate: 7.2,
},
paypal: {
enabled: false,
email: "",
exchangeRate: 7.2,
},
},
sectionPrice: 1,
baseBookPrice: 9.9,
pricePerSection: 1,
qrCodes: [
{
id: "default",
name: "Soul派对群",
url: "https://soul.cn/party",
imageUrl: "/images/image.png",
weight: 1,
enabled: true,
},
],
liveQRCodes: [
{
id: "party-group",
name: "派对群活码",
urls: ["https://soul.cn/party1", "https://soul.cn/party2", "https://soul.cn/party3"],
imageUrl: "/images/image.png",
redirectType: "random",
clickCount: 0,
enabled: true,
},
],
feishuSync: {
enabled: false,
docUrl: "",
autoSync: false,
syncInterval: 60,
},
authorInfo: {
name: "卡若",
description: "连续创业者,私域运营专家,每天早上6-9点在Soul派对房分享真实商业故事",
liveTime: "06:00-09:00",
platform: "Soul派对房",
},
siteConfig: {
siteName: "一场soul的创业实验",
siteTitle: "一场soul的创业实验",
siteDescription: "来自Soul派对房的真实商业故事",
logo: "/logo.png",
favicon: "/favicon.ico",
primaryColor: "#00CED1",
},
menuConfig: {
home: { enabled: true, label: "首页" },
chapters: { enabled: true, label: "目录" },
match: { enabled: true, label: "匹配" },
my: { enabled: true, label: "我的" },
},
pageConfig: {
homeTitle: "一场soul的创业实验",
homeSubtitle: "来自Soul派对房的真实商业故事",
chaptersTitle: "我要看",
matchTitle: "语音匹配",
myTitle: "我的",
aboutTitle: "关于作者",
},
}
export const useStore = create<StoreState>()(
persist(
(set, get) => ({
user: null,
isLoggedIn: false,
purchases: [],
withdrawals: [],
settings: initialSettings,
login: async (phone: string, code: string) => {
// 真实场景下应该调用后端API验证验证码
// 这里暂时保留简单验证用于演示
if (code !== "123456") {
return false
}
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
const existingUser = users.find((u) => u.phone === phone)
if (existingUser) {
set({ user: existingUser, isLoggedIn: true })
return true
}
return false
},
logout: () => {
set({ user: null, isLoggedIn: false })
},
register: async (phone: string, nickname: string, referralCode?: string) => {
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
if (users.find((u) => u.phone === phone)) {
return false
}
const newUser: User = {
id: `user_${Date.now()}`,
phone,
nickname,
isAdmin: false,
purchasedSections: [],
hasFullBook: false,
referralCode: `REF${Date.now().toString(36).toUpperCase()}`,
referredBy: referralCode,
earnings: 0,
pendingEarnings: 0,
withdrawnEarnings: 0,
referralCount: 0,
createdAt: new Date().toISOString(),
}
if (referralCode) {
const referrer = users.find((u) => u.referralCode === referralCode)
if (referrer) {
referrer.referralCount = (referrer.referralCount || 0) + 1
localStorage.setItem("users", JSON.stringify(users))
}
}
users.push(newUser)
localStorage.setItem("users", JSON.stringify(users))
set({ user: newUser, isLoggedIn: true })
return true
},
purchaseSection: async (sectionId: string, sectionTitle?: string, paymentMethod?: string) => {
const { user, settings } = get()
if (!user) return false
const amount = settings.sectionPrice
const purchase: Purchase = {
id: `purchase_${Date.now()}`,
userId: user.id,
userPhone: user.phone,
userNickname: user.nickname,
type: "section",
sectionId,
sectionTitle,
amount,
paymentMethod,
referralCode: user.referredBy,
status: "completed",
createdAt: new Date().toISOString(),
}
const updatedUser = {
...user,
purchasedSections: [...user.purchasedSections, sectionId],
}
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
const userIndex = users.findIndex((u) => u.id === user.id)
if (userIndex !== -1) {
users[userIndex] = updatedUser
localStorage.setItem("users", JSON.stringify(users))
}
if (user.referredBy) {
const referrer = users.find((u) => u.referralCode === user.referredBy)
if (referrer) {
const referrerEarnings = amount * (settings.distributorShare / 100)
referrer.earnings += referrerEarnings
referrer.pendingEarnings = (referrer.pendingEarnings || 0) + referrerEarnings
purchase.referrerEarnings = referrerEarnings
localStorage.setItem("users", JSON.stringify(users))
}
}
const purchases = JSON.parse(localStorage.getItem("all_purchases") || "[]") as Purchase[]
purchases.push(purchase)
localStorage.setItem("all_purchases", JSON.stringify(purchases))
set({ user: updatedUser, purchases: [...get().purchases, purchase] })
return true
},
purchaseFullBook: async (paymentMethod?: string) => {
const { user, settings } = get()
if (!user) return false
const fullBookPrice = getFullBookPrice()
const purchase: Purchase = {
id: `purchase_${Date.now()}`,
userId: user.id,
userPhone: user.phone,
userNickname: user.nickname,
type: "fullbook",
amount: fullBookPrice,
paymentMethod,
referralCode: user.referredBy,
status: "completed",
createdAt: new Date().toISOString(),
}
const updatedUser = { ...user, hasFullBook: true }
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
const userIndex = users.findIndex((u) => u.id === user.id)
if (userIndex !== -1) {
users[userIndex] = updatedUser
localStorage.setItem("users", JSON.stringify(users))
}
if (user.referredBy) {
const referrer = users.find((u) => u.referralCode === user.referredBy)
if (referrer) {
const referrerEarnings = fullBookPrice * (settings.distributorShare / 100)
referrer.earnings += referrerEarnings
referrer.pendingEarnings = (referrer.pendingEarnings || 0) + referrerEarnings
purchase.referrerEarnings = referrerEarnings
localStorage.setItem("users", JSON.stringify(users))
}
}
const purchases = JSON.parse(localStorage.getItem("all_purchases") || "[]") as Purchase[]
purchases.push(purchase)
localStorage.setItem("all_purchases", JSON.stringify(purchases))
set({ user: updatedUser, purchases: [...get().purchases, purchase] })
return true
},
hasPurchased: (sectionId: string) => {
const { user } = get()
if (!user) return false
if (user.hasFullBook) return true
return user.purchasedSections.includes(sectionId)
},
adminLogin: (username: string, password: string) => {
if (username.toLowerCase() === "admin" && password === "key123456") {
const adminUser: User = {
id: "admin",
phone: "admin",
nickname: "管理员",
isAdmin: true,
purchasedSections: [],
hasFullBook: true,
referralCode: "ADMIN",
earnings: 0,
pendingEarnings: 0,
withdrawnEarnings: 0,
referralCount: 0,
createdAt: new Date().toISOString(),
}
set({ user: adminUser, isLoggedIn: true })
return true
}
return false
},
updateSettings: (newSettings: Partial<Settings>) => {
const { settings } = get()
const updatedSettings = { ...settings, ...newSettings }
if (newSettings.distributorShare !== undefined) {
updatedSettings.authorShare = 100 - newSettings.distributorShare
}
set({ settings: updatedSettings })
if (typeof window !== "undefined") {
localStorage.setItem("app_settings", JSON.stringify(updatedSettings))
}
},
getAllUsers: () => {
if (typeof window === "undefined") return []
return JSON.parse(localStorage.getItem("users") || "[]") as User[]
},
getAllPurchases: () => {
if (typeof window === "undefined") return []
return JSON.parse(localStorage.getItem("all_purchases") || "[]") as Purchase[]
},
addUser: (userData: Partial<User>) => {
if (typeof window === "undefined") return { id: "temp", ...userData } as User
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
const newUser: User = {
id: `user_${Date.now()}`,
phone: userData.phone || "",
nickname: userData.nickname || "新用户",
isAdmin: false,
purchasedSections: [],
hasFullBook: false,
referralCode: `REF${Date.now().toString(36).toUpperCase()}`,
earnings: 0,
pendingEarnings: 0,
withdrawnEarnings: 0,
referralCount: 0,
createdAt: new Date().toISOString(),
...userData,
}
users.push(newUser)
localStorage.setItem("users", JSON.stringify(users))
return newUser
},
updateUser: (userId: string, updates: Partial<User>) => {
if (typeof window === "undefined") return
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
const index = users.findIndex((u) => u.id === userId)
if (index !== -1) {
users[index] = { ...users[index], ...updates }
localStorage.setItem("users", JSON.stringify(users))
}
},
deleteUser: (userId: string) => {
if (typeof window === "undefined") return
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
const filtered = users.filter((u) => u.id !== userId)
localStorage.setItem("users", JSON.stringify(filtered))
},
addPurchase: (purchaseData) =>
set((state) => {
const newPurchase: Purchase = {
id: Math.random().toString(36).substring(2, 9),
createdAt: new Date().toISOString(),
...purchaseData,
}
// 如果是全书购买,更新用户状态
if (state.user && purchaseData.userId === state.user.id) {
const updatedUser = { ...state.user }
if (purchaseData.type === "fullbook") {
updatedUser.hasFullBook = true
} else if (purchaseData.sectionId) {
updatedUser.purchasedSections = [...updatedUser.purchasedSections, purchaseData.sectionId]
}
// 更新 users 数组
const updatedUsers = state.users?.map((u) => (u.id === updatedUser.id ? updatedUser : u)) || []
return {
purchases: [...state.purchases, newPurchase],
user: updatedUser,
}
}
return {
purchases: [...state.purchases, newPurchase],
}
}),
requestWithdrawal: (amount, method, account, name) =>
set((state) => {
if (!state.user) return {}
if (state.user.earnings < amount) return {}
const newWithdrawal: Withdrawal = {
id: Math.random().toString(36).substring(2, 9),
userId: state.user.id,
amount,
method,
account,
name,
status: "pending",
createdAt: new Date().toISOString(),
}
// 扣除余额,增加冻结/提现中金额
const updatedUser = {
...state.user,
earnings: state.user.earnings - amount,
pendingEarnings: state.user.pendingEarnings + amount,
}
return {
withdrawals: [...(state.withdrawals || []), newWithdrawal],
user: updatedUser,
}
}),
completeWithdrawal: (id) =>
set((state) => {
const withdrawals = state.withdrawals || []
const withdrawalIndex = withdrawals.findIndex((w) => w.id === id)
if (withdrawalIndex === -1) return {}
const withdrawal = withdrawals[withdrawalIndex]
if (withdrawal.status !== "pending") return {}
const updatedWithdrawals = [...withdrawals]
updatedWithdrawals[withdrawalIndex] = {
...withdrawal,
status: "completed",
completedAt: new Date().toISOString(),
}
// 这里我们只是更新状态,资金已经在申请时扣除了
// 实际场景中可能需要确认转账成功
return {
withdrawals: updatedWithdrawals,
}
}),
updateLiveQRCode: (config) =>
set((state) => {
const { settings } = state
const updatedLiveQRCodes = settings.liveQRCodes.map((qr) => (qr.id === config.id ? { ...qr, ...config } : qr))
// 如果不存在且有id则添加? 暂时只支持更新
return {
settings: { ...settings, liveQRCodes: updatedLiveQRCodes },
}
}),
getRandomQRCode: () => {
const { settings } = get()
const enabledQRs = settings.qrCodes.filter((qr) => qr.enabled)
if (enabledQRs.length === 0) return null
const totalWeight = enabledQRs.reduce((sum, qr) => sum + qr.weight, 0)
let random = Math.random() * totalWeight
for (const qr of enabledQRs) {
random -= qr.weight
if (random <= 0) return qr
}
return enabledQRs[0]
},
getLiveQRCodeUrl: (qrId: string) => {
const { settings } = get()
const liveQR = settings.liveQRCodes.find((qr) => qr.id === qrId && qr.enabled)
if (!liveQR || liveQR.urls.length === 0) return null
// 更新点击次数
liveQR.clickCount++
if (liveQR.redirectType === "random") {
const randomIndex = Math.floor(Math.random() * liveQR.urls.length)
return liveQR.urls[randomIndex]
} else if (liveQR.redirectType === "sequential") {
const index = liveQR.clickCount % liveQR.urls.length
return liveQR.urls[index]
} else if (liveQR.redirectType === "weighted" && liveQR.weights) {
const totalWeight = liveQR.weights.reduce((sum, w) => sum + w, 0)
let random = Math.random() * totalWeight
for (let i = 0; i < liveQR.urls.length; i++) {
random -= liveQR.weights[i] || 1
if (random <= 0) return liveQR.urls[i]
}
}
return liveQR.urls[0]
},
exportData: () => {
const { user, purchases, settings } = get()
const data = {
user,
purchases,
settings,
exportDate: new Date().toISOString(),
}
return JSON.stringify(data, null, 2)
},
fetchSettings: async () => {
try {
const res = await fetch("/api/config")
if (!res.ok) throw new Error("Failed to fetch config")
const data = await res.json()
const { settings } = get()
// Deep merge payment methods to preserve existing defaults if API is partial
const mergedPaymentMethods = {
...settings.paymentMethods,
wechat: { ...settings.paymentMethods.wechat, ...data.paymentMethods?.wechat },
alipay: { ...settings.paymentMethods.alipay, ...data.paymentMethods?.alipay },
usdt: { ...settings.paymentMethods.usdt, ...data.paymentMethods?.usdt },
paypal: { ...settings.paymentMethods.paypal, ...data.paymentMethods?.paypal },
}
const newSettings: Partial<Settings> = {
paymentMethods: mergedPaymentMethods,
authorInfo: { ...settings.authorInfo, ...data.authorInfo },
siteConfig: { ...settings.siteConfig, ...data.siteConfig },
menuConfig: { ...settings.menuConfig, ...data.menuConfig },
pageConfig: { ...settings.pageConfig, ...data.pageConfig },
}
set({ settings: { ...settings, ...newSettings } })
} catch (error) {
console.error("Failed to sync settings:", error)
}
},
}),
{
name: "soul-experiment-storage",
},
),
)