vue_zhongyou 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="ai-chat-page">
|
|
3
|
+
<!-- 顶部导航栏 -->
|
|
4
|
+
<van-nav-bar
|
|
5
|
+
title="AI助手"
|
|
6
|
+
left-arrow
|
|
7
|
+
@click-left="$router.back()"
|
|
8
|
+
fixed
|
|
9
|
+
placeholder
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
<!-- 消息列表容器 -->
|
|
13
|
+
<div class="chat-container" ref="chatContainerRef">
|
|
14
|
+
<div class="messages-wrapper">
|
|
15
|
+
<!-- 消息列表 -->
|
|
16
|
+
<div
|
|
17
|
+
v-for="(message, index) in messages"
|
|
18
|
+
:key="index"
|
|
19
|
+
class="message-item"
|
|
20
|
+
:class="{ 'user-message': message.role === 'user', 'ai-message': message.role === 'assistant' }"
|
|
21
|
+
>
|
|
22
|
+
<div class="message-avatar">
|
|
23
|
+
<van-icon
|
|
24
|
+
v-if="message.role === 'user'"
|
|
25
|
+
name="user-o"
|
|
26
|
+
size="20"
|
|
27
|
+
/>
|
|
28
|
+
<van-icon
|
|
29
|
+
v-else
|
|
30
|
+
name="chat-o"
|
|
31
|
+
size="20"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="message-content">
|
|
35
|
+
<div class="message-bubble">
|
|
36
|
+
<div class="message-text" v-html="formatMessage(message.content)"></div>
|
|
37
|
+
<div v-if="message.role === 'assistant' && message.loading" class="loading-dots">
|
|
38
|
+
<span></span>
|
|
39
|
+
<span></span>
|
|
40
|
+
<span></span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- 输入区域 -->
|
|
50
|
+
<div class="input-area">
|
|
51
|
+
<div class="input-wrapper">
|
|
52
|
+
<van-field
|
|
53
|
+
v-model="inputText"
|
|
54
|
+
type="textarea"
|
|
55
|
+
rows="1"
|
|
56
|
+
autosize
|
|
57
|
+
placeholder="输入您的问题..."
|
|
58
|
+
:disabled="isLoading"
|
|
59
|
+
@keydown.enter.exact.prevent="handleEnter"
|
|
60
|
+
@keydown.shift.enter.exact="handleShiftEnter"
|
|
61
|
+
class="chat-input"
|
|
62
|
+
/>
|
|
63
|
+
<van-button
|
|
64
|
+
type="primary"
|
|
65
|
+
size="small"
|
|
66
|
+
:disabled="!inputText.trim() || isLoading"
|
|
67
|
+
:loading="isLoading"
|
|
68
|
+
@click="sendMessage"
|
|
69
|
+
class="send-btn"
|
|
70
|
+
>
|
|
71
|
+
发送
|
|
72
|
+
</van-button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script setup>
|
|
79
|
+
import { ref, nextTick, onMounted, watch } from 'vue'
|
|
80
|
+
import { showToast } from 'vant'
|
|
81
|
+
|
|
82
|
+
// 消息列表
|
|
83
|
+
const messages = ref([])
|
|
84
|
+
// 输入文本
|
|
85
|
+
const inputText = ref('')
|
|
86
|
+
// 加载状态
|
|
87
|
+
const isLoading = ref(false)
|
|
88
|
+
// 聊天容器引用
|
|
89
|
+
const chatContainerRef = ref(null)
|
|
90
|
+
|
|
91
|
+
// 初始化欢迎消息
|
|
92
|
+
onMounted(() => {
|
|
93
|
+
addMessage('assistant', '您好!我是AI助手,有什么可以帮助您的吗?', false)
|
|
94
|
+
scrollToBottom()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// 添加消息
|
|
98
|
+
const addMessage = (role, content, loading = false) => {
|
|
99
|
+
messages.value.push({
|
|
100
|
+
role,
|
|
101
|
+
content,
|
|
102
|
+
timestamp: new Date(),
|
|
103
|
+
loading
|
|
104
|
+
})
|
|
105
|
+
nextTick(() => {
|
|
106
|
+
scrollToBottom()
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 发送消息
|
|
111
|
+
const sendMessage = async () => {
|
|
112
|
+
const text = inputText.value.trim()
|
|
113
|
+
if (!text || isLoading.value) return
|
|
114
|
+
|
|
115
|
+
// 添加用户消息
|
|
116
|
+
addMessage('user', text)
|
|
117
|
+
inputText.value = ''
|
|
118
|
+
|
|
119
|
+
// 添加AI加载消息
|
|
120
|
+
addMessage('assistant', '', true)
|
|
121
|
+
isLoading.value = true
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// 模拟AI回复(实际应该调用API)
|
|
125
|
+
await simulateAIResponse(text)
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('发送消息失败:', error)
|
|
128
|
+
showToast('发送失败,请重试')
|
|
129
|
+
// 移除加载中的消息
|
|
130
|
+
messages.value.pop()
|
|
131
|
+
} finally {
|
|
132
|
+
isLoading.value = false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 模拟AI回复(实际应该替换为真实的API调用)
|
|
137
|
+
const simulateAIResponse = async (userMessage) => {
|
|
138
|
+
// 模拟网络延迟
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000))
|
|
140
|
+
|
|
141
|
+
// 移除加载中的消息
|
|
142
|
+
const loadingMessage = messages.value.pop()
|
|
143
|
+
|
|
144
|
+
// 生成模拟回复
|
|
145
|
+
let response = ''
|
|
146
|
+
if (userMessage.includes('你好') || userMessage.includes('您好')) {
|
|
147
|
+
response = '您好!很高兴为您服务。'
|
|
148
|
+
} else if (userMessage.includes('帮助') || userMessage.includes('功能')) {
|
|
149
|
+
response = '我可以帮您解答问题、提供建议、分析数据等。请告诉我您需要什么帮助?'
|
|
150
|
+
} else if (userMessage.includes('时间')) {
|
|
151
|
+
response = `当前时间是:${new Date().toLocaleString('zh-CN')}`
|
|
152
|
+
} else {
|
|
153
|
+
response = `我理解您说的是:"${userMessage}"。这是一个很好的问题,让我为您详细解答...\n\n(这是模拟回复,实际使用时需要接入真实的AI API)`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 添加AI回复
|
|
157
|
+
addMessage('assistant', response, false)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 格式化消息内容(支持换行)
|
|
161
|
+
const formatMessage = (content) => {
|
|
162
|
+
if (!content) return ''
|
|
163
|
+
return content.replace(/\n/g, '<br>')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 格式化时间
|
|
167
|
+
const formatTime = (timestamp) => {
|
|
168
|
+
if (!timestamp) return ''
|
|
169
|
+
const date = new Date(timestamp)
|
|
170
|
+
const now = new Date()
|
|
171
|
+
const diff = now - date
|
|
172
|
+
|
|
173
|
+
// 今天
|
|
174
|
+
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
|
|
175
|
+
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
176
|
+
}
|
|
177
|
+
// 昨天
|
|
178
|
+
if (diff < 48 * 60 * 60 * 1000) {
|
|
179
|
+
return `昨天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`
|
|
180
|
+
}
|
|
181
|
+
// 更早
|
|
182
|
+
return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 滚动到底部
|
|
186
|
+
const scrollToBottom = () => {
|
|
187
|
+
nextTick(() => {
|
|
188
|
+
if (chatContainerRef.value) {
|
|
189
|
+
chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 监听消息变化,自动滚动
|
|
195
|
+
watch(() => messages.value.length, () => {
|
|
196
|
+
scrollToBottom()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// 处理Enter键(发送)
|
|
200
|
+
const handleEnter = () => {
|
|
201
|
+
if (!isLoading.value && inputText.value.trim()) {
|
|
202
|
+
sendMessage()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 处理Shift+Enter(换行)
|
|
207
|
+
const handleShiftEnter = () => {
|
|
208
|
+
// 允许换行,不做处理
|
|
209
|
+
}
|
|
210
|
+
</script>
|
|
211
|
+
|
|
212
|
+
<style lang="scss" scoped>
|
|
213
|
+
.ai-chat-page {
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-direction: column;
|
|
216
|
+
height: 100vh;
|
|
217
|
+
background-color: #f5f5f5;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.chat-container {
|
|
221
|
+
flex: 1;
|
|
222
|
+
overflow-y: auto;
|
|
223
|
+
padding: 16px;
|
|
224
|
+
padding-bottom: 80px;
|
|
225
|
+
-webkit-overflow-scrolling: touch;
|
|
226
|
+
|
|
227
|
+
.messages-wrapper {
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-direction: column;
|
|
230
|
+
gap: 16px;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.message-item {
|
|
235
|
+
display: flex;
|
|
236
|
+
gap: 12px;
|
|
237
|
+
animation: fadeIn 0.3s ease-in;
|
|
238
|
+
|
|
239
|
+
&.user-message {
|
|
240
|
+
flex-direction: row-reverse;
|
|
241
|
+
|
|
242
|
+
.message-content {
|
|
243
|
+
align-items: flex-end;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.message-bubble {
|
|
247
|
+
background: linear-gradient(135deg, #1989fa 0%, #0d7ce8 100%);
|
|
248
|
+
color: #fff;
|
|
249
|
+
border-radius: 18px 18px 4px 18px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.message-avatar {
|
|
253
|
+
background: linear-gradient(135deg, #1989fa 0%, #0d7ce8 100%);
|
|
254
|
+
color: #fff;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
&.ai-message {
|
|
259
|
+
.message-bubble {
|
|
260
|
+
background: #fff;
|
|
261
|
+
color: #333;
|
|
262
|
+
border-radius: 18px 18px 18px 4px;
|
|
263
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.message-avatar {
|
|
267
|
+
background: #fff;
|
|
268
|
+
color: #1989fa;
|
|
269
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.message-avatar {
|
|
275
|
+
width: 36px;
|
|
276
|
+
height: 36px;
|
|
277
|
+
border-radius: 50%;
|
|
278
|
+
display: flex;
|
|
279
|
+
align-items: center;
|
|
280
|
+
justify-content: center;
|
|
281
|
+
flex-shrink: 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.message-content {
|
|
285
|
+
display: flex;
|
|
286
|
+
flex-direction: column;
|
|
287
|
+
max-width: 75%;
|
|
288
|
+
gap: 6px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.message-bubble {
|
|
292
|
+
padding: 12px 16px;
|
|
293
|
+
word-wrap: break-word;
|
|
294
|
+
word-break: break-word;
|
|
295
|
+
line-height: 1.5;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.message-text {
|
|
299
|
+
font-size: 15px;
|
|
300
|
+
white-space: pre-wrap;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.message-time {
|
|
304
|
+
font-size: 11px;
|
|
305
|
+
color: #999;
|
|
306
|
+
padding: 0 4px;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.loading-dots {
|
|
310
|
+
display: flex;
|
|
311
|
+
gap: 4px;
|
|
312
|
+
padding: 8px 0 4px;
|
|
313
|
+
|
|
314
|
+
span {
|
|
315
|
+
width: 6px;
|
|
316
|
+
height: 6px;
|
|
317
|
+
border-radius: 50%;
|
|
318
|
+
background-color: #1989fa;
|
|
319
|
+
animation: bounce 1.4s infinite ease-in-out both;
|
|
320
|
+
|
|
321
|
+
&:nth-child(1) {
|
|
322
|
+
animation-delay: -0.32s;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
&:nth-child(2) {
|
|
326
|
+
animation-delay: -0.16s;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@keyframes bounce {
|
|
332
|
+
0%, 80%, 100% {
|
|
333
|
+
transform: scale(0);
|
|
334
|
+
opacity: 0.5;
|
|
335
|
+
}
|
|
336
|
+
40% {
|
|
337
|
+
transform: scale(1);
|
|
338
|
+
opacity: 1;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@keyframes fadeIn {
|
|
343
|
+
from {
|
|
344
|
+
opacity: 0;
|
|
345
|
+
transform: translateY(10px);
|
|
346
|
+
}
|
|
347
|
+
to {
|
|
348
|
+
opacity: 1;
|
|
349
|
+
transform: translateY(0);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.input-area {
|
|
354
|
+
position: fixed;
|
|
355
|
+
bottom: 0;
|
|
356
|
+
left: 0;
|
|
357
|
+
right: 0;
|
|
358
|
+
background: #fff;
|
|
359
|
+
border-top: 1px solid #ebedf0;
|
|
360
|
+
padding: 8px 12px;
|
|
361
|
+
padding-bottom: calc(8px + env(safe-area-inset-bottom));
|
|
362
|
+
z-index: 100;
|
|
363
|
+
|
|
364
|
+
.input-wrapper {
|
|
365
|
+
display: flex;
|
|
366
|
+
align-items: flex-end;
|
|
367
|
+
gap: 8px;
|
|
368
|
+
max-width: 100%;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.chat-input {
|
|
372
|
+
flex: 1;
|
|
373
|
+
background: #f7f8fa;
|
|
374
|
+
border-radius: 20px;
|
|
375
|
+
padding: 8px 12px;
|
|
376
|
+
min-height: 40px;
|
|
377
|
+
max-height: 120px;
|
|
378
|
+
|
|
379
|
+
:deep(.van-field__control) {
|
|
380
|
+
font-size: 15px;
|
|
381
|
+
line-height: 1.5;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.send-btn {
|
|
386
|
+
flex-shrink: 0;
|
|
387
|
+
height: 40px;
|
|
388
|
+
padding: 0 20px;
|
|
389
|
+
border-radius: 20px;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
</style>
|
|
393
|
+
|