| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- <template>
- <view class="ai-page">
- <view class="header">
- <text class="title">💊 AI 药典助手</text>
- <text class="subtitle">基于《中华人民共和国药典》& Qwen 大模型</text>
- </view>
- <scroll-view class="chat-list" scroll-y :scroll-top="scrollTop" :scroll-with-animation="true">
- <view v-if="messages.length === 0" class="welcome">
- <text class="welcome-icon">🔬</text>
- <text>输入药品名称、成分或用药问题</text>
- </view>
- <view v-for="(msg, idx) in messages" :key="idx" class="msg-wrapper">
- <!-- 用户消息 -->
- <view v-if="msg.role === 'user'" class="msg-user">
- <text class="msg-content">{{ msg.content }}</text>
- </view>
- <!-- AI 消息 -->
- <view v-else class="msg-ai">
- <view class="msg-tag" :class="tagClass(msg.intent)">
- {{ msg.intent ? intentLabel(msg.intent) : 'AI 药典助手' }}
- </view>
- <!-- 思考中 -->
- <view v-if="msg.thinking" class="thinking">
- <view class="think-dot"></view>
- <text>{{ msg.thinking }}</text>
- </view>
- <!-- 正文 -->
- <view class="msg-body" v-html="renderMarkdown(msg.content)"></view>
- <!-- 来源 -->
- <view v-if="msg.sources && msg.sources.length" class="msg-sources">
- <text>📚 </text>
- <text v-for="(s, si) in msg.sources.slice(0,3)" :key="si" class="src-item">
- {{ s.section || s.name || '' }} ({{ (s.score || 0).toFixed(2) }})
- <text v-if="si < Math.min(2, msg.sources.length-1)"> · </text>
- </text>
- </view>
- </view>
- </view>
- </scroll-view>
- <!-- 队列提示 -->
- <view v-if="queueCount > 0" class="queue-hint">⏳ 还有 {{ queueCount }} 个问题等待回答</view>
- <view class="tags">
- <text class="tag-btn" @click="quickAsk('阿莫西林禁忌')">阿莫西林禁忌</text>
- <text class="tag-btn" @click="quickAsk('二甲双胍不良反应')">二甲双胍不良反应</text>
- <text class="tag-btn" @click="quickAsk('布洛芬用法用量')">布洛芬用法用量</text>
- <text class="tag-btn" @click="quickAsk('感冒发烧吃什么药')">感冒发烧吃什么药</text>
- </view>
- <view class="input-area">
- <input
- v-model="inputText"
- class="chat-input"
- placeholder="输入药品问题..."
- :disabled="streaming"
- @confirm="sendMessage"
- confirm-type="send"
- />
- <button class="send-btn" :disabled="!inputText.trim()" @click="sendMessage">发送</button>
- <button v-if="streaming" class="stop-btn" @click="stopCurrent">停止</button>
- </view>
- </view>
- </template>
- <script>
- import { chatStream } from '@/api/ai.js'
- export default {
- data() {
- return {
- messages: [],
- inputText: '',
- conversationId: '',
- streaming: false,
- queueCount: 0,
- messageQueue: [],
- scrollTop: 0,
- currentIntent: '',
- abortFlag: false
- }
- },
- methods: {
- intentLabel(i) {
- const m = { drug_query:'药品查询', usage_guide:'用法用量', symptom_advice:'用药建议',
- regulation:'法规条款', exam_tutor:'考试辅导' }
- return m[i] || '综合查询'
- },
- tagClass(i) {
- const m = { drug_query:'tag-drug', usage_guide:'tag-usage', symptom_advice:'tag-symptom',
- regulation:'tag-regulation', exam_tutor:'tag-exam' }
- return m[i] || ''
- },
- renderMarkdown(content) {
- if (!content) return ''
- let h = content
- .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
- h = h.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
- // 修复残缺括号(匹配已知段落名,避开已有【的合法段)
- var secs='(?:结论|详细说明|注意事项|来源明细|适应症|适应证|用法与用量|用法用量|禁忌|不良反应|副作用|药理|药物相互作用|贮藏|特殊人群|安全提醒|就医指征|非药物建议|用药建议|用药方案|通用药学知识|来源汇总)';
- h = h.replace(new RegExp('(?<![【])】\\s*('+secs+')','g'),'【$1】');
- h = h.replace(new RegExp('(?<![【])('+secs+')】','g'),'【$1】');
- h = h.replace(/\n\n+/g, '</p><p>')
- h = h.replace(/\n/g, '<br>')
- h = h.replace(/【(.+?)】/g, '<h3>【$1】</h3>')
- return '<p>' + h + '</p>'
- },
- quickAsk(text) {
- this.inputText = text
- this.sendMessage()
- },
- sendMessage() {
- const text = this.inputText.trim()
- if (!text) return
- this.inputText = ''
- this.messageQueue.push(text)
- this.queueCount = this.messageQueue.length
- if (!this.streaming) this.processQueue()
- },
- stopCurrent() {
- this.abortFlag = true
- this.messageQueue = []
- this.queueCount = 0
- // 最后一个 AI 消息追加停止标记
- if (this.messages.length) {
- const last = this.messages[this.messages.length - 1]
- if (last.role === 'assistant') {
- last.content += ' ⏹ 已停止'
- last.thinking = ''
- }
- }
- this.streaming = false
- },
- async processQueue() {
- if (this.streaming || this.messageQueue.length === 0) return
- this.streaming = true
- this.abortFlag = false
- const text = this.messageQueue.shift()
- this.queueCount = this.messageQueue.length
- this.messages.push({ role: 'user', content: text })
- const aiIdx = this.messages.length
- this.messages.push({ role: 'assistant', content: '', intent: '', sources: [], thinking: '正在分析问题...' })
- this.scrollToBottom()
- await chatStream(
- { message: text, conversation_id: this.conversationId },
- {
- onIntent: (d) => {
- if (this.abortFlag) return
- this.currentIntent = d
- this.messages[aiIdx].intent = d
- },
- onStatus: (d) => {
- if (this.abortFlag) return
- this.messages[aiIdx].thinking = d
- this.scrollToBottom()
- },
- onToken: (d) => {
- if (this.abortFlag) return
- this.messages[aiIdx].thinking = ''
- this.messages[aiIdx].content += d
- this.scrollToBottom()
- },
- onMeta: (meta) => {
- if (meta.cid) this.conversationId = meta.cid
- if (meta.intent) this.messages[aiIdx].intent = meta.intent
- if (meta.sources) this.messages[aiIdx].sources = meta.sources
- },
- onDone: () => {
- this.messages[aiIdx].thinking = ''
- this.scrollToBottom()
- },
- onError: (err) => {
- if (!this.abortFlag) this.messages[aiIdx].content = '请求失败:' + (err.message || '网络异常')
- }
- }
- )
- this.streaming = false
- this.scrollToBottom()
- if (this.messageQueue.length > 0 && !this.abortFlag) {
- setTimeout(() => this.processQueue(), 300)
- }
- },
- scrollToBottom() {
- this.$nextTick(() => {
- this.scrollTop = 999999
- })
- }
- }
- }
- </script>
- <style scoped>
- .ai-page { display: flex; flex-direction: column; height: 100vh; background: #F5F6FA; }
- .header { padding: 24rpx 32rpx 16rpx; background: linear-gradient(135deg, #002FA7, #1a3fbf); color: #fff; text-align: center; position: relative; overflow: hidden; }
- .header::after { content: ''; position: absolute; top: -40rpx; right: -30rpx; width: 120rpx; height: 120rpx; background: #C41E3A; border-radius: 50%; opacity: .15; }
- .title { font-size: 36rpx; font-weight: 700; position: relative; z-index: 1; }
- .subtitle { font-size: 24rpx; opacity: .85; margin-top: 8rpx; position: relative; z-index: 1; display: block; }
- .chat-list { flex: 1; padding: 16rpx 24rpx; }
- .msg-wrapper { margin-bottom: 24rpx; }
- .msg-user { display: flex; justify-content: flex-end; }
- .msg-user .msg-content { background: #002FA7; color: #fff; padding: 16rpx 24rpx; border-radius: 16rpx 16rpx 4rpx 16rpx; max-width: 80%; font-size: 28rpx; line-height: 1.6; }
- .msg-ai { background: #fff; padding: 20rpx 24rpx; border-radius: 0 16rpx 16rpx 16rpx; border-left: 6rpx solid #002FA7; box-shadow: 0 2rpx 8rpx rgba(0,0,0,.06); }
- .msg-tag { display: inline-block; font-size: 22rpx; padding: 2rpx 12rpx; border-radius: 10rpx; margin-bottom: 8rpx; font-weight: 600; }
- .tag-drug { background: #E8EDF8; color: #002FA7; }
- .tag-usage { background: #FDEAEE; color: #C41E3A; }
- .tag-symptom { background: #fff3e0; color: #e65100; }
- .tag-regulation { background: #f3e5f5; color: #6a1b9a; }
- .tag-exam { background: #e0f2f1; color: #00695c; }
- .thinking { color: #888; font-size: 26rpx; padding: 4rpx 0; display: flex; align-items: center; }
- .think-dot { width: 14rpx; height: 14rpx; border-radius: 50%; background: #002FA7; margin-right: 12rpx; animation: pulse 1s infinite; }
- @keyframes pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: .4; transform: scale(.7); } }
- .msg-body { font-size: 28rpx; line-height: 1.8; color: #1a1a2e; word-break: break-all; }
- .msg-body h3 { font-size: 30rpx; color: #002FA7; margin: 16rpx 0 8rpx; }
- .msg-body strong { color: #111; }
- .msg-body p { margin: 0 0 12rpx 0; }
- .msg-sources { margin-top: 16rpx; padding-top: 12rpx; border-top: 1rpx solid #eee; font-size: 22rpx; color: #aaa; }
- .src-item { color: #888; }
- .queue-hint { text-align: center; padding: 8rpx; font-size: 24rpx; color: #888; background: #fafafa; }
- .tags { display: flex; gap: 12rpx; padding: 12rpx 24rpx 16rpx; flex-wrap: wrap; }
- .tag-btn { font-size: 24rpx; color: #002FA7; background: #E8EDF8; padding: 8rpx 20rpx; border-radius: 20rpx; border: 1rpx solid rgba(0,47,167,.2); }
- .input-area { display: flex; align-items: center; padding: 16rpx 24rpx; background: #fff; border-top: 1rpx solid #e0e0e0; gap: 12rpx; }
- .chat-input { flex: 1; height: 72rpx; font-size: 28rpx; background: #F5F6FA; border-radius: 40rpx; padding: 0 24rpx; }
- .send-btn { background: #C41E3A; color: #fff; border: none; border-radius: 40rpx; padding: 12rpx 28rpx; font-size: 26rpx; font-weight: 700; }
- .send-btn[disabled] { opacity: .4; }
- .stop-btn { background: #999; color: #fff; border: none; border-radius: 40rpx; padding: 12rpx 28rpx; font-size: 26rpx; }
- .welcome { text-align: center; color: #888; padding: 80rpx 40rpx; }
- .welcome-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
- </style>
|