Add "Site Configuration" page and refactor "My" page Expand store with site, menu, and page configurations. #VERCEL_SKIP Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
718 lines
22 KiB
TypeScript
718 lines
22 KiB
TypeScript
"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
|
||
}
|
||
|
||
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: true,
|
||
qrCode: "",
|
||
account: "",
|
||
partnerId: "2088511801157159",
|
||
securityKey: "lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp",
|
||
mobilePayEnabled: true,
|
||
paymentInterface: "official_instant",
|
||
},
|
||
wechat: {
|
||
enabled: true,
|
||
qrCode: "",
|
||
account: "",
|
||
websiteAppId: "wx432c93e275548671",
|
||
websiteAppSecret: "25b7e7fdb7998e5107e242ebb6ddabd0",
|
||
serviceAppId: "wx7c0dbf34ddba300d",
|
||
serviceAppSecret: "f865ef18c43dfea6cbe3b1f1aebdb82e",
|
||
mpVerifyCode: "SP8AfZJyAvprRORT",
|
||
merchantId: "1318592501",
|
||
apiKey: "wx3e31b068be59ddc131b068be59ddc2",
|
||
groupQrCode: "",
|
||
},
|
||
usdt: {
|
||
enabled: true,
|
||
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: "卡若日记",
|
||
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) => {
|
||
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",
|
||
},
|
||
),
|
||
)
|