feat: 百科rag

This commit is contained in:
Blizzard
2026-04-23 11:13:23 +08:00
parent 40f3a8cfa8
commit 9fe2fd42e0
23 changed files with 1129 additions and 69 deletions
+113
View File
@@ -0,0 +1,113 @@
// pages/wiki/chat/history/index.js
import request from '../../../../utils/request';
Page({
data: {
list: [],
total: 0,
current: 1,
pageSize: 15,
loading: false,
hasMore: true,
showClearDialog: false,
},
onLoad() {
this.fetchHistory(true);
},
fetchHistory(reset = false) {
if (this.data.loading) return;
if (!reset && !this.data.hasMore) return;
const current = reset ? 1 : this.data.current;
this.setData({ loading: true });
request.get('/plant/chat/history', { current, pageSize: this.data.pageSize })
.then(res => {
const items = (res.list || []).map(item => ({
...item,
answerPreview: (item.answer || '').substring(0, 80) + ((item.answer || '').length > 80 ? '...' : ''),
}));
const total = res.total || 0;
if (reset) {
this.setData({
list: items,
total,
current: 2,
hasMore: items.length < total,
loading: false,
});
} else {
const old = this.data.list;
const update = {};
items.forEach((item, i) => {
update[`list[${old.length + i}]`] = item;
});
update.current = current + 1;
update.hasMore = (old.length + items.length) < total;
update.loading = false;
update.total = total;
this.setData(update);
}
})
.catch(() => {
this.setData({ loading: false });
});
},
loadMore() {
this.fetchHistory(false);
},
onTapItem(e) {
const item = e.currentTarget.dataset.item;
// Navigate to chat page with prefilled Q&A
wx.navigateTo({
url: '/pages/wiki/chat/index?fromHistory=1',
success(res) {
res.eventChannel.emit('historyData', {
question: item.question,
answer: item.answer,
});
},
});
},
onDeleteItem(e) {
const id = e.currentTarget.dataset.id;
wx.showModal({
title: '删除记录',
content: '确定删除这条问答记录吗?',
success: (res) => {
if (res.confirm) {
request.post('/plant/chat/history/delete', { id }).then(() => {
wx.showToast({ title: '已删除', icon: 'success' });
this.fetchHistory(true);
});
}
},
});
},
onClearAll() {
this.setData({ showClearDialog: true });
},
closeClearDialog() {
this.setData({ showClearDialog: false });
},
doClearAll() {
this.setData({ showClearDialog: false });
request.post('/plant/chat/history/clear').then(() => {
wx.showToast({ title: '已清空', icon: 'success' });
this.setData({ list: [], total: 0, hasMore: false });
});
},
goToChat() {
wx.navigateBack();
},
});
+12
View File
@@ -0,0 +1,12 @@
{
"navigationBarTitleText": "问答历史",
"navigationBarBackgroundColor": "#558B2F",
"navigationBarTextStyle": "white",
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-dialog": "tdesign-miniprogram/dialog/dialog",
"t-swipe-cell": "tdesign-miniprogram/swipe-cell/swipe-cell"
}
}
+67
View File
@@ -0,0 +1,67 @@
<!--pages/wiki/chat/history/index.wxml-->
<view class="history-page">
<scroll-view
class="history-scroll"
scroll-y
bindscrolltolower="loadMore"
enhanced
show-scrollbar="{{false}}"
>
<!-- Header Actions -->
<view class="header-bar" wx:if="{{list.length > 0}}">
<text class="header-count">共 {{total}} 条记录</text>
<view class="clear-btn" bindtap="onClearAll">
<t-icon name="delete" size="32rpx" color="#EF4444" />
<text>清空全部</text>
</view>
</view>
<!-- History List -->
<view wx:for="{{list}}" wx:key="id" class="history-card" bindtap="onTapItem" data-item="{{item}}">
<view class="card-header">
<text class="card-time">{{item.createdAtStr}}</text>
<view class="card-del" catchtap="onDeleteItem" data-id="{{item.id}}">
<t-icon name="close" size="28rpx" color="#9CA3AF" />
</view>
</view>
<view class="card-question">
<text class="q-label">Q</text>
<text class="q-text">{{item.question}}</text>
</view>
<view class="card-answer">
<text class="a-label">A</text>
<text class="a-text">{{item.answerPreview}}</text>
<view class="card-arrow">
<t-icon name="chevron-right" size="28rpx" color="#CCC" />
</view>
</view>
</view>
<!-- States -->
<view class="footer">
<t-loading wx:if="{{loading}}" theme="circular" size="40rpx" text="加载中..." />
<text wx:elif="{{!hasMore && list.length > 0}}" class="no-more">没有更多了</text>
</view>
<view wx:if="{{!loading && list.length === 0}}" class="empty-wrap">
<view class="empty-icon">📝</view>
<text class="empty-text">暂无问答记录</text>
<text class="empty-sub">去和AI助手聊聊吧</text>
<view class="empty-cta" bindtap="goToChat">
<text>开始提问</text>
</view>
</view>
<view style="height: 60rpx;"></view>
</scroll-view>
<t-dialog
visible="{{showClearDialog}}"
title="清空全部历史"
content="确定要清空所有问答记录吗?此操作不可恢复。"
confirm-btn="确定清空"
cancel-btn="取消"
bind:confirm="doClearAll"
bind:cancel="closeClearDialog"
/>
</view>
+195
View File
@@ -0,0 +1,195 @@
/** pages/wiki/chat/history/index.wxss **/
.history-page {
height: 100vh;
background: linear-gradient(180deg, #EEF3E5 0%, #F4F6F0 100%);
}
.history-scroll {
height: 100%;
padding: 24rpx 28rpx;
}
.header-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
padding: 0 8rpx;
}
.header-count {
font-size: 26rpx;
color: #78909C;
}
.clear-btn {
display: flex;
align-items: center;
gap: 6rpx;
font-size: 26rpx;
color: #EF4444;
font-weight: 600;
padding: 8rpx 16rpx;
border-radius: 16rpx;
}
.clear-btn:active {
background: rgba(239,68,68,0.08);
}
/* Card */
.history-card {
background: rgba(255,255,255,0.92);
backdrop-filter: blur(8px);
border-radius: 24rpx;
padding: 24rpx 24rpx 20rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(85,139,47,0.05);
border: 1rpx solid rgba(85,139,47,0.04);
transition: all 0.15s;
animation: cardIn 0.3s ease-out;
}
@keyframes cardIn {
from { opacity: 0; transform: translateY(12rpx); }
to { opacity: 1; transform: translateY(0); }
}
.history-card:active {
transform: scale(0.98);
background: #FAFDF7;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.card-time {
font-size: 22rpx;
color: #9CA3AF;
}
.card-del {
padding: 8rpx;
margin: -8rpx;
}
.card-question {
display: flex;
gap: 12rpx;
margin-bottom: 12rpx;
}
.q-label {
width: 40rpx;
height: 40rpx;
border-radius: 10rpx;
background: linear-gradient(135deg, #558B2F, #7CB342);
color: #fff;
font-size: 24rpx;
font-weight: 700;
text-align: center;
line-height: 40rpx;
flex-shrink: 0;
}
.q-text {
font-size: 30rpx;
font-weight: 600;
color: #1F2937;
line-height: 1.5;
flex: 1;
}
.card-answer {
display: flex;
gap: 12rpx;
}
.a-label {
width: 40rpx;
height: 40rpx;
border-radius: 10rpx;
background: #E8F5E9;
color: #2E7D32;
font-size: 24rpx;
font-weight: 700;
text-align: center;
line-height: 40rpx;
flex-shrink: 0;
}
.a-text {
font-size: 26rpx;
color: #6B7280;
line-height: 1.6;
flex: 1;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-arrow {
display: flex;
align-items: center;
flex-shrink: 0;
margin-left: auto;
padding-left: 8rpx;
}
/* Footer */
.footer {
padding: 32rpx;
display: flex;
justify-content: center;
}
.no-more {
font-size: 24rpx;
color: #CCC;
}
/* Empty */
.empty-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 0;
}
.empty-icon {
font-size: 96rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 32rpx;
font-weight: 600;
color: #9CA3AF;
margin-bottom: 8rpx;
}
.empty-sub {
font-size: 26rpx;
color: #CCC;
margin-bottom: 32rpx;
}
.empty-cta {
padding: 16rpx 48rpx;
border-radius: 40rpx;
background: linear-gradient(135deg, #558B2F, #7CB342);
color: #fff;
font-size: 28rpx;
font-weight: 600;
box-shadow: 0 6rpx 20rpx rgba(85,139,47,0.3);
transition: all 0.15s;
}
.empty-cta:active {
transform: scale(0.95);
}
+145
View File
@@ -0,0 +1,145 @@
// pages/wiki/chat/index.js
Page({
data: {
messages: [],
inputValue: '',
isTyping: false,
scrollAnchor: '',
_counter: 0,
},
onLoad(options) {
if (options && options.fromHistory === '1') {
const channel = this.getOpenerEventChannel();
channel.on('historyData', (data) => {
const msgs = [
{ id: 'h1', role: 'user', content: data.question },
{ id: 'h2', role: 'ai', content: this._cleanMd(data.answer) },
];
this.setData({ messages: msgs, _counter: 2 }, () => this.scrollToBottom());
});
} else if (options && options.prefillQuestion) {
const q = decodeURIComponent(options.prefillQuestion);
this.setData({ inputValue: q }, () => this.onSend());
}
},
goToHistory() {
wx.navigateTo({ url: '/pages/wiki/chat/history/index' });
},
onQuickAsk(e) {
const query = e.currentTarget.dataset.q;
this.setData({ inputValue: query }, () => this.onSend());
},
onInput(e) {
this.setData({ inputValue: e.detail.value });
},
onSend() {
const query = this.data.inputValue.trim();
if (!query || this.data.isTyping) return;
const uid = 'u' + (++this.data._counter);
const aid = 'a' + (++this.data._counter);
const len = this.data.messages.length;
// Push user msg + empty AI msg at once
this.setData({
[`messages[${len}]`]: { id: uid, role: 'user', content: query },
[`messages[${len + 1}]`]: { id: aid, role: 'ai', content: '' },
inputValue: '',
isTyping: true,
}, () => {
this.scrollToBottom();
this._streamRequest(query, aid);
});
},
_streamRequest(query, aiMsgId) {
const token = wx.getStorageSync('token');
const baseUrl = 'http://192.168.0.184:8889';
const url = `${baseUrl}/plant/chat/stream?query=${encodeURIComponent(query)}`;
let fullText = '';
const task = wx.request({
url,
method: 'GET',
enableChunked: true,
header: {
'Authorization': `Bearer ${token}`,
'Accept': 'text/event-stream',
},
success: () => {
this.setData({ isTyping: false });
this.scrollToBottom();
},
fail: () => {
this._updateAiMsg(aiMsgId, '网络连接失败,请稍后重试');
this.setData({ isTyping: false });
},
});
task.onChunkReceived((res) => {
const text = this._decode(res.data);
const lines = text.split('\n');
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const chunk = line.substring(6);
if (chunk === '[DONE]') {
this.setData({ isTyping: false });
return;
}
if (chunk.startsWith('[ERROR]')) {
fullText += '\n⚠️ ' + (chunk.substring(7) || '服务异常');
this._updateAiMsg(aiMsgId, fullText);
this.setData({ isTyping: false });
return;
}
fullText += chunk;
this._updateAiMsg(aiMsgId, fullText);
}
this.scrollToBottom();
});
},
_updateAiMsg(id, content) {
const idx = this.data.messages.findIndex(m => m.id === id);
if (idx !== -1) {
this.setData({ [`messages[${idx}].content`]: this._cleanMd(content) });
}
},
// Strip residual markdown symbols for clean display
_cleanMd(text) {
return text
.replace(/^#{1,6}\s*/gm, '') // ### headers
.replace(/\*\*(.+?)\*\*/g, '【$1】') // **bold** → 【bold】
.replace(/\*(.+?)\*/g, '$1') // *italic*
.replace(/^[\-\*]\s+/gm, '· ') // - list → · list
.replace(/^\d+\.\s+/gm, (m) => m) // keep numbered lists
.replace(/`([^`]+)`/g, '$1') // `code`
.replace(/^---+$/gm, '————') // --- → ————
.replace(/\n{3,}/g, '\n\n'); // collapse blank lines
},
_decode(buffer) {
try {
return new TextDecoder('utf-8').decode(new Uint8Array(buffer));
} catch (e) {
const bytes = new Uint8Array(buffer);
let s = '';
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
try { return decodeURIComponent(escape(s)); } catch (_) { return s; }
}
},
scrollToBottom() {
this.setData({ scrollAnchor: 'scroll-bottom' });
},
});
+9
View File
@@ -0,0 +1,9 @@
{
"navigationBarTitleText": "植物AI助手",
"navigationBarBackgroundColor": "#558B2F",
"navigationBarTextStyle": "white",
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"t-loading": "tdesign-miniprogram/loading/loading"
}
}
+90
View File
@@ -0,0 +1,90 @@
<!--pages/wiki/chat/index.wxml-->
<view class="chat-page">
<!-- Messages Area -->
<scroll-view
class="chat-messages"
scroll-y
scroll-into-view="{{scrollAnchor}}"
enhanced
show-scrollbar="{{false}}"
scroll-with-animation
>
<!-- Welcome -->
<view class="welcome" wx:if="{{messages.length === 0}}">
<view class="welcome-glow"></view>
<view class="welcome-icon anim-float">🌿</view>
<view class="welcome-title">植物AI百科</view>
<view class="welcome-sub">基于知识库的智能问答助手</view>
<view class="history-entry" bindtap="goToHistory">
<t-icon name="time" size="32rpx" color="#558B2F" />
<text>查看问答历史</text>
</view>
<view class="quick-grid">
<view class="quick-card" bindtap="onQuickAsk" data-q="龟背竹怎么养护?">
<text class="qc-emoji">🌱</text>
<text class="qc-text">龟背竹怎么养护?</text>
</view>
<view class="quick-card" bindtap="onQuickAsk" data-q="哪些植物适合室内?">
<text class="qc-emoji">🏠</text>
<text class="qc-text">哪些植物适合室内?</text>
</view>
<view class="quick-card" bindtap="onQuickAsk" data-q="多肉浇水注意什么?">
<text class="qc-emoji">💧</text>
<text class="qc-text">多肉浇水注意什么?</text>
</view>
<view class="quick-card" bindtap="onQuickAsk" data-q="植物叶子发黄怎么办?">
<text class="qc-emoji">🍂</text>
<text class="qc-text">叶子发黄怎么办?</text>
</view>
</view>
</view>
<!-- Chat Bubbles -->
<block wx:for="{{messages}}" wx:key="id">
<view id="msg-{{item.id}}" class="msg-row {{item.role}}">
<!-- AI avatar -->
<view wx:if="{{item.role === 'ai'}}" class="ai-avatar-wrap">
<text class="ai-avatar-emoji">🌱</text>
</view>
<view class="msg-bubble {{item.role}}">
<!-- AI: typing state -->
<view wx:if="{{item.role === 'ai' && !item.content}}" class="typing-wrap">
<view class="typing-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
<text class="typing-label">思考中...</text>
</view>
<text wx:elif="{{item.role === 'ai'}}" class="ai-text" user-select>{{item.content}}</text>
<!-- User text -->
<text wx:else class="msg-text" user-select>{{item.content}}</text>
</view>
</view>
</block>
<view style="height: 32rpx;" id="scroll-bottom"></view>
</scroll-view>
<!-- Input -->
<view class="input-area">
<view class="input-row">
<input
class="chat-input"
placeholder="{{isTyping ? 'AI正在回答中...' : '问我任何植物相关的问题...'}}"
value="{{inputValue}}"
bindinput="onInput"
bindconfirm="onSend"
confirm-type="send"
disabled="{{isTyping}}"
adjust-position="{{true}}"
cursor-spacing="20"
/>
<view class="send-btn {{inputValue && !isTyping ? 'active' : ''}}" bindtap="onSend">
<t-icon name="arrow-up" size="36rpx" color="#fff" />
</view>
</view>
<view class="safe-bottom"></view>
</view>
</view>
+265
View File
@@ -0,0 +1,265 @@
/** pages/wiki/chat/index.wxss **/
.chat-page {
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #EEF3E5 0%, #F4F6F0 35%, #F4F6F0 100%);
}
.chat-messages {
flex: 1;
padding: 20rpx 24rpx;
overflow-y: hidden;
}
/* ── Welcome ── */
.welcome {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 20rpx 0;
position: relative;
}
.welcome-glow {
position: absolute;
top: -60rpx;
width: 480rpx;
height: 480rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(85,139,47,0.15) 0%, rgba(85,139,47,0.04) 50%, transparent 75%);
pointer-events: none;
animation: glowPulse 4s ease-in-out infinite;
}
@keyframes glowPulse {
0%, 100% { transform: scale(1); opacity: 0.8; }
50% { transform: scale(1.08); opacity: 1; }
}
.welcome-icon { font-size: 88rpx; margin-bottom: 12rpx; position: relative; }
.anim-float { animation: floatUp 3s ease-in-out infinite; }
@keyframes floatUp {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12rpx); }
}
.welcome-title {
font-size: 42rpx;
font-weight: 800;
color: #2E7D32;
margin-bottom: 8rpx;
letter-spacing: 2rpx;
}
.welcome-sub {
font-size: 24rpx;
color: #90A4AE;
margin-bottom: 28rpx;
}
.history-entry {
display: flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 24rpx;
border-radius: 32rpx;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(10px);
box-shadow: 0 2rpx 12rpx rgba(85,139,47,0.08);
font-size: 24rpx;
font-weight: 600;
color: #558B2F;
margin-bottom: 32rpx;
transition: all 0.15s;
}
.history-entry:active {
transform: scale(0.96);
background: #F0F7EB;
}
/* Quick Ask Grid */
.quick-grid {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
}
.quick-card {
background: rgba(255,255,255,0.9);
backdrop-filter: blur(10px);
border-radius: 24rpx;
padding: 24rpx 20rpx;
display: flex;
flex-direction: column;
gap: 10rpx;
box-shadow: 0 2rpx 16rpx rgba(85,139,47,0.06);
border: 1rpx solid rgba(85,139,47,0.06);
transition: all 0.15s;
}
.quick-card:active {
transform: scale(0.96);
background: #F0F7EB;
border-color: rgba(85,139,47,0.15);
}
.qc-emoji { font-size: 40rpx; }
.qc-text {
font-size: 25rpx;
font-weight: 600;
color: #374151;
line-height: 1.45;
}
/* ── Message Row ── */
.msg-row {
display: flex;
align-items: flex-start;
margin-bottom: 24rpx;
gap: 12rpx;
animation: fadeSlideIn 0.25s ease-out;
}
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateY(16rpx); }
to { opacity: 1; transform: translateY(0); }
}
.msg-row.user { flex-direction: row-reverse; }
.ai-avatar-wrap {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: linear-gradient(135deg, #E8F5E9, #C8E6C9);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(85,139,47,0.12);
}
.ai-avatar-emoji { font-size: 32rpx; }
/* ── Bubbles ── */
.msg-bubble {
max-width: 78%;
padding: 22rpx 26rpx;
border-radius: 24rpx;
word-break: break-word;
}
.msg-bubble.ai {
max-width: 88%;
background: rgba(255,255,255,0.92);
backdrop-filter: blur(8px);
border-top-left-radius: 6rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
}
.msg-bubble.user {
background: linear-gradient(135deg, #558B2F, #7CB342);
border-top-right-radius: 6rpx;
box-shadow: 0 4rpx 16rpx rgba(85,139,47,0.2);
}
.msg-text {
font-size: 29rpx;
line-height: 1.7;
color: #fff;
}
.ai-text {
font-size: 29rpx;
line-height: 1.85;
color: #1F2937;
white-space: pre-wrap;
}
/* ── Typing ── */
.typing-wrap {
display: flex;
align-items: center;
gap: 12rpx;
}
.typing-dots {
display: flex;
gap: 8rpx;
align-items: center;
}
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #A5D6A7;
animation: bounce 1.4s infinite ease-in-out;
}
.dot:nth-child(2) { animation-delay: 0.16s; }
.dot:nth-child(3) { animation-delay: 0.32s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0.5); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
.typing-label {
font-size: 22rpx;
color: #90A4AE;
font-weight: 500;
}
/* ── Input ── */
.input-area {
background: rgba(255,255,255,0.95);
backdrop-filter: blur(16px);
border-top: 1rpx solid rgba(0,0,0,0.03);
padding: 14rpx 24rpx 0;
}
.input-row {
display: flex;
align-items: center;
gap: 14rpx;
}
.chat-input {
flex: 1;
height: 78rpx;
background: #F0F4E8;
border-radius: 40rpx;
padding: 0 28rpx;
font-size: 28rpx;
color: #1F2937;
}
.send-btn {
width: 74rpx;
height: 74rpx;
border-radius: 50%;
background: #D1D5DB;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.25s;
}
.send-btn.active {
background: linear-gradient(135deg, #558B2F, #7CB342);
box-shadow: 0 4rpx 20rpx rgba(85,139,47,0.35);
}
.send-btn:active { transform: scale(0.85); }
.safe-bottom {
height: calc(14rpx + env(safe-area-inset-bottom));
}
+7
View File
@@ -142,5 +142,12 @@ Page({
title: `植物百科 - ${this.data.plant.name}`,
path: `/pages/wiki/detail/index?id=${this.data.plant.id}`
};
},
askAiAboutPlant() {
const name = this.data.plant ? this.data.plant.name : '';
wx.navigateTo({
url: `/pages/wiki/chat/index?prefillQuestion=${encodeURIComponent(name + '怎么养护?')}`
});
}
});
+6 -1
View File
@@ -195,9 +195,14 @@
</view>
</view>
<view style="height: 40rpx;"></view>
<view style="height: 120rpx;"></view>
</scroll-view>
<!-- Ask AI FAB -->
<view class="ask-ai-fab" bindtap="askAiAboutPlant">
<text class="fab-emoji">🤖</text>
<text>问 AI</text>
</view>
</view>
+24
View File
@@ -286,3 +286,27 @@ page {
transform: scale(0.9);
transition: transform 0.1s;
}
/* Ask AI FAB */
.ask-ai-fab {
position: fixed;
right: 32rpx;
bottom: 48rpx;
background: linear-gradient(135deg, rgba(21,101,192,0.92), rgba(25,118,210,0.92));
backdrop-filter: blur(12px);
color: #fff;
padding: 20rpx 32rpx;
border-radius: 48rpx;
display: flex;
align-items: center;
gap: 10rpx;
font-size: 26rpx;
font-weight: 700;
box-shadow: 0 8rpx 28rpx rgba(21,101,192,0.3);
z-index: 100;
transition: all 0.2s;
}
.ask-ai-fab:active { transform: scale(0.92); }
.fab-emoji { font-size: 32rpx; line-height: 1; }
+16 -4
View File
@@ -21,7 +21,8 @@ Page({
scrollTop: 0,
// Modal State
showIdentifyModal: false
showIdentifyModal: false,
isRefreshing: false
},
onLoad() {
@@ -40,6 +41,13 @@ Page({
this.setData({ scrollTop: Math.random() * 0.01 });
},
onRefresh() {
this.setData({ isRefreshing: true });
this.fetchWikiList(true).finally(() => {
this.setData({ isRefreshing: false });
});
},
// Fetch categories from API
fetchCategories() {
request.get('/wiki-class/list').then(res => {
@@ -52,8 +60,8 @@ Page({
// Fetch wiki list from API
fetchWikiList(reset = false) {
if (this.data.isLoading) return;
if (!reset && !this.data.hasMore) return;
if (this.data.isLoading) return Promise.resolve();
if (!reset && !this.data.hasMore) return Promise.resolve();
const current = reset ? 1 : this.data.current;
@@ -75,7 +83,7 @@ Page({
params.classId = [this.data.activeCategory];
}
request.post('/wiki/page', params).then(res => {
return request.post('/wiki/page', params).then(res => {
const data = res || {};
const list = data.list || [];
const total = data.total || 0;
@@ -207,6 +215,10 @@ Page({
openIdentifyModal() { this.setData({ showIdentifyModal: true }); },
goToAiChat() {
wx.navigateTo({ url: '/pages/wiki/chat/index' });
},
onPopupVisibleChange(e) {
this.setData({
showIdentifyModal: e.detail.visible
+16 -5
View File
@@ -17,6 +17,9 @@
enhanced
show-scrollbar="{{false}}"
scroll-top="{{scrollTop}}"
refresher-enabled="{{true}}"
bindrefresherrefresh="onRefresh"
refresher-triggered="{{isRefreshing}}"
>
<view class="search-section">
<view class="search-box-card">
@@ -111,13 +114,9 @@
</view>
<!-- Spacer -->
<view style="height: 160rpx;"></view>
<view style="height: 200rpx;"></view>
</scroll-view>
<view class="floating-add-btn" bindtap="openIdentifyModal">
<t-icon name="scan" size="40rpx" color="#FFF" />
<text>植物识别</text>
</view>
<!-- Identify Popup -->
<t-popup visible="{{showIdentifyModal}}" bind:visible-change="onPopupVisibleChange" placement="bottom">
@@ -147,4 +146,16 @@
</view>
</view>
</t-popup>
<!-- Floating Buttons (must be after popup in DOM to stay on top) -->
<view class="floating-btns" wx:if="{{!showIdentifyModal}}">
<view class="floating-btn chat-btn" bindtap="goToAiChat">
<text class="btn-emoji">🤖</text>
<text>AI问答</text>
</view>
<view class="floating-btn scan-btn" bindtap="openIdentifyModal">
<t-icon name="scan" size="40rpx" color="#FFF" />
<text>植物识别</text>
</view>
</view>
</view>
+41 -19
View File
@@ -79,11 +79,12 @@
}
.category-item.active {
background: #558B2F;
background: linear-gradient(135deg, #558B2F, #689F38);
color: #fff;
font-weight: 700;
box-shadow: 0 8rpx 20rpx rgba(85, 139, 47, 0.3);
box-shadow: 0 6rpx 20rpx rgba(85, 139, 47, 0.3);
border-color: #558B2F;
transform: scale(1.02);
}
.wiki-list {
@@ -179,28 +180,49 @@
font-size: 28rpx;
}
/* Floating Action Button */
.floating-add-btn {
/* Floating Action Buttons */
.floating-btns {
position: fixed;
right: 40rpx;
bottom: 60rpx;
background: #558B2F;
color: white;
padding: 24rpx 40rpx;
border-radius: 60rpx;
bottom: 48rpx;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 12rpx;
box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.4);
z-index: 1000;
font-size: 28rpx;
font-weight: 700;
transition: all 0.2s ease;
flex-direction: row;
gap: 24rpx;
z-index: 11600;
}
.floating-add-btn:active {
.floating-btn {
color: white;
padding: 18rpx 28rpx;
border-radius: 48rpx;
display: flex;
align-items: center;
gap: 8rpx;
font-size: 26rpx;
font-weight: 700;
white-space: nowrap;
transition: all 0.2s ease;
backdrop-filter: blur(12px);
}
.floating-btn:active {
transform: scale(0.92);
box-shadow: 0 4rpx 16rpx rgba(85, 139, 47, 0.2);
}
.scan-btn {
background: rgba(85, 139, 47, 0.92);
box-shadow: 0 8rpx 28rpx rgba(85, 139, 47, 0.35);
}
.chat-btn {
background: linear-gradient(135deg, rgba(21,101,192,0.92), rgba(25,118,210,0.92));
box-shadow: 0 8rpx 28rpx rgba(21, 101, 192, 0.3);
}
.btn-emoji {
font-size: 32rpx;
line-height: 1;
}
/* Popup Styles */