PFr?Z;{RT%BO#Aa7t-@n3A`8>Z
z9zf@x(&wn>px{)>>rM5t++O=Q5H^-DQkwi>Ywg3}`yEN0jX^nOSg7YZ*p)=
z5QA!(s09II$gUb1Eo8B}hQE!XG#ZDF9l0Fecp#0!l#9u}nSB)~wXOHX&xGGc(dfsX
zPD8KM7#DI%g+leSriWAF{!~P;A&1H|GUNXeWvgHx5?iskSIRBc@I7
z(149qxXqF;&*t23hi|z
z@9m&V53$LG^I^(WSzLi@SDsS}!g;R5wj#;XQ?vclX(08A_W&v>34+9sU2*~ANZ%u-
zNCSzqE$i!;I9}uj`NBUzAz~?wur@c9vKe&yXyW2x)itz3FYtk)rr^|TC`2Ts@TF~Z
zNd`}q26F!2Y}cqwv_mfhnTpjvp%5{YR$a-J=WKK7BxxY+5PCW$&RqDg&;Uj}F#4Oo
zATdml212pcS$!An&YoI-&m0?YOR-^P#BQ#LiqF{=sGmm+wCNx|>YoI3+yVt&^ki$3No#|G5o6N8
zp!i_*3hJK>c`07OGYad8~v2P*31gLHCFZT-m7msCQEE
zgR$pn+ffkiRUq$|V=*>$kS&1^kvDrTMfNflgEKk$`Jk>m2d044!`E*I0#2U(T*xf+
zE!1}~;Pr~soA}Ehp68}T6ZISv)e+<24mAx;jw)~z4jI<80S`7Yx702QJT>wzC7$12
z#z7_t)rChtpC;irlMsZT0%B6oTGlfLvYWJ$pwqy=1QLX=mI)E_w4x-
zngcHaU)c@9pQbXi2PuuoBabgahOSGT{1x3c)K@iHZ{OF-oxQpM4RbMh!MOMp22?N^
zV$*4v;A_0NKO@n9dB(l!JhYKyErVf+{{CHPXg)&92R0IXh9
zu*7Q27)bqMp*eZ#;1>){eC17zdUW2!%a)6f-+fb^Bj5Pn8Lkbyn|StuHML9RcZiXp
v>c2U~r2mguFv!Tr$jHdZ$jHdZSPuRLWo^cU;d$>S00000NkvXXu0mjfwq ({ ...item }))
+}
+function createHomePageData() {
return {
- statusLabel: state.isLoggedIn ? '已登录' : '未登录',
- userName,
- tokenLabel: maskToken(state.token),
- permissionsLabel: state.permissions.length ? state.permissions.join(' / ') : '暂无权限'
+ brandName: HOME_PAGE_BLUEPRINT.brandName,
+ greeting: HOME_PAGE_BLUEPRINT.greeting,
+ subtitle: HOME_PAGE_BLUEPRINT.subtitle,
+ searchPlaceholder: HOME_PAGE_BLUEPRINT.searchPlaceholder,
+ searchBadge: HOME_PAGE_BLUEPRINT.searchBadge,
+ searchRoute: HOME_PAGE_BLUEPRINT.searchRoute,
+ portalTitle: HOME_PAGE_BLUEPRINT.portalTitle,
+ portalCards: createTcmHomeHubCards(),
+ encyclopediaTitle: HOME_PAGE_BLUEPRINT.encyclopediaTitle,
+ encyclopediaCards: cloneItems(HOME_PAGE_BLUEPRINT.encyclopediaCards),
+ toolsTitle: HOME_PAGE_BLUEPRINT.toolsTitle,
+ toolCards: cloneItems(HOME_PAGE_BLUEPRINT.toolCards),
+ wellnessTitle: HOME_PAGE_BLUEPRINT.wellnessTitle,
+ wellnessCards: cloneItems(HOME_PAGE_BLUEPRINT.wellnessCards),
+ classicsTitle: HOME_PAGE_BLUEPRINT.classicsTitle,
+ classicsActionText: HOME_PAGE_BLUEPRINT.classicsActionText,
+ classicsActionRoute: HOME_PAGE_BLUEPRINT.classicsActionRoute,
+ classicsBooks: cloneItems(HOME_PAGE_BLUEPRINT.classicsBooks)
}
}
+function navigateToRoute(route) {
+ openStaticRoute(route, wx)
+}
+
+function showPendingToast(title) {
+ if (typeof wx?.showToast !== 'function') {
+ return
+ }
+
+ wx.showToast({
+ title: title ? `${title}待开放` : '功能建设中',
+ icon: 'none'
+ })
+}
+
+function handleHomeEntry(route, title) {
+ if (openStaticRoute(route, wx)) {
+ return
+ }
+
+ showPendingToast(title)
+}
+
Page({
- data: {
- quickEntries: QUICK_ENTRIES,
- envName: '',
- apiBaseUrl: '',
- isLoggedIn: false,
- sessionView: buildSessionView(sessionStore.getState())
- },
- onLoad() {
- const runtimeConfig = getRuntimeConfig()
+ data: createHomePageData(),
- this.unsubscribe = sessionStore.subscribe(nextState => {
- this.syncSession(nextState)
- })
+ handleSearchTap() {
+ handleHomeEntry(this.data.searchRoute, '搜索')
+ },
- this.setData({
- envName: runtimeConfig.name.toUpperCase(),
- apiBaseUrl: runtimeConfig.baseURL
- })
- this.syncSession(sessionStore.getState())
- },
- onUnload() {
- this.unsubscribe?.()
- },
- syncSession(state) {
- this.setData({
- isLoggedIn: state.isLoggedIn,
- sessionView: buildSessionView(state)
- })
- },
- handlePrimaryAction() {
- if (this.data.isLoggedIn) {
- wx.navigateTo({
- url: '/packages/demo/pages/workbench/index'
- })
- return
- }
+ handleEncyclopediaTap(event) {
+ const { route, title } = event.currentTarget.dataset
- wx.navigateTo({
- url: '/pages/login/index'
- })
+ handleHomeEntry(route, title)
},
- handleLogout() {
- sessionStore.clearSession()
- wx.showToast({
- title: '已清理登录态',
- icon: 'success'
- })
+
+ handlePortalTap(event) {
+ navigateToRoute(event.currentTarget.dataset.route)
},
- handleEntryAction(event) {
- const { path } = event.detail
- if (!path) {
- return
- }
+ handleToolTap(event) {
+ const { route, title } = event.currentTarget.dataset
- wx.navigateTo({
- url: path
- })
+ handleHomeEntry(route, title)
+ },
+
+ handleWellnessTap(event) {
+ const { route, title } = event.currentTarget.dataset
+
+ handleHomeEntry(route, title)
+ },
+
+ handleClassicActionTap() {
+ handleHomeEntry(this.data.classicsActionRoute, this.data.classicsActionText)
+ },
+
+ handleClassicTap(event) {
+ const { route, title } = event.currentTarget.dataset
+
+ handleHomeEntry(route, title)
}
})
+
+module.exports = {
+ HOME_PAGE_BLUEPRINT,
+ createHomePageData
+}
diff --git a/pages/home/index.json b/pages/home/index.json
index 93aea9c..76e1d51 100644
--- a/pages/home/index.json
+++ b/pages/home/index.json
@@ -1,7 +1,5 @@
{
- "navigationBarTitleText": "首页",
- "usingComponents": {
- "app-button": "../../components/base/app-button/index",
- "entry-card": "../../components/biz/entry-card/index"
- }
+ "navigationBarTitleText": "玄知中医",
+ "navigationBarBackgroundColor": "#f8ecd8",
+ "navigationBarTextStyle": "black"
}
diff --git a/pages/home/index.wxml b/pages/home/index.wxml
index bfb5836..3fd2535 100644
--- a/pages/home/index.wxml
+++ b/pages/home/index.wxml
@@ -1,65 +1,105 @@
-
-
- Stable Native Architecture
- 玄志小程序基础骨架
-
- 原生小程序 + JS + npm + TDesign + 分包 + service/store 分层,保留微信原生性能和长期维护边界。
-
-
-
+
+ {{brandName}}
+ {{greeting}}
+ {{subtitle}}
+
+
+ 搜
+ {{searchPlaceholder}}
+ {{searchBadge}}
-
- 当前会话
-
-
- 运行环境
- {{envName}}
-
-
- API 地址
- {{apiBaseUrl}}
-
-
- 登录状态
- {{sessionView.statusLabel}}
-
-
- 当前用户
- {{sessionView.userName}}
-
-
- Token
- {{sessionView.tokenLabel}}
-
-
- 权限
- {{sessionView.permissionsLabel}}
+
+ {{encyclopediaTitle}}
+
+
+
+ {{item.status}}
+ {{item.icon}}
+ {{item.title}}
+
-
-
+
+ {{portalTitle}}
+
+
+
+ {{item.badge}}
+ {{item.title}}
+ {{item.subtitle}}
+
+
+
+
+
+
+ {{toolsTitle}}
+
+
+
+ {{item.icon}}
+ {{item.title}}
+
+
+
+
+
+
+ {{wellnessTitle}}
+
+
+
+ {{item.status}}
+
+ {{item.icon}}
+
+ {{item.title}}
+
+
+
+
+
+
+
+
+
+
+
+ {{item.coverText}}
+
+ {{item.title}}
+
+
+
diff --git a/pages/home/index.wxss b/pages/home/index.wxss
index 558f081..47262c5 100644
--- a/pages/home/index.wxss
+++ b/pages/home/index.wxss
@@ -1,68 +1,323 @@
-.hero {
- display: flex;
- flex-direction: column;
- gap: 24rpx;
- padding: 36rpx 32rpx;
- background: linear-gradient(160deg, #0f172a 0%, #1e293b 100%);
- color: #f8fafc;
+page {
+ min-height: 100%;
+ background:
+ linear-gradient(180deg, #f8edd6 0%, #f9f0de 24%, #f6ead4 100%);
}
-.hero__eyebrow {
- font-size: 22rpx;
- letter-spacing: 4rpx;
- text-transform: uppercase;
- color: #93c5fd;
+.home-page {
+ position: relative;
+ min-height: 100vh;
+ box-sizing: border-box;
+ padding: 12rpx 18rpx 36rpx;
+ background:
+ linear-gradient(135deg, rgba(255, 255, 255, 0.65) 0%, rgba(255, 248, 236, 0) 36%),
+ linear-gradient(225deg, rgba(255, 245, 227, 0.7) 0%, rgba(255, 245, 227, 0) 44%);
}
-.hero__title {
- font-size: 48rpx;
+.home-page__brand {
+ display: inline-flex;
+ padding: 12rpx 20rpx;
+ border-radius: 999rpx;
+ background: rgba(241, 222, 190, 0.95);
+ color: #946739;
+ font-size: 26rpx;
+ line-height: 1;
+ box-shadow: 0 12rpx 24rpx rgba(173, 120, 56, 0.08);
+}
+
+.home-page__greeting {
+ display: block;
+ margin-top: 12rpx;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 60rpx;
font-weight: 700;
+ line-height: 1.1;
+ color: #2f1f12;
+}
+
+.home-page__subtitle {
+ display: block;
+ margin-top: 10rpx;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 24rpx;
+ line-height: 1.65;
+ color: #8f6a42;
+}
+
+.search-card {
+ display: flex;
+ align-items: center;
+ margin-top: 16rpx;
+ padding: 16rpx 18rpx 16rpx 20rpx;
+ border: 1rpx solid rgba(182, 151, 112, 0.24);
+ border-radius: 30rpx;
+ background: rgba(255, 251, 245, 0.96);
+ box-shadow: 0 16rpx 30rpx rgba(185, 152, 108, 0.14);
+}
+
+.search-card__leading {
+ margin-right: 12rpx;
+ width: 40rpx;
+ height: 40rpx;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #78c7dd 0%, #7b60bf 100%);
+ color: #ffffff;
+ font-size: 22rpx;
+ font-weight: 700;
+ line-height: 40rpx;
+ text-align: center;
+ box-shadow: 0 10rpx 18rpx rgba(123, 96, 191, 0.22);
+}
+
+.search-card__placeholder {
+ flex: 1;
+ margin-right: 12rpx;
+ font-size: 26rpx;
+ color: #a18463;
+}
+
+.search-card__badge {
+ flex-shrink: 0;
+ min-width: 80rpx;
+ padding: 10rpx 18rpx;
+ border-radius: 999rpx;
+ background: linear-gradient(135deg, #b67b35 0%, #8f5e21 100%);
+ color: #fff4de;
+ font-family: 'Georgia', 'Times New Roman', serif;
+ font-size: 24rpx;
+ font-weight: 700;
+ line-height: 1;
+ text-align: center;
+}
+
+.section-card {
+ margin-top: 16rpx;
+ padding: 24rpx 18rpx 16rpx;
+ border: 1rpx solid rgba(189, 155, 105, 0.14);
+ border-radius: 30rpx;
+ background: rgba(255, 250, 243, 0.94);
+ box-shadow: 0 18rpx 36rpx rgba(178, 144, 99, 0.12);
+}
+
+.section-card--last {
+ margin-bottom: 0;
+}
+
+.section-card__header {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ margin-bottom: 18rpx;
+ padding: 0 4rpx;
+}
+
+.section-card__header .section-card__title {
+ margin-bottom: 0;
+ padding-left: 0;
+}
+
+.section-card__title {
+ display: block;
+ margin-bottom: 18rpx;
+ padding-left: 4rpx;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 42rpx;
+ font-weight: 700;
+ line-height: 1.2;
+ color: #2f1f12;
+}
+
+.section-card__action {
+ color: #be8b4a;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 24rpx;
line-height: 1.2;
}
-.hero__summary {
- font-size: 26rpx;
- line-height: 1.8;
- color: rgba(248, 250, 252, 0.8);
+.portal-grid {
+ overflow: hidden;
+ margin: 0 -5rpx;
}
-.session-card {
+.portal-grid__item {
+ box-sizing: border-box;
+ float: left;
+ width: 50%;
+ padding: 0 5rpx 10rpx;
+}
+
+.portal-card {
+ min-height: 164rpx;
+ padding: 18rpx 18rpx 16rpx;
+ border: 1rpx solid rgba(191, 169, 140, 0.46);
+ border-radius: 24rpx;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.78) 0%, rgba(250, 243, 232, 0.96) 100%);
+ box-shadow: 0 10rpx 20rpx rgba(182, 150, 107, 0.1);
+}
+
+.portal-card__badge {
+ display: inline-block;
+ padding: 6rpx 14rpx;
+ border-radius: 999rpx;
+ background: rgba(246, 234, 214, 0.94);
+ color: #b67c31;
+ font-size: 18rpx;
+ line-height: 1;
+}
+
+.portal-card__title {
+ display: block;
+ margin-top: 18rpx;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 28rpx;
+ font-weight: 700;
+ line-height: 1.2;
+ color: #38261a;
+}
+
+.portal-card__subtitle {
+ display: block;
+ margin-top: 10rpx;
+ font-size: 22rpx;
+ line-height: 1.55;
+ color: #8f6a42;
+}
+
+.feature-grid {
display: flex;
- flex-direction: column;
- gap: 24rpx;
- padding: 32rpx;
+ flex-wrap: wrap;
+ margin: 0 -4rpx;
}
-.session-card__rows {
+.feature-grid__item {
+ box-sizing: border-box;
+ padding: 0 4rpx 10rpx;
+}
+
+.feature-grid--four .feature-grid__item {
+ width: 25%;
+}
+
+.feature-grid--three .feature-grid__item {
+ width: 33.3333%;
+}
+
+.feature-card {
+ position: relative;
display: flex;
+ box-sizing: border-box;
+ width: 100%;
flex-direction: column;
- gap: 20rpx;
+ align-items: center;
+ justify-content: center;
+ padding: 18rpx 8rpx;
+ border: 1rpx solid rgba(191, 169, 140, 0.46);
+ border-radius: 22rpx;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.7) 0%, rgba(250, 243, 232, 0.92) 100%);
}
-.session-card__label {
- flex-shrink: 0;
+.feature-grid--four .feature-card {
+ min-height: 152rpx;
+}
+
+.feature-grid--three .feature-card {
+ min-height: 182rpx;
+}
+
+.feature-card__status {
+ position: absolute;
+ top: 8rpx;
+ right: 8rpx;
+ padding: 6rpx 12rpx;
+ border-radius: 999rpx;
+ background: rgba(246, 234, 214, 0.94);
+ color: #c39d6a;
+ font-size: 18rpx;
+ line-height: 1;
+}
+
+.feature-card__icon {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 20rpx;
+ background: linear-gradient(180deg, #f5ebdc 0%, #efe1cc 100%);
+ color: #b67c31;
+ font-size: 28rpx;
+ font-weight: 700;
+ line-height: 60rpx;
+ text-align: center;
+ box-shadow: inset 0 -8rpx 16rpx rgba(189, 150, 95, 0.08);
+}
+
+.feature-card__emoji-box {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 72rpx;
+ height: 72rpx;
+ border-radius: 20rpx;
+ background: linear-gradient(180deg, #f7efe3 0%, #efe0ca 100%);
+ box-shadow: inset 0 -8rpx 16rpx rgba(189, 150, 95, 0.06);
+}
+
+.feature-card__emoji {
+ font-size: 36rpx;
+ line-height: 1;
+}
+
+.feature-card__icon + .feature-card__title {
+ margin-top: 12rpx;
+}
+
+.feature-card__title {
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 22rpx;
+ font-weight: 700;
+ line-height: 1.35;
+ color: #38261a;
+ text-align: center;
+}
+
+.feature-card__title--wellness {
+ margin-top: 16rpx;
font-size: 24rpx;
- color: #64748b;
}
-.session-card__value {
- flex: 1;
- text-align: right;
- font-size: 24rpx;
- color: #0f172a;
+.feature-card--classic {
+ justify-content: flex-start;
+ padding-top: 18rpx;
+ padding-bottom: 14rpx;
}
-.session-card__value--mono {
- font-family: 'Cascadia Code', 'Courier New', monospace;
- word-break: break-all;
+.feature-card__book {
+ position: relative;
+ width: 76rpx;
+ height: 112rpx;
+ border-radius: 8rpx;
+ background:
+ linear-gradient(90deg, rgba(138, 93, 48, 0.44) 0, rgba(138, 93, 48, 0.44) 10rpx, transparent 10rpx, transparent 100%),
+ linear-gradient(180deg, #cfaa75 0%, #c79d67 100%);
+ box-shadow: 0 10rpx 18rpx rgba(173, 128, 70, 0.12);
}
-.entry-list {
- display: flex;
- flex-direction: column;
- gap: 24rpx;
+.feature-card__book-char {
+ position: absolute;
+ top: 50%;
+ left: 10rpx;
+ right: 0;
+ transform: translateY(-50%);
+ color: #fff7eb;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 28rpx;
+ font-weight: 700;
+ line-height: 1;
+ text-align: center;
}
-.meta-row--top {
- align-items: flex-start;
+.feature-card__title--classic {
+ margin-top: 14rpx;
+ min-height: 56rpx;
+ padding: 0 4rpx;
+ font-size: 20rpx;
+ line-height: 1.45;
}
diff --git a/pages/home/tab-home-active.png b/pages/home/tab-home-active.png
new file mode 100644
index 0000000000000000000000000000000000000000..ff9a151ad69f3810b75e5a121001fc20beaebce4
GIT binary patch
literal 1578
zcmV+_2G#kAP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1;0r|K~#8N?OZ*K
z95ob9Nl8idJam+lR44@n5)D+qdRBJ@MRe6w5e*^|1q#{qP$dmgL=~!}K|)FsuDa@!
zf+A8HQjzAQ6cnqgK%d7sWI;U{QNxEu4QCoWMpJyWMpJy
zWMuTj4}0pJN~`NF?x})*m*G&A>Ql3n`=*p9m6lCas(&t{`|X3ORQuJQdYjw4^7wX7
zy|pgY7ZAZ{dBp+^qL$yxLft?Q=I320pvP3DeBZUV4W-fY=*kU!^gN^0*L`}O(eigy
zs*@JAI_2MOrIw8;rMePB9Yd_r^3rp%?hgfd@Zo`ME0Ph1Xt#
zcyV}vc#&*d@~C^CJYuJs!kJQjF^ggu5U3Ydi{f*$D5{GEmE$*RzQlZFN_px#QV&z3
z)lYX8#fL0L3>vMz+dAwqA{Od>wl_Jz$nfx$GxWSWT0UXNW3zri#a%bAvXC1@25fgS
zK)e@Pc}*$LG340Usfqd3XgOkgYBALEatqDec}U8*C@ppPj?5!huBu6FfUIHxUW&JZWRsxdU@-w^+cGdSHNu
zY%e;&Y;j~2GDyo&!0@D4$CRFs75g_i@aA|P#40@ZBGQ$Wmd7n2wQN>js`uE>NrA0Y
z`)$hETC304*ljeWtOC7Z#lH)&TBv>gzm=BX`F9r)FPnpM?6BC+$xtUu+u;lXX*TpC
z&MhKn%HpQi&aQSeHapUCK%Ms8iKHMRh$)C{c*6chhs_CcFI*HA8+RGg5i!skr;Z(M
zI==nD(;)0L@-h}l09*EF1C{4ii_wcYvms6FbeBZeM}EHW+VDAMG@<=lxCZe18#dL
z-*)&p8_|a;iipug?Z#bLY$CS(mQLA-K1@4`{Z(vwt?**fW_(*rWv=LV-I;z7$6Ew9-BCFzzWh9^3@2K#z
zIp{&fO*ZUJA-5L-uzv
zc-Jci9^%hQuII+0$$m}>S7JP>sA*2xs~X%3rwnVh!7FU4_tUr8;M9myN?uHMQpgkn
z4zGAW?dlW{*ziR5|W_RVqU4hI88w!r8*%`~=B_J2hOj0Hi1NLTp
zKuEW%+1Y|)ukUWT*XM5LrzzlTxbHi7_xx(g@sx9=+Y3MRpX->?9C#V9HT1)urZe(5
zrkHil!$nAc=(^{neMNU0a7N*651EZm7qBpw5ig<3Ck$-DWQs$lUBWs{=QANTKb`?`
zTs`=0Y|-#%*VB+eIti_|Og9qdj{#%CUMer;89N)DxGuNLY|I>p{$U|UOdYmnDD{;$
zHeBz#$CoWT(O&wN2ll1>--O$SI=y!Ofwfzp4d795@BVJLm|9(*HDQpEk&%&+k&%&+
ck+B>63-j5dID>5YegFUf07*qoM6N<$g1r^~dH?_b
literal 0
HcmV?d00001
diff --git a/pages/home/tab-home.png b/pages/home/tab-home.png
new file mode 100644
index 0000000000000000000000000000000000000000..90ee7713f216fe2a6188d7624035a44def55c59c
GIT binary patch
literal 1601
zcmV-H2EO@;P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1=dMKK~#8N?OZ)<
z97PmPNl8gbNe2xQ1qBifqT#S_=3GIVNReowG}t?1*`k0dN)stkBxdc(U8o`oB1KAr
zT$%2wh$d1B7nW$84)oscoo>FF^X<>f?&O$H`U>0Y+4tVen|br*&EC3|k&%&+k&%&+
zk&%&+(Gum6_@Gp?FDxF-KE&rTYLr?|%%LoemM@GF*QJ*KZesoHS*gYC^5~oQvCXYV
zdG!5zrIveS1f%2?8mJ46mcN&UxJx4#-?vqu5o5IcrRiwvN~6SYTj%;b@|4(bHR_c7
z$7p$OQHe|ExizGYGFm*`3Rfo=GfKWeB|+g!b12SAwR^`Le*0;8u=CL-o)q#sqvexQ
z%U9T*-L|}<4y2Am*^&nd&2HsUY-uIv_=y73QJv~Z|L8E5(X-a_(zNYOY>N7Za74|G5G{K`H<5Ny>p5zc{?7*D2
zTM3UqhidnZN8XFk;>fa)_Z)#}B1gefV--_^A;m+~FFLAV+MWk#6&`yLX-qYJ2AzLR
zzehczg11suZ;H3&;qI4?uxogtJ)?oP
zPZ-WrTR2zPbsXD>OeKE6b_UzgaCXEt;APvLND@Y*&_|28-Hv>L`UVH*1TmWI=ZV(0B$YDTOQ6qast
z)ISJlu2_B1shZAEn5In5h;+7tE*;~L3+KZwtMY=aj)4Pi%Cmvca}=Tt6E)&fyWP+W3stVJ3QbvIk04E%q7gBhL*9pW=nWLy5uycC
zG$JNwqhz^p+t)RNLrl?#cqSdCii}d{)=N5Mv=>axh*a-rwWs72>K_Dn&fF*9PR)oI
zXj36R>K_DDZs7>uu(B1IoDnhkpydSh4+gBe=~{D>mJ#K|YKyW}G0RFQ0JRk88FZ&W
z$CZ^hL%oB7Uksywj~zY2y$j^?a!kgC4zhjVB68c6dhK3DrBjn5-$Zrg1#k;ky?l1x
z5ioh$b0M>k_fX$x!0Q!V5Anw&)^pRMiF!swRbt$)sA)`cRDq)~W>~WhSlGlo5XUHR
zYUEQ&tlu7@kjaPY!lS*PCgFGs^(Y0NtDu+T#Yaw)wJ7);igjVT
z;l>Cj>s`6W>k#+$LXk(H&w!z`*L~oY9~0>+S_+|$oS+MHM-N$E+!U1gKtWm99yL3C
z>AM6}%XAT?Oe6&8W<3gA2`%PBE;y7h#yD^(3*XF7)PM`r-*<5C`7t?R5pS;KxsNWO
z<9nCyPiPL@1irHCgg>jwOrM1`CXYP62rh|SSbO`vR&M#}0yNBK@`ADX
z69$wpiE-#OB={OHp3nGbe>~$!RSzzX`8ItqeZ)U2`Lh1Iv)UqSPZ&Q23< ({ ...item }))
+}
+
+function getCategoryById(categoryId) {
+ return LIBRARY_CATEGORIES.find(item => item.id === categoryId) || LIBRARY_CATEGORIES[0]
+}
+
+function getBooksByCategory(categoryId) {
+ const activeCategory = getCategoryById(categoryId)
+ return cloneItems(LIBRARY_BOOKS[activeCategory.id] || [])
+}
+
+function createLibraryPageData(categoryId) {
+ const activeCategory = getCategoryById(categoryId)
+
+ return {
+ title: '典籍',
+ searchButtonText: '搜索',
+ domainBridge: {
+ title: '进入易学阁',
+ subtitle: '切换到命理与经典的静态学习入口',
+ route: ROUTES.mingli.hall
+ },
+ categories: cloneItems(LIBRARY_CATEGORIES),
+ activeCategoryId: activeCategory.id,
+ currentCategoryName: activeCategory.name,
+ visibleBooks: getBooksByCategory(activeCategory.id)
+ }
+}
+
+function showNavigate(route) {
+ if (openStaticRoute(route, wx)) {
+ return
+ }
+
+ if (typeof wx?.showToast === 'function') {
+ wx.showToast({
+ title: '页面暂不可达',
+ icon: 'none'
+ })
+ }
+}
+
+Page({
+ data: createLibraryPageData(),
+
+ handleCategoryTap(event) {
+ const categoryId = event.currentTarget.dataset.categoryId
+
+ this.setData(createLibraryPageData(categoryId))
+ },
+
+ handleSearchTap() {
+ if (!wx || typeof wx.showToast !== 'function') {
+ return
+ }
+
+ wx.showToast({
+ title: '搜索功能暂未开放',
+ icon: 'none'
+ })
+ },
+
+ handleDomainBridgeTap() {
+ showNavigate(ROUTES.mingli.hall)
+ }
+})
+
+module.exports = {
+ LIBRARY_CATEGORIES,
+ LIBRARY_BOOKS,
+ createLibraryPageData,
+ getBooksByCategory
+}
diff --git a/pages/library/index.json b/pages/library/index.json
new file mode 100644
index 0000000..33b7698
--- /dev/null
+++ b/pages/library/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "典籍",
+ "navigationBarBackgroundColor": "#f8ecd8",
+ "navigationBarTextStyle": "black"
+}
diff --git a/pages/library/index.wxml b/pages/library/index.wxml
new file mode 100644
index 0000000..a9fffa9
--- /dev/null
+++ b/pages/library/index.wxml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+ {{domainBridge.title}}
+ {{domainBridge.subtitle}}
+
+
+ 当前分类: {{currentCategoryName}}
+
+
+
+
+
+ {{item.coverText}}
+
+
+
+ {{item.title}}
+ {{item.author}} / {{item.dynasty}} / {{item.period}}
+
+
+
+
+
+
diff --git a/pages/library/index.wxss b/pages/library/index.wxss
new file mode 100644
index 0000000..769aed7
--- /dev/null
+++ b/pages/library/index.wxss
@@ -0,0 +1,205 @@
+page {
+ min-height: 100%;
+ background:
+ linear-gradient(180deg, #f8edd6 0%, #f9f0de 24%, #f6ead4 100%);
+}
+
+.library-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 36rpx 20rpx 80rpx;
+ background:
+ linear-gradient(135deg, rgba(255, 255, 255, 0.58) 0%, rgba(255, 248, 236, 0) 36%),
+ linear-gradient(225deg, rgba(255, 245, 227, 0.68) 0%, rgba(255, 245, 227, 0) 48%);
+}
+
+.library-page__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18rpx 2rpx 30rpx;
+}
+
+.library-page__title {
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 60rpx;
+ font-weight: 700;
+ line-height: 1.2;
+ color: #2f1f12;
+}
+
+.library-page__search {
+ min-width: 112rpx;
+ padding: 18rpx 26rpx;
+ border-radius: 999rpx;
+ background: rgba(240, 225, 201, 0.9);
+ box-shadow: 0 12rpx 24rpx rgba(173, 120, 56, 0.08);
+ color: #a56d2d;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 30rpx;
+ line-height: 1;
+ text-align: center;
+}
+
+.library-page__content {
+ display: flex;
+ align-items: stretch;
+}
+
+.library-page__sidebar {
+ box-sizing: border-box;
+ width: 168rpx;
+ flex-shrink: 0;
+ padding: 16rpx 14rpx;
+ border: 1rpx solid rgba(189, 155, 105, 0.12);
+ border-radius: 34rpx;
+ background: rgba(255, 250, 243, 0.86);
+ box-shadow: 0 16rpx 32rpx rgba(178, 144, 99, 0.08);
+}
+
+.library-page__category {
+ padding: 22rpx 16rpx;
+ border-radius: 24rpx;
+}
+
+.library-page__category + .library-page__category {
+ margin-top: 14rpx;
+}
+
+.library-page__category--active {
+ background: linear-gradient(180deg, rgba(240, 228, 207, 0.98) 0%, rgba(236, 221, 197, 0.98) 100%);
+ box-shadow: inset 0 -8rpx 18rpx rgba(186, 149, 99, 0.08);
+}
+
+.library-page__category-label {
+ display: block;
+ color: #8f6a42;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 32rpx;
+ line-height: 1.5;
+}
+
+.library-page__category--active .library-page__category-label {
+ color: #7b501f;
+ font-weight: 700;
+}
+
+.library-page__main {
+ flex: 1;
+ margin-left: 16rpx;
+}
+
+.library-page__bridge {
+ padding: 26rpx 26rpx 24rpx;
+ border: 1rpx solid rgba(191, 154, 103, 0.2);
+ border-radius: 34rpx;
+ background: linear-gradient(135deg, rgba(255, 249, 239, 0.96) 0%, rgba(245, 233, 214, 0.98) 100%);
+ box-shadow: 0 16rpx 28rpx rgba(176, 136, 84, 0.1);
+}
+
+.library-page__bridge-title {
+ display: block;
+ color: #6f451c;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 36rpx;
+ font-weight: 700;
+ line-height: 1.35;
+}
+
+.library-page__bridge-subtitle {
+ display: block;
+ margin-top: 10rpx;
+ color: #97714a;
+ font-size: 26rpx;
+ line-height: 1.7;
+}
+
+.library-page__current {
+ margin-top: 16rpx;
+ padding: 30rpx 26rpx;
+ border: 1rpx solid rgba(189, 155, 105, 0.12);
+ border-radius: 34rpx 34rpx 0 0;
+ background: rgba(255, 250, 243, 0.88);
+ color: #9c6d35;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 30rpx;
+ line-height: 1.4;
+}
+
+.library-page__book-list {
+ padding: 0 0 8rpx;
+ border: 1rpx solid rgba(189, 155, 105, 0.08);
+ border-top: 0;
+ border-radius: 0 0 34rpx 34rpx;
+ background: rgba(255, 250, 243, 0.72);
+}
+
+.book-card {
+ display: flex;
+ align-items: center;
+ margin: 18rpx;
+ padding: 22rpx 20rpx;
+ border: 1rpx solid rgba(192, 159, 117, 0.14);
+ border-radius: 28rpx;
+ background: rgba(255, 252, 247, 0.96);
+ box-shadow: 0 12rpx 24rpx rgba(181, 145, 97, 0.08);
+}
+
+.book-card__cover {
+ position: relative;
+ width: 100rpx;
+ height: 136rpx;
+ flex-shrink: 0;
+ border-radius: 22rpx;
+ background: linear-gradient(180deg, #d5b07b 0%, #c99f67 100%);
+ box-shadow: 0 10rpx 18rpx rgba(173, 128, 70, 0.14);
+}
+
+.book-card__cover-spine {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 12rpx;
+ height: 100%;
+ border-top-left-radius: 22rpx;
+ border-bottom-left-radius: 22rpx;
+ background: rgba(138, 93, 48, 0.4);
+}
+
+.book-card__cover-char {
+ position: absolute;
+ top: 50%;
+ left: 12rpx;
+ right: 0;
+ transform: translateY(-50%);
+ color: #fff7eb;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 42rpx;
+ font-weight: 700;
+ line-height: 1;
+ text-align: center;
+}
+
+.book-card__body {
+ min-width: 0;
+ flex: 1;
+ margin-left: 18rpx;
+}
+
+.book-card__title {
+ display: block;
+ color: #2f1f12;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 42rpx;
+ font-weight: 700;
+ line-height: 1.35;
+}
+
+.book-card__meta {
+ display: block;
+ margin-top: 14rpx;
+ color: #8f6a42;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 28rpx;
+ line-height: 1.6;
+}
diff --git a/pages/library/tab-library-active.png b/pages/library/tab-library-active.png
new file mode 100644
index 0000000000000000000000000000000000000000..28eeb64683a1025537873bb5ff9ff6beea9f3a2e
GIT binary patch
literal 1562
zcmV+#2IcvQP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1+Pg&K~#8N?OZ*K
z95ob9Nl8idymXY5R44@n5)D+qcvg1>MRe6wQ5r-f3KX(CLq!^-h$>V`gM^eOy6UPE
zMT$siNJW~HC{nDh0(~A|nECAGc4zFhv-p!fDHpBA?|tK+=jZ3ScCDbGprD|jprD|j
zprBw&e1D+cX|=jO;DMU)?+Ps1T7BBg<$hDklUB>Bt<~RG(f;;9TdTwNK)ub++6>I;a_X!(l;8bvL?HI=%79?b3S70_eaTE5$~w~b|^<Z
zwpM2YYIV-PTT3mgrj~XuhB}5=tL3HRii>LYc~i~W{}xnE->CT#^HEdF@2(@=
zFm1H@>25Xqki`hWMyqev4ttD-8QUs#gZ)Sb
z%z?fkjMbAJO2vhqeT^w)*c)Yq?IioNbC^}wvxs1VXGc;KkpRz=CU#97m}9%e0_M~M
zJv?T6(Fu~pk*&`lEhiqsvtk!hIzqPWUv%Kj@jQrCIQ1g3DO)X%2SRFDExuImv7b?a
zt<>;s%EeNv&)C?j*jDP$(<>JIyU@hT=AaxqEcP=Rx`gR4oIxPXhHk=CB7#jlyUA_u
zY)50VBmIW8Iv=|eNhuI$p$E89FO3*oZOA
zN<>~SJo)l#Lj}oJ8!(QhK(84!F&s{Ls~JIh9lVNN1)f;LK)+u8O_%;11oiAZPCQL4yHopj&X2^sB6
z`a~SXN*(gEx8za(c{tQ(|AWAD&OABb&P7Bg(4NbE_CE+r%?%m9MP;j(xroTg2UNJR
z|G~g@H&bhFau5+UI)i6vSw$@?K>#{*AWYDml01}=d}hCc!p-JjHJ#mL!`>9)yxd8~
zh7PiQ;Gi_r?j;4C^p}>GQHSy+cnb*PjDii!oz7fnL{!J@Z!|d9D<&S|ry$pJW6@+k
zqr#RL2PtYwL334udtuD5W*wZurfxrdhYik+IHlypWG978KH%_*_tUmc@rVu2g=RJ8
zz&b^NXCG+Xx%k+n$+j-|j7?kE`IJXbl3~nnv))~aNr-!Up=f%bPr%UG>nU){k3qS}
zA@nh!3=iQhQ?tAB;!T0f2OA28sM#4y?0N
zfHMkbd&um4x`2hbig*c|e8RvaOkx~5?GnaeI-l{e|M3inW9z~1V2g%-+n$CD(wT3y
zW!jODJ_Za4bE&+RXY6cn;=0@}voR?U{KG<)m^w_&Q0^;lXxQF)k1tzpM04p|9+;Q%
ze-myUy7b!X53Jn=O#qLAd;7Q9Vrq3gX~LkOprD|jprD|jpkO!n2ULJYIv*eP*8l(j
M07*qoM6N<$f?>4xeEPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1KV|W8CC!q~nehNfq1}42DQ|)pbr~(ijYG
zjH^qxl}Tf8afKnxxl4F&W|z@7+U(AZw30Xa;4AD~X5Rbs_w@8MH*ORZ6ciK`6ciK`
z6ciNnggp@-ST*~?;mPbn{9QoFOh
z`NE+RSDoLjAT^ZH;@(aSbsAzu$=B#4IDBP}#f4S-x6JXkpW36nkJj<5lHVCEA6hM6
z;`7{R%Qw`3)R1Uf_8_BqSa}><3a7-mJ(3d?s4vao-q%)%pZ`lxqs8-gYQCiT$Y^mG^Ujs{932l$ZlG%VD>WF}
z(JX2QXm>W?c+a!)8ZG8L|eQ;AcwKRIkAKl2Kx#FJP{zfp#$~
zaU1zw4FqMwhmr}J&OoU@aRZ-$_dVs90H{d%z3+&
zaS!ZJ?ceg)dzmbbw3WQmF^DE|G(0!fF(n8o?xBCtQ3uoRJV>i>>P2K@s_A2B{A>Du
z^fM}WE9JDNcx#XMzw8*hj@8~i_KGtlf9m`$FsZRID5o73`WX$heZp|2-okZ_UCTrw
zGL`rN-@UaR4QEGO0j~z{L{f@Kp^p}G#}_T1qQAkxIYA7nX`=QRxDgYJ=ae(TyFA7Js4N!O-Lg-34WAVj%+&c_hJ>
zka7X&e-Ka#$!#(D3JvPb3`Ar?+!!s(Qy)<}B97nQWg=popm2PfqyIrbb4B|_=W04b
zV|p_e5$S9PhjffXE}RcXt;!2#A|jEer?xGjw}I3u&I>4|to<}kzeFG
z`x1>9#EeAjEG}ByL>)$f3yNAV;8sH;hA<-$dH>(GMTtYyVH7$$6)hj35rdeKh?KjbgA@lP_GZGQAIW+oEhtWW1J4Os)Mj~Q@HcFNow|%2CIK+%Z#53tA
zRb-SpcV03kqrIR<#6hIz;In&>>K)t9qeFf4KM3%gIY_{riHI0zQz1V39|TlxfyQrI
z*}9Ush=|DtEhp%IFks!y=$e}>M3j^Evp8E7v#f*yP{;UCL3cuShO!bD=yy=?i?J8b
zv7^VhcY%Cfj>*{2LADQEMBZ+>P`j5D?9}AQ*HMS^0=NaNUp~9rF)(|&b0M>mchKKx
z!0Q#G5AhQa>$z#sL_edVE-?;N)Rcf6RbVfS8P==<7B(@D#3>q_8~KzH>$kU2$mBzP
z;nCeslX1L-@)(~3X*I^cI!S?N8}xF#_{eFpAqAh2*%WsE*%>`4hB2a(^{$7oMq-9~0>+S`ML)oZt}VhTdj*@ur~62O7%8hN#&YNZ%!(#-__CWg;O!
zH|x>h+GsJ~=7K{BV~hirw(!mTOa)w^`MyKvo*$DF7V+jvp8MzmI=*-L{*30pb>J(z
zo$#ma%=B?cF}dgQMab^ZC1!s`cNLne8Y^$#H_k0TU4X)@CodT5KVd)#lNg6i+XP?Z
z#q${--H&HHtn0zWDc`2AruX^ZN#
z4+|~GQwM)wDD#y!G@9P|054l^LjLwmT}OWKzY}g1_%w0(fi-bTC@3f>DA*4E1+<1_y^C^_`Tzg`07*qoM6N<$g6t6Sv;Y7A
literal 0
HcmV?d00001
diff --git a/pages/login/index.js b/pages/login/index.js
index 9bcfee2..3c975f0 100644
--- a/pages/login/index.js
+++ b/pages/login/index.js
@@ -1,75 +1,58 @@
-const { sessionStore } = require('../../stores')
-const { formatDateTime } = require('../../utils/util')
+const { ROUTES, openStaticRoute } = require('../../utils/static-ux/route-map')
+
+function createLoginPageData() {
+ return {
+ title: '欢迎来到玄志',
+ subtitle: '当前阶段先提供静态登录页,后续会接入新的认证方案与后端接口。',
+ highlight: 'Static Login',
+ introTitle: '先把页面走通',
+ introDescription: '本页只保留 UI 和静态交互,用于承接首页、我的页和后续学习入口的跳转。',
+ notes: [
+ '不会写入 session、token 或本地用户信息',
+ '不会复用旧 frontend 的认证 API 和表单结构',
+ '保留一个“先看看静态页面”的直接入口'
+ ],
+ actions: [
+ {
+ key: 'wechat',
+ title: '微信授权登录',
+ description: '静态按钮,占位等待新认证方案',
+ actionType: 'toast'
+ },
+ {
+ key: 'browse',
+ title: '先看看静态页面',
+ description: '直接返回首页,继续浏览已迁移页面',
+ actionType: 'route',
+ route: ROUTES.tabs.home
+ }
+ ]
+ }
+}
+
+function showComingSoon() {
+ if (typeof wx?.showToast === 'function') {
+ wx.showToast({
+ title: '登录功能建设中',
+ icon: 'none'
+ })
+ }
+}
Page({
- data: {
- submitting: false,
- form: {
- nickname: '',
- mobile: ''
- },
- mockHint: `请求层已接好,可把接口替换为真实后端。当前时间:${formatDateTime(new Date())}`
- },
- handleNicknameChange(event) {
- this.setData({
- 'form.nickname': event.detail.value
- })
- },
- handleMobileChange(event) {
- this.setData({
- 'form.mobile': event.detail.value
- })
- },
- handleMockLogin() {
- const { nickname, mobile } = this.data.form
+ data: createLoginPageData(),
- if (!nickname.trim()) {
- wx.showToast({
- title: '请输入昵称',
- icon: 'none'
- })
+ handleActionTap(event) {
+ const { actionType, route } = event.currentTarget.dataset
+
+ if (actionType === 'route' && openStaticRoute(route, wx)) {
return
}
- this.setData({
- submitting: true
- })
-
- sessionStore.setSession({
- token: `mock_${Date.now()}`,
- userInfo: {
- nickname: nickname.trim(),
- mobile: mobile.trim()
- },
- permissions: ['workbench:view', 'profile:update']
- })
-
- wx.showToast({
- title: '模拟登录成功',
- icon: 'success'
- })
-
- this.setData({
- submitting: false
- })
-
- setTimeout(() => {
- wx.reLaunch({
- url: '/pages/home/index'
- })
- }, 300)
- },
- handleClearSession() {
- sessionStore.clearSession()
- this.setData({
- form: {
- nickname: '',
- mobile: ''
- }
- })
- wx.showToast({
- title: '已清理',
- icon: 'success'
- })
+ showComingSoon()
}
})
+
+module.exports = {
+ createLoginPageData
+}
diff --git a/pages/login/index.json b/pages/login/index.json
index 9e105ac..d240ade 100644
--- a/pages/login/index.json
+++ b/pages/login/index.json
@@ -1,8 +1,5 @@
{
"navigationBarTitleText": "登录",
- "usingComponents": {
- "app-button": "../../components/base/app-button/index",
- "t-input": "tdesign-miniprogram/input/input",
- "t-tag": "tdesign-miniprogram/tag/tag"
- }
+ "navigationBarBackgroundColor": "#f8ecd8",
+ "navigationBarTextStyle": "black"
}
diff --git a/pages/login/index.wxml b/pages/login/index.wxml
index 9899a03..a7e5912 100644
--- a/pages/login/index.wxml
+++ b/pages/login/index.wxml
@@ -1,39 +1,30 @@
-
-
- Mock Auth
- 主包认证入口
- {{mockHint}}
+
+
+ {{highlight}}
+ {{title}}
+ {{subtitle}}
-
- 登录信息
-
-
-
-
+
+ {{introTitle}}
+ {{introDescription}}
+
+ ·
+ {{item}}
+
+
+
+
+
+ {{item.title}}
+ {{item.description}}
+
diff --git a/pages/login/index.wxss b/pages/login/index.wxss
index 2f98a21..a33f8c7 100644
--- a/pages/login/index.wxss
+++ b/pages/login/index.wxss
@@ -1,19 +1,100 @@
-.login-hero {
- display: flex;
- flex-direction: column;
- gap: 20rpx;
- padding: 32rpx;
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #f8edd6 0%, #f9f0de 24%, #f6ead4 100%);
}
-.login-hero__title {
- font-size: 40rpx;
+.login-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 32rpx 20rpx 72rpx;
+ background:
+ radial-gradient(circle at top right, rgba(173, 123, 63, 0.16) 0%, rgba(173, 123, 63, 0) 26%),
+ linear-gradient(135deg, rgba(255, 255, 255, 0.54) 0%, rgba(255, 249, 240, 0) 36%);
+}
+
+.login-page__hero,
+.login-page__intro,
+.login-page__actions {
+ margin-top: 18rpx;
+ padding: 28rpx 24rpx;
+ background: rgba(255, 251, 246, 0.94);
+}
+
+.login-page__hero {
+ margin-top: 0;
+ background: linear-gradient(135deg, rgba(255, 249, 241, 0.98) 0%, rgba(247, 235, 216, 0.98) 100%);
+}
+
+.login-page__badge {
+ display: inline-block;
+ padding: 10rpx 18rpx;
+ border-radius: 999rpx;
+ background: rgba(136, 86, 38, 0.1);
+ color: #8d5b2a;
+ font-size: 24rpx;
+ line-height: 1;
+}
+
+.login-page__title {
+ display: block;
+ margin-top: 24rpx;
+ color: #2f1f12;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 60rpx;
font-weight: 700;
- line-height: 1.3;
+ line-height: 1.18;
}
-.login-form {
- display: flex;
- flex-direction: column;
- gap: 24rpx;
- padding: 32rpx;
+.login-page__subtitle,
+.login-page__intro-desc,
+.login-page__action-desc {
+ display: block;
+ margin-top: 14rpx;
+ color: #8f6a42;
+ font-size: 28rpx;
+ line-height: 1.75;
+}
+
+.login-page__section-title,
+.login-page__action-title {
+ display: block;
+ color: #2f1f12;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 36rpx;
+ font-weight: 700;
+ line-height: 1.35;
+}
+
+.login-page__note {
+ display: flex;
+ align-items: flex-start;
+ margin-top: 12rpx;
+}
+
+.login-page__note-dot {
+ margin-right: 10rpx;
+ color: #9c6b39;
+ font-size: 28rpx;
+ line-height: 1.6;
+}
+
+.login-page__note-text {
+ flex: 1;
+ color: #735033;
+ font-size: 26rpx;
+ line-height: 1.75;
+}
+
+.login-page__action {
+ padding: 22rpx 2rpx;
+ border-bottom: 1rpx solid rgba(176, 140, 96, 0.12);
+}
+
+.login-page__action:first-child {
+ padding-top: 2rpx;
+}
+
+.login-page__action:last-child {
+ padding-bottom: 2rpx;
+ border-bottom: 0;
}
diff --git a/pages/profile/index.js b/pages/profile/index.js
new file mode 100644
index 0000000..6ef9196
--- /dev/null
+++ b/pages/profile/index.js
@@ -0,0 +1,131 @@
+const { ROUTES, openStaticRoute } = require('../../utils/static-ux/route-map')
+
+function createProfilePageData() {
+ return {
+ title: '我的',
+ userCard: {
+ avatarText: '?',
+ title: '点击登录',
+ subtitle: '登录后查看你的学习资产',
+ settingsText: '设',
+ route: ROUTES.tabs.login
+ },
+ vipCard: {
+ icon: '会员',
+ title: '开通会员',
+ subtitle: '解锁更多学习资料、AI 仪表与阅读辅助能力',
+ actionText: '成为会员',
+ route: `${ROUTES.tcm.placeholder}?kind=membership`
+ },
+ assetItems: [
+ {
+ key: 'notes',
+ icon: '记',
+ title: '我的笔记',
+ count: 0,
+ route: `${ROUTES.tcm.assets}?kind=notes`
+ },
+ {
+ key: 'bookshelf',
+ icon: '架',
+ title: '我的书架',
+ count: 0,
+ route: `${ROUTES.tcm.assets}?kind=bookshelf`
+ },
+ {
+ key: 'favorites',
+ icon: '藏',
+ title: '我的收藏',
+ count: 0,
+ route: `${ROUTES.tcm.assets}?kind=favorites`
+ },
+ {
+ key: 'history',
+ icon: '史',
+ title: '浏览历史',
+ count: 0,
+ route: `${ROUTES.tcm.assets}?kind=history`
+ }
+ ],
+ recentRecord: {
+ title: '最近记录',
+ emptyTitle: '还没有回访记录',
+ emptyDescription: '去典籍阅读或 AI 页完成一次学习,记录会出现在这里。',
+ actionText: '去首页看看',
+ route: ROUTES.tabs.home
+ },
+ moreItems: [
+ {
+ key: 'learning-center',
+ title: '学习中心',
+ route: ROUTES.learning.center
+ },
+ {
+ key: 'ai-history',
+ title: 'AI历史',
+ route: ROUTES.tcm.aiHistory
+ },
+ {
+ key: 'feedback',
+ title: '意见反馈',
+ route: `${ROUTES.tcm.placeholder}?kind=feedback`
+ },
+ {
+ key: 'share',
+ title: '分享 APP',
+ route: `${ROUTES.tcm.placeholder}?kind=share`
+ },
+ {
+ key: 'about',
+ title: '关于我们',
+ route: `${ROUTES.tcm.placeholder}?kind=about`
+ },
+ {
+ key: 'settings',
+ title: '设置',
+ route: `${ROUTES.tcm.placeholder}?kind=settings`
+ }
+ ]
+ }
+}
+
+function showNavigate(route) {
+ if (openStaticRoute(route, wx)) {
+ return
+ }
+
+ if (typeof wx?.showToast === 'function') {
+ wx.showToast({
+ title: '页面建设中',
+ icon: 'none'
+ })
+ }
+}
+
+Page({
+ data: createProfilePageData(),
+
+ handleUserTap() {
+ showNavigate(ROUTES.tabs.login)
+ },
+
+ handleVipTap() {
+ showNavigate(`${ROUTES.tcm.placeholder}?kind=membership`)
+ },
+
+ handleAssetTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ },
+
+ handleRecentTap() {
+ openStaticRoute(ROUTES.tabs.home, wx)
+ },
+
+ handleMoreTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ }
+})
+
+module.exports = {
+ createProfilePageData
+}
diff --git a/pages/profile/index.json b/pages/profile/index.json
new file mode 100644
index 0000000..aa6eca9
--- /dev/null
+++ b/pages/profile/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "我的",
+ "navigationBarBackgroundColor": "#f8ecd8",
+ "navigationBarTextStyle": "black"
+}
diff --git a/pages/profile/index.wxml b/pages/profile/index.wxml
new file mode 100644
index 0000000..915b631
--- /dev/null
+++ b/pages/profile/index.wxml
@@ -0,0 +1,65 @@
+
+
+
+
+
+ {{vipCard.icon}}
+
+ {{vipCard.title}}
+ {{vipCard.subtitle}}
+
+
+ {{vipCard.actionText}}
+
+
+
+
+
+ {{item.icon}}
+ {{item.title}}
+ {{item.count}}
+
+
+
+
+
+
+ {{recentRecord.title}}
+
+
+ {{recentRecord.emptyTitle}}
+ {{recentRecord.emptyDescription}}
+ {{recentRecord.actionText}}
+
+
+
+
+
+
+
diff --git a/pages/profile/index.wxss b/pages/profile/index.wxss
new file mode 100644
index 0000000..d04b8a6
--- /dev/null
+++ b/pages/profile/index.wxss
@@ -0,0 +1,204 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #f8edd6 0%, #f9f0de 24%, #f6ead4 100%);
+}
+
+.profile-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 88rpx;
+ background:
+ radial-gradient(circle at top right, rgba(189, 144, 86, 0.16) 0%, rgba(189, 144, 86, 0) 24%),
+ linear-gradient(135deg, rgba(255, 255, 255, 0.58) 0%, rgba(255, 248, 236, 0) 38%);
+}
+
+.section-card {
+ margin-top: 18rpx;
+ padding: 26rpx;
+ background: rgba(255, 250, 242, 0.92);
+}
+
+.section-card:first-child {
+ margin-top: 0;
+}
+
+.profile-header,
+.vip-banner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.profile-header__main,
+.vip-banner__body {
+ display: flex;
+ align-items: center;
+}
+
+.profile-header__avatar {
+ width: 104rpx;
+ height: 104rpx;
+ border-radius: 999rpx;
+ background: linear-gradient(135deg, #b47b40 0%, #8c5724 100%);
+ color: #fff7ea;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 42rpx;
+ font-weight: 700;
+ line-height: 104rpx;
+ text-align: center;
+}
+
+.profile-header__body {
+ margin-left: 18rpx;
+}
+
+.profile-header__title {
+ display: block;
+ color: #2f1f12;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 36rpx;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+.profile-header__subtitle,
+.vip-banner__subtitle,
+.recent-panel__empty-desc {
+ display: block;
+ margin-top: 8rpx;
+ color: #8f6a42;
+ font-size: 26rpx;
+ line-height: 1.7;
+}
+
+.profile-header__settings {
+ width: 68rpx;
+ height: 68rpx;
+ border-radius: 20rpx;
+ background: rgba(244, 233, 215, 0.92);
+ color: #8a6339;
+ font-size: 28rpx;
+ line-height: 68rpx;
+ text-align: center;
+}
+
+.vip-banner {
+ background: linear-gradient(135deg, rgba(255, 249, 241, 0.98) 0%, rgba(244, 230, 206, 0.96) 100%);
+}
+
+.vip-banner__icon {
+ width: 82rpx;
+ height: 82rpx;
+ border-radius: 24rpx;
+ background: rgba(130, 77, 25, 0.1);
+ color: #7d4a19;
+ font-size: 24rpx;
+ line-height: 82rpx;
+ text-align: center;
+}
+
+.vip-banner__copy {
+ margin-left: 18rpx;
+}
+
+.vip-banner__title,
+.recent-panel__title,
+.recent-panel__empty-title {
+ display: block;
+ color: #2f1f12;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1.35;
+}
+
+.vip-banner__action,
+.recent-panel__action {
+ min-width: 156rpx;
+ padding: 18rpx 24rpx;
+ border-radius: 999rpx;
+ background: linear-gradient(135deg, #9e652f 0%, #75441a 100%);
+ color: #fff7eb;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 26rpx;
+ line-height: 1;
+ text-align: center;
+}
+
+.asset-grid {
+ display: flex;
+ flex-wrap: wrap;
+ margin: -6rpx;
+}
+
+.asset-grid__item {
+ box-sizing: border-box;
+ width: 25%;
+ padding: 6rpx;
+}
+
+.asset-grid__icon,
+.asset-grid__title,
+.asset-grid__count {
+ display: block;
+ text-align: center;
+}
+
+.asset-grid__icon {
+ width: 72rpx;
+ height: 72rpx;
+ margin: 0 auto;
+ border-radius: 22rpx;
+ background: rgba(139, 96, 46, 0.1);
+ color: #76461b;
+ font-size: 30rpx;
+ line-height: 72rpx;
+}
+
+.asset-grid__title {
+ margin-top: 14rpx;
+ color: #3d2a1b;
+ font-size: 24rpx;
+ line-height: 1.5;
+}
+
+.asset-grid__count {
+ margin-top: 10rpx;
+ color: #9c754d;
+ font-size: 22rpx;
+ line-height: 1;
+}
+
+.recent-panel__empty {
+ margin-top: 18rpx;
+ padding: 8rpx 2rpx 2rpx;
+}
+
+.recent-panel__action {
+ margin-top: 22rpx;
+}
+
+.menu-list__item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 22rpx 2rpx;
+ border-bottom: 1rpx solid rgba(172, 137, 94, 0.12);
+}
+
+.menu-list__item:last-child {
+ padding-bottom: 2rpx;
+ border-bottom: 0;
+}
+
+.menu-list__label {
+ color: #2f1f12;
+ font-size: 28rpx;
+ line-height: 1.5;
+}
+
+.menu-list__arrow {
+ color: #9c7449;
+ font-size: 28rpx;
+ line-height: 1;
+}
diff --git a/pages/profile/tab-profile-active.png b/pages/profile/tab-profile-active.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea1f357afc6f0d8febcd6da49e3137e0f96830c3
GIT binary patch
literal 1931
zcmV;62Xy#}P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2OvpAK~#8N?OZ*K
z95ob9Nl8hQ=b@vdq(VWWK!O4qe#W!9D<~qWYZB2QPNG2Jc0JH=4N`;@DO8DsA|a(A
zS6y`ii6RmWQ3}$WM3G{373g`qclKw`j(69yGdcW8pOoCpv)_B;pXXvPLfQhdHj
zt5cGm*OfSND8(th*GfrqRZ)E@x;l5UTGBJmNXKAy4&9qcv8N`lzCDxTjR=1Bt)w68
zik^7oS>zALACNyJ>Na{bt&i@bQMTh$ML(&sm;@ZO5~pSHkt&ONxkKsrg&Hq;JW>_?
z<{Q#Z|LuASd;4v`Ng?aWwgNEW%7GLm?;^pYU>3Fxo(^V#u
zE+;Y%Wbq-&T^KE%;`>OH=^|%%Z28`2YK#kcNrg%+W5|h~W8~F+lw-1UQ8q-Bi=Y##
zsfzwWyOY6$HBMJvHL(zK;!KL4oxe$XgmR7S)qQ*q%QEG8vdgGvYFHBT-_iag(3?xP
z!V~c~D%8c0;i{r3{te{}9cPE+mdGxpA2;@tIO9+nk79bta2LM-Cmv<>24WW&L`)$hk-C(!FeQ
z;dr>pth`{G6LHe;L!IIfW!eI7tTUE8JlW+WUj{aN#+DJ1o}&^mY;&T127jNH^xi_w
zTk4f!HzdQvI%8?JW-rQ09WU}%ah0Vfs6-4yPF#dE-ZQfVWM`txd3jjYCQii3r4qa7
zhmD{Q6uA|gpb|0k9K|%P3TIoMY>bS%)U!e$udEH6$m@T*98R*JA2x!wQkjScs6-5x
zIWaU*G&1f|Z_)krGc0QZC-M#<)RT7QqE9l|%J$$y{T!a*gu}GMAADw>d-cZ9Pu9{Q
z`e7r?D)A0faEwaCu+xd~9OFl(opZ!zM#if*hRv}QtE|)kx89VuD&Q0nDiK3;lBVTE
z92b#1OFQKS-oZL!$-N88N;|RWM66wb6sM>}47;4jUDbQ;I0%C9;5&Z3F~n>R_dfK)
z#z>)TmlN9pXsb?VIf&2K)r*M<+Fr8U-m-7S3=Xr)iP{(ptS#amOsG7pGnTj~-ODL5
zeNNh{WR;Bef+3^PiMYf&uMftGgu{F%i2mC!f*or0}yj9C#NuQLz_=ykG7mV^as&
zAz)Ct?&@U>^wM9Fp7q+57r-STllLf4!LidD3$+u)9@^U*yz3P!ZsL!Dc%GXlO|-LD
zm?Oq(9cmf_^;HdQg?)xKZSW2@HQ~iARCsLUT}qrxwsMdu1m3*D^|U#s=%i;#ZTHT8}A
zif$WtkHT9XBCVe;K*w~EKSIn;80dsaADd3wgf!L%zAm@TY|Icy{=-6sJap(YLt~$LQ^T~*L%eJmMSba;
z?&^>7e{)tw6gC-0zGBPqUGBPqUGBUP<{{V?s6-pB$
R2#Ejy002ovPDHLkV1n{xr=b7<
literal 0
HcmV?d00001
diff --git a/pages/profile/tab-profile.png b/pages/profile/tab-profile.png
new file mode 100644
index 0000000000000000000000000000000000000000..0866d3ccef4a1d00a5aaa19daca1db0c226f6822
GIT binary patch
literal 1941
zcmV;G2Wt3Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2P#QKK~#8N?OZX4
z9Yq*UX=!O`X{QK+g@RxsHon}wnY${c2?T9I3ccHz;|>e0Ck?3F7C2?+@a
z2?+@a2?+@a9Z~G7Hwv@$k;DD1H}SrN3We2^d`}m-)xYLOT@+ToY-4rr{lcp2#r|in
zW0{v8#s2576;^MP6LO=Uql1=VZuKukrf$+3jP+eA&>NFm{axtWwk&g_e!g@~pWi&A
zb~?RvM*ovr{kub>&b|8@NDnHv>hMxDb!uX{(a+FGF!(&*Q}+wAc_ZJu`d+cS@m3p;
zGkq<$`d(r66D-dyTR*1_^jn0U{V>{h#YWCbBGW8DXJ35e(a`RO$=*>GZ>H{=9
zHu(W%)9>klp&r$swvBp^1|07RBX4fi88;66$HYi`sd)Q4%*0qQ?EicG&RI?$*`
zeKp-sQul#+F*9`&?HCN)2MSG?M^C}1A()%3+tj7N1HPc?B-*of@F*+{BGkd7A;VKo
zu%F}*Xz0M4r&}X_13j9}8-l*4gT;|Wrf+!)QAdu7kBwza364~UXkYIrgK4)Pq)~Y2
zK_r-F`T!pO1^th9_6nX#IZso)DE2l#_LK$YRz1YGz%eGf`Yy^KkB$BV$YK#kcN`*=-V1pAK#~hjI
zS189M=lZffQLc>;JLXpXg?2|nl_PWql#Yo7$%(m9KY|etQI5#Y#rx2KOE{5R5=;px
z7l8Ih0i}>!BlJ^LsEv~0$3}mH|Dhbqg0n@r2H93Z+}K-|r(B|RoY;AK*W^UcL7BdW
zvM5(%8=KyLM3f6)nV@iNJ45@UfclDhMJHyuh03(C$%$T5HR{lj1&47tF`YN!(%`k(6p)?SRtRh5M4!3vW+A779-K(TYyN=ahCg^eQK&bFkgUzE
zzKecX355Y;XugR`1hCDCcpc+M{;!^M#IlG&y&hNP;45L?O^X)c-^hn4Z?+cu+CsYHiudt
z`e9|nP`1s9jRAB;Z#pR4gLQ@y6STc#xpCUpHG@+ry3mP4FR-rH32QK7c|yHG#JxLx
z>>{Joxl>7pjP{J8(cna!;?3ywhfU=P^|lea5n9wo`=bEYnU@K;C8O)pJISE`41@l#
zp**}NgcxYkMtrnC3TV3pDjd4Ab*^lH(M|oLv=SInUI@M9gVhtXKN@h~P1ij)!A1*f
zDfq^ADsLh6q%P0dsy?@sPyp&EykyWlCVEZTsQYMlRB*-kB$lcS&Y+LUL&DPURo)@@J0(bMeHrlU8+nxy_iwLbBa?*kz@s~#Cgr>&!H7ts
zF$UIyZ1AjuD#wG5oF)s{@VRbQ8$18>TF(&87~y5T%jWSk#<{&%Pce?yEMRsH5ECS4ygVUUoJkdTm&kdTm&
bupayiF@{LK&7Nn&00000NkvXXu0mjfKV-1s
literal 0
HcmV?d00001
diff --git a/project.config.json b/project.config.json
index 818139d..04a048b 100644
--- a/project.config.json
+++ b/project.config.json
@@ -1,6 +1,7 @@
{
"compileType": "miniprogram",
- "libVersion": "stable",
+ "projectname": "xuanzhi-wx",
+ "libVersion": "3.15.2",
"packOptions": {
"ignore": [],
"include": []
@@ -38,4 +39,4 @@
},
"appid": "wx3462e6109c8d301b",
"simulatorPluginLibVersion": {}
-}
+}
\ No newline at end of file
diff --git a/services/api/user.js b/services/api/user.js
index a9b2e4c..64d6bca 100644
--- a/services/api/user.js
+++ b/services/api/user.js
@@ -1,4 +1,4 @@
-const { request: defaultRequest } = require('../request')
+const { request: defaultRequest } = require('../request/index')
/**
* @typedef {Object} UserProfilePayload
diff --git a/services/request/index.js b/services/request/index.js
index 5c7f8c7..cca3dc8 100644
--- a/services/request/index.js
+++ b/services/request/index.js
@@ -1,6 +1,6 @@
const { AUTH_EXPIRED_CODES, REQUEST_TIMEOUT, RESULT_CODES } = require('../../config/constants')
const { getRuntimeConfig } = require('../../config/env')
-const { sessionStore: defaultSessionStore } = require('../../stores')
+const { sessionStore: defaultSessionStore } = require('../../stores/index')
class RequestError extends Error {
constructor(options = {}) {
diff --git a/tests/ai-page-render.test.js b/tests/ai-page-render.test.js
new file mode 100644
index 0000000..574b287
--- /dev/null
+++ b/tests/ai-page-render.test.js
@@ -0,0 +1,34 @@
+const fs = require('fs')
+const path = require('path')
+
+describe('ai page compatibility', () => {
+ test('renders tab panels with compatible layout primitives', () => {
+ const wxml = fs.readFileSync(path.join(process.cwd(), 'pages/ai/index.wxml'), 'utf8')
+ const wxss = fs.readFileSync(path.join(process.cwd(), 'pages/ai/index.wxss'), 'utf8')
+
+ expect(wxml).not.toContain('placeholder-page')
+ expect(wxml).toContain('ai-page')
+ expect(wxml).toContain('ai-page__title')
+ expect(wxml).toContain('wx:for="{{tabs}}"')
+ expect(wxml).toContain('bindtap="handleTabTap"')
+ expect(wxml).toContain('{{currentPanel.badge}}')
+ expect(wxml).toContain('{{currentPanel.heading}}')
+ expect(wxml).toContain('wx:for="{{currentPanel.highlights}}"')
+ expect(wxml).toContain('wx:for="{{currentPanel.examples}}"')
+ expect(wxml).toContain('wx:if="{{currentPanel.formType === \'qa\'}}"')
+ expect(wxml).toContain('wx:if="{{currentPanel.formType === \'analysis\'}}"')
+ expect(wxml).toContain('wx:if="{{currentPanel.formType === \'constitution\'}}"')
+ expect(wxml).toContain('bindtap="handleActionTap"')
+ expect(wxml).toContain('wx:for="{{secondaryEntries}}"')
+ expect(wxml).toContain('bindtap="handleSecondaryEntryTap"')
+
+ expect(wxss).not.toContain('display: grid')
+ expect(wxss).not.toContain('gap:')
+ expect(wxss).toContain('.ai-page__tabs')
+ expect(wxss).toContain('.ai-page__tab--active')
+ expect(wxss).toContain('.panel-card')
+ expect(wxss).toContain('.action-card')
+ expect(wxss).toContain('.example-chip')
+ expect(wxss).toContain('.ai-page__secondary-list')
+ })
+})
diff --git a/tests/ai-page.test.js b/tests/ai-page.test.js
new file mode 100644
index 0000000..326b781
--- /dev/null
+++ b/tests/ai-page.test.js
@@ -0,0 +1,170 @@
+describe('ai page', () => {
+ afterEach(() => {
+ delete global.Page
+ delete global.wx
+ jest.resetModules()
+ })
+
+ test('exposes AI assistant tabs and default QA panel data', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ const aiPageModule = require('../pages/ai/index')
+ const pageData =
+ typeof aiPageModule.createAiPageData === 'function'
+ ? aiPageModule.createAiPageData()
+ : capturedPageConfig?.data
+
+ expect(aiPageModule.createAiPageData).toEqual(expect.any(Function))
+ expect(pageData.title).toBe('AI助手')
+ expect(pageData.historyText).toBe('历史')
+ expect(pageData.activeTabKey).toBe('qa')
+ expect(pageData.tabs).toEqual([
+ expect.objectContaining({ key: 'qa', label: 'AI答疑' }),
+ expect.objectContaining({ key: 'analysis', label: '辨证分析' }),
+ expect.objectContaining({ key: 'constitution', label: '体质检测' })
+ ])
+ expect(pageData.currentPanel).toEqual(
+ expect.objectContaining({
+ key: 'qa',
+ badge: 'AI答疑',
+ heading: '您好,我是中医学习助手',
+ actionButtonText: '发送'
+ })
+ )
+ expect(pageData.currentPanel.examples).toHaveLength(3)
+ })
+
+ test('switches panel content when another tab is tapped', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ require('../pages/ai/index')
+
+ const pageInstance = {
+ data: {
+ ...(capturedPageConfig.data || {})
+ },
+ setData(update) {
+ this.data = {
+ ...this.data,
+ ...update
+ }
+ }
+ }
+
+ capturedPageConfig.handleTabTap.call(pageInstance, {
+ currentTarget: {
+ dataset: {
+ tabKey: 'analysis'
+ }
+ }
+ })
+
+ expect(pageInstance.data.activeTabKey).toBe('analysis')
+ expect(pageInstance.data.currentPanel).toEqual(
+ expect.objectContaining({
+ key: 'analysis',
+ badge: '辨证分析',
+ heading: '先整理症状,再查看学习型辨证分析',
+ actionButtonText: '开始分析'
+ })
+ )
+
+ capturedPageConfig.handleTabTap.call(pageInstance, {
+ currentTarget: {
+ dataset: {
+ tabKey: 'constitution'
+ }
+ }
+ })
+
+ expect(pageInstance.data.activeTabKey).toBe('constitution')
+ expect(pageInstance.data.currentPanel).toEqual(
+ expect.objectContaining({
+ key: 'constitution',
+ badge: '体质检测',
+ heading: '体质检测即将开放',
+ actionButtonText: '查看其它待开放能力'
+ })
+ )
+ })
+
+ test('shows a toast for history and action placeholders', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ global.wx = {
+ showToast: jest.fn()
+ }
+
+ require('../pages/ai/index')
+
+ capturedPageConfig.handleHistoryTap()
+ capturedPageConfig.handleActionTap()
+
+ expect(global.wx.showToast).toHaveBeenCalledTimes(2)
+ expect(global.wx.showToast).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ title: '功能建设中',
+ icon: 'none'
+ })
+ )
+ expect(global.wx.showToast).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ title: '功能建设中',
+ icon: 'none'
+ })
+ )
+ })
+
+ test('adds a visible interpret entry and routes history to the tcm history page', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ global.wx = {
+ navigateTo: jest.fn(),
+ showToast: jest.fn()
+ }
+
+ const aiPageModule = require('../pages/ai/index')
+ const pageData = aiPageModule.createAiPageData()
+
+ expect(pageData.secondaryEntries).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ key: 'ai-history', title: 'AI历史' }),
+ expect.objectContaining({ key: 'mingli-interpret', title: '命理解读' })
+ ])
+ )
+
+ capturedPageConfig.handleHistoryTap()
+ capturedPageConfig.handleSecondaryEntryTap({
+ currentTarget: {
+ dataset: {
+ route: '/packages/mingli/pages/interpret/index'
+ }
+ }
+ })
+
+ expect(global.wx.navigateTo).toHaveBeenNthCalledWith(1, {
+ url: '/packages/tcm/pages/ai-history/index'
+ })
+ expect(global.wx.navigateTo).toHaveBeenNthCalledWith(2, {
+ url: '/packages/mingli/pages/interpret/index'
+ })
+ })
+})
diff --git a/tests/home-page-render.test.js b/tests/home-page-render.test.js
new file mode 100644
index 0000000..a4018b6
--- /dev/null
+++ b/tests/home-page-render.test.js
@@ -0,0 +1,72 @@
+const fs = require('fs')
+const path = require('path')
+
+describe('home page compatibility', () => {
+ test('avoids fragile WXSS layout features for the native miniprogram renderer', () => {
+ const wxss = fs.readFileSync(path.join(process.cwd(), 'pages/home/index.wxss'), 'utf8')
+
+ expect(wxss).not.toContain('display: grid')
+ expect(wxss).not.toContain('width: fit-content')
+ expect(wxss).not.toContain('gap:')
+ })
+
+ test('renders existing sections and the new wellness/classics blocks with compatible layouts', () => {
+ const wxml = fs.readFileSync(path.join(process.cwd(), 'pages/home/index.wxml'), 'utf8')
+ const wxss = fs.readFileSync(path.join(process.cwd(), 'pages/home/index.wxss'), 'utf8')
+
+ expect(wxml).not.toContain('quick-links')
+ expect(wxml).not.toContain('floating-actions')
+ expect(wxml).not.toContain('home-page__corner')
+ expect(wxml).not.toContain('home-page__header')
+ expect(wxml).toContain('{{wellnessTitle}}')
+ expect(wxml).toContain('{{classicsTitle}}')
+ expect(wxml).toContain('{{classicsActionText}}')
+ expect(wxml).toContain('bindtap="handleSearchTap"')
+ expect(wxml).toContain('bindtap="handleEncyclopediaTap"')
+ expect(wxml).toContain('bindtap="handleToolTap"')
+ expect(wxml).toContain('bindtap="handleWellnessTap"')
+ expect(wxml).toContain('bindtap="handleClassicActionTap"')
+ expect(wxml).toContain('bindtap="handleClassicTap"')
+ expect(wxml).toContain('feature-grid feature-grid--three')
+ expect(wxml.match(/feature-grid feature-grid--four/g)).toHaveLength(3)
+ expect(wxml.match(/feature-grid feature-grid--three/g)).toHaveLength(1)
+ expect(wxml).toContain('section-card__header')
+ expect(wxml).toContain('feature-card__book')
+
+ expect(wxss).toContain('.feature-grid--four .feature-card')
+ expect(wxss).toContain('.feature-grid--three .feature-grid__item')
+ expect(wxss).toContain('.section-card__header')
+ expect(wxss).toContain('.section-card__action')
+ expect(wxss).toContain('.feature-card__book')
+ expect(wxss).not.toContain('.home-page__corner')
+ expect(wxss).not.toContain('.home-page__header')
+ expect(wxss).toContain('padding: 12rpx 18rpx 36rpx;')
+ expect(wxss).toContain('padding: 24rpx 18rpx 16rpx;')
+ expect(wxss).toContain('font-size: 60rpx;')
+ expect(wxss).toContain('font-size: 42rpx;')
+ expect(wxss).toContain('min-height: 152rpx;')
+ })
+
+ test('renders the portal section without forbidden grid or gap styles', () => {
+ const wxml = fs.readFileSync(path.join(process.cwd(), 'pages/home/index.wxml'), 'utf8')
+ const wxss = fs.readFileSync(path.join(process.cwd(), 'pages/home/index.wxss'), 'utf8')
+
+ expect(wxml).toContain('{{portalTitle}}')
+ expect(wxml).toContain('portal-grid')
+ expect(wxml).toContain('bindtap="handlePortalTap"')
+ expect(wxss).toContain('.portal-grid')
+ expect(wxss).toContain('.portal-card')
+ expect(wxss).not.toContain('display: grid')
+ expect(wxss).not.toContain('gap:')
+ })
+
+ test('renders encyclopedia section before the learning portal section', () => {
+ const wxml = fs.readFileSync(path.join(process.cwd(), 'pages/home/index.wxml'), 'utf8')
+ const encyclopediaIndex = wxml.indexOf('{{encyclopediaTitle}}')
+ const portalIndex = wxml.indexOf('{{portalTitle}}')
+
+ expect(encyclopediaIndex).toBeGreaterThan(-1)
+ expect(portalIndex).toBeGreaterThan(-1)
+ expect(encyclopediaIndex).toBeLessThan(portalIndex)
+ })
+})
diff --git a/tests/home-page.test.js b/tests/home-page.test.js
new file mode 100644
index 0000000..af2761c
--- /dev/null
+++ b/tests/home-page.test.js
@@ -0,0 +1,169 @@
+describe('home page', () => {
+ afterEach(() => {
+ delete global.Page
+ delete global.wx
+ jest.resetModules()
+ })
+
+ test('exposes static home sections without custom bottom navigation data', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ const homePageModule = require('../pages/home/index')
+ const pageData =
+ typeof homePageModule.createHomePageData === 'function'
+ ? homePageModule.createHomePageData()
+ : capturedPageConfig?.data
+
+ expect(homePageModule.createHomePageData).toEqual(expect.any(Function))
+ expect(pageData.brandName).toBe('玄知中医')
+ expect(pageData.quickLinks).toBeUndefined()
+ expect(pageData.floatingActions).toBeUndefined()
+ expect(pageData.encyclopediaTitle).toBe('中医百科')
+ expect(pageData.toolsTitle).toBe('学习工具')
+ expect(pageData.wellnessTitle).toBe('养生调理')
+ expect(pageData.classicsTitle).toBe('热门典籍')
+ expect(pageData.classicsActionText).toBe('进入书城')
+ expect(pageData.encyclopediaCards).toHaveLength(3)
+ expect(pageData.toolCards).toHaveLength(4)
+ expect(pageData.wellnessCards).toHaveLength(3)
+ expect(pageData.classicsBooks).toHaveLength(4)
+ expect(pageData.bottomNavItems).toBeUndefined()
+ })
+
+ test('adds direct portal cards for tcm, mingli, bazi and learning routes', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ global.wx = {
+ switchTab: jest.fn(),
+ navigateTo: jest.fn()
+ }
+
+ const homePageModule = require('../pages/home/index')
+ const pageData = homePageModule.createHomePageData()
+
+ expect(pageData.portalTitle).toBe('学习入口')
+ expect(pageData.portalCards).toEqual([
+ expect.objectContaining({ key: 'tcm-library', title: '中医馆' }),
+ expect.objectContaining({ key: 'mingli-hall', title: '易学阁' }),
+ expect.objectContaining({ key: 'bazi', title: '八字排盘' }),
+ expect.objectContaining({ key: 'learning-center', title: '学习中心' })
+ ])
+
+ capturedPageConfig.handlePortalTap({
+ currentTarget: {
+ dataset: {
+ route: '/pages/library/index'
+ }
+ }
+ })
+
+ capturedPageConfig.handlePortalTap({
+ currentTarget: {
+ dataset: {
+ route: '/packages/mingli/pages/hall/index'
+ }
+ }
+ })
+
+ expect(global.wx.switchTab).toHaveBeenCalledWith({
+ url: '/pages/library/index'
+ })
+ expect(global.wx.navigateTo).toHaveBeenCalledWith({
+ url: '/packages/mingli/pages/hall/index'
+ })
+ })
+
+ test('wires search, feature cards and classics interactions to route-first actions', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ global.wx = {
+ switchTab: jest.fn(),
+ navigateTo: jest.fn(),
+ showToast: jest.fn()
+ }
+
+ const homePageModule = require('../pages/home/index')
+ const pageData = homePageModule.createHomePageData()
+
+ expect(pageData.searchRoute).toBe('/packages/tcm/pages/search-books/index')
+ expect(pageData.encyclopediaCards).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ key: 'classic', route: '/pages/library/index' })
+ ])
+ )
+ expect(pageData.classicsBooks).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ key: 'huangdi-neijing-suwen',
+ route: '/packages/tcm/pages/book-detail/index?scene=classic-a'
+ })
+ ])
+ )
+
+ capturedPageConfig.handleSearchTap()
+ capturedPageConfig.handleEncyclopediaTap({
+ currentTarget: {
+ dataset: {
+ route: '/pages/library/index',
+ title: '经典书城'
+ }
+ }
+ })
+ capturedPageConfig.handleToolTap({
+ currentTarget: {
+ dataset: {
+ route: '/pages/ai/index',
+ title: 'AI问答'
+ }
+ }
+ })
+ capturedPageConfig.handleWellnessTap({
+ currentTarget: {
+ dataset: {
+ title: '药膳'
+ }
+ }
+ })
+ capturedPageConfig.handleClassicActionTap()
+ capturedPageConfig.handleClassicTap({
+ currentTarget: {
+ dataset: {
+ route: '/packages/tcm/pages/book-detail/index?scene=classic-a',
+ title: '黄帝内经素问'
+ }
+ }
+ })
+
+ expect(global.wx.navigateTo).toHaveBeenNthCalledWith(1, {
+ url: '/packages/tcm/pages/search-books/index'
+ })
+ expect(global.wx.switchTab).toHaveBeenNthCalledWith(1, {
+ url: '/pages/library/index'
+ })
+ expect(global.wx.switchTab).toHaveBeenNthCalledWith(2, {
+ url: '/pages/ai/index'
+ })
+ expect(global.wx.showToast).toHaveBeenCalledWith({
+ title: '药膳待开放',
+ icon: 'none'
+ })
+ expect(global.wx.switchTab).toHaveBeenNthCalledWith(3, {
+ url: '/pages/library/index'
+ })
+ expect(global.wx.navigateTo).toHaveBeenNthCalledWith(2, {
+ url: '/packages/tcm/pages/book-detail/index?scene=classic-a'
+ })
+ })
+})
diff --git a/tests/library-page-render.test.js b/tests/library-page-render.test.js
new file mode 100644
index 0000000..5c305d7
--- /dev/null
+++ b/tests/library-page-render.test.js
@@ -0,0 +1,29 @@
+const fs = require('fs')
+const path = require('path')
+
+describe('library page compatibility', () => {
+ test('renders category navigation and book cards with compatible layout primitives', () => {
+ const wxml = fs.readFileSync(path.join(process.cwd(), 'pages/library/index.wxml'), 'utf8')
+ const wxss = fs.readFileSync(path.join(process.cwd(), 'pages/library/index.wxss'), 'utf8')
+
+ expect(wxml).not.toContain('placeholder-page')
+ expect(wxml).toContain('library-page__sidebar')
+ expect(wxml).toContain('library-page__category')
+ expect(wxml).toContain('wx:for="{{categories}}"')
+ expect(wxml).toContain('bindtap="handleCategoryTap"')
+ expect(wxml).toContain('{{currentCategoryName}}')
+ expect(wxml).toContain('wx:for="{{visibleBooks}}"')
+ expect(wxml).toContain('book-card__cover-char')
+ expect(wxml).toContain('bindtap="handleSearchTap"')
+ expect(wxml).toContain('{{domainBridge.title}}')
+ expect(wxml).toContain('bindtap="handleDomainBridgeTap"')
+
+ expect(wxss).not.toContain('display: grid')
+ expect(wxss).not.toContain('gap:')
+ expect(wxss).toContain('.library-page__sidebar')
+ expect(wxss).toContain('.library-page__category--active')
+ expect(wxss).toContain('.book-card')
+ expect(wxss).toContain('.book-card__meta')
+ expect(wxss).toContain('.library-page__bridge')
+ })
+})
diff --git a/tests/library-page.test.js b/tests/library-page.test.js
new file mode 100644
index 0000000..459bdb2
--- /dev/null
+++ b/tests/library-page.test.js
@@ -0,0 +1,145 @@
+describe('library page', () => {
+ afterEach(() => {
+ delete global.Page
+ delete global.wx
+ jest.resetModules()
+ })
+
+ test('exposes mock categories and featured books by default', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ const libraryPageModule = require('../pages/library/index')
+ const pageData =
+ typeof libraryPageModule.createLibraryPageData === 'function'
+ ? libraryPageModule.createLibraryPageData()
+ : capturedPageConfig?.data
+
+ expect(libraryPageModule.createLibraryPageData).toEqual(expect.any(Function))
+ expect(pageData.title).toBe('典籍')
+ expect(pageData.searchButtonText).toBe('搜索')
+ expect(pageData.activeCategoryId).toBe('featured')
+ expect(pageData.currentCategoryName).toBe('精选')
+ expect(pageData.categories).toHaveLength(9)
+ expect(pageData.categories.map(item => item.name)).toEqual([
+ '精选',
+ '注解',
+ '经论',
+ '伤寒金匮',
+ '医方',
+ '本草',
+ '针灸',
+ '医案',
+ '综合'
+ ])
+ expect(pageData.visibleBooks).toHaveLength(6)
+ expect(pageData.visibleBooks[0]).toEqual(
+ expect.objectContaining({
+ id: 'huangdi-neijing-suwen',
+ title: '黄帝内经素问',
+ author: '王冰次',
+ dynasty: '唐',
+ period: '公元762年'
+ })
+ )
+ })
+
+ test('switches visible books when another category is selected', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ require('../pages/library/index')
+
+ const pageInstance = {
+ data: {
+ ...(capturedPageConfig.data || {})
+ },
+ setData(update) {
+ this.data = {
+ ...this.data,
+ ...update
+ }
+ }
+ }
+
+ capturedPageConfig.handleCategoryTap.call(pageInstance, {
+ currentTarget: {
+ dataset: {
+ categoryId: 'annotation'
+ }
+ }
+ })
+
+ expect(pageInstance.data.activeCategoryId).toBe('annotation')
+ expect(pageInstance.data.currentCategoryName).toBe('注解')
+ expect(pageInstance.data.visibleBooks).toEqual([
+ expect.objectContaining({
+ title: '黄帝内经素问集注'
+ }),
+ expect.objectContaining({
+ title: '神农本草经辑注'
+ }),
+ expect.objectContaining({
+ title: '难经集注'
+ })
+ ])
+ })
+
+ test('shows a toast while search is still mocked', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ global.wx = {
+ showToast: jest.fn()
+ }
+
+ require('../pages/library/index')
+
+ capturedPageConfig.handleSearchTap()
+
+ expect(global.wx.showToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: '搜索功能暂未开放',
+ icon: 'none'
+ })
+ )
+ })
+
+ test('adds a visible mingli-hall bridge entry to the library tab', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ global.wx = {
+ navigateTo: jest.fn(),
+ showToast: jest.fn()
+ }
+
+ const libraryPageModule = require('../pages/library/index')
+ const pageData = libraryPageModule.createLibraryPageData()
+
+ expect(pageData.domainBridge).toEqual(
+ expect.objectContaining({
+ title: '进入易学阁',
+ route: '/packages/mingli/pages/hall/index'
+ })
+ )
+
+ capturedPageConfig.handleDomainBridgeTap()
+
+ expect(global.wx.navigateTo).toHaveBeenCalledWith({
+ url: '/packages/mingli/pages/hall/index'
+ })
+ })
+})
diff --git a/tests/login-page-render.test.js b/tests/login-page-render.test.js
new file mode 100644
index 0000000..9b7ae08
--- /dev/null
+++ b/tests/login-page-render.test.js
@@ -0,0 +1,22 @@
+const fs = require('fs')
+const path = require('path')
+
+describe('login page compatibility', () => {
+ test('renders a static login layout with action list only', () => {
+ const wxml = fs.readFileSync(path.join(process.cwd(), 'pages/login/index.wxml'), 'utf8')
+ const wxss = fs.readFileSync(path.join(process.cwd(), 'pages/login/index.wxss'), 'utf8')
+
+ expect(wxml).toContain('login-page')
+ expect(wxml).toContain('{{title}}')
+ expect(wxml).toContain('{{subtitle}}')
+ expect(wxml).toContain('wx:for="{{actions}}"')
+ expect(wxml).toContain('bindtap="handleActionTap"')
+ expect(wxml).not.toContain('handleMockLogin')
+ expect(wxml).not.toContain('t-input')
+ expect(wxml).not.toContain('app-button')
+
+ expect(wxss).toContain('.login-page')
+ expect(wxss).toContain('.login-page__hero')
+ expect(wxss).toContain('.login-page__action')
+ })
+})
diff --git a/tests/login-page.test.js b/tests/login-page.test.js
new file mode 100644
index 0000000..e554246
--- /dev/null
+++ b/tests/login-page.test.js
@@ -0,0 +1,75 @@
+const fs = require('fs')
+const path = require('path')
+
+describe('login page', () => {
+ afterEach(() => {
+ delete global.Page
+ delete global.wx
+ jest.resetModules()
+ })
+
+ test('renders a static login surface without session-store wiring', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ const loginPageModule = require('../pages/login/index')
+ const pageData = loginPageModule.createLoginPageData()
+ const source = fs.readFileSync(path.join(process.cwd(), 'pages/login/index.js'), 'utf8')
+
+ expect(capturedPageConfig.data).toEqual(expect.objectContaining({ title: '欢迎来到玄志' }))
+ expect(pageData.title).toBe('欢迎来到玄志')
+ expect(pageData.actions).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ key: 'wechat', title: '微信授权登录' }),
+ expect.objectContaining({ key: 'browse', title: '先看看静态页面' })
+ ])
+ )
+ expect(source).not.toContain('sessionStore')
+ expect(source).not.toContain('handleMockLogin')
+ })
+
+ test('switches to home for browse action and keeps wechat action as static toast', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ global.wx = {
+ switchTab: jest.fn(),
+ showToast: jest.fn()
+ }
+
+ require('../pages/login/index')
+
+ capturedPageConfig.handleActionTap({
+ currentTarget: {
+ dataset: {
+ actionType: 'route',
+ route: '/pages/home/index'
+ }
+ }
+ })
+
+ capturedPageConfig.handleActionTap({
+ currentTarget: {
+ dataset: {
+ actionType: 'toast'
+ }
+ }
+ })
+
+ expect(global.wx.switchTab).toHaveBeenCalledWith({
+ url: '/pages/home/index'
+ })
+ expect(global.wx.showToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: '登录功能建设中',
+ icon: 'none'
+ })
+ )
+ })
+})
diff --git a/tests/mingli-bazi-page-render.test.js b/tests/mingli-bazi-page-render.test.js
new file mode 100644
index 0000000..4e86012
--- /dev/null
+++ b/tests/mingli-bazi-page-render.test.js
@@ -0,0 +1,33 @@
+const fs = require('fs')
+const path = require('path')
+
+describe('mingli bazi page compatibility', () => {
+ test('renders larger hero, form and result sections with native-compatible layout', () => {
+ const wxml = fs.readFileSync(
+ path.join(process.cwd(), 'packages/mingli/pages/bazi/index.wxml'),
+ 'utf8'
+ )
+ const wxss = fs.readFileSync(
+ path.join(process.cwd(), 'packages/mingli/pages/bazi/index.wxss'),
+ 'utf8'
+ )
+
+ expect(wxml).toContain('hero-card__eyebrow')
+ expect(wxml).toContain('hero-card__meta')
+ expect(wxml).toContain('panel-header')
+ expect(wxml).toContain('field-block__value')
+ expect(wxml).toContain('result-card__grid')
+
+ expect(wxss).toContain('.hero-card__eyebrow')
+ expect(wxss).toContain('.hero-card__meta')
+ expect(wxss).toContain('.panel-header__title')
+ expect(wxss).toContain('.field-block__value')
+ expect(wxss).toContain('.result-card__grid')
+ expect(wxss).toContain('padding: 36rpx 24rpx 96rpx;')
+ expect(wxss).toContain('padding: 34rpx 28rpx;')
+ expect(wxss).toContain('font-size: 46rpx;')
+ expect(wxss).toContain('font-size: 34rpx;')
+ expect(wxss).not.toContain('display: grid')
+ expect(wxss).not.toContain('gap:')
+ })
+})
diff --git a/tests/mingli-learning-pages.test.js b/tests/mingli-learning-pages.test.js
new file mode 100644
index 0000000..82ca3c6
--- /dev/null
+++ b/tests/mingli-learning-pages.test.js
@@ -0,0 +1,29 @@
+describe('mingli and learning pages', () => {
+ afterEach(() => {
+ delete global.Page
+ delete global.wx
+ jest.resetModules()
+ })
+
+ test('hall, bazi, interpret and learning center pages expose static route-first surfaces', () => {
+ global.Page = () => {}
+
+ const hallModule = require('../packages/mingli/pages/hall/index')
+ const baziModule = require('../packages/mingli/pages/bazi/index')
+ const interpretModule = require('../packages/mingli/pages/interpret/index')
+ const learningModule = require('../packages/learning/pages/center/index')
+
+ expect(hallModule.createMingliHallPageData()).toEqual(
+ expect.objectContaining({ title: '易学阁' })
+ )
+ expect(baziModule.createMingliBaziPageData('result')).toEqual(
+ expect.objectContaining({ scene: 'result', title: '八字排盘' })
+ )
+ expect(interpretModule.createMingliInterpretPageData('result')).toEqual(
+ expect.objectContaining({ scene: 'result', title: '命理解读' })
+ )
+ expect(learningModule.createLearningCenterPageData()).toEqual(
+ expect.objectContaining({ title: '学习中心' })
+ )
+ })
+})
diff --git a/tests/miniprogram-compatibility.test.js b/tests/miniprogram-compatibility.test.js
new file mode 100644
index 0000000..ab07417
--- /dev/null
+++ b/tests/miniprogram-compatibility.test.js
@@ -0,0 +1,83 @@
+const fs = require('fs')
+const path = require('path')
+
+const PROJECT_ROOT = process.cwd()
+const IGNORED_SEGMENTS = ['node_modules', 'miniprogram_npm', 'coverage', '.worktrees']
+
+function shouldIgnore(filePath) {
+ return IGNORED_SEGMENTS.some(segment => filePath.includes(`${path.sep}${segment}${path.sep}`))
+}
+
+function getProjectFiles(extension) {
+ const pendingDirs = [PROJECT_ROOT]
+ const collectedFiles = []
+
+ while (pendingDirs.length) {
+ const currentDir = pendingDirs.pop()
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true })
+
+ entries.forEach(entry => {
+ const absolutePath = path.join(currentDir, entry.name)
+
+ if (shouldIgnore(absolutePath)) {
+ return
+ }
+
+ if (entry.isDirectory()) {
+ pendingDirs.push(absolutePath)
+ return
+ }
+
+ if (entry.isFile() && absolutePath.endsWith(extension)) {
+ collectedFiles.push(absolutePath)
+ }
+ })
+ }
+
+ return collectedFiles
+}
+
+function getDirectoryImportViolations() {
+ const requirePattern = /require\((['"])(\.[^'"]+)\1\)/g
+
+ return getProjectFiles('.js').flatMap(filePath => {
+ const source = fs.readFileSync(filePath, 'utf8')
+ const violations = []
+ let match = requirePattern.exec(source)
+
+ while (match) {
+ const specifier = match[2]
+ const targetPath = path.resolve(path.dirname(filePath), specifier)
+
+ if (fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()) {
+ violations.push({
+ filePath: path.relative(PROJECT_ROOT, filePath),
+ specifier
+ })
+ }
+
+ match = requirePattern.exec(source)
+ }
+
+ return violations
+ })
+}
+
+function getImportTarget(wxssPath, importPath) {
+ return path.resolve(path.dirname(wxssPath), importPath)
+}
+
+describe('miniprogram compatibility', () => {
+ test('avoids directory-level require paths that the miniprogram runtime cannot resolve reliably', () => {
+ expect(getDirectoryImportViolations()).toEqual([])
+ })
+
+ test('app.wxss imports the generated TDesign style entry that exists in the project', () => {
+ const appWxssPath = path.join(PROJECT_ROOT, 'app.wxss')
+ const appWxss = fs.readFileSync(appWxssPath, 'utf8')
+ const match = appWxss.match(/@import ['"]([^'"]+)['"];/)
+
+ expect(match).not.toBeNull()
+ expect(fs.existsSync(getImportTarget(appWxssPath, match[1]))).toBe(true)
+ })
+})
diff --git a/tests/profile-page-render.test.js b/tests/profile-page-render.test.js
new file mode 100644
index 0000000..1622f8d
--- /dev/null
+++ b/tests/profile-page-render.test.js
@@ -0,0 +1,24 @@
+const fs = require('fs')
+const path = require('path')
+
+describe('profile page compatibility', () => {
+ test('renders the five static profile sections with route-first shortcuts', () => {
+ const wxml = fs.readFileSync(path.join(process.cwd(), 'pages/profile/index.wxml'), 'utf8')
+ const wxss = fs.readFileSync(path.join(process.cwd(), 'pages/profile/index.wxss'), 'utf8')
+
+ expect(wxml).not.toContain('placeholder-page')
+ expect(wxml).toContain('profile-page')
+ expect(wxml).toContain('{{userCard.title}}')
+ expect(wxml).toContain('{{vipCard.title}}')
+ expect(wxml).toContain('wx:for="{{assetItems}}"')
+ expect(wxml).toContain('bindtap="handleAssetTap"')
+ expect(wxml).toContain('{{recentRecord.emptyTitle}}')
+ expect(wxml).toContain('wx:for="{{moreItems}}"')
+ expect(wxml).toContain('bindtap="handleMoreTap"')
+
+ expect(wxss).toContain('.profile-page')
+ expect(wxss).toContain('.profile-header')
+ expect(wxss).toContain('.asset-grid')
+ expect(wxss).toContain('.menu-list')
+ })
+})
diff --git a/tests/profile-page.test.js b/tests/profile-page.test.js
new file mode 100644
index 0000000..7e503b3
--- /dev/null
+++ b/tests/profile-page.test.js
@@ -0,0 +1,43 @@
+describe('profile page', () => {
+ afterEach(() => {
+ delete global.Page
+ delete global.wx
+ jest.resetModules()
+ })
+
+ test('exposes static profile sections and navigable shortcuts', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ global.wx = {
+ navigateTo: jest.fn()
+ }
+
+ const profilePageModule = require('../pages/profile/index')
+ const pageData = profilePageModule.createProfilePageData()
+
+ expect(pageData.userCard.title).toBe('点击登录')
+ expect(pageData.assetItems).toHaveLength(4)
+ expect(pageData.moreItems).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ key: 'learning-center', title: '学习中心' }),
+ expect.objectContaining({ key: 'ai-history', title: 'AI历史' })
+ ])
+ )
+
+ capturedPageConfig.handleMoreTap({
+ currentTarget: {
+ dataset: {
+ route: '/packages/learning/pages/center/index'
+ }
+ }
+ })
+
+ expect(global.wx.navigateTo).toHaveBeenCalledWith({
+ url: '/packages/learning/pages/center/index'
+ })
+ })
+})
diff --git a/tests/project-config.test.js b/tests/project-config.test.js
new file mode 100644
index 0000000..988ca1d
--- /dev/null
+++ b/tests/project-config.test.js
@@ -0,0 +1,82 @@
+const fs = require('fs')
+const path = require('path')
+
+function readJson(fileName) {
+ return JSON.parse(
+ fs.readFileSync(path.join(process.cwd(), fileName), {
+ encoding: 'utf8'
+ })
+ )
+}
+
+describe('project config', () => {
+ test('declares an explicit project name and base library version in root config', () => {
+ const projectConfig = readJson('project.config.json')
+
+ expect(projectConfig.projectname).toEqual(expect.any(String))
+ expect(projectConfig.projectname.trim()).not.toBe('')
+ expect(projectConfig.libVersion).toMatch(/^\d+\.\d+\.\d+$/)
+ })
+
+ test('registers native tab pages and matching tabBar items in app config', () => {
+ const appConfig = readJson('app.json')
+ const expectedTabPages = [
+ 'pages/home/index',
+ 'pages/library/index',
+ 'pages/ai/index',
+ 'pages/profile/index'
+ ]
+
+ expect(appConfig.pages).toEqual(
+ expect.arrayContaining([...expectedTabPages, 'pages/login/index'])
+ )
+ expect(appConfig.tabBar).toEqual(
+ expect.objectContaining({
+ list: expect.arrayContaining(
+ expectedTabPages.map(pagePath =>
+ expect.objectContaining({
+ pagePath
+ })
+ )
+ )
+ })
+ )
+ expect(appConfig.tabBar.list).toHaveLength(4)
+ })
+
+ test('registers static migration subpackages in app config', () => {
+ const appConfig = readJson('app.json')
+
+ expect(appConfig.subPackages).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ root: 'packages/tcm',
+ pages: expect.arrayContaining([
+ 'pages/ai-history/index',
+ 'pages/assets/index',
+ 'pages/bianzheng/index',
+ 'pages/book-detail/index',
+ 'pages/search-books/index',
+ 'pages/section/index',
+ 'pages/placeholder/index'
+ ])
+ }),
+ expect.objectContaining({
+ root: 'packages/mingli',
+ pages: expect.arrayContaining([
+ 'pages/hall/index',
+ 'pages/bazi/index',
+ 'pages/book-detail/index',
+ 'pages/search-books/index',
+ 'pages/section/index',
+ 'pages/interpret/index'
+ ])
+ }),
+ expect.objectContaining({
+ root: 'packages/learning',
+ pages: ['pages/center/index']
+ })
+ ])
+ )
+ })
+})
diff --git a/tests/request.test.js b/tests/request.test.js
index 47c637f..b284390 100644
--- a/tests/request.test.js
+++ b/tests/request.test.js
@@ -1,4 +1,4 @@
-const { createRequester, RequestError } = require('../services/request')
+const { createRequester, RequestError } = require('../services/request/index')
describe('createRequester', () => {
test('normalizes successful responses', async () => {
diff --git a/tests/static-ux-domain-data.test.js b/tests/static-ux-domain-data.test.js
new file mode 100644
index 0000000..62ff3bf
--- /dev/null
+++ b/tests/static-ux-domain-data.test.js
@@ -0,0 +1,46 @@
+const {
+ createTcmHomeHubCards,
+ createTcmAssetPageData
+} = require('../utils/static-ux/tcm')
+const {
+ createMingliHallPageData,
+ createBaziPageData
+} = require('../utils/static-ux/mingli')
+const { createLearningCenterPageData } = require('../utils/static-ux/learning')
+
+describe('static domain factories', () => {
+ test('returns scene-safe static data for tcm, mingli and learning domains', () => {
+ expect(createTcmHomeHubCards()).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ key: 'tcm-library', title: '中医馆' }),
+ expect.objectContaining({ key: 'mingli-hall', title: '易学阁' })
+ ])
+ )
+
+ expect(createTcmAssetPageData('notes')).toEqual(
+ expect.objectContaining({
+ title: '学习资产',
+ activeKind: 'notes'
+ })
+ )
+
+ expect(createMingliHallPageData()).toEqual(
+ expect.objectContaining({
+ title: '易学阁'
+ })
+ )
+
+ expect(createBaziPageData('result')).toEqual(
+ expect.objectContaining({
+ title: '八字排盘',
+ scene: 'result'
+ })
+ )
+
+ expect(createLearningCenterPageData()).toEqual(
+ expect.objectContaining({
+ title: '学习中心'
+ })
+ )
+ })
+})
diff --git a/tests/static-ux-navigation.test.js b/tests/static-ux-navigation.test.js
new file mode 100644
index 0000000..f754b3f
--- /dev/null
+++ b/tests/static-ux-navigation.test.js
@@ -0,0 +1,26 @@
+const { isTabRoute, openStaticRoute } = require('../utils/static-ux/route-map')
+
+describe('static ux navigation', () => {
+ test('recognizes tab routes by page path', () => {
+ expect(isTabRoute('/pages/home/index')).toBe(true)
+ expect(isTabRoute('/pages/home/index?from=portal')).toBe(true)
+ expect(isTabRoute('/packages/tcm/pages/assets/index?kind=history')).toBe(false)
+ })
+
+ test('uses switchTab for tab routes and navigateTo for non-tab routes', () => {
+ const wxApi = {
+ switchTab: jest.fn(),
+ navigateTo: jest.fn()
+ }
+
+ expect(openStaticRoute('/pages/library/index', wxApi)).toBe(true)
+ expect(openStaticRoute('/packages/tcm/pages/assets/index?kind=history', wxApi)).toBe(true)
+
+ expect(wxApi.switchTab).toHaveBeenCalledWith({
+ url: '/pages/library/index'
+ })
+ expect(wxApi.navigateTo).toHaveBeenCalledWith({
+ url: '/packages/tcm/pages/assets/index?kind=history'
+ })
+ })
+})
diff --git a/tests/static-ux-route-map.test.js b/tests/static-ux-route-map.test.js
new file mode 100644
index 0000000..2179b2e
--- /dev/null
+++ b/tests/static-ux-route-map.test.js
@@ -0,0 +1,12 @@
+const { ROUTES } = require('../utils/static-ux/route-map')
+
+describe('static route map', () => {
+ test('exposes full route paths for all static migration pages', () => {
+ expect(ROUTES.tabs.home).toBe('/pages/home/index')
+ expect(ROUTES.tcm.aiHistory).toBe('/packages/tcm/pages/ai-history/index')
+ expect(ROUTES.tcm.section).toBe('/packages/tcm/pages/section/index')
+ expect(ROUTES.mingli.hall).toBe('/packages/mingli/pages/hall/index')
+ expect(ROUTES.mingli.interpret).toBe('/packages/mingli/pages/interpret/index')
+ expect(ROUTES.learning.center).toBe('/packages/learning/pages/center/index')
+ })
+})
diff --git a/tests/tcm-reading-pages.test.js b/tests/tcm-reading-pages.test.js
new file mode 100644
index 0000000..ab6a4d1
--- /dev/null
+++ b/tests/tcm-reading-pages.test.js
@@ -0,0 +1,25 @@
+describe('tcm reading pages', () => {
+ afterEach(() => {
+ delete global.Page
+ delete global.wx
+ jest.resetModules()
+ })
+
+ test('book detail, search and section pages use static scenes instead of business ids', () => {
+ global.Page = () => {}
+
+ const bookDetailModule = require('../packages/tcm/pages/book-detail/index')
+ const searchModule = require('../packages/tcm/pages/search-books/index')
+ const sectionModule = require('../packages/tcm/pages/section/index')
+
+ expect(bookDetailModule.createTcmBookDetailPageData('classic-a')).toEqual(
+ expect.objectContaining({ scene: 'classic-a', title: '典籍详情' })
+ )
+ expect(searchModule.createTcmSearchBooksPageData('keyword-demo')).toEqual(
+ expect.objectContaining({ keyword: 'keyword-demo', title: '搜索典籍' })
+ )
+ expect(sectionModule.createTcmSectionPageData('reader-a')).toEqual(
+ expect.objectContaining({ scene: 'reader-a', title: '典籍阅读' })
+ )
+ })
+})
diff --git a/tests/tcm-support-pages.test.js b/tests/tcm-support-pages.test.js
new file mode 100644
index 0000000..6825510
--- /dev/null
+++ b/tests/tcm-support-pages.test.js
@@ -0,0 +1,99 @@
+describe('tcm support pages', () => {
+ afterEach(() => {
+ delete global.Page
+ delete global.wx
+ jest.resetModules()
+ })
+
+ test('assets page switches by static kind and keeps route-only navigation', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ const assetsPageModule = require('../packages/tcm/pages/assets/index')
+ const pageData = assetsPageModule.createTcmAssetsPageData('favorites')
+
+ expect(capturedPageConfig.data).toEqual(expect.objectContaining({ activeKind: 'notes' }))
+ expect(pageData.activeKind).toBe('favorites')
+ expect(pageData.title).toBe('学习资产')
+ expect(pageData.filterItems).toHaveLength(4)
+ })
+
+ test('routes TCM asset history items back to the AI tab with switchTab', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ global.wx = {
+ switchTab: jest.fn(),
+ navigateTo: jest.fn()
+ }
+
+ const assetsPageModule = require('../packages/tcm/pages/assets/index')
+ const pageData = assetsPageModule.createTcmAssetsPageData('history')
+
+ capturedPageConfig.handleEntryTap({
+ currentTarget: {
+ dataset: {
+ route: pageData.items[0].route
+ }
+ }
+ })
+
+ expect(global.wx.switchTab).toHaveBeenCalledWith({
+ url: '/pages/ai/index'
+ })
+ expect(global.wx.navigateTo).not.toHaveBeenCalled()
+ })
+
+ test('ai history, bianzheng and placeholder pages expose scene-driven static data', () => {
+ global.Page = () => {}
+
+ const aiHistoryModule = require('../packages/tcm/pages/ai-history/index')
+ const bianzhengModule = require('../packages/tcm/pages/bianzheng/index')
+ const placeholderModule = require('../packages/tcm/pages/placeholder/index')
+
+ expect(aiHistoryModule.createTcmAiHistoryPageData('empty')).toEqual(
+ expect.objectContaining({ scene: 'empty', title: 'AI对话历史' })
+ )
+ expect(bianzhengModule.createTcmBianzhengPageData('result')).toEqual(
+ expect.objectContaining({ scene: 'result', title: '辨证分析' })
+ )
+ expect(placeholderModule.createTcmPlaceholderPageData('membership')).toEqual(
+ expect.objectContaining({ kind: 'membership' })
+ )
+ })
+
+ test('routes AI history items targeting the AI tab with switchTab', () => {
+ let capturedPageConfig
+
+ global.Page = config => {
+ capturedPageConfig = config
+ }
+
+ global.wx = {
+ switchTab: jest.fn(),
+ navigateTo: jest.fn()
+ }
+
+ const aiHistoryModule = require('../packages/tcm/pages/ai-history/index')
+ const pageData = aiHistoryModule.createTcmAiHistoryPageData('default')
+
+ capturedPageConfig.handleHistoryTap({
+ currentTarget: {
+ dataset: {
+ route: pageData.groups[0].items[0].route
+ }
+ }
+ })
+
+ expect(global.wx.switchTab).toHaveBeenCalledWith({
+ url: '/pages/ai/index'
+ })
+ expect(global.wx.navigateTo).not.toHaveBeenCalled()
+ })
+})
diff --git a/utils/static-ux/learning.js b/utils/static-ux/learning.js
new file mode 100644
index 0000000..94a8a5b
--- /dev/null
+++ b/utils/static-ux/learning.js
@@ -0,0 +1,15 @@
+function createLearningCenterPageData() {
+ return {
+ title: '学习中心',
+ summaryCards: [
+ { key: 'qa', value: 8, label: '问答' },
+ { key: 'analysis', value: 5, label: '辨证' },
+ { key: 'interpret', value: 6, label: '解读' },
+ { key: 'bazi', value: 4, label: '排盘' }
+ ]
+ }
+}
+
+module.exports = {
+ createLearningCenterPageData
+}
diff --git a/utils/static-ux/mingli.js b/utils/static-ux/mingli.js
new file mode 100644
index 0000000..e915a23
--- /dev/null
+++ b/utils/static-ux/mingli.js
@@ -0,0 +1,48 @@
+const { cloneList, resolveScene } = require('./shared')
+const { ROUTES } = require('./route-map')
+
+const MINGLI_QUICK_CARDS = Object.freeze([
+ {
+ key: 'hall',
+ title: '易学阁',
+ subtitle: '总览入口',
+ route: ROUTES.mingli.hall,
+ icon: '阁'
+ },
+ {
+ key: 'bazi',
+ title: '八字排盘',
+ subtitle: '静态排盘结果',
+ route: ROUTES.mingli.bazi,
+ icon: '盘'
+ },
+ {
+ key: 'interpret',
+ title: '命理解读',
+ subtitle: '问题与解读',
+ route: ROUTES.mingli.interpret,
+ icon: '解'
+ }
+])
+
+function createMingliHallPageData() {
+ return {
+ title: '易学阁',
+ quickCards: cloneList(MINGLI_QUICK_CARDS)
+ }
+}
+
+function createBaziPageData(rawScene) {
+ const scene = resolveScene(rawScene, ['default', 'result'], 'default')
+
+ return {
+ title: '八字排盘',
+ scene,
+ quickCards: cloneList(MINGLI_QUICK_CARDS)
+ }
+}
+
+module.exports = {
+ createMingliHallPageData,
+ createBaziPageData
+}
diff --git a/utils/static-ux/route-map.js b/utils/static-ux/route-map.js
new file mode 100644
index 0000000..d69f959
--- /dev/null
+++ b/utils/static-ux/route-map.js
@@ -0,0 +1,81 @@
+const ROUTES = Object.freeze({
+ tabs: {
+ home: '/pages/home/index',
+ library: '/pages/library/index',
+ ai: '/pages/ai/index',
+ profile: '/pages/profile/index',
+ login: '/pages/login/index'
+ },
+ tcm: {
+ aiHistory: '/packages/tcm/pages/ai-history/index',
+ assets: '/packages/tcm/pages/assets/index',
+ bianzheng: '/packages/tcm/pages/bianzheng/index',
+ bookDetail: '/packages/tcm/pages/book-detail/index',
+ searchBooks: '/packages/tcm/pages/search-books/index',
+ section: '/packages/tcm/pages/section/index',
+ placeholder: '/packages/tcm/pages/placeholder/index'
+ },
+ mingli: {
+ hall: '/packages/mingli/pages/hall/index',
+ bazi: '/packages/mingli/pages/bazi/index',
+ bookDetail: '/packages/mingli/pages/book-detail/index',
+ searchBooks: '/packages/mingli/pages/search-books/index',
+ section: '/packages/mingli/pages/section/index',
+ interpret: '/packages/mingli/pages/interpret/index'
+ },
+ learning: {
+ center: '/packages/learning/pages/center/index'
+ }
+})
+
+const TAB_PAGE_ROUTES = Object.freeze([
+ ROUTES.tabs.home,
+ ROUTES.tabs.library,
+ ROUTES.tabs.ai,
+ ROUTES.tabs.profile
+])
+
+function normalizeRoute(route) {
+ if (typeof route !== 'string') {
+ return ''
+ }
+
+ return route.split('?')[0]
+}
+
+function isTabRoute(route) {
+ return TAB_PAGE_ROUTES.includes(normalizeRoute(route))
+}
+
+function openStaticRoute(route, wxApi) {
+ if (!route || !wxApi) {
+ return false
+ }
+
+ if (isTabRoute(route)) {
+ if (typeof wxApi.switchTab !== 'function') {
+ return false
+ }
+
+ wxApi.switchTab({
+ url: normalizeRoute(route)
+ })
+ return true
+ }
+
+ if (typeof wxApi.navigateTo !== 'function') {
+ return false
+ }
+
+ wxApi.navigateTo({
+ url: route
+ })
+ return true
+}
+
+module.exports = {
+ ROUTES,
+ TAB_PAGE_ROUTES,
+ isTabRoute,
+ openStaticRoute
+}
diff --git a/utils/static-ux/shared.js b/utils/static-ux/shared.js
new file mode 100644
index 0000000..956c833
--- /dev/null
+++ b/utils/static-ux/shared.js
@@ -0,0 +1,22 @@
+function cloneItem(item) {
+ return { ...item }
+}
+
+function cloneList(items) {
+ return items.map(cloneItem)
+}
+
+function resolveScene(rawScene, allowedScenes, fallbackScene) {
+ return allowedScenes.includes(rawScene) ? rawScene : fallbackScene
+}
+
+function resolveKind(rawKind, allowedKinds, fallbackKind) {
+ return allowedKinds.includes(rawKind) ? rawKind : fallbackKind
+}
+
+module.exports = {
+ cloneItem,
+ cloneList,
+ resolveScene,
+ resolveKind
+}
diff --git a/utils/static-ux/tcm.js b/utils/static-ux/tcm.js
new file mode 100644
index 0000000..32c465c
--- /dev/null
+++ b/utils/static-ux/tcm.js
@@ -0,0 +1,74 @@
+const { cloneList, resolveKind } = require('./shared')
+const { ROUTES } = require('./route-map')
+
+const TCM_HOME_HUB_CARDS = Object.freeze([
+ {
+ key: 'tcm-library',
+ title: '中医馆',
+ subtitle: '典籍与目录',
+ route: ROUTES.tabs.library,
+ badge: 'TCM'
+ },
+ {
+ key: 'mingli-hall',
+ title: '易学阁',
+ subtitle: '命理与经典',
+ route: ROUTES.mingli.hall,
+ badge: 'NEW'
+ },
+ {
+ key: 'bazi',
+ title: '八字排盘',
+ subtitle: '静态排盘结果',
+ route: ROUTES.mingli.bazi,
+ badge: 'BETA'
+ },
+ {
+ key: 'learning-center',
+ title: '学习中心',
+ subtitle: '继续学习与回顾',
+ route: ROUTES.learning.center,
+ badge: 'GO'
+ }
+])
+
+const TCM_ASSET_SURFACES = Object.freeze({
+ notes: {
+ title: '学习资产',
+ activeKind: 'notes',
+ kinds: ['notes', 'bookshelf', 'favorites', 'history']
+ },
+ bookshelf: {
+ title: '学习资产',
+ activeKind: 'bookshelf',
+ kinds: ['notes', 'bookshelf', 'favorites', 'history']
+ },
+ favorites: {
+ title: '学习资产',
+ activeKind: 'favorites',
+ kinds: ['notes', 'bookshelf', 'favorites', 'history']
+ },
+ history: {
+ title: '学习资产',
+ activeKind: 'history',
+ kinds: ['notes', 'bookshelf', 'favorites', 'history']
+ }
+})
+
+function createTcmHomeHubCards() {
+ return cloneList(TCM_HOME_HUB_CARDS)
+}
+
+function createTcmAssetPageData(rawKind) {
+ const activeKind = resolveKind(rawKind, ['notes', 'bookshelf', 'favorites', 'history'], 'notes')
+
+ return {
+ ...TCM_ASSET_SURFACES[activeKind],
+ cards: createTcmHomeHubCards()
+ }
+}
+
+module.exports = {
+ createTcmHomeHubCards,
+ createTcmAssetPageData
+}