Clear existing content
This commit is contained in:
676
lib/book-data.ts
676
lib/book-data.ts
@@ -1,676 +0,0 @@
|
||||
export interface Section {
|
||||
id: string
|
||||
title: string
|
||||
price: number
|
||||
isFree: boolean
|
||||
filePath: string
|
||||
content?: string
|
||||
createdAt?: string
|
||||
unlockAfterDays?: number
|
||||
}
|
||||
|
||||
export interface Chapter {
|
||||
id: string
|
||||
title: string
|
||||
sections: Section[]
|
||||
}
|
||||
|
||||
export interface Part {
|
||||
id: string
|
||||
number: string
|
||||
title: string
|
||||
subtitle: string
|
||||
chapters: Chapter[]
|
||||
}
|
||||
|
||||
export const BASE_BOOK_PRICE = 9.9
|
||||
export const PRICE_INCREMENT_PER_SECTION = 1
|
||||
export const SECTION_PRICE = 1
|
||||
export const AUTHOR_SHARE = 0.9
|
||||
export const DISTRIBUTOR_SHARE = 0.1
|
||||
|
||||
export function getFullBookPrice(sectionsCount?: number): number {
|
||||
return 9.9
|
||||
}
|
||||
|
||||
export const bookData: Part[] = [
|
||||
{
|
||||
id: "part-1",
|
||||
number: "01",
|
||||
title: "真实的人",
|
||||
subtitle: "人性观察与社交逻辑",
|
||||
chapters: [
|
||||
{
|
||||
id: "chapter-1",
|
||||
title: "人与人之间的底层逻辑",
|
||||
sections: [
|
||||
{
|
||||
id: "1.1",
|
||||
title: "自行车荷总:一个行业做到极致是什么样",
|
||||
price: 1,
|
||||
isFree: true,
|
||||
filePath: "book/_第一篇|真实的人/第1章|人与人之间的底层逻辑/1.1 自行车荷总:一个行业做到极致是什么样.md",
|
||||
},
|
||||
{
|
||||
id: "1.2",
|
||||
title: "老墨:资源整合高手的社交方法",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/_第一篇|真实的人/第1章|人与人之间的底层逻辑/1.2 老墨:资源整合高手的社交方法.md",
|
||||
},
|
||||
{
|
||||
id: "1.3",
|
||||
title: "笑声背后的MBTI:为什么ENTJ适合做资源,INTP适合做系统",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
filePath:
|
||||
"book/_第一篇|真实的人/第1章|人与人之间的底层逻辑/1.3 笑声背后的MBTI:为什么ENTJ适合做资源,INTP适合做系统.md",
|
||||
},
|
||||
{
|
||||
id: "1.4",
|
||||
title: "人性的三角结构:情绪、价值、利益",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
filePath: "book/_第一篇|真实的人/第1章|人与人之间的底层逻辑/1.4 人性的三角结构:情绪、价值、利益.md",
|
||||
},
|
||||
{
|
||||
id: "1.5",
|
||||
title: "为什么99%的合作死在沟通差而不是能力差",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
filePath: "book/_第一篇|真实的人/第1章|人与人之间的底层逻辑/1.5 为什么99%的合作死在沟通差而不是能力差.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "chapter-2",
|
||||
title: "人性困境案例",
|
||||
sections: [
|
||||
{
|
||||
id: "2.1",
|
||||
title: "相亲故事:你以为找的是人,实际是在找模式",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/_第一篇|真实的人/第2章|人性困境案例/2.1 相亲故事:你以为找的是人,实际是在找模式.md",
|
||||
},
|
||||
{
|
||||
id: "2.2",
|
||||
title: "找工作迷茫者:为什么简历解决不了人生",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/_第一篇|真实的人/第2章|人性困境案例/2.2 找工作迷茫者:为什么简历解决不了人生.md",
|
||||
},
|
||||
{
|
||||
id: "2.3",
|
||||
title: "撸运费险:小钱困住大脑的真实心理",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/_第一篇|真实的人/第2章|人性困境案例/2.3 撸运费险:小钱困住大脑的真实心理.md",
|
||||
},
|
||||
{
|
||||
id: "2.4",
|
||||
title: "游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath:
|
||||
"book/_第一篇|真实的人/第2章|人性困境案例/2.4 游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力.md",
|
||||
},
|
||||
{
|
||||
id: "2.5",
|
||||
title: "健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath:
|
||||
"book/_第一篇|真实的人/第2章|人性困境案例/2.5 健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "part-2",
|
||||
number: "02",
|
||||
title: "真实的行业",
|
||||
subtitle: "社会运作的底层规则",
|
||||
chapters: [
|
||||
{
|
||||
id: "chapter-3",
|
||||
title: "电商篇",
|
||||
sections: [
|
||||
{
|
||||
id: "3.1",
|
||||
title: "电商财税窗口:我错过的第一桶金",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第3章|电商篇/3.1 电商财税窗口:我错过的第一桶金.md",
|
||||
},
|
||||
{
|
||||
id: "3.2",
|
||||
title: "3000万流水如何跑出来(退税模式解析)",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第3章|电商篇/3.2 3000万流水如何跑出来(退税模式解析).md",
|
||||
},
|
||||
{
|
||||
id: "3.3",
|
||||
title: "供应链之王vs打工人:利润不在前端",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第3章|电商篇/3.3 供应链之王 vs 打工人:利润不在前端.md",
|
||||
},
|
||||
{
|
||||
id: "3.4",
|
||||
title: "社区团购的底层逻辑",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第3章|电商篇/3.4 社区团购的底层逻辑.md",
|
||||
},
|
||||
{
|
||||
id: "3.5",
|
||||
title: "跨境电商与退税套利",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第3章|电商篇/3.5 跨境电商与退税套利.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "chapter-4",
|
||||
title: "内容商业篇",
|
||||
sections: [
|
||||
{
|
||||
id: "4.1",
|
||||
title: "旅游号:30天10万粉的真实逻辑",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第4章|内容商业篇/4.1 旅游号:30天10万粉的真实逻辑.md",
|
||||
},
|
||||
{
|
||||
id: "4.2",
|
||||
title: "做号工厂:如何让一个号变成一个机器",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第4章|内容商业篇/4.2 做号工厂:如何让一个号变成一个机器.md",
|
||||
},
|
||||
{
|
||||
id: "4.3",
|
||||
title: "情绪内容为什么比专业内容更赚钱",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第4章|内容商业篇/4.3 情绪内容为什么比专业内容更赚钱.md",
|
||||
},
|
||||
{
|
||||
id: "4.4",
|
||||
title: "猫与宠物号:为什么宠物赛道永不过时",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第4章|内容商业篇/4.4 猫与宠物号:为什么宠物赛道永不过时.md",
|
||||
},
|
||||
{
|
||||
id: "4.5",
|
||||
title: "直播间里的三种人:演员、技术工、系统流",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第4章|内容商业篇/4.5 直播间里的三种人:演员、技术工、系统流.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "chapter-5",
|
||||
title: "传统行业篇",
|
||||
sections: [
|
||||
{
|
||||
id: "5.1",
|
||||
title: "羽毛球馆:为什么体育培训是最稳定的现金流",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.1 羽毛球馆:为什么体育培训是最稳定的现金流.md",
|
||||
},
|
||||
{
|
||||
id: "5.2",
|
||||
title: "旅游供应链:资源越老越值钱",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.2 旅游供应链:资源越老越值钱.md",
|
||||
},
|
||||
{
|
||||
id: "5.3",
|
||||
title: "景区联盟:门票不是目的,是流量入口",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.3 景区联盟:门票不是目的,是流量入口.md",
|
||||
},
|
||||
{
|
||||
id: "5.4",
|
||||
title: "拍卖行抱朴:我人生错过的4件大钱机会(完整版)",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.4 拍卖行抱朴:我人生错过的4件大钱机会(完整版).md",
|
||||
},
|
||||
{
|
||||
id: "5.5",
|
||||
title: "飞机票供应链:为什么越便宜越亏",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.5 飞机票供应链:为什么越便宜越亏.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "part-3",
|
||||
number: "03",
|
||||
title: "真实的错误",
|
||||
subtitle: "错过机会比失败更贵",
|
||||
chapters: [
|
||||
{
|
||||
id: "chapter-6",
|
||||
title: "我人生错过的4件大钱",
|
||||
sections: [
|
||||
{
|
||||
id: "6.1",
|
||||
title: "错过电商财税(2016-2017)",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第6章|我人生错过的4件大钱/6.1 错过电商财税(2016-2017).md",
|
||||
},
|
||||
{
|
||||
id: "6.2",
|
||||
title: "错过供应链(2017-2018)",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第6章|我人生错过的4件大钱/6.2 错过供应链(2017-2018).md",
|
||||
},
|
||||
{
|
||||
id: "6.3",
|
||||
title: "错过内容红利(2018-2019)",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第6章|我人生错过的4件大钱/6.3 错过内容红利(2018-2019).md",
|
||||
},
|
||||
{
|
||||
id: "6.4",
|
||||
title: "错过资源资产化(2019-2020)",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第6章|我人生错过的4件大钱/6.4 错过资源资产化(2019-2020).md",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "chapter-7",
|
||||
title: "别人犯的错误",
|
||||
sections: [
|
||||
{
|
||||
id: "7.1",
|
||||
title: "投资房年轻人的迷茫:资金vs能力",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第7章|别人犯的错误/7.1 投资房年轻人的迷茫:资金 vs 能力.md",
|
||||
},
|
||||
{
|
||||
id: "7.2",
|
||||
title: "信息差骗局:永远有人靠卖学习赚钱",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第7章|别人犯的错误/7.2 信息差骗局:永远有人靠卖学习赚钱.md",
|
||||
},
|
||||
{
|
||||
id: "7.3",
|
||||
title: "在Soul找恋爱但想赚钱的人",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第7章|别人犯的错误/7.3 在Soul找恋爱但想赚钱的人.md",
|
||||
},
|
||||
{
|
||||
id: "7.4",
|
||||
title: "创业者的三种死法:冲动、轻信、没结构",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第7章|别人犯的错误/7.4 创业者的三种死法:冲动、轻信、没结构.md",
|
||||
},
|
||||
{
|
||||
id: "7.5",
|
||||
title: "人情生意的终点:关系越多亏得越多",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第7章|别人犯的错误/7.5 人情生意的终点:关系越多亏得越多.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "part-4",
|
||||
number: "04",
|
||||
title: "真实的赚钱",
|
||||
subtitle: "所有行业的杠杆结构",
|
||||
chapters: [
|
||||
{
|
||||
id: "chapter-8",
|
||||
title: "底层结构",
|
||||
sections: [
|
||||
{
|
||||
id: "8.1",
|
||||
title: "流量杠杆:抖音、Soul、飞书",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第8章|底层结构/8.1 流量杠杆:抖音、Soul、飞书.md",
|
||||
},
|
||||
{
|
||||
id: "8.2",
|
||||
title: "价格杠杆:供应链与信息差",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第8章|底层结构/8.2 价格杠杆:供应链与信息差.md",
|
||||
},
|
||||
{
|
||||
id: "8.3",
|
||||
title: "时间杠杆:自动化+AI",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第8章|底层结构/8.3 时间杠杆:自动化 + AI.md",
|
||||
},
|
||||
{
|
||||
id: "8.4",
|
||||
title: "情绪杠杆:咨询、婚恋、生意场",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第8章|底层结构/8.4 情绪杠杆:咨询、婚恋、生意场.md",
|
||||
},
|
||||
{
|
||||
id: "8.5",
|
||||
title: "社交杠杆:认识谁比你会什么更重要",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第8章|底层结构/8.5 社交杠杆:认识谁比你会什么更重要.md",
|
||||
},
|
||||
{
|
||||
id: "8.6",
|
||||
title: "云阿米巴:分不属于自己的钱",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第8章|底层结构/8.6 云阿米巴:分不属于自己的钱.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "chapter-9",
|
||||
title: "我在Soul上亲访的赚钱案例",
|
||||
sections: [
|
||||
{
|
||||
id: "9.1",
|
||||
title: "游戏账号私域:账号即资产",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/9.1 游戏账号私域:账号即资产.md",
|
||||
},
|
||||
{
|
||||
id: "9.2",
|
||||
title: "健康包模式:高复购、高毛利",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/9.2 健康包模式:高复购、高毛利.md",
|
||||
},
|
||||
{
|
||||
id: "9.3",
|
||||
title: "药物私域:长期关系赛道",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/9.3 药物私域:长期关系赛道.md",
|
||||
},
|
||||
{
|
||||
id: "9.4",
|
||||
title: "残疾机构合作:退税×AI×人力成本",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath:
|
||||
"book/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/9.4 残疾机构合作:退税 × AI × 人力成本.md",
|
||||
},
|
||||
{
|
||||
id: "9.5",
|
||||
title: "私域银行:粉丝即小股东",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/9.5 私域银行:粉丝即小股东.md",
|
||||
},
|
||||
{
|
||||
id: "9.6",
|
||||
title: "Soul派对房:陌生人成交的最快场景",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/9.6 Soul派对房:陌生人成交的最快场景.md",
|
||||
},
|
||||
{
|
||||
id: "9.7",
|
||||
title: "飞书中台:从聊天到成交的流程化体系",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath:
|
||||
"book/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/9.7 飞书中台:从聊天到成交的流程化体系.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "part-5",
|
||||
number: "05",
|
||||
title: "真实的未来",
|
||||
subtitle: "人与系统的关系",
|
||||
chapters: [
|
||||
{
|
||||
id: "chapter-10",
|
||||
title: "未来职业的变化趋势",
|
||||
sections: [
|
||||
{
|
||||
id: "10.1",
|
||||
title: "AI代聊与岗位替换",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第10章|未来职业的变化趋势/10.1 AI代聊与岗位替换.md",
|
||||
},
|
||||
{
|
||||
id: "10.2",
|
||||
title: "系统化工作vs杂乱工作",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第10章|未来职业的变化趋势/10.2 系统化工作 vs 杂乱工作.md",
|
||||
},
|
||||
{
|
||||
id: "10.3",
|
||||
title: "为什么链接能力会成为第一价值",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第10章|未来职业的变化趋势/10.3 为什么链接能力会成为第一价值.md",
|
||||
},
|
||||
{
|
||||
id: "10.4",
|
||||
title: "新型公司:Soul-飞书-线下的三位一体",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第10章|未来职业的变化趋势/10.4 新型公司:Soul-飞书-线下的三位一体.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "chapter-11",
|
||||
title: "中国社会商业生态的未来",
|
||||
sections: [
|
||||
{
|
||||
id: "11.1",
|
||||
title: "城市之间的模式差",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第11章|中国社会商业生态的未来/11.1 城市之间的模式差.md",
|
||||
},
|
||||
{
|
||||
id: "11.2",
|
||||
title: "厦门样本:低成本高效率经济",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第11章|中国社会商业生态的未来/11.2 厦门样本:低成本高效率经济.md",
|
||||
},
|
||||
{
|
||||
id: "11.3",
|
||||
title: "流量红利的终局",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第11章|中国社会商业生态的未来/11.3 流量红利的终局.md",
|
||||
},
|
||||
{
|
||||
id: "11.4",
|
||||
title: "大模型+供应链的组合拳",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第11章|中国社会商业生态的未来/11.4 大模型 + 供应链的组合拳.md",
|
||||
},
|
||||
{
|
||||
id: "11.5",
|
||||
title: "社会分层的最终逻辑",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第11章|中国社会商业生态的未来/11.5 社会分层的最终逻辑.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const specialSections = {
|
||||
preface: {
|
||||
id: "preface",
|
||||
title: "序言|为什么我每天早上6点在Soul开播?",
|
||||
price: 0,
|
||||
isFree: true,
|
||||
filePath: "book/序言|为什么我每天早上6点在Soul开播?.md",
|
||||
},
|
||||
epilogue: {
|
||||
id: "epilogue",
|
||||
title: "尾声|终极答案:努力不是关键,选择才是",
|
||||
price: 0,
|
||||
isFree: true,
|
||||
filePath: "book/尾声|终极答案:努力不是关键,选择才是.md",
|
||||
},
|
||||
}
|
||||
|
||||
export const FULL_BOOK_PRICE = getFullBookPrice()
|
||||
|
||||
export function getAllSections(): Section[] {
|
||||
const sections: Section[] = []
|
||||
if (typeof window !== "undefined") {
|
||||
const customSections = JSON.parse(localStorage.getItem("custom_sections") || "[]")
|
||||
sections.push(...customSections)
|
||||
}
|
||||
bookData.forEach((part) => {
|
||||
part.chapters.forEach((chapter) => {
|
||||
sections.push(...chapter.sections)
|
||||
})
|
||||
})
|
||||
return sections
|
||||
}
|
||||
|
||||
export function getSectionById(id: string): Section | undefined {
|
||||
if (typeof window !== "undefined") {
|
||||
const customSections = JSON.parse(localStorage.getItem("custom_sections") || "[]") as Section[]
|
||||
const customSection = customSections.find((s) => s.id === id)
|
||||
if (customSection) return customSection
|
||||
}
|
||||
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
const section = chapter.sections.find((s) => s.id === id)
|
||||
if (section) return section
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getChapterBySection(sectionId: string): { part: Part; chapter: Chapter } | undefined {
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
if (chapter.sections.some((s) => s.id === sectionId)) {
|
||||
return { part, chapter }
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function isSectionUnlocked(section: Section): boolean {
|
||||
if (section.isFree) return true
|
||||
if (!section.unlockAfterDays || !section.createdAt) return false
|
||||
|
||||
const createdDate = new Date(section.createdAt)
|
||||
const unlockDate = new Date(createdDate.getTime() + section.unlockAfterDays * 24 * 60 * 60 * 1000)
|
||||
return new Date() >= unlockDate
|
||||
}
|
||||
|
||||
export function addCustomSection(section: Omit<Section, "createdAt">): Section {
|
||||
const newSection: Section = {
|
||||
...section,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const customSections = JSON.parse(localStorage.getItem("custom_sections") || "[]") as Section[]
|
||||
customSections.push(newSection)
|
||||
localStorage.setItem("custom_sections", JSON.stringify(customSections))
|
||||
}
|
||||
|
||||
return newSection
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import type { Part, Chapter, Section } from "./book-data"
|
||||
|
||||
const BOOK_DIR = path.join(process.cwd(), "book")
|
||||
|
||||
const CHINESE_NUM_MAP: Record<string, string> = {
|
||||
一: "01",
|
||||
二: "02",
|
||||
三: "03",
|
||||
四: "04",
|
||||
五: "05",
|
||||
六: "06",
|
||||
七: "07",
|
||||
八: "08",
|
||||
九: "09",
|
||||
十: "10",
|
||||
}
|
||||
|
||||
const SUBTITLES: Record<string, string> = {
|
||||
真实的人: "人性观察与社交逻辑",
|
||||
真实的行业: "社会运作的底层规则",
|
||||
真实的错误: "错过机会比失败更贵",
|
||||
真实的赚钱: "所有行业的杠杆结构",
|
||||
真实的社会: "人与系统的关系",
|
||||
}
|
||||
|
||||
function parsePartFolderName(folderName: string): { number: string; title: string } | null {
|
||||
const match = folderName.match(/_第([一二三四五六七八九十]+)篇|(.+)/)
|
||||
if (match) {
|
||||
return {
|
||||
number: CHINESE_NUM_MAP[match[1]] || match[1],
|
||||
title: match[2],
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function parseChapterFolderName(folderName: string): { id: string; title: string } | null {
|
||||
const match = folderName.match(/第(\d+)章|(.+)/)
|
||||
if (match) {
|
||||
return {
|
||||
id: match[1],
|
||||
title: match[2],
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function parseSectionFileName(fileName: string): { id: string; title: string } | null {
|
||||
if (!fileName.endsWith(".md")) return null
|
||||
const name = fileName.replace(".md", "")
|
||||
const match = name.match(/^([\d.]+)\s+(.+)/)
|
||||
if (match) {
|
||||
return {
|
||||
id: match[1],
|
||||
title: match[2],
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: fileName,
|
||||
title: name,
|
||||
}
|
||||
}
|
||||
|
||||
export function getBookStructure(): Part[] {
|
||||
if (!fs.existsSync(BOOK_DIR)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const partFolders = fs.readdirSync(BOOK_DIR).filter((f) => {
|
||||
const fullPath = path.join(BOOK_DIR, f)
|
||||
return fs.statSync(fullPath).isDirectory() && f.startsWith("_")
|
||||
})
|
||||
|
||||
const parts: Part[] = partFolders
|
||||
.map((folderName) => {
|
||||
const parsed = parsePartFolderName(folderName)
|
||||
if (!parsed) return null
|
||||
|
||||
const partPath = path.join(BOOK_DIR, folderName)
|
||||
const chapterFolders = fs.readdirSync(partPath).filter((f) => {
|
||||
const fullPath = path.join(partPath, f)
|
||||
return fs.statSync(fullPath).isDirectory() && f.startsWith("第")
|
||||
})
|
||||
|
||||
const chapters: Chapter[] = chapterFolders
|
||||
.map((chapterFolderName) => {
|
||||
const parsedChapter = parseChapterFolderName(chapterFolderName)
|
||||
if (!parsedChapter) return null
|
||||
|
||||
const chapterPath = path.join(partPath, chapterFolderName)
|
||||
const sectionFiles = fs.readdirSync(chapterPath).filter((f) => f.endsWith(".md"))
|
||||
|
||||
const sections: Section[] = sectionFiles
|
||||
.map((fileName) => {
|
||||
const parsedSection = parseSectionFileName(fileName)
|
||||
if (!parsedSection) return null
|
||||
|
||||
return {
|
||||
id: parsedSection.id,
|
||||
title: parsedSection.title,
|
||||
price: 1,
|
||||
isFree: parsedSection.id.endsWith(".1"),
|
||||
filePath: path.relative(process.cwd(), path.join(chapterPath, fileName)),
|
||||
}
|
||||
})
|
||||
.filter((s): s is Section => s !== null)
|
||||
.sort((a, b) => {
|
||||
const partsA = a.id.split(".").map(Number)
|
||||
const partsB = b.id.split(".").map(Number)
|
||||
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
||||
const valA = partsA[i] || 0
|
||||
const valB = partsB[i] || 0
|
||||
if (valA !== valB) return valA - valB
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
return {
|
||||
id: parsedChapter.id,
|
||||
title: parsedChapter.title,
|
||||
sections,
|
||||
}
|
||||
})
|
||||
.filter((c): c is Chapter => c !== null)
|
||||
.sort((a, b) => Number(a.id) - Number(b.id))
|
||||
|
||||
return {
|
||||
id: parsed.number,
|
||||
number: parsed.number,
|
||||
title: parsed.title,
|
||||
subtitle: SUBTITLES[parsed.title] || "",
|
||||
chapters,
|
||||
}
|
||||
})
|
||||
.filter((p): p is Part => p !== null)
|
||||
.sort((a, b) => Number(a.number) - Number(b.number))
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
export function getSectionBySlug(slug: string): Section | null {
|
||||
const parts = getBookStructure()
|
||||
for (const part of parts) {
|
||||
for (const chapter of part.chapters) {
|
||||
for (const section of chapter.sections) {
|
||||
if (section.id === slug) {
|
||||
return section
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getChapterBySectionSlug(slug: string): { part: Part; chapter: Chapter } | null {
|
||||
const parts = getBookStructure()
|
||||
for (const part of parts) {
|
||||
for (const chapter of part.chapters) {
|
||||
for (const section of chapter.sections) {
|
||||
if (section.id === slug) {
|
||||
return { part, chapter }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import { bookData, specialSections } from "@/lib/book-data"
|
||||
|
||||
export type DocumentationPage = {
|
||||
path: string
|
||||
title: string
|
||||
subtitle?: string
|
||||
caption?: string
|
||||
group: string
|
||||
waitForSelector?: string
|
||||
order?: number
|
||||
}
|
||||
|
||||
function pickRepresentativeReadIds(): { id: string; title: string; group: string }[] {
|
||||
const picks: { id: string; title: string; group: string }[] = []
|
||||
|
||||
picks.push({ id: specialSections.preface.id, title: specialSections.preface.title, group: "阅读页面" })
|
||||
|
||||
for (const part of bookData) {
|
||||
const firstChapter = part.chapters[0]
|
||||
const firstSection = firstChapter?.sections?.[0]
|
||||
if (firstSection) {
|
||||
picks.push({
|
||||
id: firstSection.id,
|
||||
title: `${part.number} ${part.title}|${firstSection.title}`,
|
||||
group: "阅读页面",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const extraReadIds = ["9.11", "9.10", "9.9"]
|
||||
for (const targetId of extraReadIds) {
|
||||
const found = bookData
|
||||
.flatMap((p) => p.chapters)
|
||||
.flatMap((c) => c.sections)
|
||||
.find((s) => s.id === targetId)
|
||||
if (found) {
|
||||
picks.push({ id: found.id, title: found.title, group: "阅读页面" })
|
||||
}
|
||||
}
|
||||
|
||||
const seen = new Set<string>()
|
||||
return picks.filter((p) => {
|
||||
if (seen.has(p.id)) return false
|
||||
seen.add(p.id)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function getDocumentationCatalog(): DocumentationPage[] {
|
||||
const pages: DocumentationPage[] = [
|
||||
{
|
||||
path: "/",
|
||||
title: "首页",
|
||||
subtitle: "应用主入口",
|
||||
caption:
|
||||
"首页是用户进入应用的第一个页面,展示书籍封面、简介、目录预览和购买入口。用户可以快速了解内容概要并进行购买决策。",
|
||||
group: "核心页面",
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
path: "/chapters",
|
||||
title: "目录页",
|
||||
subtitle: "章节浏览与导航",
|
||||
caption:
|
||||
"目录页展示全书的完整章节结构,用户可以浏览各篇、各章内容,查看已解锁和待解锁章节,并快速跳转到阅读页面。",
|
||||
group: "核心页面",
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
title: "关于页面",
|
||||
subtitle: "作者与产品介绍",
|
||||
caption: "关于页面展示作者信息、产品理念、运营数据等,帮助用户建立对内容的信任和理解。",
|
||||
group: "核心页面",
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
path: "/my",
|
||||
title: "个人中心",
|
||||
subtitle: "用户账户入口",
|
||||
caption: "个人中心聚合用户的账户信息、购买记录、分销收益等功能入口,是用户管理个人信息的核心页面。",
|
||||
group: "用户中心",
|
||||
order: 4,
|
||||
},
|
||||
{
|
||||
path: "/my/purchases",
|
||||
title: "我的购买",
|
||||
subtitle: "已购内容管理",
|
||||
caption: "展示用户已购买的所有章节,包括购买时间、解锁进度,用户可快速跳转到已购内容继续阅读。",
|
||||
group: "用户中心",
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
path: "/my/settings",
|
||||
title: "账户设置",
|
||||
subtitle: "个人信息配置",
|
||||
caption: "用户可在此页面管理个人基础信息、通知偏好、隐私设置等账户相关配置。",
|
||||
group: "用户中心",
|
||||
order: 6,
|
||||
},
|
||||
{
|
||||
path: "/my/referral",
|
||||
title: "分销中心",
|
||||
subtitle: "邀请与收益管理",
|
||||
caption: "分销中心展示用户的专属邀请链接、邀请人数统计、收益明细,支持一键分享到朋友圈或Soul派对。",
|
||||
group: "用户中心",
|
||||
order: 7,
|
||||
},
|
||||
{
|
||||
path: "/admin/login",
|
||||
title: "后台登录",
|
||||
subtitle: "管理员入口",
|
||||
caption: "管理后台的登录页面,管理员通过账号密码验证后进入管理系统。",
|
||||
group: "管理后台",
|
||||
order: 8,
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
title: "后台管理",
|
||||
subtitle: "系统配置中心",
|
||||
caption: "管理后台的核心页面,包含数据概览、内容管理、用户管理、支付配置、二维码管理、系统设置等功能模块。",
|
||||
group: "管理后台",
|
||||
order: 9,
|
||||
},
|
||||
{
|
||||
path: "/docs",
|
||||
title: "开发文档",
|
||||
subtitle: "技术与配置说明",
|
||||
caption: "面向开发者和运营人员的技术文档,包含支付接口配置说明、分销规则详解、提现流程等内容。",
|
||||
group: "运营支持",
|
||||
order: 10,
|
||||
},
|
||||
]
|
||||
|
||||
const readPicks = pickRepresentativeReadIds()
|
||||
for (let i = 0; i < readPicks.length; i++) {
|
||||
const pick = readPicks[i]
|
||||
pages.push({
|
||||
path: `/read/${encodeURIComponent(pick.id)}`,
|
||||
title: pick.title,
|
||||
subtitle: "章节阅读",
|
||||
caption: "阅读页面展示章节的完整内容,未购买用户可预览部分内容,付费墙引导购买解锁全文。",
|
||||
group: pick.group,
|
||||
waitForSelector: "main",
|
||||
order: 100 + i,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by order
|
||||
return pages.sort((a, b) => (a.order || 999) - (b.order || 999))
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
import {
|
||||
Document,
|
||||
HeadingLevel,
|
||||
ImageRun,
|
||||
Packer,
|
||||
Paragraph,
|
||||
TableOfContents,
|
||||
TextRun,
|
||||
AlignmentType,
|
||||
PageBreak,
|
||||
BorderStyle,
|
||||
} from "docx"
|
||||
import type { DocumentationPage } from "@/lib/documentation/catalog"
|
||||
|
||||
export type DocumentationRenderItem = {
|
||||
page: DocumentationPage
|
||||
screenshotPng?: Buffer
|
||||
error?: string
|
||||
}
|
||||
|
||||
function groupBy<T>(items: T[], getKey: (item: T) => string): Record<string, T[]> {
|
||||
const map: Record<string, T[]> = {}
|
||||
for (const item of items) {
|
||||
const key = getKey(item)
|
||||
if (!map[key]) map[key] = []
|
||||
map[key].push(item)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
export async function renderDocumentationDocx(items: DocumentationRenderItem[]) {
|
||||
const now = new Date()
|
||||
const title = "Soul派对 - 应用功能文档"
|
||||
const subtitle = `生成时间:${now.toLocaleString("zh-CN", { hour12: false })}`
|
||||
const version = `文档版本:v1.0`
|
||||
|
||||
const children: Paragraph[] = []
|
||||
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: title,
|
||||
heading: HeadingLevel.TITLE,
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: subtitle, size: 24 })],
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: version, size: 20, color: "666666" })],
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
)
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: "文档概述",
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "本文档自动生成,包含应用程序所有核心页面的功能说明与界面截图。文档按功能模块分组,便于快速查阅和理解应用结构。",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `共包含 ${items.length} 个页面,${Object.keys(groupBy(items, (i) => i.page.group)).length} 个功能模块。`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: "目录",
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new TableOfContents("目录", {
|
||||
hyperlink: true,
|
||||
headingStyleRange: "1-3",
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new PageBreak()],
|
||||
}),
|
||||
)
|
||||
|
||||
const grouped = groupBy(items, (i) => i.page.group)
|
||||
const groupNames = Object.keys(grouped)
|
||||
|
||||
let pageNumber = 1
|
||||
for (const groupName of groupNames) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: groupName,
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
border: {
|
||||
bottom: { color: "2DD4BF", size: 6, style: BorderStyle.SINGLE },
|
||||
},
|
||||
}),
|
||||
)
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
|
||||
for (const item of grouped[groupName]) {
|
||||
const { page } = item
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: `${pageNumber}. ${page.title}`,
|
||||
heading: HeadingLevel.HEADING_2,
|
||||
}),
|
||||
)
|
||||
|
||||
if (page.subtitle) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: page.subtitle, italics: true, color: "666666" })],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({ text: "页面路径:", bold: true }),
|
||||
new TextRun({ text: page.path, color: "2563EB" }),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
if (page.caption) {
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: "功能说明:", bold: true })],
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: page.caption })],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
|
||||
if (item.error) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({ text: "截图状态:", bold: true }),
|
||||
new TextRun({ text: `失败 - ${item.error}`, color: "DC2626" }),
|
||||
],
|
||||
}),
|
||||
)
|
||||
} else if (item.screenshotPng) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: "界面截图:", bold: true })],
|
||||
}),
|
||||
)
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new ImageRun({
|
||||
data: item.screenshotPng,
|
||||
transformation: { width: 320, height: 693 },
|
||||
}),
|
||||
],
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: `图${pageNumber}: ${page.title}界面`, size: 20, color: "666666" })],
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
pageNumber++
|
||||
}
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new PageBreak()],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: "附录",
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: "技术说明", bold: true })],
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "• 截图尺寸:430×932像素 (iPhone 14 Pro Max)",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "• 截图方式:Playwright自动化浏览器截图",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "• 文档格式:Microsoft Word (.docx)",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: "本文档由系统自动生成,如有问题请联系技术支持。", color: "666666", size: 20 })],
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
)
|
||||
|
||||
const doc = new Document({
|
||||
title: "Soul派对 - 应用功能文档",
|
||||
description: "自动生成的应用功能文档",
|
||||
creator: "Soul派对文档生成器",
|
||||
sections: [
|
||||
{
|
||||
properties: {},
|
||||
children,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return await Packer.toBuffer(doc)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import type { DocumentationPage } from "@/lib/documentation/catalog"
|
||||
|
||||
export type ScreenshotResult = {
|
||||
page: DocumentationPage
|
||||
screenshotPng?: Buffer
|
||||
error?: string
|
||||
}
|
||||
|
||||
type CaptureOptions = {
|
||||
baseUrl: string
|
||||
timeoutMs: number
|
||||
viewport: { width: number; height: number }
|
||||
}
|
||||
|
||||
export async function captureScreenshots(
|
||||
pages: DocumentationPage[],
|
||||
options: CaptureOptions,
|
||||
): Promise<ScreenshotResult[]> {
|
||||
const { chromium } = await import("playwright")
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
})
|
||||
|
||||
try {
|
||||
const results: ScreenshotResult[] = []
|
||||
|
||||
for (const pageInfo of pages) {
|
||||
const page = await browser.newPage({ viewport: options.viewport })
|
||||
try {
|
||||
const captureUrl = new URL("/documentation/capture", options.baseUrl)
|
||||
captureUrl.searchParams.set("path", pageInfo.path)
|
||||
|
||||
console.log(`[v0] Capturing: ${pageInfo.path}`)
|
||||
|
||||
await page.goto(captureUrl.toString(), {
|
||||
waitUntil: "networkidle",
|
||||
timeout: options.timeoutMs,
|
||||
})
|
||||
|
||||
const iframeHandle = await page.waitForSelector('iframe[data-doc-iframe="true"]', {
|
||||
timeout: options.timeoutMs,
|
||||
})
|
||||
|
||||
const frame = await iframeHandle.contentFrame()
|
||||
if (!frame) {
|
||||
throw new Error("无法获取iframe内容")
|
||||
}
|
||||
|
||||
await frame.waitForLoadState("domcontentloaded", { timeout: options.timeoutMs })
|
||||
|
||||
// Allow network to settle
|
||||
await frame.waitForLoadState("networkidle", { timeout: options.timeoutMs }).catch(() => {
|
||||
console.log(`[v0] Network idle timeout for ${pageInfo.path}, continuing...`)
|
||||
})
|
||||
|
||||
if (pageInfo.waitForSelector) {
|
||||
await frame
|
||||
.waitForSelector(pageInfo.waitForSelector, {
|
||||
timeout: options.timeoutMs,
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(`[v0] Selector timeout for ${pageInfo.path}, continuing...`)
|
||||
})
|
||||
}
|
||||
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
const screenshot = await iframeHandle.screenshot({
|
||||
type: "png",
|
||||
animations: "disabled",
|
||||
})
|
||||
|
||||
results.push({
|
||||
page: pageInfo,
|
||||
screenshotPng: Buffer.from(screenshot),
|
||||
})
|
||||
|
||||
console.log(`[v0] Success: ${pageInfo.path}`)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.log(`[v0] Error capturing ${pageInfo.path}: ${message}`)
|
||||
results.push({ page: pageInfo, error: message })
|
||||
} finally {
|
||||
await page.close().catch(() => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
} finally {
|
||||
await browser.close().catch(() => undefined)
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
export interface MarkdownContent {
|
||||
frontmatter: {
|
||||
[key: string]: any
|
||||
}
|
||||
content: string
|
||||
}
|
||||
|
||||
export function getMarkdownContent(filePath: string): MarkdownContent {
|
||||
try {
|
||||
const fullPath = path.join(process.cwd(), filePath)
|
||||
|
||||
// Ensure file exists
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.warn(`File not found: ${filePath}`)
|
||||
return {
|
||||
frontmatter: {},
|
||||
content: 'Content not found.'
|
||||
}
|
||||
}
|
||||
|
||||
const fileContents = fs.readFileSync(fullPath, 'utf8')
|
||||
const { data, content } = matter(fileContents)
|
||||
|
||||
return {
|
||||
frontmatter: data,
|
||||
content,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading markdown file: ${filePath}`, error)
|
||||
return {
|
||||
frontmatter: {},
|
||||
content: 'Error loading content.'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
export type CampaignType = 'popup' | 'banner' | 'modal' | 'toast';
|
||||
|
||||
export interface Campaign {
|
||||
id: string;
|
||||
name: string;
|
||||
type: CampaignType;
|
||||
isActive: boolean;
|
||||
rules: CampaignRule[];
|
||||
content: CampaignContent;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}
|
||||
|
||||
export interface CampaignRule {
|
||||
type: 'time_on_page' | 'scroll_depth' | 'exit_intent' | 'user_segment';
|
||||
value: number | string; // e.g., 30 (seconds), 50 (percent)
|
||||
}
|
||||
|
||||
export interface CampaignContent {
|
||||
title?: string;
|
||||
body?: string;
|
||||
imageUrl?: string;
|
||||
ctaText?: string;
|
||||
ctaUrl?: string;
|
||||
}
|
||||
|
||||
export interface MarketingService {
|
||||
getActiveCampaigns(context: UserContext): Promise<Campaign[]>;
|
||||
trackImpression(campaignId: string): void;
|
||||
trackClick(campaignId: string): void;
|
||||
}
|
||||
|
||||
export interface UserContext {
|
||||
userId?: string;
|
||||
pageUrl: string;
|
||||
device: 'mobile' | 'desktop';
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
export type PaymentStatus = 'pending' | 'success' | 'failed' | 'refunded';
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
userId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
status: PaymentStatus;
|
||||
items: OrderItem[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
id: string;
|
||||
productId: string;
|
||||
productType: 'section' | 'full_book' | 'membership';
|
||||
quantity: number;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export interface PaymentProvider {
|
||||
name: string;
|
||||
createOrder(order: Order): Promise<{ payUrl: string; orderId: string }>;
|
||||
checkStatus(orderId: string): Promise<PaymentStatus>;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
export interface ReferralCode {
|
||||
code: string;
|
||||
ownerId: string;
|
||||
createdAt: Date;
|
||||
campaignId?: string;
|
||||
}
|
||||
|
||||
export interface ReferralStats {
|
||||
userId: string;
|
||||
totalClicks: number;
|
||||
totalConversions: number;
|
||||
totalEarnings: number;
|
||||
pendingEarnings: number;
|
||||
}
|
||||
|
||||
export interface ReferralRecord {
|
||||
id: string;
|
||||
referrerId: string;
|
||||
refereeId: string; // The new user
|
||||
action: 'register' | 'purchase';
|
||||
amount?: number; // Purchase amount if applicable
|
||||
commission: number;
|
||||
status: 'pending' | 'paid' | 'cancelled';
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface ReferralService {
|
||||
generateCode(userId: string): Promise<string>;
|
||||
trackVisit(code: string, visitorInfo: any): Promise<void>;
|
||||
recordConversion(code: string, action: 'register' | 'purchase', amount?: number): Promise<ReferralRecord>;
|
||||
getStats(userId: string): Promise<ReferralStats>;
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
export interface PaymentOrder {
|
||||
orderId: string
|
||||
userId: string
|
||||
type: "section" | "fullbook"
|
||||
sectionId?: string
|
||||
sectionTitle?: string
|
||||
amount: number
|
||||
paymentMethod: "wechat" | "alipay" | "usdt" | "paypal"
|
||||
referralCode?: string
|
||||
status: "pending" | "completed" | "failed" | "refunded"
|
||||
createdAt: string
|
||||
expireAt: string
|
||||
transactionId?: string
|
||||
completedAt?: string
|
||||
}
|
||||
|
||||
export class PaymentService {
|
||||
/**
|
||||
* Create a new payment order
|
||||
*/
|
||||
static async createOrder(params: {
|
||||
userId: string
|
||||
type: "section" | "fullbook"
|
||||
sectionId?: string
|
||||
sectionTitle?: string
|
||||
amount: number
|
||||
paymentMethod: string
|
||||
referralCode?: string
|
||||
}): Promise<PaymentOrder> {
|
||||
const response = await fetch("/api/payment/create-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
if (result.code !== 0) {
|
||||
throw new Error(result.message || "创建订单失败")
|
||||
}
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify payment completion
|
||||
*/
|
||||
static async verifyPayment(orderId: string, transactionId?: string): Promise<boolean> {
|
||||
const response = await fetch("/api/payment/verify", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ orderId, transactionId }),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
return result.code === 0 && result.data.status === "completed"
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user orders
|
||||
*/
|
||||
static async getUserOrders(userId: string): Promise<PaymentOrder[]> {
|
||||
const response = await fetch(`/api/orders?userId=${userId}`)
|
||||
const result = await response.json()
|
||||
|
||||
if (result.code !== 0) {
|
||||
throw new Error(result.message || "获取订单失败")
|
||||
}
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get payment gateway config
|
||||
*/
|
||||
static getPaymentConfig(method: "wechat" | "alipay" | "usdt" | "paypal") {
|
||||
// In production, fetch from settings API
|
||||
// For now, use localStorage
|
||||
const settings = JSON.parse(localStorage.getItem("settings") || "{}")
|
||||
return settings.paymentMethods?.[method] || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate payment QR code URL
|
||||
*/
|
||||
static getPaymentQRCode(method: "wechat" | "alipay", amount: number, orderId: string): string {
|
||||
const config = this.getPaymentConfig(method)
|
||||
|
||||
// If it's a redirect URL, return it directly
|
||||
if (
|
||||
config.qrCode?.startsWith("http") ||
|
||||
config.qrCode?.startsWith("weixin://") ||
|
||||
config.qrCode?.startsWith("alipays://")
|
||||
) {
|
||||
return config.qrCode
|
||||
}
|
||||
|
||||
// Otherwise return the QR code image
|
||||
return config.qrCode || ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Open payment app (Wechat/Alipay)
|
||||
*/
|
||||
static openPaymentApp(method: "wechat" | "alipay", orderId: string): boolean {
|
||||
const config = this.getPaymentConfig(method)
|
||||
const redirectUrl = config.qrCode
|
||||
|
||||
if (!redirectUrl) {
|
||||
console.error("[v0] No payment URL configured for", method)
|
||||
return false
|
||||
}
|
||||
|
||||
// Open URL in new window/tab
|
||||
window.open(redirectUrl, "_blank")
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
export interface Order {
|
||||
id: string
|
||||
userId: string
|
||||
amount: number
|
||||
currency: string
|
||||
status: "pending" | "paid" | "failed" | "refunded"
|
||||
items: { type: "book" | "section"; id: string; title: string; price: number }[]
|
||||
gateway?: string
|
||||
transactionId?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface PaymentConfig {
|
||||
wechat: {
|
||||
enabled: boolean
|
||||
qrcode: string
|
||||
appId?: string
|
||||
}
|
||||
alipay: {
|
||||
enabled: boolean
|
||||
qrcode: string
|
||||
appId?: string
|
||||
}
|
||||
usdt: {
|
||||
enabled: boolean
|
||||
walletAddress: string
|
||||
network: string
|
||||
}
|
||||
}
|
||||
|
||||
const ORDERS_KEY = "soul_orders"
|
||||
const PAYMENT_CONFIG_KEY = "soul_payment_config"
|
||||
|
||||
// 订单管理
|
||||
export function getOrders(): Order[] {
|
||||
if (typeof window === "undefined") return []
|
||||
const data = localStorage.getItem(ORDERS_KEY)
|
||||
return data ? JSON.parse(data) : []
|
||||
}
|
||||
|
||||
export function getOrderById(id: string): Order | undefined {
|
||||
return getOrders().find((o) => o.id === id)
|
||||
}
|
||||
|
||||
export function getOrdersByUser(userId: string): Order[] {
|
||||
return getOrders().filter((o) => o.userId === userId)
|
||||
}
|
||||
|
||||
export function createOrder(order: Omit<Order, "id" | "createdAt" | "updatedAt">): Order {
|
||||
const orders = getOrders()
|
||||
const newOrder: Order = {
|
||||
...order,
|
||||
id: `order_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
orders.push(newOrder)
|
||||
localStorage.setItem(ORDERS_KEY, JSON.stringify(orders))
|
||||
return newOrder
|
||||
}
|
||||
|
||||
export function updateOrder(id: string, updates: Partial<Order>): Order | null {
|
||||
const orders = getOrders()
|
||||
const index = orders.findIndex((o) => o.id === id)
|
||||
if (index === -1) return null
|
||||
|
||||
orders[index] = {
|
||||
...orders[index],
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
localStorage.setItem(ORDERS_KEY, JSON.stringify(orders))
|
||||
return orders[index]
|
||||
}
|
||||
|
||||
// 支付配置管理
|
||||
export function getPaymentConfig(): PaymentConfig {
|
||||
if (typeof window === "undefined") {
|
||||
return {
|
||||
wechat: { enabled: false, qrcode: "" },
|
||||
alipay: { enabled: false, qrcode: "" },
|
||||
usdt: { enabled: false, walletAddress: "", network: "TRC20" },
|
||||
}
|
||||
}
|
||||
const data = localStorage.getItem(PAYMENT_CONFIG_KEY)
|
||||
return data
|
||||
? JSON.parse(data)
|
||||
: {
|
||||
wechat: { enabled: true, qrcode: "" },
|
||||
alipay: { enabled: true, qrcode: "" },
|
||||
usdt: { enabled: false, walletAddress: "", network: "TRC20" },
|
||||
}
|
||||
}
|
||||
|
||||
export function updatePaymentConfig(config: Partial<PaymentConfig>): PaymentConfig {
|
||||
const current = getPaymentConfig()
|
||||
const updated = { ...current, ...config }
|
||||
localStorage.setItem(PAYMENT_CONFIG_KEY, JSON.stringify(updated))
|
||||
return updated
|
||||
}
|
||||
|
||||
// 模拟支付流程(实际生产环境需要对接真实支付网关)
|
||||
export async function simulatePayment(orderId: string, gateway: string): Promise<boolean> {
|
||||
// 模拟支付延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
const order = updateOrder(orderId, {
|
||||
status: "paid",
|
||||
gateway,
|
||||
transactionId: `txn_${Date.now()}`,
|
||||
})
|
||||
|
||||
return !!order
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import crypto from "crypto"
|
||||
|
||||
export interface AlipayConfig {
|
||||
appId: string
|
||||
partnerId: string
|
||||
key: string
|
||||
returnUrl: string
|
||||
notifyUrl: string
|
||||
}
|
||||
|
||||
export class AlipayService {
|
||||
constructor(private config: AlipayConfig) {}
|
||||
|
||||
// 创建支付宝订单
|
||||
createOrder(params: {
|
||||
outTradeNo: string
|
||||
subject: string
|
||||
totalAmount: number
|
||||
body?: string
|
||||
}) {
|
||||
const orderInfo = {
|
||||
app_id: this.config.appId,
|
||||
method: "alipay.trade.wap.pay",
|
||||
format: "JSON",
|
||||
charset: "utf-8",
|
||||
sign_type: "MD5",
|
||||
timestamp: new Date().toISOString().slice(0, 19).replace("T", " "),
|
||||
version: "1.0",
|
||||
notify_url: this.config.notifyUrl,
|
||||
return_url: this.config.returnUrl,
|
||||
biz_content: JSON.stringify({
|
||||
out_trade_no: params.outTradeNo,
|
||||
product_code: "QUICK_WAP_WAY",
|
||||
total_amount: params.totalAmount.toFixed(2),
|
||||
subject: params.subject,
|
||||
body: params.body || params.subject,
|
||||
}),
|
||||
}
|
||||
|
||||
const sign = this.generateSign(orderInfo)
|
||||
return {
|
||||
...orderInfo,
|
||||
sign,
|
||||
paymentUrl: this.buildPaymentUrl(orderInfo, sign),
|
||||
}
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
generateSign(params: Record<string, string>): string {
|
||||
const sortedKeys = Object.keys(params).sort()
|
||||
const signString = sortedKeys
|
||||
.filter((key) => params[key] && key !== "sign")
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join("&")
|
||||
|
||||
const signWithKey = `${signString}${this.config.key}`
|
||||
return crypto.createHash("md5").update(signWithKey, "utf8").digest("hex")
|
||||
}
|
||||
|
||||
// 验证回调签名
|
||||
verifySign(params: Record<string, string>): boolean {
|
||||
const receivedSign = params.sign
|
||||
if (!receivedSign) return false
|
||||
|
||||
const calculatedSign = this.generateSign(params)
|
||||
return receivedSign.toLowerCase() === calculatedSign.toLowerCase()
|
||||
}
|
||||
|
||||
// 构建支付URL
|
||||
private buildPaymentUrl(params: Record<string, string>, sign: string): string {
|
||||
const gateway = "https://openapi.alipay.com/gateway.do"
|
||||
const queryParams = new URLSearchParams({ ...params, sign })
|
||||
return `${gateway}?${queryParams.toString()}`
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import crypto from "crypto"
|
||||
|
||||
export interface WechatPayConfig {
|
||||
appId: string
|
||||
appSecret: string
|
||||
mchId: string
|
||||
apiKey: string
|
||||
notifyUrl: string
|
||||
}
|
||||
|
||||
export class WechatPayService {
|
||||
constructor(private config: WechatPayConfig) {}
|
||||
|
||||
// 创建微信支付订单(扫码支付)
|
||||
async createOrder(params: {
|
||||
outTradeNo: string
|
||||
body: string
|
||||
totalFee: number
|
||||
spbillCreateIp: string
|
||||
}) {
|
||||
const orderParams = {
|
||||
appid: this.config.appId,
|
||||
mch_id: this.config.mchId,
|
||||
nonce_str: this.generateNonceStr(),
|
||||
body: params.body,
|
||||
out_trade_no: params.outTradeNo,
|
||||
total_fee: Math.round(params.totalFee * 100).toString(), // 转换为分
|
||||
spbill_create_ip: params.spbillCreateIp,
|
||||
notify_url: this.config.notifyUrl,
|
||||
trade_type: "NATIVE", // 扫码支付
|
||||
}
|
||||
|
||||
const sign = this.generateSign(orderParams)
|
||||
const xmlData = this.buildXML({ ...orderParams, sign })
|
||||
|
||||
// In production, make actual API call to WeChat
|
||||
// const response = await fetch("https://api.mch.weixin.qq.com/pay/unifiedorder", {
|
||||
// method: "POST",
|
||||
// body: xmlData,
|
||||
// headers: { "Content-Type": "application/xml" },
|
||||
// })
|
||||
|
||||
// Mock response for development
|
||||
return {
|
||||
codeUrl: `weixin://wxpay/bizpayurl?pr=${this.generateNonceStr()}`,
|
||||
prepayId: `prepay_${Date.now()}`,
|
||||
outTradeNo: params.outTradeNo,
|
||||
}
|
||||
}
|
||||
|
||||
// 生成随机字符串
|
||||
private generateNonceStr(): string {
|
||||
return crypto.randomBytes(16).toString("hex")
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
generateSign(params: Record<string, string>): string {
|
||||
const sortedKeys = Object.keys(params).sort()
|
||||
const signString = sortedKeys
|
||||
.filter((key) => params[key] && key !== "sign")
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join("&")
|
||||
|
||||
const signWithKey = `${signString}&key=${this.config.apiKey}`
|
||||
return crypto.createHash("md5").update(signWithKey, "utf8").digest("hex").toUpperCase()
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
verifySign(params: Record<string, string>): boolean {
|
||||
const receivedSign = params.sign
|
||||
if (!receivedSign) return false
|
||||
|
||||
const calculatedSign = this.generateSign(params)
|
||||
return receivedSign === calculatedSign
|
||||
}
|
||||
|
||||
// 构建XML数据
|
||||
private buildXML(params: Record<string, string>): string {
|
||||
const xml = ["<xml>"]
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
xml.push(`<${key}><![CDATA[${value}]]></${key}>`)
|
||||
}
|
||||
xml.push("</xml>")
|
||||
return xml.join("")
|
||||
}
|
||||
|
||||
// 解析XML数据
|
||||
private async parseXML(xml: string): Promise<Record<string, string>> {
|
||||
const result: Record<string, string> = {}
|
||||
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(xml)) !== null) {
|
||||
result[match[1]] = match[2]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
664
lib/store.ts
664
lib/store.ts
@@ -1,664 +0,0 @@
|
||||
"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 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
|
||||
}
|
||||
}
|
||||
|
||||
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派对房",
|
||||
},
|
||||
}
|
||||
|
||||
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 },
|
||||
}
|
||||
|
||||
set({ settings: { ...settings, ...newSettings } })
|
||||
} catch (error) {
|
||||
console.error("Failed to sync settings:", error)
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "soul-experiment-storage",
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user