diff --git a/app.json b/app.json index 2aaa0f5..ba39e7b 100644 --- a/app.json +++ b/app.json @@ -12,6 +12,8 @@ "pages/plant-detail/growth-record/index", "pages/wiki/detail/index", "pages/wiki/identify/index", + "pages/wiki/chat/index", + "pages/wiki/chat/history/index", "pages/profile/identify-history/index", "pages/profile/badges/index", "pages/profile/badges/level-detail/index", diff --git a/pages/community/index.wxml b/pages/community/index.wxml index db294ed..ded0cbb 100644 --- a/pages/community/index.wxml +++ b/pages/community/index.wxml @@ -135,11 +135,16 @@ - - + + + 💬 - 暂无相关动态 + 暂无相关动态 快来发布第一条动态吧 + + + 发布动态 + diff --git a/pages/community/index.wxss b/pages/community/index.wxss index 9bcdf05..c07155f 100644 --- a/pages/community/index.wxss +++ b/pages/community/index.wxss @@ -276,31 +276,75 @@ page { flex-direction: column; align-items: center; justify-content: center; - padding: 120rpx 40rpx; - color: #999; + padding: 100rpx 40rpx 60rpx; } -.empty-icon { - width: 160rpx; - height: 160rpx; - background: #f5f5f5; - border-radius: 50%; +.empty-scene { + position: relative; display: flex; align-items: center; justify-content: center; margin-bottom: 32rpx; } -.empty-text { +.empty-glow { + position: absolute; + width: 240rpx; + height: 240rpx; + border-radius: 50%; + background: radial-gradient(circle, rgba(85,139,47,0.12) 0%, transparent 70%); + animation: emptyPulse 3s ease-in-out infinite; +} + +@keyframes emptyPulse { + 0%, 100% { transform: scale(1); opacity: 0.7; } + 50% { transform: scale(1.1); opacity: 1; } +} + +.empty-emoji { + font-size: 96rpx; + position: relative; + z-index: 2; +} + +.anim-breathe { + animation: breathe 3s ease-in-out infinite; +} + +@keyframes breathe { + 0%, 100% { transform: scale(1) translateY(0); } + 50% { transform: scale(1.05) translateY(-8rpx); } +} + +.empty-title { font-size: 32rpx; - font-weight: 600; - color: #666; - margin-bottom: 12rpx; + font-weight: 700; + color: #558B2F; + margin-bottom: 8rpx; } .empty-hint { font-size: 26rpx; - color: #999; + color: #90A4AE; + margin-bottom: 36rpx; +} + +.empty-cta { + display: flex; + align-items: center; + gap: 8rpx; + padding: 16rpx 40rpx; + 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); } /* Floating Action Button */ @@ -308,23 +352,23 @@ page { position: fixed; right: 40rpx; bottom: 60rpx; - background: #558B2F; + background: rgba(85, 139, 47, 0.92); + backdrop-filter: blur(12px); color: white; - padding: 24rpx 40rpx; - border-radius: 60rpx; + padding: 20rpx 36rpx; + border-radius: 48rpx; display: flex; align-items: center; - gap: 12rpx; - box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.4); + gap: 10rpx; + box-shadow: 0 8rpx 28rpx rgba(85, 139, 47, 0.35); z-index: 100; - font-size: 28rpx; + font-size: 26rpx; font-weight: 700; transition: all 0.2s ease; } .floating-add-btn:active { transform: scale(0.92); - box-shadow: 0 4rpx 16rpx rgba(85, 139, 47, 0.2); } /* WeChat Style Action Container */ diff --git a/pages/garden/index.js b/pages/garden/index.js index 4820d8f..c3ba937 100644 --- a/pages/garden/index.js +++ b/pages/garden/index.js @@ -14,6 +14,7 @@ Page({ total: 0, isLastPage: false, isLoading: false, + isRefreshing: false, scrollTop: 0 }, @@ -34,13 +35,23 @@ Page({ this.loadPlants(true); }, - // Pull to refresh + // Pull to refresh (page-level) onPullDownRefresh() { this.loadPlants(true).then(() => { wx.stopPullDownRefresh(); }); }, + // Pull to refresh (scroll-view) + onRefresh() { + this.setData({ isRefreshing: true }); + this.loadPlants(true).then(() => { + this.setData({ isRefreshing: false }); + }).catch(() => { + this.setData({ isRefreshing: false }); + }); + }, + // Infinite scroll onReachBottom() { if (!this.data.isLastPage && !this.data.isLoading) { diff --git a/pages/garden/index.wxml b/pages/garden/index.wxml index e0fb37d..2d9c077 100644 --- a/pages/garden/index.wxml +++ b/pages/garden/index.wxml @@ -17,7 +17,9 @@ - + @@ -55,6 +57,7 @@ width="100%" height="100%" t-class="uploaded-img" + lazy /> {{item.daysPlanted}}天 diff --git a/pages/garden/index.wxss b/pages/garden/index.wxss index 0770adc..4861b06 100644 --- a/pages/garden/index.wxss +++ b/pages/garden/index.wxss @@ -83,9 +83,19 @@ flex-shrink: 0; } -.garden-banner { +.garden-banner-bg { width: 100%; height: 100%; + background: linear-gradient(135deg, #558B2F 0%, #7CB342 40%, #AED581 100%); + display: flex; + align-items: center; + justify-content: center; +} + +.banner-deco { + font-size: 56rpx; + letter-spacing: 24rpx; + opacity: 0.4; } .banner-overlay { @@ -207,23 +217,23 @@ position: fixed; right: 40rpx; bottom: 60rpx; - background: #558B2F; + background: rgba(85, 139, 47, 0.92); + backdrop-filter: blur(12px); color: white; - padding: 24rpx 40rpx; - border-radius: 60rpx; + padding: 20rpx 36rpx; + border-radius: 48rpx; display: flex; align-items: center; - gap: 12rpx; - box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.4); + gap: 10rpx; + box-shadow: 0 8rpx 28rpx rgba(85, 139, 47, 0.35); z-index: 1000; - font-size: 28rpx; + font-size: 26rpx; font-weight: 700; transition: all 0.2s ease; } .floating-add-btn:active { transform: scale(0.92); - box-shadow: 0 4rpx 16rpx rgba(85, 139, 47, 0.2); } /* List Footer */ .list-footer { diff --git a/pages/profile/index.wxml b/pages/profile/index.wxml index 5d97c12..2bbe1c0 100644 --- a/pages/profile/index.wxml +++ b/pages/profile/index.wxml @@ -69,7 +69,9 @@ 我的收藏 - + + + @@ -79,7 +81,9 @@ 我的发布 - + + + @@ -89,7 +93,9 @@ 识别记录 - + + + 更多服务 @@ -101,7 +107,9 @@ 帮助与关于 - + + + diff --git a/pages/wiki/chat/history/index.js b/pages/wiki/chat/history/index.js new file mode 100644 index 0000000..1073cb8 --- /dev/null +++ b/pages/wiki/chat/history/index.js @@ -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(); + }, +}); diff --git a/pages/wiki/chat/history/index.json b/pages/wiki/chat/history/index.json new file mode 100644 index 0000000..3f36a76 --- /dev/null +++ b/pages/wiki/chat/history/index.json @@ -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" + } +} diff --git a/pages/wiki/chat/history/index.wxml b/pages/wiki/chat/history/index.wxml new file mode 100644 index 0000000..cdb117f --- /dev/null +++ b/pages/wiki/chat/history/index.wxml @@ -0,0 +1,67 @@ + + + + + + 共 {{total}} 条记录 + + + 清空全部 + + + + + + + {{item.createdAtStr}} + + + + + + Q + {{item.question}} + + + A + {{item.answerPreview}} + + + + + + + + + + 没有更多了 + + + + 📝 + 暂无问答记录 + 去和AI助手聊聊吧 + + 开始提问 + + + + + + + + diff --git a/pages/wiki/chat/history/index.wxss b/pages/wiki/chat/history/index.wxss new file mode 100644 index 0000000..bcfcea8 --- /dev/null +++ b/pages/wiki/chat/history/index.wxss @@ -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); +} diff --git a/pages/wiki/chat/index.js b/pages/wiki/chat/index.js new file mode 100644 index 0000000..dab7c83 --- /dev/null +++ b/pages/wiki/chat/index.js @@ -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' }); + }, +}); diff --git a/pages/wiki/chat/index.json b/pages/wiki/chat/index.json new file mode 100644 index 0000000..35faae8 --- /dev/null +++ b/pages/wiki/chat/index.json @@ -0,0 +1,9 @@ +{ + "navigationBarTitleText": "植物AI助手", + "navigationBarBackgroundColor": "#558B2F", + "navigationBarTextStyle": "white", + "usingComponents": { + "t-icon": "tdesign-miniprogram/icon/icon", + "t-loading": "tdesign-miniprogram/loading/loading" + } +} diff --git a/pages/wiki/chat/index.wxml b/pages/wiki/chat/index.wxml new file mode 100644 index 0000000..052cd17 --- /dev/null +++ b/pages/wiki/chat/index.wxml @@ -0,0 +1,90 @@ + + + + + + + + 🌿 + 植物AI百科 + 基于知识库的智能问答助手 + + + 查看问答历史 + + + + 🌱 + 龟背竹怎么养护? + + + 🏠 + 哪些植物适合室内? + + + 💧 + 多肉浇水注意什么? + + + 🍂 + 叶子发黄怎么办? + + + + + + + + + + 🌱 + + + + + + + + + + + 思考中... + + {{item.content}} + + {{item.content}} + + + + + + + + + + + + + + + + + + diff --git a/pages/wiki/chat/index.wxss b/pages/wiki/chat/index.wxss new file mode 100644 index 0000000..6f744b4 --- /dev/null +++ b/pages/wiki/chat/index.wxss @@ -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)); +} diff --git a/pages/wiki/detail/index.js b/pages/wiki/detail/index.js index b14650c..2f516ab 100644 --- a/pages/wiki/detail/index.js +++ b/pages/wiki/detail/index.js @@ -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 + '怎么养护?')}` + }); } }); diff --git a/pages/wiki/detail/index.wxml b/pages/wiki/detail/index.wxml index 19fb07d..e46400a 100644 --- a/pages/wiki/detail/index.wxml +++ b/pages/wiki/detail/index.wxml @@ -195,9 +195,14 @@ - + + + + 🤖 + 问 AI + diff --git a/pages/wiki/detail/index.wxss b/pages/wiki/detail/index.wxss index 555069e..4ab948c 100644 --- a/pages/wiki/detail/index.wxss +++ b/pages/wiki/detail/index.wxss @@ -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; } diff --git a/pages/wiki/index.js b/pages/wiki/index.js index 72a1629..782acbf 100644 --- a/pages/wiki/index.js +++ b/pages/wiki/index.js @@ -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 diff --git a/pages/wiki/index.wxml b/pages/wiki/index.wxml index 1701be4..45c6400 100644 --- a/pages/wiki/index.wxml +++ b/pages/wiki/index.wxml @@ -17,6 +17,9 @@ enhanced show-scrollbar="{{false}}" scroll-top="{{scrollTop}}" + refresher-enabled="{{true}}" + bindrefresherrefresh="onRefresh" + refresher-triggered="{{isRefreshing}}" > @@ -111,13 +114,9 @@ - + - - - 植物识别 - @@ -147,4 +146,16 @@ + + + + + 🤖 + AI问答 + + + + 植物识别 + + diff --git a/pages/wiki/index.wxss b/pages/wiki/index.wxss index d7066b7..c88df79 100644 --- a/pages/wiki/index.wxss +++ b/pages/wiki/index.wxss @@ -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 */ diff --git a/project.private.config.json b/project.private.config.json index 1a50a2c..e1ffeca 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -3,7 +3,7 @@ "projectname": "plant-mp", "condition": {}, "setting": { - "urlCheck": true, + "urlCheck": false, "coverView": true, "lazyloadPlaceholderEnable": false, "skylineRenderEnable": false, diff --git a/utils/request.js b/utils/request.js index bb6108a..ec4f0e2 100644 --- a/utils/request.js +++ b/utils/request.js @@ -270,8 +270,8 @@ class WxRequest { // Initialize with default instance const request = new WxRequest({ - //baseUrl: 'http://192.168.0.184:8889', - baseUrl: 'https://go.sundynix.cn/api', + baseUrl: 'http://192.168.0.184:8889', + //baseUrl: 'https://go.sundynix.cn/api', header: { 'Content-Type': 'application/json' }