feat: migrate static pages to native tabbar

This commit is contained in:
2026-04-23 21:25:24 +08:00
parent f3cd0c3a98
commit cd30f57f2c
116 changed files with 7143 additions and 311 deletions

185
pages/ai/index.js Normal file
View File

@@ -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
}

5
pages/ai/index.json Normal file
View File

@@ -0,0 +1,5 @@
{
"navigationBarTitleText": "AI助手",
"navigationBarBackgroundColor": "#f8ecd8",
"navigationBarTextStyle": "black"
}

107
pages/ai/index.wxml Normal file
View File

@@ -0,0 +1,107 @@
<view class="ai-page">
<view class="ai-page__nav">
<text class="ai-page__title">{{title}}</text>
<view class="ai-page__history" bindtap="handleHistoryTap">{{historyText}}</view>
</view>
<view class="ai-page__tabs">
<view
class="ai-page__tab {{activeTabKey === item.key ? 'ai-page__tab--active' : ''}}"
wx:for="{{tabs}}"
wx:key="key"
data-tab-key="{{item.key}}"
bindtap="handleTabTap"
>
<text class="ai-page__tab-label">{{item.label}}</text>
</view>
</view>
<view class="ai-page__secondary-list">
<view
class="ai-page__secondary-item"
wx:for="{{secondaryEntries}}"
wx:key="key"
data-route="{{item.route}}"
bindtap="handleSecondaryEntryTap"
>
<text class="ai-page__secondary-title">{{item.title}}</text>
<text class="ai-page__secondary-subtitle">{{item.subtitle}}</text>
</view>
</view>
<view class="panel-card">
<view class="panel-card__badge">{{currentPanel.badge}}</view>
<text class="panel-card__heading">{{currentPanel.heading}}</text>
<text class="panel-card__subheading">{{currentPanel.subheading}}</text>
<view class="panel-card__highlights">
<view class="panel-card__highlight" wx:for="{{currentPanel.highlights}}" wx:key="index">
<text class="panel-card__highlight-dot">·</text>
<text class="panel-card__highlight-text">{{item}}</text>
</view>
</view>
<view class="panel-card__examples">
<view class="example-chip" wx:for="{{currentPanel.examples}}" wx:key="index">
<text class="example-chip__text">{{item}}</text>
</view>
</view>
<view class="panel-card__notice">
<text class="panel-card__notice-icon">⚠</text>
<text class="panel-card__notice-text">{{disclaimerText}}</text>
</view>
</view>
<view class="action-card" wx:if="{{currentPanel.formType === 'qa'}}">
<text class="action-card__title">{{currentPanel.actionTitle}}</text>
<input
class="action-card__input action-card__input--single"
placeholder="{{currentPanel.inputPlaceholder}}"
disabled="{{true}}"
/>
<view class="action-card__button" bindtap="handleActionTap">
{{currentPanel.actionButtonText}}
</view>
</view>
<view class="action-card" wx:if="{{currentPanel.formType === 'analysis'}}">
<text class="action-card__title">{{currentPanel.actionTitle}}</text>
<text class="action-card__label">{{currentPanel.primaryField.label}}</text>
<input
class="action-card__input action-card__input--single"
placeholder="{{currentPanel.primaryField.placeholder}}"
disabled="{{true}}"
/>
<view class="action-card__grid">
<view class="action-card__grid-item" wx:for="{{currentPanel.secondaryFields}}" wx:key="key">
<input
class="action-card__input action-card__input--half"
placeholder="{{item.placeholder}}"
disabled="{{true}}"
/>
</view>
</view>
<text class="action-card__label">{{currentPanel.noteField.label}}</text>
<textarea
class="action-card__textarea"
placeholder="{{currentPanel.noteField.placeholder}}"
auto-height="{{true}}"
disabled="{{true}}"
></textarea>
<view class="action-card__button" bindtap="handleActionTap">
{{currentPanel.actionButtonText}}
</view>
</view>
<view class="action-card" wx:if="{{currentPanel.formType === 'constitution'}}">
<text class="action-card__title">{{currentPanel.actionTitle}}</text>
<text class="action-card__description">{{currentPanel.actionDescription}}</text>
<view class="action-card__button action-card__button--full" bindtap="handleActionTap">
{{currentPanel.actionButtonText}}
</view>
</view>
</view>

306
pages/ai/index.wxss Normal file
View File

@@ -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;
}

BIN
pages/ai/tab-ai-active.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
pages/ai/tab-ai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB