vue_zhongyou 1.0.5 → 1.0.7

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue_zhongyou",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "keywords": [],
@@ -0,0 +1,135 @@
1
+ <template>
2
+ <div class="mobile-sort-bar">
3
+ <div
4
+ v-for="option in normalizedOptions"
5
+ :key="option.value"
6
+ class="sort-item"
7
+ @click="handleSelect(option)"
8
+ >
9
+ <span class="label">{{ option.label }}</span>
10
+ <div class="icons">
11
+ <up-one
12
+ style="margin-bottom: -18px;"
13
+ theme="filled"
14
+ size="18"
15
+ :fill="option.value === currentField && currentOrder === 'asc' ? '#1989fa':'#ddd'"
16
+ />
17
+ <down-one
18
+ style="margin-top: 5px;margin-bottom: -2px;"
19
+ theme="filled"
20
+ size="18"
21
+ :fill="option.value === currentField && currentOrder === 'desc' ? '#1989fa':'#ddd'"
22
+ />
23
+
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup>
30
+ import { computed, ref, watch } from 'vue'
31
+ import { UpOne,DownOne } from '@icon-park/vue-next'
32
+ const props = defineProps({
33
+ modelValue: {
34
+ type: Object,
35
+ default: () => ({ field: '', order: 'desc' })
36
+ },
37
+ options: {
38
+ type: Array,
39
+ default: () => []
40
+ },
41
+ defaultOrder: {
42
+ type: String,
43
+ default: 'desc'
44
+ }
45
+ })
46
+
47
+ const emit = defineEmits(['update:modelValue', 'change'])
48
+
49
+ const currentField = ref(props.modelValue.field || props.options[0]?.value || '')
50
+ const currentOrder = ref(props.modelValue.order || props.defaultOrder)
51
+
52
+ const normalizedOptions = computed(() =>
53
+ (props.options.length ? props.options : [{ label: '默认排序', value: 'default' }]).map(
54
+ (item) => ({
55
+ ...item,
56
+ value: item.value ?? item.label
57
+ })
58
+ )
59
+ )
60
+
61
+ watch(
62
+ () => props.modelValue,
63
+ (val) => {
64
+ if (!val) return
65
+ if (val.field !== undefined) currentField.value = val.field
66
+ if (val.order !== undefined) currentOrder.value = val.order
67
+ }
68
+ )
69
+
70
+ const emitChange = () => {
71
+ const payload = {
72
+ field: currentField.value,
73
+ order: currentOrder.value
74
+ }
75
+ emit('update:modelValue', payload)
76
+ emit('change', payload)
77
+ }
78
+
79
+ const handleSelect = (option) => {
80
+ if (currentField.value === option.value) {
81
+ currentOrder.value = currentOrder.value === 'desc' ? 'asc' : 'desc'
82
+ } else {
83
+ currentField.value = option.value
84
+ currentOrder.value = option.defaultOrder || props.defaultOrder
85
+ }
86
+ emitChange()
87
+ }
88
+ </script>
89
+
90
+ <style scoped lang="scss">
91
+ .mobile-sort-bar {
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: space-between;
95
+ padding: 8px 12px;
96
+ background-color: #fff;
97
+ border-radius: 12px;
98
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
99
+ gap: 8px;
100
+ }
101
+
102
+ .sort-item {
103
+ flex: 1;
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ gap: 4px;
108
+ padding: 6px;
109
+ border-radius: 999px;
110
+ color: #666;
111
+ font-size: 14px;
112
+ border: 1px solid transparent;
113
+
114
+ .icons {
115
+ display: flex;
116
+ flex-direction: column;
117
+ line-height: 1;
118
+ }
119
+
120
+ .icon {
121
+ font-size: 12px;
122
+ color: #c8c9cc;
123
+ &.on {
124
+ color: #1989fa;
125
+ }
126
+ }
127
+
128
+ &.active {
129
+ color: #1989fa;
130
+ border-color: rgba(25, 137, 250, 0.2);
131
+ background-color: rgba(25, 137, 250, 0.08);
132
+ }
133
+ }
134
+ </style>
135
+
@@ -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
+