Переглянути джерело

feat: 新增AI问答聊天页面及相关接口

1. 新增AI对话接口sendChatMessage
2. 新增pages/ai/index问答页面
3. 在pages.json中注册AI问答页面路由
huoyi 1 тиждень тому
батько
коміт
6699e93f98
3 змінених файлів з 339 додано та 0 видалено
  1. 10 0
      src/api/ai/ai.js
  2. 6 0
      src/pages.json
  3. 323 0
      src/pages/ai/index.vue

+ 10 - 0
src/api/ai/ai.js

@@ -0,0 +1,10 @@
1
+import request from '@/utils/request'
2
+
3
+// AI 对话
4
+export function sendChatMessage(data) {
5
+  return request({
6
+    url: '/ai/chat',
7
+    method: 'post',
8
+    data: data
9
+  })
10
+}

+ 6 - 0
src/pages.json

@@ -255,6 +255,12 @@
255 255
       }
256 256
     },
257 257
     {
258
+      "path": "pages/ai/index",
259
+      "style": {
260
+        "navigationBarTitleText": "AI问答"
261
+      }
262
+    },
263
+    {
258 264
       "path": "pages/announcement/index",
259 265
       "style": {
260 266
         "navigationBarTitleText": "公告"

+ 323 - 0
src/pages/ai/index.vue

@@ -0,0 +1,323 @@
1
+<template>
2
+  <view class="ai-chat-page">
3
+    <!-- 消息列表 -->
4
+    <scroll-view
5
+      class="message-list"
6
+      scroll-y
7
+      :scroll-into-view="scrollToId"
8
+      @scrolltoupper="loadMore"
9
+      enable-back-to-top
10
+    >
11
+      <view class="messages-wrapper">
12
+        <view
13
+          v-for="(msg, index) in messages"
14
+          :key="msg.id"
15
+          :id="'msg-' + msg.id"
16
+          class="message-item"
17
+          :class="msg.role === 'user' ? 'user-msg' : 'ai-msg'"
18
+        >
19
+          <!-- AI 头像 -->
20
+          <view v-if="msg.role !== 'user'" class="avatar ai-avatar">
21
+            <text class="avatar-text">AI</text>
22
+          </view>
23
+          <view class="bubble-wrapper">
24
+            <view class="bubble" :class="msg.role === 'user' ? 'user-bubble' : 'ai-bubble'">
25
+              <text class="bubble-text">{{ msg.content }}</text>
26
+              <view v-if="msg.role === 'assistant' && index === messages.length - 1 && loading" class="typing-dots">
27
+                <text class="dot">.</text>
28
+                <text class="dot">.</text>
29
+                <text class="dot">.</text>
30
+              </view>
31
+            </view>
32
+          </view>
33
+          <!-- 用户头像 -->
34
+          <view v-if="msg.role === 'user'" class="avatar user-avatar">
35
+            <text class="avatar-text">我</text>
36
+          </view>
37
+        </view>
38
+      </view>
39
+    </scroll-view>
40
+
41
+    <!-- 输入区域 -->
42
+    <view class="input-area">
43
+      <view class="input-wrapper">
44
+        <input
45
+          v-model="inputText"
46
+          class="chat-input"
47
+          type="text"
48
+          placeholder="请输入您的问题..."
49
+          placeholder-class="placeholder-style"
50
+          :disabled="loading"
51
+          @confirm="sendMessage"
52
+          confirm-type="send"
53
+        />
54
+        <view class="send-btn" :class="{ disabled: !inputText.trim() || loading }" @click="sendMessage">
55
+          <text class="send-icon">↑</text>
56
+        </view>
57
+      </view>
58
+    </view>
59
+  </view>
60
+</template>
61
+
62
+<script>
63
+import { sendChatMessage } from '@/api/ai/ai'
64
+
65
+let msgId = 0
66
+
67
+export default {
68
+  data() {
69
+    return {
70
+      inputText: '',
71
+      messages: [],
72
+      loading: false,
73
+      scrollToId: ''
74
+    }
75
+  },
76
+  onLoad() {
77
+    this.addWelcomeMessage()
78
+  },
79
+  methods: {
80
+    addWelcomeMessage() {
81
+      this.messages.push({
82
+        id: ++msgId,
83
+        role: 'assistant',
84
+        content: '您好!我是智慧安检 AI 助手,有什么可以帮助您的吗?'
85
+      })
86
+      this.$nextTick(() => {
87
+        this.scrollToBottom()
88
+      })
89
+    },
90
+    async sendMessage() {
91
+      const text = this.inputText.trim()
92
+      if (!text || this.loading) return
93
+
94
+      // 添加用户消息
95
+      this.messages.push({
96
+        id: ++msgId,
97
+        role: 'user',
98
+        content: text
99
+      })
100
+      this.inputText = ''
101
+      this.scrollToBottom()
102
+
103
+      // 添加占位 AI 消息
104
+      this.loading = true
105
+      const aiMsgId = ++msgId
106
+      this.messages.push({
107
+        id: aiMsgId,
108
+        role: 'assistant',
109
+        content: ''
110
+      })
111
+      this.scrollToBottom()
112
+
113
+      try {
114
+        const res = await sendChatMessage({
115
+          message: text,
116
+          history: this.messages
117
+            .filter(m => m.content)
118
+            .slice(-10)
119
+            .map(m => ({
120
+              role: m.role,
121
+              content: m.content
122
+            }))
123
+        })
124
+        const reply = res?.data?.reply || res?.data?.content || '抱歉,我暂时无法回答这个问题,请稍后再试。'
125
+        // 更新 AI 回复
126
+        const aiMsg = this.messages.find(m => m.id === aiMsgId)
127
+        if (aiMsg) {
128
+          aiMsg.content = reply
129
+        }
130
+      } catch (error) {
131
+        const aiMsg = this.messages.find(m => m.id === aiMsgId)
132
+        if (aiMsg) {
133
+          aiMsg.content = '网络开小差了,请稍后再试。'
134
+        }
135
+      } finally {
136
+        this.loading = false
137
+        this.$nextTick(() => {
138
+          this.scrollToBottom()
139
+        })
140
+      }
141
+    },
142
+    scrollToBottom() {
143
+      if (this.messages.length > 0) {
144
+        this.scrollToId = 'msg-' + this.messages[this.messages.length - 1].id
145
+      }
146
+    },
147
+    loadMore() {
148
+      // 预留加载更多历史消息
149
+    }
150
+  }
151
+}
152
+</script>
153
+
154
+<style lang="scss" scoped>
155
+.ai-chat-page {
156
+  display: flex;
157
+  flex-direction: column;
158
+  height: 100vh;
159
+  background-color: #f5f5f5;
160
+  padding-top: var(--status-bar-height);
161
+}
162
+
163
+.message-list {
164
+  flex: 1;
165
+  overflow-y: auto;
166
+  -webkit-overflow-scrolling: touch;
167
+}
168
+
169
+.messages-wrapper {
170
+  padding: 20rpx 30rpx;
171
+  padding-bottom: 30rpx;
172
+}
173
+
174
+.message-item {
175
+  display: flex;
176
+  align-items: flex-start;
177
+  margin-bottom: 30rpx;
178
+
179
+  &.user-msg {
180
+    justify-content: flex-end;
181
+  }
182
+
183
+  &.ai-msg {
184
+    justify-content: flex-start;
185
+  }
186
+}
187
+
188
+.avatar {
189
+  width: 64rpx;
190
+  height: 64rpx;
191
+  border-radius: 50%;
192
+  display: flex;
193
+  align-items: center;
194
+  justify-content: center;
195
+  flex-shrink: 0;
196
+
197
+  .avatar-text {
198
+    font-size: 24rpx;
199
+    font-weight: bold;
200
+    color: #fff;
201
+  }
202
+}
203
+
204
+.ai-avatar {
205
+  background: linear-gradient(135deg, #409EFF, #36cfc9);
206
+  margin-right: 16rpx;
207
+}
208
+
209
+.user-avatar {
210
+  background: linear-gradient(135deg, #36d399, #409EFF);
211
+  margin-left: 16rpx;
212
+}
213
+
214
+.bubble-wrapper {
215
+  max-width: 70%;
216
+  display: flex;
217
+}
218
+
219
+.bubble {
220
+  padding: 20rpx 24rpx;
221
+  border-radius: 12rpx;
222
+  font-size: 28rpx;
223
+  line-height: 1.6;
224
+  word-break: break-word;
225
+}
226
+
227
+.user-bubble {
228
+  background: linear-gradient(135deg, #409EFF, #36cfc9);
229
+  color: #fff;
230
+  border-bottom-right-radius: 4rpx;
231
+}
232
+
233
+.ai-bubble {
234
+  background: #fff;
235
+  color: #333;
236
+  border-bottom-left-radius: 4rpx;
237
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
238
+}
239
+
240
+.bubble-text {
241
+  white-space: pre-wrap;
242
+}
243
+
244
+.typing-dots {
245
+  display: inline-flex;
246
+  align-items: center;
247
+  margin-left: 4rpx;
248
+
249
+  .dot {
250
+    font-size: 40rpx;
251
+    line-height: 1;
252
+    color: #999;
253
+    animation: blink 1.4s infinite both;
254
+
255
+    &:nth-child(2) {
256
+      animation-delay: 0.2s;
257
+    }
258
+
259
+    &:nth-child(3) {
260
+      animation-delay: 0.4s;
261
+    }
262
+  }
263
+}
264
+
265
+@keyframes blink {
266
+  0%, 80%, 100% {
267
+    opacity: 0;
268
+  }
269
+  40% {
270
+    opacity: 1;
271
+  }
272
+}
273
+
274
+/* 输入区域 */
275
+.input-area {
276
+  padding: 16rpx 24rpx;
277
+  padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
278
+  background: #fff;
279
+  border-top: 1rpx solid #eee;
280
+}
281
+
282
+.input-wrapper {
283
+  display: flex;
284
+  align-items: center;
285
+  gap: 16rpx;
286
+}
287
+
288
+.chat-input {
289
+  flex: 1;
290
+  height: 72rpx;
291
+  background: #f5f5f5;
292
+  border-radius: 36rpx;
293
+  padding: 0 30rpx;
294
+  font-size: 28rpx;
295
+  color: #333;
296
+}
297
+
298
+.placeholder-style {
299
+  color: #999;
300
+  font-size: 28rpx;
301
+}
302
+
303
+.send-btn {
304
+  width: 72rpx;
305
+  height: 72rpx;
306
+  border-radius: 50%;
307
+  background: linear-gradient(135deg, #409EFF, #36cfc9);
308
+  display: flex;
309
+  align-items: center;
310
+  justify-content: center;
311
+  flex-shrink: 0;
312
+
313
+  &.disabled {
314
+    opacity: 0.4;
315
+  }
316
+
317
+  .send-icon {
318
+    color: #fff;
319
+    font-size: 36rpx;
320
+    font-weight: bold;
321
+  }
322
+}
323
+</style>