feat: 同步今日小程序与后台迭代版本

集中提交今日 API、管理端、小程序与部署文档调整,确保 Gitea 主分支与本地最新开发版本一致。

Made-with: Cursor
This commit is contained in:
卡若
2026-04-20 11:07:55 +08:00
parent 7108f28280
commit ce55c48c64
180 changed files with 11659 additions and 6078 deletions

View File

@@ -8,7 +8,10 @@ Page({
hasMore: true
},
onShow() {
this.setData({ list: [], page: 1, hasMore: true }, () => this.fetchPage())
const { ensureRuntimeThenGate } = require('../../utils/miniprogramAuditGate.js')
ensureRuntimeThenGate(() => {
this.setData({ list: [], page: 1, hasMore: true }, () => this.fetchPage())
})
},
fetchPage() {
const { page, pageSize, list, hasMore } = this.data

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,6 @@
"navigationBarTitleText": "神仙 AI",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#F5F3FF",
"backgroundColor": "#ffffff",
"usingComponents": {}
}

View File

@@ -1,72 +1,75 @@
<!-- 神仙 AI · 对话页 -->
<!-- 神仙 AI · 对话页(精选推荐嵌入第 2 轮及以后的助手气泡 · MBTI 去测进测评列表) -->
<view class="ai-page">
<!-- 推荐文章:超管开启后显示;默认折叠(由后台 sectionExpandedDefault 控制首次是否展开) -->
<view class="soul-articles-wrap" wx:if="{{articlesDisplayEnabled}}">
<view class="articles-fold-bar" bindtap="onToggleArticlesFold">
<view class="articles-fold-left">
<text class="articles-fold-title">精选推荐</text>
<text class="articles-fold-pill">Soul</text>
<text class="articles-fold-count" wx:if="{{!articlesBlockExpanded}}">· {{articles.length}} 篇</text>
</view>
<text class="articles-fold-chevron">{{articlesBlockExpanded ? '▼' : '▶'}}</text>
</view>
<view class="soul-articles" wx:if="{{articlesBlockExpanded && articles.length > 0}}">
<view class="articles-list">
<view class="article-card" wx:for="{{articles}}" wx:key="id" bindtap="onTapArticle" data-id="{{item.id}}" data-url="{{item.url}}" data-title="{{item.title}}">
<view class="article-cover-wrap">
<image class="article-cover" src="{{item.cover || defaultCover}}" mode="aspectFill" />
</view>
<view class="article-body">
<text class="article-title">{{item.title}}</text>
<view class="article-meta">
<text class="article-pill">{{item.tag || 'MBTI'}}</text>
<text class="article-time" wx:if="{{item.publishedAt}}">{{item.publishedAt}}</text>
</view>
</view>
<scroll-view class="chat-scroll" scroll-y="true" scroll-into-view="{{scrollTarget}}" scroll-with-animation="true" enhanced show-scrollbar="{{false}}">
<!-- MBTI 引导:去测 → 详细性格测试列表(不直跳拍摄) -->
<view class="mbti-remind-card" wx:if="{{messages.length === 0}}">
<image class="mbti-remind-avatar" src="/images/shenxian-ai-logo.png" mode="aspectFill" />
<view class="mbti-remind-body">
<view class="mbti-remind-title">我是神仙 AI</view>
<view class="mbti-remind-sub" wx:if="{{mbtiType}}">
已了解你的 <text class="mbti-badge">{{mbtiType}}</text>,继续聊~
</view>
<view class="mbti-remind-sub" wx:else>先做一下 MBTI 测评,我能帮你聊得更懂你哦~</view>
<view class="mbti-remind-actions">
<text class="mbti-remind-tag">MBTI</text>
<view class="mbti-remind-btn" hover-class="mbti-remind-btn--press" bindtap="onGoMbtiTest">去测 MBTI</view>
</view>
</view>
</view>
</view>
<!-- MBTI 提醒:去测 → 详细性格测试列表 -->
<view class="mbti-remind-card">
<view class="mbti-remind-row">
<image class="mbti-remind-avatar" src="/images/shenxian-oldman-circle.png" mode="aspectFill" />
<view class="mbti-remind-texts">
<view class="mbti-remind-title">我是神仙 AI</view>
<view class="mbti-remind-sub">{{mbtiType ? ('已了解你的 ' + mbtiType + ',继续聊~') : '先做一下 MBTI 测评,我能帮你聊得更懂你哦~'}}</view>
</view>
</view>
<view class="mbti-remind-actions">
<text class="mbti-remind-tag">MBTI</text>
<view class="mbti-remind-btn" hover-class="mbti-remind-btn--press" bindtap="onGoMbtiTest">去测 MBTI</view>
</view>
</view>
<!-- 聊天区 -->
<scroll-view class="chat-scroll" scroll-y="true" scroll-into-view="{{scrollTarget}}" scroll-with-animation="true" enhanced show-scrollbar="{{false}}">
<!-- 消息气泡 -->
<view class="msg-wrap" wx:for="{{messages}}" wx:key="id" id="msg-{{item.id}}">
<view wx:if="{{item.role === 'assistant'}}" class="msg msg-ai">
<image class="avatar-ai" src="/images/shenxian-oldman-circle.png" mode="aspectFill" />
<image class="avatar-ai" src="/images/shenxian-ai-logo.png" mode="aspectFill" />
<view class="bubble bubble-ai {{item.isDegraded ? 'bubble-degraded' : ''}}">
<text selectable="true">{{item.content}}</text>
<view class="bubble-ai-inner">
<text class="bubble-ai-text" user-select="true">{{item.content}}</text>
</view>
<!-- 精选推荐:周期抽检 + 意图与正文相关;功能卡点击跳转,文章卡走原文/外链 -->
<view
wx:if="{{item.inlineArticle}}"
class="inline-reco"
catchtap="onTapArticle"
data-id="{{item.inlineArticle.id}}"
data-url="{{item.inlineArticle.url}}"
data-title="{{item.inlineArticle.title}}"
data-source-id="{{item.inlineArticle.sourceId}}"
data-nav-path="{{item.inlineArticle.navPath}}"
>
<view class="inline-reco-icons" wx:if="{{item.inlineArticle.icons && item.inlineArticle.icons.length}}">
<text class="inline-reco-ic" wx:for="{{item.inlineArticle.icons}}" wx:for-item="ic" wx:key="*this">{{ic}}</text>
</view>
<view class="inline-reco-bar">
<text class="inline-reco-label">精选推荐</text>
<text class="inline-reco-hint">{{item.inlineArticle.readCountLabel || '阅读'}}</text>
</view>
<view class="inline-reco-card">
<image class="inline-reco-cover" src="{{item.inlineArticle.cover || defaultCover}}" mode="aspectFill" />
<view class="inline-reco-body">
<text class="inline-reco-title">{{item.inlineArticle.title}}</text>
<view class="inline-reco-meta">
<text class="inline-reco-tag">{{item.inlineArticle.tag || 'MBTI'}}</text>
<text class="inline-reco-time">{{item.inlineArticle.publishedAt}}</text>
</view>
</view>
</view>
</view>
<view class="bubble-meta" wx:if="{{item.isDegraded}}">(降级兜底)</view>
</view>
</view>
<view wx:else class="msg msg-me">
<view class="bubble bubble-me">
<text selectable="true">{{item.content}}</text>
<text user-select="true">{{item.content}}</text>
</view>
</view>
</view>
<view class="typing" wx:if="{{sending}}" id="msg-typing">
<image class="avatar-ai" src="/images/shenxian-oldman-circle.png" mode="aspectFill" />
<view class="bubble bubble-ai"><text>神仙在思考…</text></view>
<image class="avatar-ai" src="/images/shenxian-ai-logo.png" mode="aspectFill" />
<view class="bubble bubble-ai">
<view class="bubble-ai-inner"><text class="bubble-ai-text">神仙AI在思考…</text></view>
</view>
</view>
<!-- 深度报告 CTA对话 >=10 条且还没付费时显示 -->
<view class="report-cta" wx:if="{{showReportCta}}" bindtap="onTapReportCta">
<view class="report-cta-text">✨ 聊了这么多,来份专属 AI 深度画像报告¥9.9</view>
<view class="report-cta-btn">立即生成</view>
@@ -75,39 +78,58 @@
<view class="scroll-bottom" id="bottom"></view>
</scroll-view>
<!-- 底部快捷问法(常驻 -->
<scroll-view class="quick-bottom-scroll" scroll-x="true" show-scrollbar="{{false}}" wx:if="{{quickQuestions.length > 0}}">
<view class="quick-bottom-list">
<view class="quick-bottom-item" wx:for="{{quickQuestions}}" wx:for-item="q" wx:key="*this" bindtap="onTapQuick" data-q="{{q}}">
{{q}}
</view>
<!-- 与 Tab 之间的白底垫层,避免聊天记录从输入条/底栏缝隙透出pointer-events:none 不挡操作 -->
<view class="ai-bottom-solid" aria-hidden="true"></view>
<!-- 底部:快捷语(横滑)+ 输入区,固定在 Tab 之上 -->
<view class="composer-wrap">
<view class="quick-chips-outer" wx:if="{{quickQuestions.length > 0}}">
<scroll-view
class="quick-chips-scroll"
scroll-x="{{true}}"
enhanced="{{true}}"
show-scrollbar="{{false}}"
>
<view class="quick-chips-row">
<view
class="quick-chip"
wx:for="{{quickQuestions}}"
wx:for-item="q"
wx:key="*this"
data-q="{{q}}"
bindtap="onTapQuick"
>{{q}}</view>
</view>
</scroll-view>
<view class="quick-chips-edge-fade" aria-hidden="true"></view>
</view>
</scroll-view>
<view class="input-bar">
<view class="resume-entry" bindtap="onTapUploadResume" hover-class="resume-entry--press">
<text class="resume-entry-icon">📎</text>
<text class="resume-entry-txt">简历</text>
</view>
<input
class="input"
placeholder="问你想了解的自己"
placeholder-class="input-ph"
value="{{draft}}"
bindinput="onInput"
bindconfirm="onSend"
confirm-type="send"
confirm-hold="true"
adjust-position="{{true}}"
maxlength="800"
disabled="{{sending}}"
/>
<!-- 输入区:输入框 + 发送 -->
<view class="input-bar">
<input
class="input"
placeholder="问我你想了解的自己"
placeholder-class="input-ph"
value="{{draft}}"
bindinput="onInput"
bindconfirm="onSend"
confirm-type="send"
confirm-hold="true"
adjust-position="{{true}}"
maxlength="800"
disabled="{{sending}}"
/>
<view
class="send-btn {{draft && !sending ? 'send-btn--active' : ''}} {{sending || !draft ? 'send-btn--disabled' : ''}}"
hover-class="{{draft && !sending ? 'send-btn--press' : ''}}"
hover-start-time="0"
hover-stay-time="80"
bindtap="onSend"
>
<text class="send-btn__label">发送</text>
<button
class="send-icon-btn {{draft ? 'send-icon-btn--active' : ''}}"
bindtap="onSend"
disabled="{{sending || !draft}}"
hover-class="send-icon-btn--hover"
>
<view class="send-icon-plane"></view>
</button>
</view>
</view>
</view>

View File

@@ -1,95 +1,42 @@
/* 神仙 AI · 对话页样式 */
page {
height: 100%;
background: linear-gradient(180deg, #F5F3FF 0%, #FFFFFF 40%);
background-color: #ffffff;
}
.ai-page {
min-height: 100vh;
display: flex;
flex-direction: column;
/* 给底部 tabBar + 输入栏预留空间;使用全局变量避免不同机型遮挡 */
padding-bottom: calc(var(--tabbar-pad, 160rpx) + 150rpx + env(safe-area-inset-bottom));
background-color: #ffffff;
/* TabBar + 底部 composer快捷语 + 输入行) */
padding-bottom: calc(var(--tabbar-pad, 170rpx) + 200rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
/* 推荐区块:折叠条 */
.soul-articles-wrap {
margin: 0 24rpx 12rpx;
}
.articles-fold-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18rpx 20rpx;
background: #ffffff;
border-radius: 16rpx;
border: 1rpx solid #e9d5ff;
box-shadow: 0 2rpx 10rpx rgba(124, 58, 237, 0.06);
}
.articles-fold-left {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10rpx;
min-width: 0;
}
.articles-fold-title {
font-size: 28rpx;
font-weight: 700;
color: #4c1d95;
}
.articles-fold-pill {
font-size: 20rpx;
color: #6d28d9;
background: #ede9fe;
padding: 4rpx 14rpx;
border-radius: 100rpx;
font-weight: 600;
}
.articles-fold-count {
font-size: 22rpx;
color: #9ca3af;
}
.articles-fold-chevron {
font-size: 22rpx;
color: #7c3aed;
flex-shrink: 0;
margin-left: 12rpx;
}
/* MBTI 测评提醒卡 */
/* MBTI 引导卡 */
.mbti-remind-card {
margin: 0 24rpx 14rpx;
padding: 20rpx 22rpx;
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
border-radius: 20rpx;
margin: 8rpx 0 16rpx;
padding: 22rpx 24rpx;
background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%);
border: 1rpx solid #e9d5ff;
box-shadow: 0 4rpx 14rpx rgba(124, 58, 237, 0.08);
}
.mbti-remind-row {
border-radius: 24rpx;
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 14rpx;
margin-bottom: 16rpx;
gap: 18rpx;
}
.mbti-remind-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
border: 2rpx solid #e9d5ff;
flex-shrink: 0;
border: 2rpx solid #e9d5ff;
background: #fff;
}
.mbti-remind-texts {
.mbti-remind-body {
flex: 1;
min-width: 0;
}
@@ -98,213 +45,61 @@ page {
font-size: 30rpx;
font-weight: 700;
color: #1f1b4d;
margin-bottom: 6rpx;
margin-bottom: 8rpx;
}
.mbti-remind-sub {
font-size: 24rpx;
color: #6b5f8a;
font-size: 26rpx;
color: #4b5563;
line-height: 1.45;
margin-bottom: 16rpx;
}
.mbti-badge {
color: #7c3aed;
font-weight: 700;
}
.mbti-remind-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
flex-wrap: wrap;
}
.mbti-remind-tag {
font-size: 22rpx;
font-weight: 600;
color: #5b21b6;
background: rgba(255, 255, 255, 0.85);
padding: 8rpx 20rpx;
color: #6d28d9;
background: #fff;
padding: 6rpx 18rpx;
border-radius: 100rpx;
border: 1rpx solid #ddd6fe;
}
.mbti-remind-btn {
flex-shrink: 0;
padding: 14rpx 36rpx;
font-size: 26rpx;
font-weight: 700;
color: #ffffff;
border-radius: 100rpx;
background: linear-gradient(135deg, #9333ea 0%, #7c3aed 100%);
box-shadow: 0 4rpx 14rpx rgba(124, 58, 237, 0.35);
font-weight: 600;
color: #fff;
padding: 12rpx 36rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
box-shadow: 0 6rpx 20rpx rgba(124, 58, 237, 0.35);
}
.mbti-remind-btn--press {
opacity: 0.88;
transform: scale(0.98);
}
/* 顶部推荐文章列表 */
.soul-articles {
padding: 12rpx 0 0;
}
.articles-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.article-card {
display: flex;
align-items: stretch;
width: 100%;
background: #ffffff;
border-radius: 18rpx;
box-shadow: 0 6rpx 20rpx rgba(124, 58, 237, 0.1);
border: 1rpx solid rgba(124, 58, 237, 0.08);
overflow: hidden;
}
.article-cover-wrap {
width: 200rpx;
min-width: 200rpx;
padding: 10rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #faf5ff 0%, #f3e8ff 100%);
}
.article-cover {
width: 100%;
height: 120rpx;
border-radius: 14rpx;
background: #ede9fe;
box-shadow: 0 2rpx 8rpx rgba(124, 58, 237, 0.12);
}
.article-body {
flex: 1;
min-width: 0;
padding: 10rpx 12rpx;
}
.article-title {
font-size: 22rpx;
color: #1F1B4D;
line-height: 1.4;
font-weight: 600;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
white-space: normal;
}
.article-meta {
margin-top: 6rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.article-pill {
font-size: 20rpx;
color: #5b21b6;
background: linear-gradient(135deg, #ede9fe 0%, #f5f3ff 100%);
padding: 4rpx 14rpx;
border-radius: 100rpx;
font-weight: 600;
border: 1rpx solid #ddd6fe;
}
.article-time {
font-size: 18rpx;
color: #9CA3AF;
}
/* 聊天区flex:1 撑开,不再用 100vh 硬算 */
/* 聊天区:不透明白底,避免页面渐变从气泡后透出 */
.chat-scroll {
flex: 1;
min-height: 0;
padding: 8rpx 24rpx 160rpx;
margin: 0 16rpx;
padding: 16rpx 20rpx 32rpx;
box-sizing: border-box;
}
/* 欢迎卡:紧凑化,不再占整屏 */
.welcome {
margin: 8rpx 0 20rpx;
padding: 24rpx 28rpx;
background: linear-gradient(135deg, #F5F3FF 0%, #EDE9FE 100%);
border: 1rpx solid #EDE9FE;
background-color: #ffffff;
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.welcome-head {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
}
.welcome-logo {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
border: 2rpx solid #E9D5FF;
box-shadow: 0 2rpx 10rpx rgba(124, 58, 237, 0.2);
}
.welcome-title {
font-size: 30rpx;
font-weight: 700;
color: #1F1B4D;
}
.welcome-sub {
font-size: 24rpx;
color: #6B6894;
margin-bottom: 16rpx;
line-height: 1.5;
}
.mbti-badge {
display: inline-block;
margin: 0 4rpx;
padding: 2rpx 14rpx;
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
color: #ffffff;
border-radius: 100rpx;
font-weight: 600;
font-size: 22rpx;
}
.quick-title {
font-size: 24rpx;
color: #4C1D95;
font-weight: 600;
margin-bottom: 10rpx;
}
.quick-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.quick-item {
background: #ffffff;
border-radius: 100rpx;
padding: 14rpx 22rpx;
font-size: 24rpx;
color: #4C1D95;
border: 1rpx solid #DDD6FE;
}
.quick-item:active {
background: #EDE9FE;
transform: scale(0.98);
border: 1rpx solid rgba(229, 231, 235, 0.9);
}
/* 消息气泡 */
@@ -347,10 +142,30 @@ page {
}
.bubble-ai {
background: #ffffff;
background-color: #ffffff;
color: #1F1B4D;
border-top-left-radius: 4rpx;
box-shadow: 0 2rpx 10rpx rgba(31, 27, 77, 0.06);
box-shadow: 0 2rpx 12rpx rgba(31, 27, 77, 0.08);
border: 1rpx solid rgba(243, 244, 246, 0.98);
/* 避免开发者工具/部分机型上 text 与 scroll-view 叠层导致「透底」 */
isolation: isolate;
overflow: hidden;
}
.bubble-ai-inner {
background-color: #ffffff !important;
border-radius: inherit;
}
.bubble-ai-text {
display: block;
background-color: #ffffff !important;
color: inherit;
}
/* 强制不透底(部分基础库上 text 在 scroll-view 内会透出下层背景) */
.bubble-ai:not(.bubble-degraded) {
background-color: #ffffff !important;
}
.bubble-me {
@@ -365,6 +180,96 @@ page {
border: 1rpx solid #FED7AA;
}
.bubble-degraded .bubble-ai-inner,
.bubble-degraded .bubble-ai-text {
background-color: #FFF7ED;
color: #C2410C;
}
/* 助手气泡内嵌:精选推荐(第 2 轮及以后) */
.inline-reco-icons {
display: flex;
flex-direction: row;
align-items: center;
gap: 10rpx;
margin-bottom: 10rpx;
padding: 0 4rpx;
}
.inline-reco-ic {
font-size: 34rpx;
line-height: 1;
}
.inline-reco {
margin-top: 16rpx;
padding-top: 14rpx;
border-top: 1rpx solid rgba(124, 58, 237, 0.12);
}
.inline-reco-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10rpx;
}
.inline-reco-label {
font-size: 22rpx;
font-weight: 700;
color: #7c3aed;
}
.inline-reco-hint {
font-size: 22rpx;
color: #9ca3af;
}
.inline-reco-card {
display: flex;
align-items: stretch;
border-radius: 14rpx;
overflow: hidden;
background: #faf5ff;
border: 1rpx solid rgba(124, 58, 237, 0.15);
}
.inline-reco-cover {
width: 148rpx;
min-width: 148rpx;
height: 100rpx;
background: linear-gradient(135deg, #a78bfa 0%, #7c3aed 100%);
}
.inline-reco-body {
flex: 1;
min-width: 0;
padding: 10rpx 12rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.inline-reco-title {
font-size: 24rpx;
font-weight: 600;
color: #1f1b4d;
line-height: 1.35;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.inline-reco-meta {
margin-top: 6rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.inline-reco-tag {
font-size: 18rpx;
color: #7c3aed;
background: #ede9fe;
padding: 2rpx 10rpx;
border-radius: 100rpx;
}
.inline-reco-time {
font-size: 18rpx;
color: #9ca3af;
}
.bubble-meta {
margin-top: 8rpx;
font-size: 20rpx;
@@ -398,62 +303,160 @@ page {
font-weight: 600;
flex: 1;
padding-right: 12rpx;
line-height: 1.4;
display: flex;
align-items: center;
}
.report-cta-btn {
padding: 10rpx 22rpx;
flex-shrink: 0;
min-height: 56rpx;
padding: 12rpx 22rpx;
box-sizing: border-box;
border-radius: 100rpx;
background: #78350F;
color: #FEF3C7;
font-size: 24rpx;
}
.quick-bottom-scroll {
position: fixed;
left: 0;
right: 0;
bottom: calc(var(--tabbar-height, 132rpx) + env(safe-area-inset-bottom) + 84rpx);
padding: 0 16rpx 4rpx;
z-index: 21;
}
.quick-bottom-list {
white-space: nowrap;
}
.quick-bottom-item {
display: inline-block;
margin-right: 10rpx;
padding: 10rpx 16rpx;
border-radius: 999rpx;
background: #F5F3FF;
color: #5B21B6;
font-size: 22rpx;
border: 1rpx solid #DDD6FE;
}
/* ==== 输入区:紧贴自定义 tabBar 上方 ==== */
.input-bar {
position: fixed;
left: 0;
right: 0;
bottom: calc(var(--tabbar-height, 132rpx) + env(safe-area-inset-bottom));
background: #ffffff;
padding: 12rpx 16rpx 12rpx 20rpx;
font-weight: 600;
line-height: 1.25;
display: flex;
align-items: center;
gap: 10rpx;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.04);
border-top: 1rpx solid #F3F4F6;
z-index: 20;
justify-content: center;
text-align: center;
}
/* 底栏与输入区背后的不透明白层(低于自定义 TabBar 9999高于页面滚动内容 */
.ai-bottom-solid {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 9990;
height: calc(var(--tabbar-height, 168rpx) + env(safe-area-inset-bottom) + 260rpx);
background-color: #ffffff;
pointer-events: none;
}
/* ==== 底部 composer快捷语 + 输入(整块贴在 Tab 上沿)==== */
.composer-wrap {
position: fixed;
left: 0;
right: 0;
bottom: calc(var(--tabbar-height, 168rpx) + env(safe-area-inset-bottom));
background-color: #ffffff;
z-index: 9998;
box-shadow: 0 -6rpx 24rpx rgba(15, 23, 42, 0.08);
border-top: 1rpx solid #e5e7eb;
}
/* 横滑快捷语:右侧极淡渐变,暗示可横滑(无文字) */
.quick-chips-outer {
position: relative;
width: 100%;
background-color: #ffffff;
}
.quick-chips-edge-fade {
pointer-events: none;
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 56rpx;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.75) 45%,
#ffffff 100%
);
}
.quick-chips-scroll {
width: 100%;
white-space: nowrap;
height: 72rpx;
padding: 10rpx 0 6rpx;
box-sizing: border-box;
background-color: #ffffff;
}
.quick-chips-row {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 12rpx;
padding: 0 20rpx;
height: 56rpx;
}
.quick-chip {
flex-shrink: 0;
max-width: 520rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 24rpx;
color: #374151;
line-height: 1.3;
padding: 12rpx 24rpx;
background: #f3f4f6;
border-radius: 999rpx;
border: 1rpx solid #e5e7eb;
}
.quick-chip:active {
opacity: 0.95;
background: #ffffff;
}
.input-bar {
position: relative;
left: auto;
right: auto;
bottom: auto;
background-color: #ffffff;
padding: 8rpx 20rpx 14rpx;
display: flex;
align-items: center;
gap: 12rpx;
border-top: 1rpx solid #f3f4f6;
box-shadow: none;
}
.resume-entry {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 64rpx;
padding: 4rpx 8rpx 0;
box-sizing: border-box;
}
.resume-entry--press {
opacity: 0.85;
}
.resume-entry-icon {
font-size: 32rpx;
line-height: 1;
}
.resume-entry-txt {
font-size: 20rpx;
color: #6b7280;
margin-top: 2rpx;
line-height: 1.2;
}
.input {
flex: 1;
min-width: 0;
background: #F3F4F6;
border-radius: 100rpx;
padding: 12rpx 20rpx;
font-size: 27rpx;
min-height: 62rpx;
padding: 16rpx 28rpx;
font-size: 28rpx;
min-height: 72rpx;
color: #1F1B4D;
}
@@ -462,46 +465,47 @@ page {
font-size: 26rpx;
}
/* 发送:独立按钮块(非原生 button样式可控 */
.send-btn {
flex-shrink: 0;
display: flex;
/* 发送:仅图标,小圆钮 */
.send-icon-btn {
width: 72rpx !important;
height: 72rpx !important;
min-width: 72rpx !important;
padding: 0 !important;
margin: 0 !important;
border-radius: 50% !important;
background: #e5e7eb !important;
display: flex !important;
align-items: center;
justify-content: center;
padding: 0 28rpx;
min-width: 108rpx;
height: 64rpx;
border-radius: 16rpx;
margin-left: 16rpx;
margin-right: 4rpx;
box-sizing: border-box;
background: linear-gradient(180deg, #f3f4f6 0%, #e5e7eb 100%);
border: 2rpx solid #d1d5db;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.06);
line-height: 1;
}
.send-btn__label {
font-size: 28rpx;
font-weight: 600;
color: #6b7280;
.send-icon-btn::after {
border: none !important;
}
.send-btn--active {
background: linear-gradient(180deg, #9333ea 0%, #7c3aed 100%);
border-color: #6d28d9;
box-shadow: 0 4rpx 14rpx rgba(124, 58, 237, 0.35);
.send-icon-btn[disabled] {
opacity: 0.55;
}
.send-btn--active .send-btn__label {
color: #ffffff;
.send-icon-btn--active {
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%) !important;
}
.send-btn--disabled {
opacity: 0.5;
.send-icon-btn--active .send-icon-plane {
filter: brightness(0) invert(1);
}
.send-btn--press {
transform: scale(0.97);
.send-icon-btn--hover {
opacity: 0.92;
}
.send-icon-plane {
width: 36rpx;
height: 36rpx;
background-repeat: no-repeat;
background-position: center;
background-size: 36rpx 36rpx;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><line x1='22' y1='2' x2='11' y2='13'/><polygon points='22 2 15 22 11 13 2 9 22 2' fill='%236b7280' stroke='%236b7280'/></svg>");
}

View File

@@ -1,6 +1,8 @@
// 神仙 AI · 深度画像报告页
const { request } = require('../../utils/request.js')
const analytics = require('../../utils/analytics.js')
const { wxPay } = require('../../utils/payment.js')
const { getEnterpriseIdForApiPayload } = require('../../utils/enterpriseContext.js')
Page({
data: {
@@ -12,11 +14,14 @@ Page({
},
onLoad(options) {
const cid = parseInt(options && options.cid, 10) || 0
const rid = parseInt(options && options.rid, 10) || 0
this.setData({ cid, reportId: rid })
analytics.track('page_view', { pagePath: 'pages/ai-chat/report' })
this.initReport()
const { ensureRuntimeThenGate } = require('../../utils/miniprogramAuditGate.js')
ensureRuntimeThenGate(() => {
const cid = parseInt(options && options.cid, 10) || 0
const rid = parseInt(options && options.rid, 10) || 0
this.setData({ cid, reportId: rid })
analytics.track('page_view', { pagePath: 'pages/ai-chat/report' })
this.initReport()
})
},
onUnload() {
@@ -77,76 +82,66 @@ Page({
this.loadReport(this.data.reportId)
},
/** 付费解锁:先尝试 dev 模式,生产环境走 payment/create 下单 */
/** 付费解锁:与全站 payment.js 一致orderId + openId + 支付后轮询查单触发 markPaid */
onTapPay() {
if (this.data.paying) return
this.setData({ paying: true })
analytics.track('ai_report_pay_tap', { reportId: this.data.reportId })
const report = this.data.report || {}
const orderSn = report.orderSn || ''
const orderSn = String(report.orderSn || '').trim()
if (!orderSn) {
wx.showToast({ title: '订单初始化中,请稍后再试', icon: 'none' })
return
}
// 1) 尝试调用现有 /api/payment/create 下单
request({
url: '/api/payment/create',
method: 'POST',
data: {
productType: 'ai_deep_report',
productId: this.data.reportId,
amount: report.priceFen || 990,
orderSn
},
success: (res) => {
const body = (res && res.data) || {}
if (body.code === 200 && body.data && body.data.timeStamp) {
const p = body.data
wx.requestPayment({
timeStamp: p.timeStamp,
nonceStr: p.nonceStr,
package: p.package,
signType: p.signType || 'RSA',
paySign: p.paySign,
success: () => {
wx.showToast({ title: '支付成功', icon: 'success' })
analytics.track('ai_report_pay_success', { reportId: this.data.reportId })
setTimeout(() => this.loadReport(this.data.reportId), 1500)
this.setData({ paying: false })
},
fail: () => {
wx.showToast({ title: '已取消支付', icon: 'none' })
this.setData({ paying: false })
}
})
} else {
// 2) 下单失败(比如接口不支持该 productType→ 兜底 dev markPaid仅超管可用
this._fallbackDevMarkPaid()
}
},
fail: () => this._fallbackDevMarkPaid()
})
},
_fallbackDevMarkPaid() {
// 仅在配置了 dev 模式或超管账号时生效
request({
url: `/api/ai/report/${this.data.reportId}/mark-paid-dev`,
method: 'POST',
success: (res) => {
const body = (res && res.data) || {}
if (body.code === 200) {
wx.showToast({ title: '解锁成功', icon: 'success' })
this.setData({ paying: false })
setTimeout(() => this.loadReport(this.data.reportId), 800)
} else {
wx.showToast({ title: body.message || '支付接口未开通', icon: 'none' })
this.setData({ paying: false })
}
},
fail: () => {
wx.showToast({ title: '支付通道异常,稍后再试', icon: 'none' })
this.setData({ paying: false })
const runPay = () => {
const appInst = getApp()
const oid =
(appInst && appInst.globalData && appInst.globalData.openId) ||
(() => {
try {
const u = wx.getStorageSync('userInfo')
return u ? (u.openid || u.openId || '') : ''
} catch (e) {
return ''
}
})()
if (!oid) {
wx.showToast({ title: '缺少微信支付授权,请重新登录后再试', icon: 'none' })
return
}
})
this.setData({ paying: true })
analytics.track('ai_report_pay_tap', { reportId: this.data.reportId })
const eidRaw = getEnterpriseIdForApiPayload()
const enterpriseId = eidRaw != null && Number(eidRaw) > 0 ? Number(eidRaw) : 0
const amountFen = Number(report.priceFen) > 0 ? Number(report.priceFen) : 990
wxPay({
orderId: orderSn,
amount: amountFen,
description: '神仙 AI 深度画像报告',
productType: 'ai_deep_report',
enterpriseId,
success: () => {
this.setData({ paying: false })
analytics.track('ai_report_pay_success', { reportId: this.data.reportId })
setTimeout(() => this.loadReport(this.data.reportId), 800)
},
fail: () => {
this.setData({ paying: false })
}
})
}
const app = getApp()
if (app && typeof app.ensureLogin === 'function') {
app.ensureLogin().then((ok) => {
if (ok) runPay()
else wx.showToast({ title: '请先登录后再购买', icon: 'none' })
})
return
}
runPay()
},
onRetry() {

View File

@@ -15,7 +15,7 @@
<view class="unlock-row">
<view class="price">¥{{report.priceYuan || '9.9'}}</view>
<button class="unlock-btn" bindtap="onTapPay" disabled="{{paying}}">
{{paying ? '处理中…' : '立即解锁'}}
{{paying ? '处理中…' : '深度详细分析报告'}}
</button>
</view>
</view>
@@ -40,7 +40,7 @@
<!-- 已完成 -->
<view class="section" wx:elif="{{report.status === 'done'}}">
<view class="report-body">
<text selectable="true" user-select="true">{{report.content}}</text>
<text user-select="true">{{report.content}}</text>
</view>
<view class="done-actions">
<button class="share-btn" open-type="share">📤 分享给好友</button>

View File

@@ -56,15 +56,21 @@ page { background: #F5F3FF; }
.price { font-size: 42rpx; font-weight: 700; color: #7c3aed; }
.unlock-btn {
flex: 1;
max-width: 380rpx;
max-width: 400rpx;
min-height: 82rpx;
padding: 12rpx 20rpx;
box-sizing: border-box;
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
color: #fff;
border: none;
border-radius: 100rpx;
height: 82rpx;
line-height: 82rpx;
font-size: 28rpx;
font-size: 26rpx;
font-weight: 600;
line-height: 1.35;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.unlock-btn::after { border: none; }