diff --git a/app.js b/app.js
index 555e979..e3523cd 100644
--- a/app.js
+++ b/app.js
@@ -1,6 +1,6 @@
const { getRuntimeConfig } = require('./config/env')
-const { setUnauthorizedHandler } = require('./services/request')
-const { sessionStore } = require('./stores')
+const { setUnauthorizedHandler } = require('./services/request/index')
+const { sessionStore } = require('./stores/index')
App({
globalData: {
diff --git a/app.json b/app.json
index 83cdbc6..2f5ef5d 100644
--- a/app.json
+++ b/app.json
@@ -1,6 +1,9 @@
{
"pages": [
"pages/home/index",
+ "pages/library/index",
+ "pages/ai/index",
+ "pages/profile/index",
"pages/login/index"
],
"subPackages": [
@@ -9,6 +12,35 @@
"pages": [
"pages/workbench/index"
]
+ },
+ {
+ "root": "packages/tcm",
+ "pages": [
+ "pages/ai-history/index",
+ "pages/assets/index",
+ "pages/bianzheng/index",
+ "pages/book-detail/index",
+ "pages/search-books/index",
+ "pages/section/index",
+ "pages/placeholder/index"
+ ]
+ },
+ {
+ "root": "packages/mingli",
+ "pages": [
+ "pages/hall/index",
+ "pages/bazi/index",
+ "pages/book-detail/index",
+ "pages/search-books/index",
+ "pages/section/index",
+ "pages/interpret/index"
+ ]
+ },
+ {
+ "root": "packages/learning",
+ "pages": [
+ "pages/center/index"
+ ]
}
],
"preloadRule": {
@@ -25,6 +57,38 @@
"navigationBarBackgroundColor": "#f5f7fa",
"backgroundColor": "#f5f7fa"
},
+ "tabBar": {
+ "color": "#9f7c56",
+ "selectedColor": "#8f5c1f",
+ "backgroundColor": "#fff8ee",
+ "borderStyle": "white",
+ "list": [
+ {
+ "pagePath": "pages/home/index",
+ "text": "首页",
+ "iconPath": "pages/home/tab-home.png",
+ "selectedIconPath": "pages/home/tab-home-active.png"
+ },
+ {
+ "pagePath": "pages/library/index",
+ "text": "典籍",
+ "iconPath": "pages/library/tab-library.png",
+ "selectedIconPath": "pages/library/tab-library-active.png"
+ },
+ {
+ "pagePath": "pages/ai/index",
+ "text": "AI",
+ "iconPath": "pages/ai/tab-ai.png",
+ "selectedIconPath": "pages/ai/tab-ai-active.png"
+ },
+ {
+ "pagePath": "pages/profile/index",
+ "text": "我的",
+ "iconPath": "pages/profile/tab-profile.png",
+ "selectedIconPath": "pages/profile/tab-profile-active.png"
+ }
+ ]
+ },
"networkTimeout": {
"request": 10000
},
diff --git a/app.wxss b/app.wxss
index 03299cd..defb07c 100644
--- a/app.wxss
+++ b/app.wxss
@@ -1,4 +1,4 @@
-@import 'tdesign-miniprogram/common/style/index.wxss';
+@import './miniprogram_npm/tdesign-miniprogram/common/style/index.wxss';
page {
min-height: 100%;
@@ -45,3 +45,19 @@ text {
justify-content: space-between;
gap: 24rpx;
}
+
+.xz-page--warm {
+ min-height: 100vh;
+ background: linear-gradient(180deg, #f8edd6 0%, #f9f0de 24%, #f6ead4 100%);
+}
+
+.xz-page--mingli {
+ min-height: 100vh;
+ background: linear-gradient(180deg, #fbf6f3 0%, #f4ebe6 100%);
+}
+
+.xz-card {
+ border-radius: 28rpx;
+ border: 1rpx solid rgba(84, 58, 29, 0.08);
+ box-shadow: 0 16rpx 36rpx rgba(84, 58, 29, 0.06);
+}
diff --git a/packages/demo/pages/workbench/index.js b/packages/demo/pages/workbench/index.js
index 6f418cf..44aed68 100644
--- a/packages/demo/pages/workbench/index.js
+++ b/packages/demo/pages/workbench/index.js
@@ -1,4 +1,5 @@
-const { sessionStore } = require('../../../../stores')
+const { sessionStore } = require('../../../../stores/index')
+const { openStaticRoute } = require('../../../../utils/static-ux/route-map')
const MODULES = [
{
@@ -41,8 +42,6 @@ Page({
return
}
- wx.navigateTo({
- url: path
- })
+ openStaticRoute(path, wx)
}
})
diff --git a/packages/learning/pages/center/index.js b/packages/learning/pages/center/index.js
new file mode 100644
index 0000000..6f66f37
--- /dev/null
+++ b/packages/learning/pages/center/index.js
@@ -0,0 +1,74 @@
+const {
+ createLearningCenterPageData: createBaseLearningCenterPageData
+} = require('../../../../utils/static-ux/learning')
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+function createLearningCenterPageData() {
+ const baseData = createBaseLearningCenterPageData()
+
+ return {
+ ...baseData,
+ heroTitle: '先继续上次学习,再回看静态记录',
+ heroDescription: '学习中心不再承接旧后台列表,而是把最值得继续的学习动作放到最上面。',
+ quickDeck: [
+ {
+ key: 'qa-history',
+ title: 'AI历史',
+ description: '继续查看中医问答与辨证分析的静态记录页',
+ route: ROUTES.tcm.aiHistory,
+ domain: 'tcm'
+ },
+ {
+ key: 'assets',
+ title: '学习资产',
+ description: '回到笔记、书架、收藏和历史四类资产入口',
+ route: `${ROUTES.tcm.assets}?kind=notes`,
+ domain: 'tcm'
+ },
+ {
+ key: 'interpret',
+ title: '命理解读',
+ description: '进入易学侧的静态问题解读页',
+ route: `${ROUTES.mingli.interpret}?scene=result`,
+ domain: 'mingli'
+ },
+ {
+ key: 'bazi',
+ title: '八字排盘',
+ description: '继续查看静态排盘结果与四柱结构',
+ route: `${ROUTES.mingli.bazi}?scene=result`,
+ domain: 'mingli'
+ }
+ ],
+ recentItems: [
+ {
+ key: 'recent-reading',
+ title: '典籍阅读',
+ description: '回到《黄帝内经素问》的静态阅读页。',
+ route: `${ROUTES.tcm.section}?scene=reader-a`
+ },
+ {
+ key: 'recent-yixue',
+ title: '易学阅读',
+ description: '继续浏览《滴天髓》的静态阅读页。',
+ route: `${ROUTES.mingli.section}?scene=reader-a`
+ }
+ ]
+ }
+}
+
+function showNavigate(route) {
+ openStaticRoute(route, wx)
+}
+
+Page({
+ data: createLearningCenterPageData(),
+
+ handleRouteTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ }
+})
+
+module.exports = {
+ createLearningCenterPageData
+}
diff --git a/packages/learning/pages/center/index.json b/packages/learning/pages/center/index.json
new file mode 100644
index 0000000..ce57338
--- /dev/null
+++ b/packages/learning/pages/center/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "学习中心",
+ "navigationBarBackgroundColor": "#f7f0e6",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/learning/pages/center/index.wxml b/packages/learning/pages/center/index.wxml
new file mode 100644
index 0000000..f2457a1
--- /dev/null
+++ b/packages/learning/pages/center/index.wxml
@@ -0,0 +1,44 @@
+
+
+ {{heroTitle}}
+ {{heroDescription}}
+
+
+ {{item.value}}
+ {{item.label}}
+
+
+
+
+
+
+ {{item.domain === 'tcm' ? '中医继续学习' : '易学继续学习'}}
+ {{item.title}}
+ {{item.description}}
+
+
+
+
+ 最近学习动作
+
+ 读
+
+ {{item.title}}
+ {{item.description}}
+
+ >
+
+
+
diff --git a/packages/learning/pages/center/index.wxss b/packages/learning/pages/center/index.wxss
new file mode 100644
index 0000000..bd0ff83
--- /dev/null
+++ b/packages/learning/pages/center/index.wxss
@@ -0,0 +1,150 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #f7f2ea 0%, #efe7dc 100%);
+}
+
+.learning-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 72rpx;
+}
+
+.hero-card,
+.section-card,
+.quick-card {
+ border: 1rpx solid rgba(84, 58, 29, 0.08);
+ border-radius: 28rpx;
+ background: rgba(255, 251, 244, 0.92);
+ box-shadow: 0 10rpx 26rpx rgba(78, 50, 22, 0.06);
+}
+
+.hero-card {
+ padding: 24rpx;
+}
+
+.hero-card__title,
+.section-card__title,
+.quick-card__title,
+.timeline-item__title {
+ display: block;
+ color: #2f261d;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+.hero-card__desc,
+.quick-card__desc,
+.timeline-item__desc {
+ display: block;
+ margin-top: 10rpx;
+ color: #7b6d60;
+ font-size: 24rpx;
+ line-height: 1.6;
+}
+
+.stats-row {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 14rpx -5rpx 0;
+}
+
+.stat-pill {
+ box-sizing: border-box;
+ width: 25%;
+ padding: 5rpx;
+}
+
+.stat-pill__value,
+.stat-pill__label {
+ display: block;
+ text-align: center;
+}
+
+.stat-pill__value {
+ padding: 12rpx 0 6rpx;
+ border-radius: 18rpx 18rpx 0 0;
+ background: rgba(255, 250, 242, 0.8);
+ color: #2f261d;
+ font-size: 28rpx;
+ font-weight: 700;
+}
+
+.stat-pill__label {
+ padding: 0 0 12rpx;
+ border-radius: 0 0 18rpx 18rpx;
+ background: rgba(255, 250, 242, 0.8);
+ color: #7b6d60;
+ font-size: 22rpx;
+}
+
+.quick-grid {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 12rpx -6rpx 0;
+}
+
+.quick-card {
+ box-sizing: border-box;
+ width: 50%;
+ margin-top: 12rpx;
+ padding: 24rpx 18rpx;
+ margin-left: 6rpx;
+ margin-right: 6rpx;
+}
+
+.quick-card__tag {
+ display: inline-block;
+ padding: 8rpx 12rpx;
+ border-radius: 999rpx;
+ background: rgba(84, 58, 29, 0.08);
+ color: #6d4d2f;
+ font-size: 18rpx;
+ line-height: 1;
+}
+
+.quick-card__title {
+ margin-top: 14rpx;
+}
+
+.section-card {
+ margin-top: 18rpx;
+ padding: 24rpx;
+}
+
+.timeline-item {
+ display: flex;
+ align-items: center;
+ margin-top: 16rpx;
+ padding-top: 16rpx;
+ border-top: 1rpx solid rgba(84, 58, 29, 0.08);
+}
+
+.timeline-item:first-of-type {
+ margin-top: 10rpx;
+ padding-top: 0;
+ border-top: 0;
+}
+
+.timeline-item__icon {
+ width: 72rpx;
+ height: 72rpx;
+ border-radius: 18rpx;
+ background: rgba(84, 58, 29, 0.08);
+ color: #6d4d2f;
+ font-size: 26rpx;
+ line-height: 72rpx;
+ text-align: center;
+}
+
+.timeline-item__body {
+ flex: 1;
+ margin-left: 14rpx;
+}
+
+.timeline-item__arrow {
+ color: #b4946a;
+ font-size: 28rpx;
+ line-height: 1;
+}
diff --git a/packages/mingli/pages/bazi/index.js b/packages/mingli/pages/bazi/index.js
new file mode 100644
index 0000000..ab813ad
--- /dev/null
+++ b/packages/mingli/pages/bazi/index.js
@@ -0,0 +1,52 @@
+const { resolveScene } = require('../../../../utils/static-ux/shared')
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+function createMingliBaziPageData(rawScene) {
+ const scene = resolveScene(rawScene, ['default', 'result'], 'default')
+
+ return {
+ title: '八字排盘',
+ scene,
+ form: {
+ name: '张三',
+ gender: '男',
+ birthDate: '1990-01-01',
+ birthTime: '08:30'
+ },
+ result:
+ scene === 'result'
+ ? {
+ headline: '学习型排盘结果',
+ subline: '以静态四柱结果承接旧页面的结果层级,不迁移旧排盘引擎。',
+ pillars: [
+ { key: 'year', label: '年柱', value: '庚午' },
+ { key: 'month', label: '月柱', value: '戊寅' },
+ { key: 'day', label: '日柱', value: '甲辰' },
+ { key: 'time', label: '时柱', value: '丁卯' }
+ ]
+ }
+ : null,
+ primaryActionText: scene === 'result' ? '重新排盘' : '开始排盘',
+ secondaryActionText: '命理解读'
+ }
+}
+
+Page({
+ data: createMingliBaziPageData('default'),
+
+ onLoad(options) {
+ this.setData(createMingliBaziPageData(options.scene))
+ },
+
+ handlePrimaryTap() {
+ this.setData(createMingliBaziPageData(this.data.scene === 'result' ? 'default' : 'result'))
+ },
+
+ handleSecondaryTap() {
+ openStaticRoute(`${ROUTES.mingli.interpret}?scene=result`, wx)
+ }
+})
+
+module.exports = {
+ createMingliBaziPageData
+}
diff --git a/packages/mingli/pages/bazi/index.json b/packages/mingli/pages/bazi/index.json
new file mode 100644
index 0000000..4d7c784
--- /dev/null
+++ b/packages/mingli/pages/bazi/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "八字排盘",
+ "navigationBarBackgroundColor": "#f8f0ee",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/mingli/pages/bazi/index.wxml b/packages/mingli/pages/bazi/index.wxml
new file mode 100644
index 0000000..ac54c2f
--- /dev/null
+++ b/packages/mingli/pages/bazi/index.wxml
@@ -0,0 +1,63 @@
+
+
+ 命理练习台
+ {{title}}
+
+ 排盘页当前只保留输入层级、四柱结果和跳转关系,并把阅读节奏整体放大。
+
+
+ 静态录入
+ 四柱结果
+ 命理解读
+
+
+
+
+
+
+
+ 姓名
+ {{form.name}}
+
+
+ 性别
+ {{form.gender}}
+
+
+
+
+ 出生日期
+ {{form.birthDate}}
+
+
+ 出生时间
+ {{form.birthTime}}
+
+
+
+ {{primaryActionText}}
+
+ {{secondaryActionText}}
+
+
+
+
+
+
+
+
+
+ {{item.label}}
+ {{item.value}}
+
+
+
+ 结果区保留静态展示,可继续进入命理解读页查看后续层级。
+
+
diff --git a/packages/mingli/pages/bazi/index.wxss b/packages/mingli/pages/bazi/index.wxss
new file mode 100644
index 0000000..c4bb0fe
--- /dev/null
+++ b/packages/mingli/pages/bazi/index.wxss
@@ -0,0 +1,194 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #f9f7f5 0%, #f3ebe8 100%);
+}
+
+.bazi-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 36rpx 24rpx 96rpx;
+}
+
+.hero-card,
+.form-card,
+.result-card {
+ margin-top: 22rpx;
+ padding: 34rpx 28rpx;
+ border: 1rpx solid rgba(139, 59, 49, 0.08);
+ border-radius: 32rpx;
+ background: rgba(255, 252, 248, 0.94);
+ box-shadow: 0 14rpx 34rpx rgba(139, 59, 49, 0.08);
+}
+
+.hero-card {
+ margin-top: 0;
+}
+
+.hero-card__eyebrow {
+ display: block;
+ color: #9f594e;
+ font-size: 22rpx;
+ font-weight: 600;
+ letter-spacing: 6rpx;
+ line-height: 1.4;
+}
+
+.hero-card__title {
+ display: block;
+ margin-top: 14rpx;
+ color: #2c2621;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 46rpx;
+ font-weight: 700;
+ line-height: 1.28;
+}
+
+.hero-card__desc {
+ display: block;
+ margin-top: 18rpx;
+ color: #7a6f64;
+ font-size: 28rpx;
+ line-height: 1.8;
+}
+
+.hero-card__meta {
+ margin: 20rpx -6rpx 0;
+ font-size: 0;
+}
+
+.hero-card__meta-item {
+ display: inline-block;
+ margin: 10rpx 6rpx 0;
+ padding: 10rpx 18rpx;
+ border-radius: 999rpx;
+ background: rgba(139, 59, 49, 0.08);
+ color: #8b3b31;
+ font-size: 22rpx;
+ line-height: 1.4;
+}
+
+.panel-header {
+ margin-bottom: 4rpx;
+}
+
+.panel-header__title {
+ display: block;
+ color: #2c2621;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1.35;
+}
+
+.panel-header__desc,
+.result-card__hint {
+ display: block;
+ margin-top: 12rpx;
+ color: #7a6f64;
+ font-size: 24rpx;
+ line-height: 1.7;
+}
+
+.panel-header--result {
+ margin-bottom: 8rpx;
+}
+
+.field-row {
+ display: flex;
+ margin: 0 -8rpx;
+}
+
+.field-block {
+ box-sizing: border-box;
+ width: 50%;
+ padding: 0 8rpx;
+ margin-top: 20rpx;
+}
+
+.field-block__label {
+ display: block;
+ color: #8b5a3c;
+ font-size: 24rpx;
+ line-height: 1.5;
+}
+
+.field-block__value {
+ box-sizing: border-box;
+ width: 100%;
+ min-height: 94rpx;
+ margin-top: 12rpx;
+ padding: 24rpx 22rpx;
+ border: 1rpx solid rgba(139, 90, 60, 0.1);
+ border-radius: 24rpx;
+ background: linear-gradient(180deg, #fffdfa 0%, #f7f1ec 100%);
+ color: #5a4335;
+ font-size: 30rpx;
+ font-weight: 600;
+ line-height: 1.4;
+}
+
+.action-group {
+ margin-top: 26rpx;
+}
+
+.action-button {
+ margin-top: 18rpx;
+ padding: 28rpx 24rpx;
+ border-radius: 24rpx;
+ background: linear-gradient(135deg, #a64e44 0%, #7a342b 100%);
+ color: #fff;
+ font-size: 30rpx;
+ font-weight: 600;
+ line-height: 1.2;
+ text-align: center;
+}
+
+.action-button--ghost {
+ background: rgba(139, 59, 49, 0.08);
+ color: #8b3b31;
+}
+
+.result-card__grid {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 18rpx -8rpx 0;
+}
+
+.pillar-card {
+ box-sizing: border-box;
+ width: 50%;
+ padding: 0 8rpx;
+ margin-top: 16rpx;
+}
+
+.pillar-card__inner {
+ min-height: 156rpx;
+ padding: 24rpx 18rpx;
+ border-radius: 24rpx;
+ background: linear-gradient(180deg, #fff7f3 0%, #fff 100%);
+ border: 1rpx solid rgba(139, 59, 49, 0.08);
+ box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.7);
+}
+
+.pillar-card__label {
+ display: block;
+ color: #8b3b31;
+ font-size: 24rpx;
+ line-height: 1.4;
+ text-align: center;
+}
+
+.pillar-card__value {
+ display: block;
+ margin-top: 16rpx;
+ color: #5d312a;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 40rpx;
+ font-weight: 700;
+ line-height: 1.3;
+ text-align: center;
+}
+
+.result-card__hint {
+ margin-top: 24rpx;
+}
diff --git a/packages/mingli/pages/book-detail/index.js b/packages/mingli/pages/book-detail/index.js
new file mode 100644
index 0000000..daa58c6
--- /dev/null
+++ b/packages/mingli/pages/book-detail/index.js
@@ -0,0 +1,63 @@
+const { resolveScene } = require('../../../../utils/static-ux/shared')
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+const BOOK_SURFACES = Object.freeze({
+ 'classic-a': {
+ coverText: '滴',
+ title: '滴天髓',
+ subtitle: '命理经典研习入口',
+ route: `${ROUTES.mingli.section}?scene=reader-a`,
+ catalog: ['总论', '天干地支', '格局取法'],
+ recommends: [{ key: 'classic-b', title: '穷通宝鉴', route: `${ROUTES.mingli.bookDetail}?scene=classic-b` }]
+ },
+ 'classic-b': {
+ coverText: '穷',
+ title: '穷通宝鉴',
+ subtitle: '格局与用神的静态阅读入口',
+ route: `${ROUTES.mingli.section}?scene=reader-b`,
+ catalog: ['四时旺衰', '五行喜忌', '命局评析'],
+ recommends: [{ key: 'classic-a', title: '滴天髓', route: `${ROUTES.mingli.bookDetail}?scene=classic-a` }]
+ }
+})
+
+function createMingliBookDetailPageData(rawScene) {
+ const scene = resolveScene(rawScene, ['classic-a', 'classic-b'], 'classic-a')
+ const surface = BOOK_SURFACES[scene]
+
+ return {
+ title: '易学典籍详情',
+ scene,
+ coverText: surface.coverText,
+ book: {
+ title: surface.title,
+ subtitle: surface.subtitle
+ },
+ catalog: [...surface.catalog],
+ primaryRoute: surface.route,
+ recommends: surface.recommends.map(item => ({ ...item }))
+ }
+}
+
+function showNavigate(route) {
+ openStaticRoute(route, wx)
+}
+
+Page({
+ data: createMingliBookDetailPageData('classic-a'),
+
+ onLoad(options) {
+ this.setData(createMingliBookDetailPageData(options.scene))
+ },
+
+ handlePrimaryTap() {
+ showNavigate(this.data.primaryRoute)
+ },
+
+ handleRecommendTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ }
+})
+
+module.exports = {
+ createMingliBookDetailPageData
+}
diff --git a/packages/mingli/pages/book-detail/index.json b/packages/mingli/pages/book-detail/index.json
new file mode 100644
index 0000000..be9b2a2
--- /dev/null
+++ b/packages/mingli/pages/book-detail/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "易学典籍详情",
+ "navigationBarBackgroundColor": "#f8f0ee",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/mingli/pages/book-detail/index.wxml b/packages/mingli/pages/book-detail/index.wxml
new file mode 100644
index 0000000..3243577
--- /dev/null
+++ b/packages/mingli/pages/book-detail/index.wxml
@@ -0,0 +1,28 @@
+
+
+ {{coverText}}
+
+ {{book.title}}
+ {{book.subtitle}}
+ 进入阅读
+
+
+
+
+ 目录预览
+ {{item}}
+
+
+
+ 相关推荐
+
+ {{item.title}}
+
+
+
diff --git a/packages/mingli/pages/book-detail/index.wxss b/packages/mingli/pages/book-detail/index.wxss
new file mode 100644
index 0000000..d5b4ea0
--- /dev/null
+++ b/packages/mingli/pages/book-detail/index.wxss
@@ -0,0 +1,75 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #f9f7f5 0%, #f3ebe8 100%);
+}
+
+.mingli-book-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 72rpx;
+}
+
+.hero-card,
+.section-card {
+ margin-top: 18rpx;
+ padding: 24rpx;
+ border: 1rpx solid rgba(139, 59, 49, 0.08);
+ border-radius: 28rpx;
+ background: rgba(255, 252, 248, 0.94);
+ box-shadow: 0 10rpx 26rpx rgba(139, 59, 49, 0.06);
+}
+
+.hero-card {
+ display: flex;
+ align-items: flex-start;
+ margin-top: 0;
+}
+
+.hero-card__cover {
+ width: 110rpx;
+ height: 150rpx;
+ border-radius: 22rpx;
+ background: rgba(139, 59, 49, 0.12);
+ color: #8b3b31;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 38rpx;
+ font-weight: 700;
+ line-height: 150rpx;
+ text-align: center;
+}
+
+.hero-card__body {
+ flex: 1;
+ margin-left: 18rpx;
+}
+
+.hero-card__title,
+.section-card__title {
+ display: block;
+ color: #2c2621;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+.hero-card__subtitle,
+.section-card__item {
+ display: block;
+ margin-top: 10rpx;
+ color: #7a6f64;
+ font-size: 24rpx;
+ line-height: 1.6;
+}
+
+.hero-card__button,
+.section-card__recommend {
+ margin-top: 16rpx;
+ padding: 18rpx 22rpx;
+ border-radius: 20rpx;
+ background: rgba(139, 59, 49, 0.08);
+ color: #8b3b31;
+ font-size: 24rpx;
+ line-height: 1;
+ text-align: center;
+}
diff --git a/packages/mingli/pages/hall/index.js b/packages/mingli/pages/hall/index.js
new file mode 100644
index 0000000..c5bfab0
--- /dev/null
+++ b/packages/mingli/pages/hall/index.js
@@ -0,0 +1,81 @@
+const { createMingliHallPageData: createBaseMingliHallPageData } = require('../../../../utils/static-ux/mingli')
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+function createMingliHallPageData() {
+ const baseData = createBaseMingliHallPageData()
+
+ return {
+ ...baseData,
+ almanac: {
+ date: '乙巳年 · 宜研习 · 忌浮躁',
+ ganzhi: '壬寅日 · 辰时',
+ yi: '宜:开卷、推演 · 忌:断章',
+ icon: '盘'
+ },
+ wisdomCard: {
+ text: '知进退存亡,而不失其正者,其唯圣人乎。',
+ from: '《周易》'
+ },
+ hotTopics: [
+ {
+ key: 'topic-drops',
+ label: '滴天髓',
+ route: `${ROUTES.mingli.searchBooks}?keyword=滴天髓`
+ },
+ {
+ key: 'topic-bazi',
+ label: '八字',
+ route: ROUTES.mingli.bazi
+ },
+ {
+ key: 'topic-interpret',
+ label: '命理解读',
+ route: ROUTES.mingli.interpret
+ }
+ ],
+ guideCards: [
+ {
+ key: 'guide-1',
+ title: '从排盘到解读',
+ description: '先完成静态排盘,再进入命理解读页承接结果结构。',
+ route: ROUTES.mingli.bazi
+ },
+ {
+ key: 'guide-2',
+ title: '从经典到术语',
+ description: '从易学典籍页进入静态阅读,再回到问题解释。',
+ route: `${ROUTES.mingli.bookDetail}?scene=classic-a`
+ }
+ ],
+ recommendedBooks: [
+ {
+ key: 'book-a',
+ title: '滴天髓',
+ subtitle: '命理经典研习入口',
+ route: `${ROUTES.mingli.bookDetail}?scene=classic-a`
+ },
+ {
+ key: 'book-b',
+ title: '穷通宝鉴',
+ subtitle: '格局与用神的静态学习入口',
+ route: `${ROUTES.mingli.bookDetail}?scene=classic-b`
+ }
+ ]
+ }
+}
+
+function showNavigate(route) {
+ openStaticRoute(route, wx)
+}
+
+Page({
+ data: createMingliHallPageData(),
+
+ handleRouteTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ }
+})
+
+module.exports = {
+ createMingliHallPageData
+}
diff --git a/packages/mingli/pages/hall/index.json b/packages/mingli/pages/hall/index.json
new file mode 100644
index 0000000..cdb8629
--- /dev/null
+++ b/packages/mingli/pages/hall/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "易学阁",
+ "navigationBarBackgroundColor": "#f8f0ee",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/mingli/pages/hall/index.wxml b/packages/mingli/pages/hall/index.wxml
new file mode 100644
index 0000000..20fef1b
--- /dev/null
+++ b/packages/mingli/pages/hall/index.wxml
@@ -0,0 +1,76 @@
+
+
+
+ {{almanac.date}}
+ {{almanac.ganzhi}}
+ {{almanac.yi}}
+
+ {{almanac.icon}}
+
+
+
+
+ {{item.icon}}
+ {{item.title}}
+ {{item.subtitle}}
+
+
+
+
+ 每日一言
+ {{wisdomCard.text}}
+ —— {{wisdomCard.from}}
+
+
+
+ 搜索热词
+
+
+ {{item.label}}
+
+
+
+
+
+ 导读推荐
+
+ {{item.title}}
+ {{item.description}}
+
+
+
+
+ 经典研习
+
+ 书
+
+ {{item.title}}
+ {{item.subtitle}}
+
+
+
+
diff --git a/packages/mingli/pages/hall/index.wxss b/packages/mingli/pages/hall/index.wxss
new file mode 100644
index 0000000..18bec5f
--- /dev/null
+++ b/packages/mingli/pages/hall/index.wxss
@@ -0,0 +1,166 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #f9f7f5 0%, #f3ebe8 100%);
+}
+
+.hall-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 72rpx;
+}
+
+.almanac-card,
+.section-card,
+.hall-card {
+ border: 1rpx solid rgba(139, 59, 49, 0.08);
+ border-radius: 28rpx;
+ background: rgba(255, 252, 248, 0.94);
+ box-shadow: 0 10rpx 26rpx rgba(139, 59, 49, 0.06);
+}
+
+.almanac-card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 24rpx;
+}
+
+.almanac-card__date,
+.almanac-card__yi,
+.section-card__from {
+ display: block;
+ color: #7a6f64;
+ font-size: 22rpx;
+ line-height: 1.6;
+}
+
+.almanac-card__ganzhi {
+ display: block;
+ margin: 8rpx 0;
+ color: #8b3b31;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+.almanac-card__icon {
+ color: #8b3b31;
+ font-size: 46rpx;
+}
+
+.hall-grid {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 12rpx -6rpx 0;
+}
+
+.hall-card {
+ box-sizing: border-box;
+ width: 50%;
+ margin-top: 12rpx;
+ padding: 24rpx 18rpx;
+ background: #fff;
+ border-radius: 24rpx;
+ margin-left: 6rpx;
+ margin-right: 6rpx;
+}
+
+.hall-card__icon {
+ width: 82rpx;
+ height: 82rpx;
+ border-radius: 20rpx;
+ background: rgba(139, 59, 49, 0.08);
+ color: #8b3b31;
+ font-size: 34rpx;
+ line-height: 82rpx;
+ text-align: center;
+}
+
+.hall-card__title,
+.section-card__title,
+.guide-card__title,
+.book-row__title {
+ display: block;
+ color: #2c2621;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 32rpx;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+.hall-card__title {
+ margin-top: 16rpx;
+}
+
+.hall-card__subtitle,
+.guide-card__desc,
+.book-row__subtitle,
+.section-card__text {
+ display: block;
+ margin-top: 8rpx;
+ color: #7a6f64;
+ font-size: 24rpx;
+ line-height: 1.6;
+}
+
+.section-card {
+ margin-top: 18rpx;
+ padding: 24rpx;
+}
+
+.section-card__text {
+ margin-top: 14rpx;
+ font-size: 30rpx;
+ line-height: 1.8;
+ color: #352f29;
+}
+
+.chips-row {
+ margin: 14rpx -6rpx 0;
+}
+
+.chips-row__item {
+ display: inline-block;
+ margin: 6rpx;
+ padding: 12rpx 18rpx;
+ border-radius: 999rpx;
+ background: rgba(139, 59, 49, 0.08);
+ color: #8b3b31;
+ font-size: 22rpx;
+ line-height: 1;
+}
+
+.guide-card,
+.book-row {
+ margin-top: 16rpx;
+ padding: 20rpx 0 0;
+ border-top: 1rpx solid rgba(139, 59, 49, 0.08);
+}
+
+.guide-card:first-of-type,
+.book-row:first-of-type {
+ padding-top: 16rpx;
+ border-top: 0;
+}
+
+.book-row {
+ display: flex;
+ align-items: center;
+}
+
+.book-row__cover {
+ width: 72rpx;
+ height: 96rpx;
+ border-radius: 18rpx;
+ background: rgba(139, 59, 49, 0.1);
+ color: #8b3b31;
+ font-size: 28rpx;
+ line-height: 96rpx;
+ text-align: center;
+}
+
+.book-row__body {
+ flex: 1;
+ margin-left: 16rpx;
+}
diff --git a/packages/mingli/pages/interpret/index.js b/packages/mingli/pages/interpret/index.js
new file mode 100644
index 0000000..2b685eb
--- /dev/null
+++ b/packages/mingli/pages/interpret/index.js
@@ -0,0 +1,42 @@
+const { resolveScene } = require('../../../../utils/static-ux/shared')
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+function createMingliInterpretPageData(rawScene) {
+ const scene = resolveScene(rawScene, ['default', 'result'], 'default')
+
+ return {
+ title: '命理解读',
+ scene,
+ contextText: '围绕命理问题或盘面结果,保留一个学习型解释区块。',
+ result:
+ scene === 'result'
+ ? {
+ title: '学习型命理解读',
+ summary: '当前静态结果围绕“甲木日主”的基本气象来展示结果版式和引用层级。',
+ references: ['《滴天髓》重视日主气势', '先看时令,再看格局与用神']
+ }
+ : null,
+ primaryActionText: scene === 'result' ? '重新解读' : '提交解读',
+ secondaryActionText: '查看排盘'
+ }
+}
+
+Page({
+ data: createMingliInterpretPageData('default'),
+
+ onLoad(options) {
+ this.setData(createMingliInterpretPageData(options.scene))
+ },
+
+ handlePrimaryTap() {
+ this.setData(createMingliInterpretPageData(this.data.scene === 'result' ? 'default' : 'result'))
+ },
+
+ handleSecondaryTap() {
+ openStaticRoute(`${ROUTES.mingli.bazi}?scene=result`, wx)
+ }
+})
+
+module.exports = {
+ createMingliInterpretPageData
+}
diff --git a/packages/mingli/pages/interpret/index.json b/packages/mingli/pages/interpret/index.json
new file mode 100644
index 0000000..daf5567
--- /dev/null
+++ b/packages/mingli/pages/interpret/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "命理解读",
+ "navigationBarBackgroundColor": "#f8f0ee",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/mingli/pages/interpret/index.wxml b/packages/mingli/pages/interpret/index.wxml
new file mode 100644
index 0000000..5e7c75d
--- /dev/null
+++ b/packages/mingli/pages/interpret/index.wxml
@@ -0,0 +1,24 @@
+
+
+ {{title}}
+ {{contextText}}
+
+
+
+
+
+ {{primaryActionText}}
+
+ {{secondaryActionText}}
+
+
+
+
+ {{result.title}}
+ {{result.summary}}
+
+ ·
+ {{item}}
+
+
+
diff --git a/packages/mingli/pages/interpret/index.wxss b/packages/mingli/pages/interpret/index.wxss
new file mode 100644
index 0000000..62d7795
--- /dev/null
+++ b/packages/mingli/pages/interpret/index.wxss
@@ -0,0 +1,91 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #f9f7f5 0%, #f3ebe8 100%);
+}
+
+.interpret-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 72rpx;
+}
+
+.hero-card,
+.form-card,
+.result-card {
+ margin-top: 18rpx;
+ padding: 24rpx;
+ border: 1rpx solid rgba(139, 59, 49, 0.08);
+ border-radius: 28rpx;
+ background: rgba(255, 252, 248, 0.94);
+ box-shadow: 0 10rpx 26rpx rgba(139, 59, 49, 0.06);
+}
+
+.hero-card {
+ margin-top: 0;
+}
+
+.hero-card__title,
+.result-card__title {
+ display: block;
+ color: #2c2621;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 36rpx;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+.hero-card__desc,
+.result-card__desc,
+.reference-item__text {
+ display: block;
+ margin-top: 10rpx;
+ color: #7a6f64;
+ font-size: 24rpx;
+ line-height: 1.7;
+}
+
+.form-card__textarea,
+.form-card__input {
+ box-sizing: border-box;
+ width: 100%;
+ padding: 18rpx 20rpx;
+ border: 1rpx solid rgba(139, 90, 60, 0.12);
+ border-radius: 18rpx;
+ background: #fff;
+ color: #5a4335;
+ font-size: 24rpx;
+ line-height: 1.6;
+}
+
+.form-card__input {
+ margin-top: 12rpx;
+}
+
+.action-button {
+ margin-top: 16rpx;
+ padding: 22rpx 24rpx;
+ border-radius: 22rpx;
+ background: linear-gradient(135deg, #a64e44 0%, #7a342b 100%);
+ color: #fff;
+ font-size: 26rpx;
+ line-height: 1;
+ text-align: center;
+}
+
+.action-button--ghost {
+ background: rgba(139, 59, 49, 0.08);
+ color: #8b3b31;
+}
+
+.reference-item {
+ display: flex;
+ align-items: flex-start;
+ margin-top: 10rpx;
+}
+
+.reference-item__dot {
+ margin-right: 10rpx;
+ color: #8b3b31;
+ font-size: 26rpx;
+ line-height: 1.6;
+}
diff --git a/packages/mingli/pages/search-books/index.js b/packages/mingli/pages/search-books/index.js
new file mode 100644
index 0000000..e9ed77e
--- /dev/null
+++ b/packages/mingli/pages/search-books/index.js
@@ -0,0 +1,61 @@
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+const SEARCH_CATALOG = Object.freeze([
+ {
+ key: 'classic-a',
+ title: '滴天髓',
+ subtitle: '命理经典结果',
+ aliases: ['滴天', '天髓'],
+ route: `${ROUTES.mingli.bookDetail}?scene=classic-a`
+ },
+ {
+ key: 'classic-b',
+ title: '穷通宝鉴',
+ subtitle: '格局与用神',
+ aliases: ['穷通', '宝鉴'],
+ route: `${ROUTES.mingli.bookDetail}?scene=classic-b`
+ }
+])
+
+function createMingliSearchBooksPageData(keyword) {
+ const normalizedKeyword = (keyword || '').trim()
+ const results = normalizedKeyword
+ ? SEARCH_CATALOG.filter(item => {
+ return (
+ item.title.includes(normalizedKeyword) ||
+ item.aliases.some(alias => alias.includes(normalizedKeyword))
+ )
+ }).map(item => ({ ...item }))
+ : []
+
+ return {
+ title: '搜索易学典籍',
+ keyword: normalizedKeyword,
+ history: ['滴天髓', '穷通宝鉴', '子平真诠'],
+ results
+ }
+}
+
+function showNavigate(route) {
+ openStaticRoute(route, wx)
+}
+
+Page({
+ data: createMingliSearchBooksPageData(''),
+
+ onLoad(options) {
+ this.setData(createMingliSearchBooksPageData(options.keyword || options.q))
+ },
+
+ handleHistoryTap(event) {
+ this.setData(createMingliSearchBooksPageData(event.currentTarget.dataset.keyword))
+ },
+
+ handleResultTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ }
+})
+
+module.exports = {
+ createMingliSearchBooksPageData
+}
diff --git a/packages/mingli/pages/search-books/index.json b/packages/mingli/pages/search-books/index.json
new file mode 100644
index 0000000..452401d
--- /dev/null
+++ b/packages/mingli/pages/search-books/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "搜索易学典籍",
+ "navigationBarBackgroundColor": "#f8f0ee",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/mingli/pages/search-books/index.wxml b/packages/mingli/pages/search-books/index.wxml
new file mode 100644
index 0000000..7a1cdff
--- /dev/null
+++ b/packages/mingli/pages/search-books/index.wxml
@@ -0,0 +1,21 @@
+
+
+ {{title}}
+
+ {{item}}
+
+
+
+
+ 搜索结果
+
+ {{item.title}} · {{item.subtitle}}
+
+
+
diff --git a/packages/mingli/pages/search-books/index.wxss b/packages/mingli/pages/search-books/index.wxss
new file mode 100644
index 0000000..73cfa8d
--- /dev/null
+++ b/packages/mingli/pages/search-books/index.wxss
@@ -0,0 +1,43 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #f9f7f5 0%, #f3ebe8 100%);
+}
+
+.mingli-search-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 72rpx;
+}
+
+.section-card {
+ margin-top: 18rpx;
+ padding: 24rpx;
+ border: 1rpx solid rgba(139, 59, 49, 0.08);
+ border-radius: 28rpx;
+ background: rgba(255, 252, 248, 0.94);
+ box-shadow: 0 10rpx 26rpx rgba(139, 59, 49, 0.06);
+}
+
+.section-card:first-child {
+ margin-top: 0;
+}
+
+.section-card__title {
+ display: block;
+ color: #2c2621;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+.section-card__item {
+ display: block;
+ margin-top: 14rpx;
+ padding: 18rpx 20rpx;
+ border-radius: 20rpx;
+ background: rgba(139, 59, 49, 0.08);
+ color: #8b3b31;
+ font-size: 24rpx;
+ line-height: 1.6;
+}
diff --git a/packages/mingli/pages/section/index.js b/packages/mingli/pages/section/index.js
new file mode 100644
index 0000000..c52a2db
--- /dev/null
+++ b/packages/mingli/pages/section/index.js
@@ -0,0 +1,57 @@
+const { resolveScene } = require('../../../../utils/static-ux/shared')
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+const READER_SURFACES = Object.freeze({
+ 'reader-a': {
+ title: '滴天髓 · 总论',
+ passages: ['欲识三元万法宗,先观帝载与神功。', '气机流转之间,先看格局成败。']
+ },
+ 'reader-b': {
+ title: '穷通宝鉴 · 四时旺衰',
+ passages: ['论命之法,首重月令。', '得时得地者旺,失时失地者衰。']
+ }
+})
+
+function createMingliSectionPageData(rawScene) {
+ const scene = resolveScene(rawScene, ['reader-a', 'reader-b'], 'reader-a')
+ const surface = READER_SURFACES[scene]
+
+ return {
+ title: '易学阅读',
+ scene,
+ chapterTitle: surface.title,
+ passages: [...surface.passages],
+ actions: [
+ {
+ key: 'interpret',
+ title: '打开命理解读',
+ route: ROUTES.mingli.interpret
+ },
+ {
+ key: 'hall',
+ title: '返回易学阁',
+ route: ROUTES.mingli.hall
+ }
+ ]
+ }
+}
+
+function showNavigate(route) {
+ openStaticRoute(route, wx)
+}
+
+Page({
+ data: createMingliSectionPageData('reader-a'),
+
+ onLoad(options) {
+ this.setData(createMingliSectionPageData(options.scene))
+ },
+
+ handleActionTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ }
+})
+
+module.exports = {
+ createMingliSectionPageData
+}
diff --git a/packages/mingli/pages/section/index.json b/packages/mingli/pages/section/index.json
new file mode 100644
index 0000000..03b5552
--- /dev/null
+++ b/packages/mingli/pages/section/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "易学阅读",
+ "navigationBarBackgroundColor": "#f8f0ee",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/mingli/pages/section/index.wxml b/packages/mingli/pages/section/index.wxml
new file mode 100644
index 0000000..46cb46b
--- /dev/null
+++ b/packages/mingli/pages/section/index.wxml
@@ -0,0 +1,19 @@
+
+
+ {{chapterTitle}}
+
+
+
+ {{item}}
+
+
+
+ {{item.title}}
+
+
diff --git a/packages/mingli/pages/section/index.wxss b/packages/mingli/pages/section/index.wxss
new file mode 100644
index 0000000..017cad7
--- /dev/null
+++ b/packages/mingli/pages/section/index.wxss
@@ -0,0 +1,56 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #f9f7f5 0%, #f3ebe8 100%);
+}
+
+.mingli-section-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 72rpx;
+}
+
+.hero-card,
+.passage-card {
+ margin-top: 18rpx;
+ padding: 24rpx;
+ border: 1rpx solid rgba(139, 59, 49, 0.08);
+ border-radius: 28rpx;
+ background: rgba(255, 252, 248, 0.94);
+ box-shadow: 0 10rpx 26rpx rgba(139, 59, 49, 0.06);
+}
+
+.hero-card {
+ margin-top: 0;
+}
+
+.hero-card__title {
+ display: block;
+ color: #2c2621;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+.passage-card__item {
+ display: block;
+ margin-top: 16rpx;
+ color: #453730;
+ font-size: 28rpx;
+ line-height: 1.8;
+}
+
+.passage-card__item:first-child {
+ margin-top: 0;
+}
+
+.action-button {
+ margin-top: 16rpx;
+ padding: 22rpx 24rpx;
+ border-radius: 22rpx;
+ background: rgba(139, 59, 49, 0.08);
+ color: #8b3b31;
+ font-size: 26rpx;
+ line-height: 1;
+ text-align: center;
+}
diff --git a/packages/tcm/pages/ai-history/index.js b/packages/tcm/pages/ai-history/index.js
new file mode 100644
index 0000000..b335f67
--- /dev/null
+++ b/packages/tcm/pages/ai-history/index.js
@@ -0,0 +1,80 @@
+const { resolveScene } = require('../../../../utils/static-ux/shared')
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+const HISTORY_GROUPS = Object.freeze([
+ {
+ key: 'today',
+ label: '今天',
+ items: [
+ {
+ key: 'qa-1',
+ tag: 'AI答疑',
+ title: '什么是阴阳?',
+ summary: '从基础概念切回到静态 AI 答疑面板。',
+ route: ROUTES.tabs.ai
+ },
+ {
+ key: 'analysis-1',
+ tag: '辨证分析',
+ title: '脾虚湿困的学习型分析',
+ summary: '查看静态辨证结果展示页。',
+ route: `${ROUTES.tcm.bianzheng}?scene=result`
+ }
+ ]
+ },
+ {
+ key: 'week',
+ label: '近七天',
+ items: [
+ {
+ key: 'qa-2',
+ tag: 'AI答疑',
+ title: '肝主疏泄如何理解?',
+ summary: '保留历史列表样式,不保留旧问答记录模型。',
+ route: ROUTES.tabs.ai
+ }
+ ]
+ }
+])
+
+function cloneGroups(groups) {
+ return groups.map(group => ({
+ ...group,
+ items: group.items.map(item => ({ ...item }))
+ }))
+}
+
+function createTcmAiHistoryPageData(rawScene) {
+ const scene = resolveScene(rawScene, ['default', 'empty'], 'default')
+
+ return {
+ title: 'AI对话历史',
+ scene,
+ emptyTitle: '还没有 AI 记录',
+ emptyDescription: '去 AI 页先完成一次答疑或辨证分析。',
+ actionText: '返回 AI',
+ groups: scene === 'empty' ? [] : cloneGroups(HISTORY_GROUPS)
+ }
+}
+
+Page({
+ data: createTcmAiHistoryPageData('default'),
+
+ onLoad(options) {
+ this.setData(createTcmAiHistoryPageData(options.scene))
+ },
+
+ handlePrimaryTap() {
+ openStaticRoute(ROUTES.tabs.ai, wx)
+ },
+
+ handleHistoryTap(event) {
+ const { route } = event.currentTarget.dataset
+
+ openStaticRoute(route, wx)
+ }
+})
+
+module.exports = {
+ createTcmAiHistoryPageData
+}
diff --git a/packages/tcm/pages/ai-history/index.json b/packages/tcm/pages/ai-history/index.json
new file mode 100644
index 0000000..121c905
--- /dev/null
+++ b/packages/tcm/pages/ai-history/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "AI对话历史",
+ "navigationBarBackgroundColor": "#f8ecd8",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/tcm/pages/ai-history/index.wxml b/packages/tcm/pages/ai-history/index.wxml
new file mode 100644
index 0000000..0447eaf
--- /dev/null
+++ b/packages/tcm/pages/ai-history/index.wxml
@@ -0,0 +1,27 @@
+
+ {{title}}
+
+
+ {{emptyTitle}}
+ {{emptyDescription}}
+ {{actionText}}
+
+
+
+
+ {{item.label}}
+
+ {{historyItem.tag}}
+ {{historyItem.title}}
+ {{historyItem.summary}}
+
+
+
+
diff --git a/packages/tcm/pages/ai-history/index.wxss b/packages/tcm/pages/ai-history/index.wxss
new file mode 100644
index 0000000..0bbf312
--- /dev/null
+++ b/packages/tcm/pages/ai-history/index.wxss
@@ -0,0 +1,82 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #faf3e8 0%, #f1e9dd 100%);
+}
+
+.ai-history-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 72rpx;
+}
+
+.ai-history-page__title {
+ display: block;
+ color: #2f1f12;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 56rpx;
+ font-weight: 700;
+ line-height: 1.2;
+}
+
+.empty-card,
+.history-group {
+ margin-top: 18rpx;
+ padding: 26rpx;
+ border: 1rpx solid rgba(118, 83, 42, 0.08);
+ border-radius: 32rpx;
+ background: rgba(255, 250, 242, 0.92);
+ box-shadow: 0 18rpx 42rpx rgba(86, 58, 25, 0.08);
+}
+
+.empty-card__title,
+.history-group__label,
+.history-item__title {
+ display: block;
+ color: #2c2419;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1.35;
+}
+
+.empty-card__description,
+.history-item__description {
+ display: block;
+ margin-top: 10rpx;
+ color: #7c705e;
+ font-size: 26rpx;
+ line-height: 1.7;
+}
+
+.empty-card__action {
+ display: inline-block;
+ margin-top: 20rpx;
+ padding: 18rpx 24rpx;
+ border-radius: 999rpx;
+ background: linear-gradient(135deg, #9e652f 0%, #75441a 100%);
+ color: #fff7eb;
+ font-size: 26rpx;
+ line-height: 1;
+}
+
+.history-item {
+ margin-top: 14rpx;
+ padding: 22rpx;
+ border-radius: 24rpx;
+ background: rgba(255, 254, 250, 0.94);
+ border: 1rpx solid rgba(118, 83, 42, 0.06);
+}
+
+.history-item__tag {
+ display: inline-block;
+ padding: 8rpx 14rpx;
+ border-radius: 999rpx;
+ background: rgba(111, 66, 22, 0.08);
+ color: #6f4216;
+ font-size: 22rpx;
+ line-height: 1;
+}
+
+.history-item__title {
+ margin-top: 12rpx;
+}
diff --git a/packages/tcm/pages/assets/index.js b/packages/tcm/pages/assets/index.js
new file mode 100644
index 0000000..b8f4af7
--- /dev/null
+++ b/packages/tcm/pages/assets/index.js
@@ -0,0 +1,153 @@
+const { createTcmAssetPageData } = require('../../../../utils/static-ux/tcm')
+const { resolveKind } = require('../../../../utils/static-ux/shared')
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+const ASSET_KIND_LABELS = Object.freeze({
+ notes: '笔记',
+ bookshelf: '书架',
+ favorites: '收藏',
+ history: '历史'
+})
+
+const ASSET_ITEMS = Object.freeze({
+ notes: [
+ {
+ key: 'note-1',
+ title: '阴阳学习摘记',
+ subtitle: '来自《黄帝内经素问》静态演示卡片',
+ description: '把阴阳作为纲领来理解脏腑、经络与病机的对应关系。',
+ route: `${ROUTES.tcm.bookDetail}?scene=classic-a`
+ },
+ {
+ key: 'note-2',
+ title: '脾虚湿困辨证提要',
+ subtitle: '辨证分析里的高频主题',
+ description: '先看脾运失健,再看湿困中焦的症状组合。',
+ route: `${ROUTES.tcm.bianzheng}?scene=result`
+ }
+ ],
+ bookshelf: [
+ {
+ key: 'book-1',
+ title: '黄帝内经素问',
+ subtitle: '已加入静态书架',
+ description: '继续阅读:上古天真论',
+ route: `${ROUTES.tcm.bookDetail}?scene=classic-a`
+ },
+ {
+ key: 'book-2',
+ title: '伤寒论',
+ subtitle: '已加入静态书架',
+ description: '继续阅读:太阳病篇',
+ route: `${ROUTES.tcm.bookDetail}?scene=classic-b`
+ }
+ ],
+ favorites: [
+ {
+ key: 'favorite-1',
+ title: '经络总览卡片',
+ subtitle: '已收藏条目',
+ description: '保留为静态收藏示例,后续再接新的收藏体系。',
+ route: `${ROUTES.tcm.placeholder}?kind=about`
+ },
+ {
+ key: 'favorite-2',
+ title: '命理解读入口',
+ subtitle: '跨域收藏示例',
+ description: '当前用于展示从中医域跳到命理域的 UI 连接。',
+ route: ROUTES.mingli.interpret
+ }
+ ],
+ history: [
+ {
+ key: 'history-1',
+ title: 'AI答疑: 什么是气虚?',
+ subtitle: '最近浏览',
+ description: '返回 AI 页查看静态问答入口。',
+ route: ROUTES.tabs.ai
+ },
+ {
+ key: 'history-2',
+ title: '典籍阅读: 上古天真论',
+ subtitle: '最近阅读',
+ description: '回到典籍阅读页继续浏览。',
+ route: `${ROUTES.tcm.section}?scene=reader-a`
+ }
+ ]
+})
+
+const ASSET_EMPTY_COPY = Object.freeze({
+ notes: {
+ title: '还没有学习笔记',
+ description: '后续接入新后端后,这里会汇总你的静态摘录与标注。'
+ },
+ bookshelf: {
+ title: '书架还是空的',
+ description: '可以先从典籍页进入静态详情,再决定如何组织新的书架逻辑。'
+ },
+ favorites: {
+ title: '还没有收藏内容',
+ description: '本轮只保留收藏区块的 UI 结构,不迁移旧收藏模型。'
+ },
+ history: {
+ title: '还没有浏览记录',
+ description: '继续浏览典籍、AI 或易学页后,后续可在这里接入新的记录源。'
+ }
+})
+
+function cloneItems(items) {
+ return items.map(item => ({ ...item }))
+}
+
+function createFilterItems(kinds) {
+ return kinds.map(kind => ({
+ key: kind,
+ label: ASSET_KIND_LABELS[kind],
+ route: `${ROUTES.tcm.assets}?kind=${kind}`
+ }))
+}
+
+function createTcmAssetsPageData(rawKind) {
+ const baseData = createTcmAssetPageData(rawKind)
+ const items = cloneItems(ASSET_ITEMS[baseData.activeKind] || [])
+ const emptyCopy = ASSET_EMPTY_COPY[baseData.activeKind]
+
+ return {
+ title: baseData.title,
+ activeKind: baseData.activeKind,
+ filterItems: createFilterItems(baseData.kinds),
+ items,
+ emptyTitle: emptyCopy.title,
+ emptyDescription: emptyCopy.description
+ }
+}
+
+function showNavigate(route) {
+ openStaticRoute(route, wx)
+}
+
+Page({
+ data: createTcmAssetsPageData('notes'),
+
+ onLoad(options) {
+ this.setData(
+ createTcmAssetsPageData(
+ resolveKind(options.kind, ['notes', 'bookshelf', 'favorites', 'history'], 'notes')
+ )
+ )
+ },
+
+ handleFilterTap(event) {
+ const { kind } = event.currentTarget.dataset
+
+ this.setData(createTcmAssetsPageData(kind))
+ },
+
+ handleEntryTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ }
+})
+
+module.exports = {
+ createTcmAssetsPageData
+}
diff --git a/packages/tcm/pages/assets/index.json b/packages/tcm/pages/assets/index.json
new file mode 100644
index 0000000..aaf8a18
--- /dev/null
+++ b/packages/tcm/pages/assets/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "学习资产",
+ "navigationBarBackgroundColor": "#f8ecd8",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/tcm/pages/assets/index.wxml b/packages/tcm/pages/assets/index.wxml
new file mode 100644
index 0000000..fbbca41
--- /dev/null
+++ b/packages/tcm/pages/assets/index.wxml
@@ -0,0 +1,36 @@
+
+
+
+
+
+ {{item.label}}
+
+
+
+
+
+ {{item.title}}
+ {{item.subtitle}}
+ {{item.description}}
+
+
+
+
+ {{emptyTitle}}
+ {{emptyDescription}}
+
+
diff --git a/packages/tcm/pages/assets/index.wxss b/packages/tcm/pages/assets/index.wxss
new file mode 100644
index 0000000..ef30a57
--- /dev/null
+++ b/packages/tcm/pages/assets/index.wxss
@@ -0,0 +1,80 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #faf3e8 0%, #f1e9dd 100%);
+}
+
+.asset-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 72rpx;
+ background:
+ radial-gradient(circle at top right, rgba(177, 130, 74, 0.14) 0%, rgba(177, 130, 74, 0) 24%),
+ linear-gradient(135deg, rgba(255, 255, 255, 0.54) 0%, rgba(255, 249, 240, 0) 38%);
+}
+
+.asset-page__header {
+ padding: 8rpx 0 22rpx;
+}
+
+.asset-page__title {
+ display: block;
+ color: #2f1f12;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 56rpx;
+ font-weight: 700;
+ line-height: 1.2;
+}
+
+.asset-page__filters {
+ margin: 0 -6rpx 14rpx;
+}
+
+.asset-page__filter {
+ display: inline-block;
+ padding: 6rpx;
+}
+
+.asset-page__filter-label {
+ display: block;
+ padding: 14rpx 20rpx;
+ border-radius: 999rpx;
+ background: rgba(111, 66, 22, 0.08);
+ color: #6f4216;
+ font-size: 24rpx;
+ line-height: 1;
+}
+
+.asset-page__filter--active .asset-page__filter-label {
+ background: linear-gradient(135deg, #a56a31 0%, #7c4b1d 100%);
+ color: #fff7eb;
+}
+
+.asset-card,
+.asset-page__empty {
+ margin-top: 16rpx;
+ padding: 26rpx;
+ border: 1rpx solid rgba(118, 83, 42, 0.08);
+ border-radius: 32rpx;
+ background: rgba(255, 250, 242, 0.92);
+ box-shadow: 0 18rpx 42rpx rgba(86, 58, 25, 0.08);
+}
+
+.asset-card__title,
+.asset-page__empty-title {
+ display: block;
+ color: #2c2419;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1.35;
+}
+
+.asset-card__subtitle,
+.asset-card__description,
+.asset-page__empty-description {
+ display: block;
+ margin-top: 10rpx;
+ color: #7c705e;
+ font-size: 26rpx;
+ line-height: 1.7;
+}
diff --git a/packages/tcm/pages/bianzheng/index.js b/packages/tcm/pages/bianzheng/index.js
new file mode 100644
index 0000000..3652fc4
--- /dev/null
+++ b/packages/tcm/pages/bianzheng/index.js
@@ -0,0 +1,50 @@
+const { resolveScene } = require('../../../../utils/static-ux/shared')
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+function createTcmBianzhengPageData(rawScene) {
+ const scene = resolveScene(rawScene, ['default', 'result'], 'default')
+
+ return {
+ title: '辨证分析',
+ scene,
+ eyebrow: 'AI 辨证分析',
+ intro: '把症状整理成结构化输入,再输出学习型辨证结果。',
+ fields: [
+ { key: 'mainSymptoms', label: '核心症状', placeholder: '主要症状,用逗号分隔' },
+ { key: 'tongue', label: '舌象', placeholder: '舌象' },
+ { key: 'pulse', label: '脉象', placeholder: '脉象' },
+ { key: 'constitution', label: '体质', placeholder: '体质' },
+ { key: 'duration', label: '病程', placeholder: '病程' }
+ ],
+ result:
+ scene === 'result'
+ ? {
+ title: '学习型辨证结果',
+ summary: '当前静态结果以“脾虚湿困”为例,用于承接老页面的信息层级和结果区块。',
+ references: ['《素问》重视脾胃运化', '症见困倦乏力、纳呆便溏可作为学习型线索']
+ }
+ : null,
+ primaryActionText: scene === 'result' ? '重新整理症状' : '提交辨证分析',
+ secondaryActionText: '返回 AI'
+ }
+}
+
+Page({
+ data: createTcmBianzhengPageData('default'),
+
+ onLoad(options) {
+ this.setData(createTcmBianzhengPageData(options.scene))
+ },
+
+ handlePrimaryTap() {
+ this.setData(createTcmBianzhengPageData(this.data.scene === 'result' ? 'default' : 'result'))
+ },
+
+ handleSecondaryTap() {
+ openStaticRoute(ROUTES.tabs.ai, wx)
+ }
+})
+
+module.exports = {
+ createTcmBianzhengPageData
+}
diff --git a/packages/tcm/pages/bianzheng/index.json b/packages/tcm/pages/bianzheng/index.json
new file mode 100644
index 0000000..b0f4ca1
--- /dev/null
+++ b/packages/tcm/pages/bianzheng/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "辨证分析",
+ "navigationBarBackgroundColor": "#f8ecd8",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/tcm/pages/bianzheng/index.wxml b/packages/tcm/pages/bianzheng/index.wxml
new file mode 100644
index 0000000..ac1f56b
--- /dev/null
+++ b/packages/tcm/pages/bianzheng/index.wxml
@@ -0,0 +1,30 @@
+
+
+ {{eyebrow}}
+ {{title}}
+ {{intro}}
+
+
+
+
+ {{item.label}}
+
+
+
+
+ {{primaryActionText}}
+
+ {{secondaryActionText}}
+
+
+
+
+
+ {{result.title}}
+ {{result.summary}}
+
+ ·
+ {{item}}
+
+
+
diff --git a/packages/tcm/pages/bianzheng/index.wxss b/packages/tcm/pages/bianzheng/index.wxss
new file mode 100644
index 0000000..74aebb5
--- /dev/null
+++ b/packages/tcm/pages/bianzheng/index.wxss
@@ -0,0 +1,120 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #faf3e8 0%, #f1e9dd 100%);
+}
+
+.bianzheng-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 72rpx;
+}
+
+.hero-card,
+.form-card,
+.result-card {
+ margin-top: 18rpx;
+ padding: 26rpx;
+ border: 1rpx solid rgba(118, 83, 42, 0.08);
+ border-radius: 32rpx;
+ background: rgba(255, 250, 242, 0.92);
+ box-shadow: 0 18rpx 42rpx rgba(86, 58, 25, 0.08);
+}
+
+.hero-card {
+ margin-top: 0;
+}
+
+.hero-card__eyebrow {
+ display: inline-block;
+ padding: 8rpx 14rpx;
+ border-radius: 999rpx;
+ background: rgba(111, 66, 22, 0.08);
+ color: #6f4216;
+ font-size: 22rpx;
+ line-height: 1;
+}
+
+.hero-card__title,
+.result-card__title {
+ display: block;
+ margin-top: 18rpx;
+ color: #2c2419;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 38rpx;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+.hero-card__description,
+.result-card__summary,
+.result-card__reference-text {
+ display: block;
+ margin-top: 12rpx;
+ color: #7c705e;
+ font-size: 26rpx;
+ line-height: 1.75;
+}
+
+.field-block {
+ margin-top: 16rpx;
+}
+
+.field-block:first-child {
+ margin-top: 0;
+}
+
+.field-block__label {
+ display: block;
+ color: #6f4b2d;
+ font-size: 24rpx;
+ line-height: 1.5;
+}
+
+.field-block__input {
+ box-sizing: border-box;
+ width: 100%;
+ margin-top: 10rpx;
+ padding: 18rpx 20rpx;
+ border: 1rpx solid rgba(199, 171, 133, 0.5);
+ border-radius: 22rpx;
+ background: rgba(255, 254, 250, 0.92);
+ color: #8f6842;
+ font-size: 26rpx;
+ line-height: 1.5;
+}
+
+.form-card__actions {
+ margin-top: 22rpx;
+}
+
+.form-card__button {
+ padding: 20rpx 24rpx;
+ border-radius: 24rpx;
+ background: linear-gradient(135deg, #9e652f 0%, #75441a 100%);
+ color: #fff7eb;
+ font-size: 28rpx;
+ line-height: 1;
+ text-align: center;
+}
+
+.form-card__button + .form-card__button {
+ margin-top: 12rpx;
+}
+
+.form-card__button--ghost {
+ background: rgba(111, 66, 22, 0.08);
+ color: #6f4216;
+}
+
+.result-card__reference {
+ display: flex;
+ align-items: flex-start;
+ margin-top: 10rpx;
+}
+
+.result-card__reference-dot {
+ margin-right: 10rpx;
+ color: #9a622d;
+ font-size: 28rpx;
+ line-height: 1.6;
+}
diff --git a/packages/tcm/pages/book-detail/index.js b/packages/tcm/pages/book-detail/index.js
new file mode 100644
index 0000000..8707c9e
--- /dev/null
+++ b/packages/tcm/pages/book-detail/index.js
@@ -0,0 +1,125 @@
+const { resolveScene } = require('../../../../utils/static-ux/shared')
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+const BOOK_DETAIL_SURFACES = Object.freeze({
+ 'classic-a': {
+ coverText: '黄',
+ title: '黄帝内经素问',
+ subtitle: '传世中医典籍 · 学习阅读入口',
+ tags: ['经典', '入门'],
+ description: '当前典籍详情页只保留封面、简介、目录和推荐区块,不迁移旧项目的详情接口。',
+ progressText: '已读到:上古天真论',
+ primaryRoute: `${ROUTES.tcm.section}?scene=reader-a`,
+ catalog: [
+ {
+ key: 'chapter-1',
+ title: '上古天真论',
+ route: `${ROUTES.tcm.section}?scene=reader-a`,
+ children: ['节录一', '节录二']
+ },
+ {
+ key: 'chapter-2',
+ title: '四气调神大论',
+ route: `${ROUTES.tcm.section}?scene=reader-a`,
+ children: ['节录一', '节录二']
+ }
+ ],
+ recommends: [
+ {
+ key: 'recommend-b',
+ title: '伤寒论',
+ route: `${ROUTES.tcm.bookDetail}?scene=classic-b`
+ }
+ ]
+ },
+ 'classic-b': {
+ coverText: '伤',
+ title: '伤寒论',
+ subtitle: '经方经典 · 学习阅读入口',
+ tags: ['经方', '进阶'],
+ description: '静态详情页保留目录结构和推荐卡片,为后续新数据层留出清晰接口边界。',
+ progressText: '已读到:太阳病篇',
+ primaryRoute: `${ROUTES.tcm.section}?scene=reader-b`,
+ catalog: [
+ {
+ key: 'chapter-1',
+ title: '太阳病篇',
+ route: `${ROUTES.tcm.section}?scene=reader-b`,
+ children: ['条文一', '条文二']
+ },
+ {
+ key: 'chapter-2',
+ title: '阳明病篇',
+ route: `${ROUTES.tcm.section}?scene=reader-b`,
+ children: ['条文一', '条文二']
+ }
+ ],
+ recommends: [
+ {
+ key: 'recommend-a',
+ title: '黄帝内经素问',
+ route: `${ROUTES.tcm.bookDetail}?scene=classic-a`
+ }
+ ]
+ }
+})
+
+function cloneCatalog(items) {
+ return items.map(item => ({
+ ...item,
+ children: [...item.children]
+ }))
+}
+
+function cloneItems(items) {
+ return items.map(item => ({ ...item }))
+}
+
+function createTcmBookDetailPageData(rawScene) {
+ const scene = resolveScene(rawScene, ['classic-a', 'classic-b'], 'classic-a')
+ const surface = BOOK_DETAIL_SURFACES[scene]
+
+ return {
+ title: '典籍详情',
+ scene,
+ coverText: surface.coverText,
+ book: {
+ title: surface.title,
+ subtitle: surface.subtitle
+ },
+ tags: [...surface.tags],
+ description: surface.description,
+ progressText: surface.progressText,
+ primaryRoute: surface.primaryRoute,
+ catalog: cloneCatalog(surface.catalog),
+ recommends: cloneItems(surface.recommends)
+ }
+}
+
+function showNavigate(route) {
+ openStaticRoute(route, wx)
+}
+
+Page({
+ data: createTcmBookDetailPageData('classic-a'),
+
+ onLoad(options) {
+ this.setData(createTcmBookDetailPageData(options.scene))
+ },
+
+ handlePrimaryTap() {
+ showNavigate(this.data.primaryRoute)
+ },
+
+ handleCatalogTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ },
+
+ handleRecommendTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ }
+})
+
+module.exports = {
+ createTcmBookDetailPageData
+}
diff --git a/packages/tcm/pages/book-detail/index.json b/packages/tcm/pages/book-detail/index.json
new file mode 100644
index 0000000..10efc9d
--- /dev/null
+++ b/packages/tcm/pages/book-detail/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "典籍详情",
+ "navigationBarBackgroundColor": "#f8ecd8",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/tcm/pages/book-detail/index.wxml b/packages/tcm/pages/book-detail/index.wxml
new file mode 100644
index 0000000..b523516
--- /dev/null
+++ b/packages/tcm/pages/book-detail/index.wxml
@@ -0,0 +1,54 @@
+
+
+ {{coverText}}
+
+ {{book.title}}
+ {{book.subtitle}}
+
+ {{item}}
+
+ 开始阅读
+
+
+
+
+ 简介
+ {{description}}
+
+
+
+ 我的阅读进度
+ {{progressText}}
+
+
+
+ 章节目录
+
+
+ {{item.title}}
+ {{item}}
+
+ >
+
+
+
+
+ 相关推荐
+
+ 典
+ {{item.title}}
+
+
+
diff --git a/packages/tcm/pages/book-detail/index.wxss b/packages/tcm/pages/book-detail/index.wxss
new file mode 100644
index 0000000..2339bfc
--- /dev/null
+++ b/packages/tcm/pages/book-detail/index.wxss
@@ -0,0 +1,147 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #fbf4e9 0%, #f3eadf 100%);
+}
+
+.book-detail-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 72rpx;
+}
+
+.hero-card,
+.section-card {
+ margin-top: 18rpx;
+ padding: 28rpx;
+ border: 1rpx solid rgba(118, 83, 42, 0.08);
+ border-radius: 32rpx;
+ background: rgba(255, 250, 242, 0.92);
+ box-shadow: 0 18rpx 42rpx rgba(86, 58, 25, 0.08);
+}
+
+.hero-card {
+ display: flex;
+ align-items: flex-start;
+ margin-top: 0;
+}
+
+.hero-card__cover {
+ width: 126rpx;
+ height: 168rpx;
+ border-radius: 24rpx;
+ background: linear-gradient(180deg, #d0b288 0%, #b98b5a 100%);
+ color: #fff9ef;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 42rpx;
+ font-weight: 700;
+ line-height: 168rpx;
+ text-align: center;
+}
+
+.hero-card__body {
+ flex: 1;
+ margin-left: 22rpx;
+}
+
+.hero-card__title,
+.section-card__title,
+.catalog-row__title {
+ display: block;
+ color: #2c2419;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 36rpx;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+.hero-card__subtitle,
+.section-card__description,
+.catalog-row__child {
+ display: block;
+ margin-top: 10rpx;
+ color: #7d705d;
+ font-size: 26rpx;
+ line-height: 1.7;
+}
+
+.hero-card__tags {
+ margin-top: 14rpx;
+}
+
+.hero-card__tag {
+ display: inline-block;
+ margin-right: 8rpx;
+ padding: 8rpx 16rpx;
+ border-radius: 999rpx;
+ background: rgba(111, 66, 22, 0.08);
+ color: #6f4216;
+ font-size: 22rpx;
+ line-height: 1;
+}
+
+.hero-card__button {
+ display: inline-block;
+ margin-top: 18rpx;
+ padding: 18rpx 24rpx;
+ border-radius: 24rpx;
+ background: linear-gradient(135deg, #9a622d 0%, #6f4216 100%);
+ color: #fff8ef;
+ font-size: 26rpx;
+ line-height: 1;
+}
+
+.catalog-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 16rpx;
+ padding: 18rpx 0;
+ border-bottom: 1rpx solid rgba(118, 83, 42, 0.08);
+}
+
+.catalog-row:last-child {
+ border-bottom: 0;
+}
+
+.catalog-row__body {
+ min-width: 0;
+ flex: 1;
+}
+
+.catalog-row__arrow {
+ color: #bfa882;
+ font-size: 28rpx;
+ line-height: 1;
+}
+
+.recommend-card {
+ display: flex;
+ align-items: center;
+ margin-top: 16rpx;
+ padding: 18rpx 0;
+ border-bottom: 1rpx solid rgba(118, 83, 42, 0.08);
+}
+
+.recommend-card:last-child {
+ border-bottom: 0;
+}
+
+.recommend-card__cover {
+ width: 70rpx;
+ height: 90rpx;
+ border-radius: 18rpx;
+ background: linear-gradient(180deg, #d0b288 0%, #b98b5a 100%);
+ color: #fff9ef;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 28rpx;
+ font-weight: 700;
+ line-height: 90rpx;
+ text-align: center;
+}
+
+.recommend-card__title {
+ margin-left: 16rpx;
+ color: #2c2419;
+ font-size: 28rpx;
+ line-height: 1.5;
+}
diff --git a/packages/tcm/pages/placeholder/index.js b/packages/tcm/pages/placeholder/index.js
new file mode 100644
index 0000000..0ab2a5a
--- /dev/null
+++ b/packages/tcm/pages/placeholder/index.js
@@ -0,0 +1,99 @@
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+const PLACEHOLDER_SURFACES = Object.freeze({
+ membership: {
+ title: '开通会员',
+ eyebrow: 'Membership',
+ description: '会员页当前只承接静态展示,后续接入新的权益体系时保持当前信息层级。',
+ tips: [
+ '保留老页面的会员引导区块和 CTA 位置',
+ '不迁移旧项目的付费逻辑、订单状态和接口',
+ '后续只在这里接新的会员说明与权益列表'
+ ]
+ },
+ feedback: {
+ title: '意见反馈',
+ eyebrow: 'Feedback',
+ description: '反馈入口先保留为静态说明页,用来承接“我的”页的跳转。',
+ tips: [
+ '后续可接表单或工单系统',
+ '当前不提交任何内容',
+ '只保留页面层级和提示文案'
+ ]
+ },
+ share: {
+ title: '分享 APP',
+ eyebrow: 'Share',
+ description: '分享页先保留 UI 占位,不接旧项目的传播逻辑。',
+ tips: [
+ '后续可接小程序分享卡片',
+ '当前只展示说明与返回入口',
+ '不接邀请码、海报或裂变结构'
+ ]
+ },
+ about: {
+ title: '关于我们',
+ eyebrow: 'About',
+ description: '关于页保持静态信息卡片结构,为后续新内容预留容器。',
+ tips: [
+ '保留页面标题和说明卡片',
+ '后续可以接版本信息与项目介绍',
+ '当前不复用旧工程中的关于数据结构'
+ ]
+ },
+ settings: {
+ title: '设置',
+ eyebrow: 'Settings',
+ description: '设置页先只保留静态说明,不接旧登录态和缓存策略。',
+ tips: [
+ '后续可接新账号体系下的设置项',
+ '当前不读写 storage',
+ '不迁移旧项目的 session 或偏好逻辑'
+ ]
+ }
+})
+
+function createTcmPlaceholderPageData(kind) {
+ const activeKind = Object.prototype.hasOwnProperty.call(PLACEHOLDER_SURFACES, kind) ? kind : 'about'
+ const surface = PLACEHOLDER_SURFACES[activeKind]
+
+ return {
+ kind: activeKind,
+ title: surface.title,
+ eyebrow: surface.eyebrow,
+ description: surface.description,
+ tips: [...surface.tips],
+ actions: [
+ {
+ key: 'back-home',
+ title: '返回首页',
+ actionType: 'tab',
+ route: ROUTES.tabs.home
+ },
+ {
+ key: 'go-profile',
+ title: '去我的页',
+ actionType: 'tab',
+ route: ROUTES.tabs.profile
+ }
+ ]
+ }
+}
+
+Page({
+ data: createTcmPlaceholderPageData('about'),
+
+ onLoad(options) {
+ this.setData(createTcmPlaceholderPageData(options.kind))
+ },
+
+ handleActionTap(event) {
+ const { route } = event.currentTarget.dataset
+
+ openStaticRoute(route, wx)
+ }
+})
+
+module.exports = {
+ createTcmPlaceholderPageData
+}
diff --git a/packages/tcm/pages/placeholder/index.json b/packages/tcm/pages/placeholder/index.json
new file mode 100644
index 0000000..86b98f8
--- /dev/null
+++ b/packages/tcm/pages/placeholder/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "说明页",
+ "navigationBarBackgroundColor": "#f8ecd8",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/tcm/pages/placeholder/index.wxml b/packages/tcm/pages/placeholder/index.wxml
new file mode 100644
index 0000000..9e1b0a5
--- /dev/null
+++ b/packages/tcm/pages/placeholder/index.wxml
@@ -0,0 +1,26 @@
+
+
+ {{eyebrow}}
+ {{title}}
+ {{description}}
+
+
+
+ 当前状态
+
+ •
+ {{item}}
+
+
+
+
+ {{item.title}}
+
+
diff --git a/packages/tcm/pages/placeholder/index.wxss b/packages/tcm/pages/placeholder/index.wxss
new file mode 100644
index 0000000..81bea9f
--- /dev/null
+++ b/packages/tcm/pages/placeholder/index.wxss
@@ -0,0 +1,83 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #faf3e8 0%, #f1e9dd 100%);
+}
+
+.placeholder-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 72rpx;
+}
+
+.hero-card,
+.tips-card {
+ margin-top: 18rpx;
+ padding: 28rpx;
+ border: 1rpx solid rgba(118, 83, 42, 0.08);
+ border-radius: 32rpx;
+ background: rgba(255, 250, 242, 0.92);
+ box-shadow: 0 18rpx 42rpx rgba(86, 58, 25, 0.08);
+}
+
+.hero-card {
+ margin-top: 0;
+}
+
+.hero-card__eyebrow {
+ display: inline-block;
+ padding: 8rpx 16rpx;
+ border-radius: 999rpx;
+ background: rgba(111, 66, 22, 0.08);
+ color: #6f4216;
+ font-size: 22rpx;
+ line-height: 1;
+}
+
+.hero-card__title,
+.tips-card__title {
+ display: block;
+ margin-top: 18rpx;
+ color: #2c2419;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 36rpx;
+ font-weight: 700;
+ line-height: 1.35;
+}
+
+.hero-card__description,
+.tips-card__text {
+ display: block;
+ margin-top: 12rpx;
+ color: #7c705e;
+ font-size: 26rpx;
+ line-height: 1.75;
+}
+
+.tips-card__item {
+ display: flex;
+ align-items: flex-start;
+ margin-top: 12rpx;
+}
+
+.tips-card__dot {
+ margin-right: 10rpx;
+ color: #9a622d;
+ font-size: 28rpx;
+ line-height: 1.6;
+}
+
+.action-row__button {
+ margin-top: 16rpx;
+ padding: 22rpx 24rpx;
+ border-radius: 24rpx;
+ background: linear-gradient(135deg, #9e652f 0%, #75441a 100%);
+ color: #fff7eb;
+ font-size: 28rpx;
+ line-height: 1;
+ text-align: center;
+}
+
+.action-row__button--ghost {
+ background: rgba(111, 66, 22, 0.08);
+ color: #6f4216;
+}
diff --git a/packages/tcm/pages/search-books/index.js b/packages/tcm/pages/search-books/index.js
new file mode 100644
index 0000000..0ce498d
--- /dev/null
+++ b/packages/tcm/pages/search-books/index.js
@@ -0,0 +1,90 @@
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+const SEARCH_CATALOG = Object.freeze([
+ {
+ key: 'classic-a',
+ title: '黄帝内经素问',
+ subtitle: '书名命中',
+ aliases: ['黄帝内经', '素问'],
+ route: `${ROUTES.tcm.bookDetail}?scene=classic-a`
+ },
+ {
+ key: 'classic-b',
+ title: '伤寒论',
+ subtitle: '经典结果',
+ aliases: ['伤寒', '仲景'],
+ route: `${ROUTES.tcm.bookDetail}?scene=classic-b`
+ }
+])
+
+const SEARCH_HISTORY = Object.freeze(['黄帝内经', '伤寒论', '温病条辨'])
+
+function createResults(keyword) {
+ const normalizedKeyword = (keyword || '').trim()
+
+ if (!normalizedKeyword) {
+ return []
+ }
+
+ return SEARCH_CATALOG.filter(item => {
+ return (
+ item.title.includes(normalizedKeyword) ||
+ item.aliases.some(alias => alias.includes(normalizedKeyword))
+ )
+ }).map(item => ({ ...item }))
+}
+
+function createTcmSearchBooksPageData(keyword) {
+ const normalizedKeyword = (keyword || '').trim()
+
+ return {
+ title: '搜索典籍',
+ keyword: normalizedKeyword,
+ draftKeyword: normalizedKeyword,
+ summary: '只保留静态搜索 UI 和结果列表,不接旧项目的检索接口与搜索历史存储。',
+ placeholder: '搜索书名或常见别名',
+ historyTitle: '最近搜索',
+ history: SEARCH_HISTORY.map(item => ({
+ key: item,
+ label: item
+ })),
+ results: createResults(normalizedKeyword),
+ searched: Boolean(normalizedKeyword)
+ }
+}
+
+function showNavigate(route) {
+ openStaticRoute(route, wx)
+}
+
+Page({
+ data: createTcmSearchBooksPageData(''),
+
+ onLoad(options) {
+ this.setData(createTcmSearchBooksPageData(options.keyword || options.q))
+ },
+
+ handleKeywordInput(event) {
+ this.setData({
+ draftKeyword: event.detail.value
+ })
+ },
+
+ handleSearchTap() {
+ this.setData(createTcmSearchBooksPageData(this.data.draftKeyword))
+ },
+
+ handleHistoryTap(event) {
+ const keyword = event.currentTarget.dataset.keyword
+
+ this.setData(createTcmSearchBooksPageData(keyword))
+ },
+
+ handleResultTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ }
+})
+
+module.exports = {
+ createTcmSearchBooksPageData
+}
diff --git a/packages/tcm/pages/search-books/index.json b/packages/tcm/pages/search-books/index.json
new file mode 100644
index 0000000..7f6e415
--- /dev/null
+++ b/packages/tcm/pages/search-books/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "搜索典籍",
+ "navigationBarBackgroundColor": "#f8ecd8",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/tcm/pages/search-books/index.wxml b/packages/tcm/pages/search-books/index.wxml
new file mode 100644
index 0000000..a2b8c44
--- /dev/null
+++ b/packages/tcm/pages/search-books/index.wxml
@@ -0,0 +1,50 @@
+
+
+ {{title}}
+ {{summary}}
+
+
+
+ 搜索
+
+
+
+
+ {{historyTitle}}
+
+
+ {{item.label}}
+
+
+
+
+
+ 搜索结果
+
+ {{item.title}}
+ {{item.subtitle}}
+
+
+
+
+ 没有找到相关典籍
+ 换一个书名或常见别名再试试。
+
+
diff --git a/packages/tcm/pages/search-books/index.wxss b/packages/tcm/pages/search-books/index.wxss
new file mode 100644
index 0000000..d091ab5
--- /dev/null
+++ b/packages/tcm/pages/search-books/index.wxss
@@ -0,0 +1,96 @@
+page {
+ min-height: 100%;
+ background: linear-gradient(180deg, #fbf4e9 0%, #f3eadf 100%);
+}
+
+.search-books-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 28rpx 20rpx 72rpx;
+}
+
+.hero-card,
+.section-card {
+ margin-top: 18rpx;
+ padding: 28rpx;
+ border: 1rpx solid rgba(118, 83, 42, 0.08);
+ border-radius: 32rpx;
+ background: rgba(255, 250, 242, 0.92);
+ box-shadow: 0 18rpx 42rpx rgba(86, 58, 25, 0.08);
+}
+
+.hero-card {
+ margin-top: 0;
+}
+
+.hero-card__title,
+.section-card__title,
+.result-card__title {
+ display: block;
+ color: #2c2419;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 36rpx;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+.hero-card__description,
+.section-card__description,
+.result-card__subtitle {
+ display: block;
+ margin-top: 10rpx;
+ color: #7d705d;
+ font-size: 26rpx;
+ line-height: 1.7;
+}
+
+.search-row {
+ display: flex;
+ align-items: center;
+ margin-top: 18rpx;
+}
+
+.search-row__input {
+ flex: 1;
+ height: 84rpx;
+ padding: 0 20rpx;
+ border-radius: 22rpx;
+ background: rgba(255, 254, 250, 0.92);
+ color: #3f2e1f;
+ font-size: 26rpx;
+}
+
+.search-row__button {
+ margin-left: 12rpx;
+ padding: 20rpx 24rpx;
+ border-radius: 22rpx;
+ background: linear-gradient(135deg, #9a622d 0%, #6f4216 100%);
+ color: #fff8ef;
+ font-size: 26rpx;
+ line-height: 1;
+}
+
+.history-list {
+ margin: 14rpx -6rpx 0;
+}
+
+.history-chip {
+ display: inline-block;
+ margin: 6rpx;
+ padding: 12rpx 18rpx;
+ border-radius: 999rpx;
+ background: rgba(111, 66, 22, 0.08);
+ color: #6f4216;
+ font-size: 22rpx;
+ line-height: 1;
+}
+
+.result-card {
+ margin-top: 16rpx;
+ padding: 22rpx 0;
+ border-bottom: 1rpx solid rgba(118, 83, 42, 0.08);
+}
+
+.result-card:last-child {
+ border-bottom: 0;
+}
diff --git a/packages/tcm/pages/section/index.js b/packages/tcm/pages/section/index.js
new file mode 100644
index 0000000..ed135e6
--- /dev/null
+++ b/packages/tcm/pages/section/index.js
@@ -0,0 +1,97 @@
+const { resolveScene } = require('../../../../utils/static-ux/shared')
+const { ROUTES, openStaticRoute } = require('../../../../utils/static-ux/route-map')
+
+const READER_SURFACES = Object.freeze({
+ 'reader-a': {
+ chapterTitle: '上古天真论',
+ passages: [
+ '上古之人,其知道者,法于阴阳,和于术数。',
+ '食饮有节,起居有常,不妄作劳,故能形与神俱。'
+ ]
+ },
+ 'reader-b': {
+ chapterTitle: '太阳病篇',
+ passages: [
+ '伤寒者,太阳病也。',
+ '太阳之为病,脉浮,头项强痛而恶寒。'
+ ]
+ }
+})
+
+function createTcmSectionPageData(rawScene) {
+ const scene = resolveScene(rawScene, ['reader-a', 'reader-b'], 'reader-a')
+ const surface = READER_SURFACES[scene]
+
+ return {
+ title: '典籍阅读',
+ scene,
+ chapterTitle: surface.chapterTitle,
+ passages: [...surface.passages],
+ toolbarVisible: false,
+ settings: {
+ fontScale: 1,
+ lineMode: 'normal',
+ bgMode: 'default'
+ },
+ lineModes: [
+ { key: 'compact', label: '紧凑' },
+ { key: 'normal', label: '适中' },
+ { key: 'relaxed', label: '宽松' }
+ ],
+ backgroundModes: [
+ { key: 'default', label: '默认' },
+ { key: 'paper', label: '纸张' },
+ { key: 'care', label: '护眼' }
+ ],
+ catalogItems: [
+ {
+ key: 'reader-a',
+ title: '上古天真论',
+ route: `${ROUTES.tcm.section}?scene=reader-a`
+ },
+ {
+ key: 'reader-b',
+ title: '太阳病篇',
+ route: `${ROUTES.tcm.section}?scene=reader-b`
+ }
+ ]
+ }
+}
+
+function showNavigate(route) {
+ openStaticRoute(route, wx)
+}
+
+Page({
+ data: createTcmSectionPageData('reader-a'),
+
+ onLoad(options) {
+ this.setData(createTcmSectionPageData(options.scene))
+ },
+
+ handleToolbarToggle() {
+ this.setData({
+ toolbarVisible: !this.data.toolbarVisible
+ })
+ },
+
+ handleLineModeTap(event) {
+ this.setData({
+ 'settings.lineMode': event.currentTarget.dataset.mode
+ })
+ },
+
+ handleBackgroundTap(event) {
+ this.setData({
+ 'settings.bgMode': event.currentTarget.dataset.mode
+ })
+ },
+
+ handleCatalogTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ }
+})
+
+module.exports = {
+ createTcmSectionPageData
+}
diff --git a/packages/tcm/pages/section/index.json b/packages/tcm/pages/section/index.json
new file mode 100644
index 0000000..2eddc0a
--- /dev/null
+++ b/packages/tcm/pages/section/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "典籍阅读",
+ "navigationBarBackgroundColor": "#f8ecd8",
+ "navigationBarTextStyle": "black"
+}
diff --git a/packages/tcm/pages/section/index.wxml b/packages/tcm/pages/section/index.wxml
new file mode 100644
index 0000000..51c0fd9
--- /dev/null
+++ b/packages/tcm/pages/section/index.wxml
@@ -0,0 +1,49 @@
+
+
+ {{chapterTitle}}
+ {{item}}
+
+
+
+ {{title}}
+
+
+ 行间距
+
+ {{item.label}}
+
+
+
+
+ 背景
+
+ {{item.label}}
+
+
+
+
+ 目录
+
+ {{item.title}}
+
+
+
+
diff --git a/packages/tcm/pages/section/index.wxss b/packages/tcm/pages/section/index.wxss
new file mode 100644
index 0000000..b489957
--- /dev/null
+++ b/packages/tcm/pages/section/index.wxss
@@ -0,0 +1,102 @@
+page {
+ min-height: 100%;
+ background: #f8f1e6;
+}
+
+.reading-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 0 0 40rpx;
+}
+
+.reading-page--default {
+ background: #f8f1e6;
+}
+
+.reading-page--paper {
+ background: #f4ead1;
+}
+
+.reading-page--care {
+ background: #eef4e8;
+}
+
+.reading-page__article {
+ padding: 72rpx 40rpx 48rpx;
+}
+
+.reading-page__chapter {
+ display: block;
+ color: #2c2419;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 40rpx;
+ font-weight: 700;
+ line-height: 1.35;
+ text-align: center;
+}
+
+.reading-page__passage {
+ display: block;
+ margin-top: 24rpx;
+ color: #3b3027;
+ font-size: 30rpx;
+ line-height: 1.9;
+ text-align: justify;
+}
+
+.reading-toolbar {
+ margin: 0 20rpx;
+ padding: 24rpx;
+ border: 1rpx solid rgba(118, 83, 42, 0.08);
+ border-radius: 28rpx;
+ background: rgba(255, 251, 244, 0.96);
+ box-shadow: 0 18rpx 42rpx rgba(86, 58, 25, 0.08);
+}
+
+.reading-toolbar__title,
+.reading-toolbar__label {
+ display: block;
+ color: #2c2419;
+}
+
+.reading-toolbar__title {
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 32rpx;
+ font-weight: 700;
+ line-height: 1.35;
+}
+
+.reading-toolbar__group {
+ margin-top: 20rpx;
+}
+
+.reading-toolbar__label {
+ font-size: 24rpx;
+ line-height: 1.5;
+}
+
+.reading-toolbar__pill {
+ display: inline-block;
+ margin: 10rpx 8rpx 0 0;
+ padding: 12rpx 18rpx;
+ border-radius: 999rpx;
+ background: rgba(111, 66, 22, 0.08);
+ color: #6f4216;
+ font-size: 22rpx;
+ line-height: 1;
+}
+
+.reading-toolbar__pill--active {
+ background: linear-gradient(135deg, #9a622d 0%, #6f4216 100%);
+ color: #fff8ef;
+}
+
+.catalog-item {
+ margin-top: 12rpx;
+ padding: 18rpx 20rpx;
+ border-radius: 20rpx;
+ background: rgba(255, 254, 250, 0.94);
+ color: #4f3e2e;
+ font-size: 24rpx;
+ line-height: 1.5;
+}
diff --git a/pages/ai/index.js b/pages/ai/index.js
new file mode 100644
index 0000000..601428d
--- /dev/null
+++ b/pages/ai/index.js
@@ -0,0 +1,185 @@
+const { ROUTES, openStaticRoute } = require('../../utils/static-ux/route-map')
+
+const AI_PAGE_BLUEPRINT = Object.freeze({
+ title: 'AI助手',
+ historyText: '历史',
+ disclaimerText: '本功能仅供学习参考,不能替代专业医师诊断。',
+ tabs: [
+ {
+ key: 'qa',
+ label: 'AI答疑'
+ },
+ {
+ key: 'analysis',
+ label: '辨证分析'
+ },
+ {
+ key: 'constitution',
+ label: '体质检测'
+ }
+ ],
+ panels: {
+ qa: {
+ key: 'qa',
+ badge: 'AI答疑',
+ heading: '您好,我是中医学习助手',
+ subheading: '围绕中医理论、术语和典籍原文快速提问',
+ highlights: ['解答中医理论问题', '解释典籍原文含义', '给出学习型出处引用'],
+ examples: ['什么是阴阳?', '气虚和阳虚有什么区别?', '如何理解肝主疏泄?'],
+ formType: 'qa',
+ actionTitle: '输入你的问题',
+ inputPlaceholder: '例如:什么是气虚?',
+ actionButtonText: '发送'
+ },
+ analysis: {
+ key: 'analysis',
+ badge: '辨证分析',
+ heading: '先整理症状,再查看学习型辨证分析',
+ subheading: '把症状整理为结构化输入,再看学习型辨证思路',
+ highlights: ['输入主要症状', '补充舌象、脉象、体质', '查看学习型辨证结论'],
+ examples: ['乏力、睡眠差应如何整理输入?', '舌淡苔白常见于哪些证型?', '如何理解脾虚湿困?'],
+ formType: 'analysis',
+ actionTitle: '整理辨证输入',
+ primaryField: {
+ label: '主要症状',
+ placeholder: '主要症状,多个用逗号分隔'
+ },
+ secondaryFields: [
+ {
+ key: 'tongue',
+ placeholder: '舌象'
+ },
+ {
+ key: 'pulse',
+ placeholder: '脉象'
+ },
+ {
+ key: 'constitution',
+ placeholder: '体质'
+ },
+ {
+ key: 'course',
+ placeholder: '病程'
+ }
+ ],
+ noteField: {
+ label: '补充说明',
+ placeholder: '例如:近两周明显乏力,睡眠浅。'
+ },
+ actionButtonText: '开始分析'
+ },
+ constitution: {
+ key: 'constitution',
+ badge: '体质检测',
+ heading: '体质检测即将开放',
+ subheading: '九种体质入口先按设计稿保留为 UI-first 模式',
+ highlights: ['后续会补齐问卷式体质评估', '保持学习参考语义', '不会输出医疗诊断结论'],
+ examples: ['我总怕冷属于哪种体质?', '气虚质有哪些特征?', '体质结果将如何回到典籍学习?'],
+ formType: 'constitution',
+ actionTitle: '体质检测即将开放',
+ actionDescription:
+ '当前回合先严格对齐 UI 结构,问卷式体质检测会在后续接入真实题目与结果逻辑。',
+ actionButtonText: '查看其它待开放能力'
+ }
+ }
+})
+
+function cloneTabs(tabs) {
+ return tabs.map(item => ({ ...item }))
+}
+
+function cloneSecondaryFields(fields) {
+ return fields.map(item => ({ ...item }))
+}
+
+function createPanelByKey(panelKey) {
+ const sourcePanel = AI_PAGE_BLUEPRINT.panels[panelKey] || AI_PAGE_BLUEPRINT.panels.qa
+
+ return {
+ ...sourcePanel,
+ highlights: [...sourcePanel.highlights],
+ examples: [...sourcePanel.examples],
+ primaryField: sourcePanel.primaryField ? { ...sourcePanel.primaryField } : undefined,
+ secondaryFields: sourcePanel.secondaryFields
+ ? cloneSecondaryFields(sourcePanel.secondaryFields)
+ : undefined,
+ noteField: sourcePanel.noteField ? { ...sourcePanel.noteField } : undefined
+ }
+}
+
+function createAiPageData() {
+ return {
+ title: AI_PAGE_BLUEPRINT.title,
+ historyText: AI_PAGE_BLUEPRINT.historyText,
+ disclaimerText: AI_PAGE_BLUEPRINT.disclaimerText,
+ secondaryEntries: [
+ {
+ key: 'ai-history',
+ title: 'AI历史',
+ subtitle: '查看中医问答与辨证分析的静态历史页',
+ route: ROUTES.tcm.aiHistory
+ },
+ {
+ key: 'mingli-interpret',
+ title: '命理解读',
+ subtitle: '进入命理方向的静态解读页',
+ route: ROUTES.mingli.interpret
+ }
+ ],
+ tabs: cloneTabs(AI_PAGE_BLUEPRINT.tabs),
+ activeTabKey: 'qa',
+ currentPanel: createPanelByKey('qa')
+ }
+}
+
+function showPlaceholderToast(title) {
+ if (typeof wx?.showToast === 'function') {
+ wx.showToast({
+ title: title || '功能建设中',
+ icon: 'none'
+ })
+ }
+}
+
+function showNavigate(route) {
+ if (openStaticRoute(route, wx)) {
+ return
+ }
+
+ showPlaceholderToast()
+}
+
+Page({
+ data: createAiPageData(),
+
+ handleTabTap(event) {
+ const { tabKey } = event.currentTarget.dataset
+
+ if (!AI_PAGE_BLUEPRINT.panels[tabKey] || tabKey === this.data.activeTabKey) {
+ return
+ }
+
+ this.setData({
+ activeTabKey: tabKey,
+ currentPanel: createPanelByKey(tabKey)
+ })
+ },
+
+ handleHistoryTap() {
+ showNavigate(ROUTES.tcm.aiHistory)
+ },
+
+ handleSecondaryEntryTap(event) {
+ showNavigate(event.currentTarget.dataset.route)
+ },
+
+ handleActionTap() {
+ showPlaceholderToast()
+ }
+})
+
+module.exports = {
+ AI_PAGE_BLUEPRINT,
+ createPanelByKey,
+ createAiPageData
+}
diff --git a/pages/ai/index.json b/pages/ai/index.json
new file mode 100644
index 0000000..40cbce1
--- /dev/null
+++ b/pages/ai/index.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "AI助手",
+ "navigationBarBackgroundColor": "#f8ecd8",
+ "navigationBarTextStyle": "black"
+}
diff --git a/pages/ai/index.wxml b/pages/ai/index.wxml
new file mode 100644
index 0000000..4e19874
--- /dev/null
+++ b/pages/ai/index.wxml
@@ -0,0 +1,107 @@
+
+
+ {{title}}
+ {{historyText}}
+
+
+
+
+ {{item.label}}
+
+
+
+
+
+ {{item.title}}
+ {{item.subtitle}}
+
+
+
+
+ {{currentPanel.badge}}
+ {{currentPanel.heading}}
+ {{currentPanel.subheading}}
+
+
+
+ ·
+ {{item}}
+
+
+
+
+
+ {{item}}
+
+
+
+
+ ⚠
+ {{disclaimerText}}
+
+
+
+
+ {{currentPanel.actionTitle}}
+
+
+ {{currentPanel.actionButtonText}}
+
+
+
+
+ {{currentPanel.actionTitle}}
+ {{currentPanel.primaryField.label}}
+
+
+
+
+
+
+
+
+ {{currentPanel.noteField.label}}
+
+
+
+ {{currentPanel.actionButtonText}}
+
+
+
+
+ {{currentPanel.actionTitle}}
+ {{currentPanel.actionDescription}}
+
+ {{currentPanel.actionButtonText}}
+
+
+
diff --git a/pages/ai/index.wxss b/pages/ai/index.wxss
new file mode 100644
index 0000000..49db62f
--- /dev/null
+++ b/pages/ai/index.wxss
@@ -0,0 +1,306 @@
+page {
+ min-height: 100%;
+ background:
+ linear-gradient(180deg, #f7ecd9 0%, #fbf3e6 24%, #f7eedf 100%);
+}
+
+.ai-page {
+ box-sizing: border-box;
+ min-height: 100vh;
+ padding: 16rpx 20rpx 48rpx;
+ background:
+ radial-gradient(circle at top right, rgba(197, 150, 86, 0.12) 0, rgba(197, 150, 86, 0) 26%),
+ linear-gradient(135deg, rgba(255, 255, 255, 0.7) 0%, rgba(255, 248, 238, 0) 38%);
+}
+
+.ai-page__nav {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 22rpx 0 26rpx;
+}
+
+.ai-page__title {
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 62rpx;
+ font-weight: 700;
+ line-height: 1.1;
+ color: #2b1a0e;
+}
+
+.ai-page__history {
+ min-width: 94rpx;
+ padding: 16rpx 22rpx;
+ border-radius: 999rpx;
+ background: rgba(247, 236, 220, 0.92);
+ color: #9f7142;
+ font-size: 24rpx;
+ line-height: 1;
+ text-align: center;
+ box-shadow: 0 10rpx 20rpx rgba(186, 141, 83, 0.08);
+}
+
+.ai-page__tabs {
+ display: flex;
+ margin: 0 -8rpx;
+}
+
+.ai-page__tab {
+ box-sizing: border-box;
+ width: 33.3333%;
+ padding: 0 8rpx;
+}
+
+.ai-page__tab-label {
+ display: block;
+ padding: 24rpx 0;
+ border: 1rpx solid rgba(213, 194, 165, 0.6);
+ border-radius: 999rpx;
+ background: rgba(255, 252, 246, 0.96);
+ color: #a37545;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 30rpx;
+ line-height: 1;
+ text-align: center;
+ box-shadow: 0 12rpx 28rpx rgba(188, 152, 103, 0.08);
+}
+
+.ai-page__tab--active .ai-page__tab-label {
+ border-color: rgba(150, 94, 34, 0.92);
+ background: linear-gradient(135deg, #b07434 0%, #8f5920 100%);
+ color: #fff6ea;
+ box-shadow: 0 14rpx 24rpx rgba(144, 89, 31, 0.26);
+}
+
+.ai-page__secondary-list {
+ margin-top: 18rpx;
+}
+
+.ai-page__secondary-item {
+ padding: 22rpx 24rpx;
+ border: 1rpx solid rgba(212, 189, 152, 0.24);
+ border-radius: 28rpx;
+ background: rgba(255, 250, 243, 0.94);
+ box-shadow: 0 14rpx 28rpx rgba(186, 148, 96, 0.1);
+}
+
+.ai-page__secondary-item + .ai-page__secondary-item {
+ margin-top: 14rpx;
+}
+
+.ai-page__secondary-title {
+ display: block;
+ color: #6d431d;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 34rpx;
+ font-weight: 700;
+ line-height: 1.35;
+}
+
+.ai-page__secondary-subtitle {
+ display: block;
+ margin-top: 8rpx;
+ color: #96704a;
+ font-size: 26rpx;
+ line-height: 1.7;
+}
+
+.panel-card,
+.action-card {
+ margin-top: 20rpx;
+ padding: 28rpx 20rpx 20rpx;
+ border: 1rpx solid rgba(209, 185, 148, 0.22);
+ border-radius: 34rpx;
+ background: rgba(255, 251, 245, 0.95);
+ box-shadow: 0 18rpx 36rpx rgba(178, 145, 96, 0.14);
+}
+
+.panel-card__badge {
+ display: inline-block;
+ padding: 10rpx 18rpx;
+ border-radius: 999rpx;
+ background: rgba(247, 238, 227, 0.96);
+ color: #a36f3d;
+ font-size: 24rpx;
+ line-height: 1;
+}
+
+.panel-card__heading {
+ display: block;
+ margin-top: 24rpx;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 56rpx;
+ font-weight: 700;
+ line-height: 1.22;
+ color: #2c1b10;
+}
+
+.panel-card__subheading {
+ display: block;
+ margin-top: 18rpx;
+ color: #9a7248;
+ font-size: 28rpx;
+ line-height: 1.75;
+}
+
+.panel-card__highlights {
+ margin-top: 16rpx;
+}
+
+.panel-card__highlight {
+ display: flex;
+ align-items: flex-start;
+ margin-top: 8rpx;
+}
+
+.panel-card__highlight-dot {
+ margin-right: 10rpx;
+ color: #a66c34;
+ font-size: 32rpx;
+ line-height: 1.5;
+}
+
+.panel-card__highlight-text {
+ flex: 1;
+ color: #6f4b2d;
+ font-size: 30rpx;
+ line-height: 1.8;
+}
+
+.panel-card__examples {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 18rpx -6rpx 0;
+}
+
+.example-chip {
+ box-sizing: border-box;
+ padding: 0 6rpx 12rpx;
+}
+
+.example-chip__text {
+ display: block;
+ padding: 14rpx 22rpx;
+ border-radius: 999rpx;
+ background: rgba(246, 236, 225, 0.94);
+ color: #9d6d3e;
+ font-size: 26rpx;
+ line-height: 1.2;
+}
+
+.panel-card__notice {
+ display: flex;
+ align-items: center;
+ margin-top: 10rpx;
+ padding: 22rpx 20rpx;
+ border-radius: 24rpx;
+ background: rgba(247, 239, 229, 0.92);
+}
+
+.panel-card__notice-icon {
+ margin-right: 12rpx;
+ color: #d08c2f;
+ font-size: 24rpx;
+ line-height: 1;
+}
+
+.panel-card__notice-text {
+ flex: 1;
+ color: #9b7247;
+ font-size: 26rpx;
+ line-height: 1.6;
+}
+
+.action-card__title {
+ display: block;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 46rpx;
+ font-weight: 700;
+ line-height: 1.25;
+ color: #2d1c11;
+}
+
+.action-card__label {
+ display: block;
+ margin-top: 20rpx;
+ color: #6d492c;
+ font-size: 28rpx;
+ line-height: 1.5;
+}
+
+.action-card__input,
+.action-card__textarea {
+ box-sizing: border-box;
+ width: 100%;
+ margin-top: 16rpx;
+ padding: 18rpx 22rpx;
+ border: 1rpx solid rgba(214, 193, 162, 0.72);
+ border-radius: 24rpx;
+ background: rgba(255, 252, 247, 0.96);
+ color: #8f6842;
+ font-size: 30rpx;
+ line-height: 1.5;
+}
+
+.action-card__input--single {
+ min-height: 86rpx;
+}
+
+.action-card__grid {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 8rpx -8rpx 0;
+}
+
+.action-card__grid-item {
+ box-sizing: border-box;
+ width: 50%;
+ padding: 8rpx;
+}
+
+.action-card__input--half {
+ margin-top: 0;
+}
+
+.action-card__textarea {
+ min-height: 104rpx;
+}
+
+.action-card__description {
+ display: block;
+ margin-top: 20rpx;
+ color: #8e6640;
+ font-size: 30rpx;
+ line-height: 1.8;
+}
+
+.action-card__button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ align-self: center;
+ min-width: 180rpx;
+ margin: 28rpx auto 4rpx;
+ padding: 20rpx 36rpx;
+ border-radius: 24rpx;
+ background: linear-gradient(135deg, #b07434 0%, #8f5920 100%);
+ color: #fff7eb;
+ font-family: 'STSong', 'Songti SC', serif;
+ font-size: 36rpx;
+ font-weight: 700;
+ line-height: 1;
+ text-align: center;
+ box-shadow: 0 14rpx 24rpx rgba(144, 89, 31, 0.22);
+}
+
+.action-card__button--full {
+ display: flex;
+ width: 100%;
+ min-width: 0;
+ min-height: 84rpx;
+ margin-top: 24rpx;
+ border-radius: 22rpx;
+ background: rgba(244, 232, 213, 0.95);
+ color: #7d5327;
+ box-shadow: none;
+}
diff --git a/pages/ai/tab-ai-active.png b/pages/ai/tab-ai-active.png
new file mode 100644
index 0000000..0eef72e
Binary files /dev/null and b/pages/ai/tab-ai-active.png differ
diff --git a/pages/ai/tab-ai.png b/pages/ai/tab-ai.png
new file mode 100644
index 0000000..4e99e92
Binary files /dev/null and b/pages/ai/tab-ai.png differ
diff --git a/pages/home/index.js b/pages/home/index.js
index 0e50e90..67307b3 100644
--- a/pages/home/index.js
+++ b/pages/home/index.js
@@ -1,93 +1,205 @@
-const { getRuntimeConfig } = require('../../config/env')
-const { sessionStore } = require('../../stores')
-const { maskToken } = require('../../utils/util')
+const { ROUTES, openStaticRoute } = require('../../utils/static-ux/route-map')
+const { createTcmHomeHubCards } = require('../../utils/static-ux/tcm')
-const QUICK_ENTRIES = [
- {
- title: '登录主包',
- description: '演示主包内的认证入口、全局会话同步和基础 UI 组件封装。',
- badge: '主包',
- actionText: '打开登录页',
- actionPath: '/pages/login/index'
- },
- {
- title: '业务工作台',
- description: '演示分包页面,只在需要时加载,保持主包轻量和首页启动稳定。',
- badge: '分包',
- actionText: '进入工作台',
- actionPath: '/packages/demo/pages/workbench/index'
- }
-]
+const HOME_PAGE_BLUEPRINT = Object.freeze({
+ brandName: '玄知中医',
+ greeting: '晚上好',
+ subtitle: '今天想学点什么?从典籍、工具或养生主题开始。',
+ searchPlaceholder: '搜索典籍、术语、AI问答...',
+ searchBadge: 'AI',
+ searchRoute: ROUTES.tcm.searchBooks,
+ portalTitle: '学习入口',
+ encyclopediaTitle: '中医百科',
+ encyclopediaCards: [
+ {
+ key: 'classic',
+ icon: '书',
+ title: '经典书城',
+ route: ROUTES.tabs.library
+ },
+ {
+ key: 'meridian',
+ icon: '穴',
+ title: '经络穴位',
+ status: '待开放'
+ },
+ {
+ key: 'disease',
+ icon: '病',
+ title: '疾病百科',
+ status: '待开放'
+ }
+ ],
+ toolsTitle: '学习工具',
+ toolCards: [
+ {
+ key: 'qa',
+ icon: '问',
+ title: 'AI问答',
+ route: ROUTES.tabs.ai
+ },
+ {
+ key: 'formula',
+ icon: '方',
+ title: '方剂笔记',
+ route: `${ROUTES.tcm.assets}?kind=notes`
+ },
+ {
+ key: 'constitution',
+ icon: '诊',
+ title: '体质诊断',
+ route: ROUTES.tabs.ai
+ },
+ {
+ key: 'wellness',
+ icon: '养',
+ title: '智能饮片'
+ }
+ ],
+ wellnessTitle: '养生调理',
+ wellnessCards: [
+ {
+ key: 'constitution-check',
+ icon: '🧬',
+ title: 'AI体质检测',
+ status: '待开放',
+ route: ROUTES.tabs.ai
+ },
+ {
+ key: 'medicated-diet',
+ icon: '🍲',
+ title: '药膳',
+ status: '待开放'
+ },
+ {
+ key: 'ingredient',
+ icon: '🥬',
+ title: '食材',
+ status: '待开放'
+ }
+ ],
+ classicsTitle: '热门典籍',
+ classicsActionText: '进入书城',
+ classicsActionRoute: ROUTES.tabs.library,
+ classicsBooks: [
+ {
+ key: 'huangdi-neijing-suwen',
+ coverText: '黄',
+ title: '黄帝内经素问',
+ route: `${ROUTES.tcm.bookDetail}?scene=classic-a`
+ },
+ {
+ key: 'shang-han-lun',
+ coverText: '伤',
+ title: '伤寒论',
+ route: `${ROUTES.tcm.bookDetail}?scene=classic-b`
+ },
+ {
+ key: 'wen-bing-tiao-bian',
+ coverText: '温',
+ title: '温病条辨',
+ route: `${ROUTES.tcm.searchBooks}?keyword=温病条辨`
+ },
+ {
+ key: 'bencao-gangmu-bieming-lu',
+ coverText: '本',
+ title: '本草纲目别名录',
+ route: `${ROUTES.tcm.searchBooks}?keyword=本草纲目`
+ }
+ ]
+})
-function buildSessionView(state) {
- const userName = state.userInfo?.nickname || state.userInfo?.name || '访客'
+function cloneItems(items) {
+ return items.map(item => ({ ...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 0000000..ff9a151
Binary files /dev/null and b/pages/home/tab-home-active.png differ
diff --git a/pages/home/tab-home.png b/pages/home/tab-home.png
new file mode 100644
index 0000000..90ee771
Binary files /dev/null and b/pages/home/tab-home.png differ
diff --git a/pages/library/index.js b/pages/library/index.js
new file mode 100644
index 0000000..420748b
--- /dev/null
+++ b/pages/library/index.js
@@ -0,0 +1,350 @@
+const { ROUTES, openStaticRoute } = require('../../utils/static-ux/route-map')
+
+const LIBRARY_CATEGORIES = Object.freeze([
+ { id: 'featured', name: '精选' },
+ { id: 'annotation', name: '注解' },
+ { id: 'classic-theory', name: '经论' },
+ { id: 'cold-damage', name: '伤寒金匮' },
+ { id: 'formula', name: '医方' },
+ { id: 'materia-medica', name: '本草' },
+ { id: 'acupuncture', name: '针灸' },
+ { id: 'medical-record', name: '医案' },
+ { id: 'comprehensive', name: '综合' }
+])
+
+const LIBRARY_BOOKS = Object.freeze({
+ featured: [
+ {
+ id: 'huangdi-neijing-suwen',
+ coverText: '典',
+ title: '黄帝内经素问',
+ author: '王冰次',
+ dynasty: '唐',
+ period: '公元762年'
+ },
+ {
+ id: 'shang-han-lun',
+ coverText: '典',
+ title: '伤寒论',
+ author: '张仲景',
+ dynasty: '东汉',
+ period: '公元25-220年'
+ },
+ {
+ id: 'wen-bing-tiao-bian',
+ coverText: '典',
+ title: '温病条辨',
+ author: '吴鞠通',
+ dynasty: '清',
+ period: '公元1644-1911年'
+ },
+ {
+ id: 'bencao-gangmu-bieming-lu',
+ coverText: '典',
+ title: '本草纲目别名录',
+ author: '李时珍',
+ dynasty: '明',
+ period: '公元1368-1644年'
+ },
+ {
+ id: 'huangdi-neijing-jizhu',
+ coverText: '典',
+ title: '黄帝内经灵枢集注',
+ author: '张志聪',
+ dynasty: '清',
+ period: '公元1672年'
+ },
+ {
+ id: 'jingui-yaolue-fanglun',
+ coverText: '典',
+ title: '金匮要略方论',
+ author: '张仲景',
+ dynasty: '汉',
+ period: '公元219年'
+ }
+ ],
+ annotation: [
+ {
+ id: 'huangdi-neijing-suwen-jizhu',
+ coverText: '典',
+ title: '黄帝内经素问集注',
+ author: '张志聪',
+ dynasty: '清',
+ period: '公元1672年'
+ },
+ {
+ id: 'shennong-bencao-jizhu',
+ coverText: '典',
+ title: '神农本草经辑注',
+ author: '孙星衍',
+ dynasty: '清',
+ period: '公元1796年'
+ },
+ {
+ id: 'nanjing-jizhu',
+ coverText: '典',
+ title: '难经集注',
+ author: '王九思',
+ dynasty: '明',
+ period: '公元1506年'
+ }
+ ],
+ 'classic-theory': [
+ {
+ id: 'nan-jing',
+ coverText: '典',
+ title: '难经',
+ author: '扁鹊',
+ dynasty: '战国',
+ period: '约公元前475-221年'
+ },
+ {
+ id: 'lei-jing',
+ coverText: '典',
+ title: '类经',
+ author: '张介宾',
+ dynasty: '明',
+ period: '公元1624年'
+ },
+ {
+ id: 'yi-zong-bi-du',
+ coverText: '典',
+ title: '医宗必读',
+ author: '李中梓',
+ dynasty: '明',
+ period: '公元1637年'
+ }
+ ],
+ 'cold-damage': [
+ {
+ id: 'jin-kui-yao-lve',
+ coverText: '典',
+ title: '金匮要略',
+ author: '张仲景',
+ dynasty: '东汉',
+ period: '公元219年'
+ },
+ {
+ id: 'zhu-jie-shang-han-lun',
+ coverText: '典',
+ title: '注解伤寒论',
+ author: '成无己',
+ dynasty: '金',
+ period: '公元1144年'
+ },
+ {
+ id: 'shang-han-ming-li-lun',
+ coverText: '典',
+ title: '伤寒明理论',
+ author: '成无己',
+ dynasty: '金',
+ period: '公元1156年'
+ }
+ ],
+ formula: [
+ {
+ id: 'tai-ping-hui-min-he-ji-ju-fang',
+ coverText: '典',
+ title: '太平惠民和剂局方',
+ author: '陈师文',
+ dynasty: '宋',
+ period: '公元1107年'
+ },
+ {
+ id: 'pu-ji-fang',
+ coverText: '典',
+ title: '普济方',
+ author: '朱橚',
+ dynasty: '明',
+ period: '公元1406年'
+ },
+ {
+ id: 'yi-fang-kao',
+ coverText: '典',
+ title: '医方考',
+ author: '吴昆',
+ dynasty: '明',
+ period: '公元1584年'
+ }
+ ],
+ 'materia-medica': [
+ {
+ id: 'shennong-bencao-jing',
+ coverText: '典',
+ title: '神农本草经',
+ author: '佚名',
+ dynasty: '汉',
+ period: '约公元1-2世纪'
+ },
+ {
+ id: 'bencao-gangmu',
+ coverText: '典',
+ title: '本草纲目',
+ author: '李时珍',
+ dynasty: '明',
+ period: '公元1578年'
+ },
+ {
+ id: 'bencao-cong-xin',
+ coverText: '典',
+ title: '本草从新',
+ author: '吴仪洛',
+ dynasty: '清',
+ period: '公元1757年'
+ }
+ ],
+ acupuncture: [
+ {
+ id: 'zhen-jiu-jia-yi-jing',
+ coverText: '典',
+ title: '针灸甲乙经',
+ author: '皇甫谧',
+ dynasty: '西晋',
+ period: '公元282年'
+ },
+ {
+ id: 'zhen-jiu-da-cheng',
+ coverText: '典',
+ title: '针灸大成',
+ author: '杨继洲',
+ dynasty: '明',
+ period: '公元1601年'
+ },
+ {
+ id: 'tong-ren-shu-xue-zhen-jiu-tu-jing',
+ coverText: '典',
+ title: '铜人腧穴针灸图经',
+ author: '王惟一',
+ dynasty: '宋',
+ period: '公元1026年'
+ }
+ ],
+ 'medical-record': [
+ {
+ id: 'lin-zheng-zhi-nan-yi-an',
+ coverText: '典',
+ title: '临证指南医案',
+ author: '叶天士',
+ dynasty: '清',
+ period: '公元1746年'
+ },
+ {
+ id: 'shi-an-de-xiao-fang',
+ coverText: '典',
+ title: '石案得效方',
+ author: '危亦林',
+ dynasty: '元',
+ period: '公元1337年'
+ },
+ {
+ id: 'wang-meng-ying-yi-an',
+ coverText: '典',
+ title: '王孟英医案',
+ author: '王孟英',
+ dynasty: '清',
+ period: '公元1862年'
+ }
+ ],
+ comprehensive: [
+ {
+ id: 'yi-xue-ru-men',
+ coverText: '典',
+ title: '医学入门',
+ author: '李梴',
+ dynasty: '明',
+ period: '公元1575年'
+ },
+ {
+ id: 'jing-yue-quan-shu',
+ coverText: '典',
+ title: '景岳全书',
+ author: '张介宾',
+ dynasty: '明',
+ period: '公元1624年'
+ },
+ {
+ id: 'yi-zong-jin-jian',
+ coverText: '典',
+ title: '医宗金鉴',
+ author: '吴谦',
+ dynasty: '清',
+ period: '公元1742年'
+ }
+ ]
+})
+
+function cloneItems(items) {
+ return items.map(item => ({ ...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 0000000..28eeb64
Binary files /dev/null and b/pages/library/tab-library-active.png differ
diff --git a/pages/library/tab-library.png b/pages/library/tab-library.png
new file mode 100644
index 0000000..563ad0e
Binary files /dev/null and b/pages/library/tab-library.png differ
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 0000000..ea1f357
Binary files /dev/null and b/pages/profile/tab-profile-active.png differ
diff --git a/pages/profile/tab-profile.png b/pages/profile/tab-profile.png
new file mode 100644
index 0000000..0866d3c
Binary files /dev/null and b/pages/profile/tab-profile.png differ
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
+}