@syfei49/mini-dynamic-renderer 1.0.7 → 1.2.0

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.
@@ -1,557 +1,916 @@
1
- <template>
2
- <view>
3
- <view v-if="visible" class="ly-ai-float" :style="floatStyle" @click="openChat">
4
- <image v-if="floatIconUrl" class="ly-ai-float-icon" :src="floatIconUrl" mode="aspectFit" />
5
- <view v-else class="ly-ai-float-text">AI</view>
6
- </view>
7
-
8
- <view v-if="opened" class="ly-ai-overlay" @click="onOverlayClick">
9
- <view class="ly-ai-drawer" :style="drawerStyle" @click.stop>
10
- <view class="ly-ai-header">
11
- <text class="ly-ai-title">AI 智能客服</text>
12
- <view class="ly-ai-close" @click="closeChat">×</view>
13
- </view>
14
-
15
- <scroll-view class="ly-ai-list" scroll-y :scroll-into-view="scrollIntoView" scroll-with-animation>
16
- <view v-if="messages.length === 0" class="ly-ai-welcome">
17
- <view class="ly-ai-welcome-title">AI 智能客服</view>
18
- <view class="ly-ai-welcome-desc">我可以帮你处理商品分类相关操作。新增一级分类时,若未提供图标,会引导你拍照或从相册选择。</view>
19
- <view v-for="(item, index) in quickQuestions" :key="index" class="ly-ai-quick" @click="sendQuick(item)">
20
- {{ item }}
21
- </view>
22
- </view>
23
-
24
- <view v-for="(item, index) in messages" :key="index" :id="'msg-' + index"
25
- class="ly-ai-msg-row" :class="item.role === 'user' ? 'ly-ai-msg-row-user' : 'ly-ai-msg-row-ai'">
26
- <view class="ly-ai-bubble" :class="item.role === 'user' ? 'ly-ai-bubble-user' : 'ly-ai-bubble-ai'">
27
- <image v-for="(img, imgIndex) in item.imageUrls" :key="imgIndex" class="ly-ai-msg-image"
28
- :src="img" mode="aspectFill" @click="previewImage(img, item.imageUrls)" />
29
- <text v-if="item.content" class="ly-ai-msg-text">{{ item.content }}</text>
30
- </view>
31
- </view>
32
-
33
- <view v-if="loading" class="ly-ai-msg-row ly-ai-msg-row-ai">
34
- <view class="ly-ai-bubble ly-ai-bubble-ai ly-ai-loading-bubble">
35
- <text class="ly-ai-msg-text">正在思考中...</text>
36
- </view>
37
- </view>
38
- </scroll-view>
39
-
40
- <view v-if="iconPickMode" class="ly-ai-icon-pick-bar">
41
- <view class="ly-ai-icon-pick-title">请为「{{ pendingIconCategory && pendingIconCategory.name }}」选择分类图标</view>
42
- <view class="ly-ai-icon-pick-actions">
43
- <view class="ly-ai-icon-pick-btn" :class="{ disabled: loading || uploading }" @click="pickCategoryIcon('camera')">拍照</view>
44
- <view class="ly-ai-icon-pick-btn" :class="{ disabled: loading || uploading }" @click="pickCategoryIcon('album')">从相册选择</view>
45
- <view class="ly-ai-icon-pick-btn cancel" :class="{ disabled: loading || uploading }" @click="cancelIconPick">取消</view>
46
- </view>
47
- </view>
48
-
49
- <template v-else>
50
- <view v-if="pendingImageUrl" class="ly-ai-image-preview-bar">
51
- <image class="ly-ai-preview-image" :src="pendingImageUrl" mode="aspectFill" />
52
- <view class="ly-ai-preview-remove" @click="clearPendingImage">×</view>
53
- </view>
54
-
55
- <view class="ly-ai-input-bar">
56
- <view class="ly-ai-attach-btn" :class="{ disabled: loading || uploading }" @click="chooseImage">+</view>
57
- <input class="ly-ai-chat-input" :value="inputText" placeholder="请输入您的问题" confirm-type="send"
58
- :disabled="loading || uploading" :adjust-position="false" :hold-keyboard="true" @input="onInput" @confirm="handleSend" />
59
- <view class="ly-ai-send-btn" :class="{ disabled: loading || uploading || !canSend }" @click="handleSend">发送</view>
60
- </view>
61
- </template>
62
- </view>
63
- </view>
64
- </view>
65
- </template>
66
-
67
- <script setup>
68
- import {
69
- ref,
70
- computed,
71
- nextTick,
72
- onMounted,
73
- onUnmounted
74
- } from 'vue'
75
- import {
76
- onShow
77
- } from '@dcloudio/uni-app'
78
- import chatService from '../../src/chat/service.js'
79
- import configModule from '../../src/config.js'
80
-
81
- const visible = ref(false)
82
- const opened = ref(false)
83
- const messages = ref([])
84
- const loading = ref(false)
85
- const uploading = ref(false)
86
- const iconPickMode = ref(false)
87
- const pendingIconCategory = ref(null)
88
- const pendingImageUrl = ref('')
89
- const inputText = ref('')
90
- const quickQuestions = ref([])
91
- const scrollIntoView = ref('')
92
- const floatIconUrl = ref('')
93
- const keyboardHeight = ref(0)
94
-
95
- const canSend = computed(() => {
96
- return !!(inputText.value && inputText.value.trim()) || !!pendingImageUrl.value
97
- })
98
-
99
- const floatStyle = computed(() => {
100
- return {
101
- right: '32rpx',
102
- bottom: '180rpx'
103
- }
104
- })
105
-
106
- const drawerStyle = computed(() => {
107
- return {
108
- bottom: keyboardHeight.value + 'px'
109
- }
110
- })
111
-
112
- let updateHandler = null
113
- let openHandler = null
114
- let closeHandler = null
115
- let keyboardHandler = null
116
-
117
- function syncState(snapshot) {
118
- messages.value = snapshot.messages || []
119
- loading.value = snapshot.loading || false
120
- uploading.value = snapshot.uploading || false
121
- iconPickMode.value = snapshot.iconPickMode || false
122
- pendingIconCategory.value = snapshot.pendingIconCategory || null
123
- pendingImageUrl.value = snapshot.pendingImageUrl || ''
124
- inputText.value = snapshot.inputText || ''
125
- quickQuestions.value = snapshot.quickQuestions || []
126
- scrollToBottom()
127
- }
128
-
129
- function scrollToBottom() {
130
- nextTick(() => {
131
- scrollIntoView.value = 'msg-' + (messages.value.length - 1)
132
- setTimeout(() => {
133
- scrollIntoView.value = ''
134
- }, 300)
135
- })
136
- }
137
-
138
- function checkVisible() {
139
- const config = configModule.getConfig()
140
- const pages = getCurrentPages()
141
- const current = pages[pages.length - 1]
142
- const route = current ? current.route : ''
143
- const visiblePages = config.visiblePages || []
144
- const shouldShow = visiblePages.some(function (page) {
145
- return route === page || route.indexOf(page) === 0
146
- })
147
- visible.value = shouldShow
148
- }
149
-
150
- function openChat() {
151
- if (!chatService.openConversation()) {
152
- return
153
- }
154
- opened.value = true
155
- keyboardHeight.value = 0
156
- scrollToBottom()
157
- }
158
-
159
- function closeChat() {
160
- opened.value = false
161
- keyboardHeight.value = 0
162
- chatService.closeConversation()
163
- }
164
-
165
- function onOverlayClick(e) {
166
- if (e && e.target && e.currentTarget && e.target !== e.currentTarget) {
167
- return
168
- }
169
- closeChat()
170
- }
171
-
172
- function onInput(e) {
173
- inputText.value = e.detail.value
174
- chatService.setInputText(e.detail.value)
175
- }
176
-
177
- function handleSend() {
178
- chatService.handleSend().then(() => {
179
- scrollToBottom()
180
- })
181
- }
182
-
183
- function sendQuick(text) {
184
- chatService.sendQuick(text).then(() => {
185
- scrollToBottom()
186
- })
187
- }
188
-
189
- function chooseImage() {
190
- chatService.chooseImage().catch(function (error) {
191
- uni.showToast({
192
- title: error && (error.message || error.msg) ? (error.message || error.msg) : '图片上传失败',
193
- icon: 'none'
194
- })
195
- })
196
- }
197
-
198
- function pickCategoryIcon(source) {
199
- chatService.pickCategoryIcon(source).catch(function (error) {
200
- uni.showToast({
201
- title: error && (error.message || error.msg) ? (error.message || error.msg) : '图片上传失败',
202
- icon: 'none'
203
- })
204
- })
205
- }
206
-
207
- function cancelIconPick() {
208
- chatService.cancelIconPick()
209
- }
210
-
211
- function clearPendingImage() {
212
- chatService.clearPendingImage()
213
- }
214
-
215
- function previewImage(current, urls) {
216
- const list = (urls && urls.length ? urls : [current]).filter(Boolean)
217
- if (!list.length) {
218
- return
219
- }
220
- uni.previewImage({
221
- current: current,
222
- urls: list
223
- })
224
- }
225
-
226
- onMounted(() => {
227
- const config = configModule.getConfig()
228
- floatIconUrl.value = config.floatIcon || ''
229
- updateHandler = function (snapshot) {
230
- syncState(snapshot)
231
- }
232
- openHandler = function () {
233
- opened.value = true
234
- keyboardHeight.value = 0
235
- scrollToBottom()
236
- }
237
- closeHandler = function () {
238
- opened.value = false
239
- keyboardHeight.value = 0
240
- }
241
- keyboardHandler = function (res) {
242
- keyboardHeight.value = res.height || 0
243
- if (keyboardHeight.value > 0) {
244
- scrollToBottom()
245
- }
246
- }
247
- chatService.onUpdate(updateHandler)
248
- chatService.onOpen(openHandler)
249
- chatService.onClose(closeHandler)
250
- if (typeof uni.onKeyboardHeightChange === 'function') {
251
- uni.onKeyboardHeightChange(keyboardHandler)
252
- }
253
- syncState(chatService.getSnapshot())
254
- checkVisible()
255
- })
256
-
257
- onUnmounted(() => {
258
- if (updateHandler) {
259
- chatService.offUpdate(updateHandler)
260
- }
261
- if (openHandler) {
262
- chatService.offOpen(openHandler)
263
- }
264
- if (closeHandler) {
265
- chatService.offClose(closeHandler)
266
- }
267
- if (keyboardHandler && typeof uni.offKeyboardHeightChange === 'function') {
268
- uni.offKeyboardHeightChange(keyboardHandler)
269
- }
270
- })
271
-
272
- onShow(() => {
273
- checkVisible()
274
- })
275
- </script>
276
-
277
- <style scoped>
278
- .ly-ai-float {
279
- position: fixed;
280
- z-index: 9999;
281
- width: 96rpx;
282
- height: 96rpx;
283
- border-radius: 50%;
284
- background: #f84616;
285
- display: flex;
286
- align-items: center;
287
- justify-content: center;
288
- box-shadow: 0 4rpx 16rpx rgba(248, 70, 22, 0.35);
289
- }
290
-
291
- .ly-ai-float-icon {
292
- width: 56rpx;
293
- height: 56rpx;
294
- }
295
-
296
- .ly-ai-float-text {
297
- color: #fff;
298
- font-size: 28rpx;
299
- font-weight: 600;
300
- }
301
-
302
- .ly-ai-overlay {
303
- position: fixed;
304
- top: 0;
305
- left: 0;
306
- right: 0;
307
- bottom: 0;
308
- z-index: 10000;
309
- background: rgba(0, 0, 0, 0.4);
310
- }
311
-
312
- .ly-ai-drawer {
313
- position: absolute;
314
- left: 0;
315
- right: 0;
316
- bottom: 0;
317
- height: 82vh;
318
- background: #f5f6f8;
319
- border-radius: 32rpx 32rpx 0 0;
320
- display: flex;
321
- flex-direction: column;
322
- overflow: hidden;
323
- transition: bottom 0.2s ease;
324
- }
325
-
326
- .ly-ai-header {
327
- display: flex;
328
- align-items: center;
329
- justify-content: center;
330
- padding: 24rpx 32rpx;
331
- background: #fff;
332
- position: relative;
333
- border-bottom: 1rpx solid #eee;
334
- }
335
-
336
- .ly-ai-title {
337
- font-size: 32rpx;
338
- font-weight: 600;
339
- color: #333;
340
- }
341
-
342
- .ly-ai-close {
343
- position: absolute;
344
- right: 32rpx;
345
- top: 50%;
346
- transform: translateY(-50%);
347
- width: 56rpx;
348
- height: 56rpx;
349
- line-height: 52rpx;
350
- text-align: center;
351
- font-size: 40rpx;
352
- color: #999;
353
- }
354
-
355
- .ly-ai-list {
356
- flex: 1;
357
- padding: 24rpx;
358
- padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
359
- box-sizing: border-box;
360
- }
361
-
362
- .ly-ai-welcome {
363
- background: #fff;
364
- border-radius: 16rpx;
365
- padding: 32rpx;
366
- margin-bottom: 24rpx;
367
- }
368
-
369
- .ly-ai-welcome-title {
370
- font-size: 32rpx;
371
- font-weight: 600;
372
- color: #333;
373
- margin-bottom: 12rpx;
374
- }
375
-
376
- .ly-ai-welcome-desc {
377
- font-size: 26rpx;
378
- color: #666;
379
- line-height: 1.6;
380
- margin-bottom: 20rpx;
381
- }
382
-
383
- .ly-ai-quick {
384
- background: #fff5f2;
385
- color: #f84616;
386
- font-size: 24rpx;
387
- padding: 18rpx 24rpx;
388
- border-radius: 12rpx;
389
- margin-top: 16rpx;
390
- }
391
-
392
- .ly-ai-msg-row {
393
- display: flex;
394
- margin-bottom: 20rpx;
395
- }
396
-
397
- .ly-ai-msg-row-user {
398
- justify-content: flex-end;
399
- }
400
-
401
- .ly-ai-msg-row-ai {
402
- justify-content: flex-start;
403
- }
404
-
405
- .ly-ai-bubble {
406
- max-width: 78%;
407
- padding: 20rpx 24rpx;
408
- border-radius: 16rpx;
409
- word-break: break-all;
410
- }
411
-
412
- .ly-ai-bubble-user {
413
- background: #f84616;
414
- color: #fff;
415
- border-bottom-right-radius: 4rpx;
416
- }
417
-
418
- .ly-ai-bubble-ai {
419
- background: #fff;
420
- color: #333;
421
- border-bottom-left-radius: 4rpx;
422
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
423
- }
424
-
425
- .ly-ai-loading-bubble {
426
- color: #999;
427
- }
428
-
429
- .ly-ai-msg-image {
430
- width: 200rpx;
431
- height: 200rpx;
432
- border-radius: 12rpx;
433
- display: block;
434
- margin-bottom: 12rpx;
435
- }
436
-
437
- .ly-ai-msg-text {
438
- font-size: 28rpx;
439
- line-height: 1.6;
440
- white-space: pre-wrap;
441
- }
442
-
443
- .ly-ai-icon-pick-bar {
444
- padding: 24rpx;
445
- padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
446
- background: #fff;
447
- border-top: 1rpx solid #eee;
448
- }
449
-
450
- .ly-ai-icon-pick-title {
451
- font-size: 28rpx;
452
- color: #333;
453
- margin-bottom: 20rpx;
454
- text-align: center;
455
- }
456
-
457
- .ly-ai-icon-pick-actions {
458
- display: flex;
459
- gap: 16rpx;
460
- }
461
-
462
- .ly-ai-icon-pick-btn {
463
- flex: 1;
464
- height: 80rpx;
465
- line-height: 80rpx;
466
- text-align: center;
467
- background: #f84616;
468
- color: #fff;
469
- border-radius: 12rpx;
470
- font-size: 28rpx;
471
- }
472
-
473
- .ly-ai-icon-pick-btn.cancel {
474
- background: #f5f6f8;
475
- color: #666;
476
- }
477
-
478
- .ly-ai-icon-pick-btn.disabled {
479
- opacity: 0.5;
480
- }
481
-
482
- .ly-ai-image-preview-bar {
483
- display: flex;
484
- align-items: center;
485
- padding: 12rpx 24rpx;
486
- background: #fff;
487
- border-top: 1rpx solid #eee;
488
- }
489
-
490
- .ly-ai-preview-image {
491
- width: 96rpx;
492
- height: 96rpx;
493
- border-radius: 12rpx;
494
- }
495
-
496
- .ly-ai-preview-remove {
497
- margin-left: 16rpx;
498
- width: 48rpx;
499
- height: 48rpx;
500
- line-height: 44rpx;
501
- text-align: center;
502
- font-size: 36rpx;
503
- color: #999;
504
- background: #f5f6f8;
505
- border-radius: 50%;
506
- }
507
-
508
- .ly-ai-input-bar {
509
- display: flex;
510
- align-items: center;
511
- padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
512
- background: #fff;
513
- border-top: 1rpx solid #eee;
514
- }
515
-
516
- .ly-ai-attach-btn {
517
- width: 72rpx;
518
- height: 72rpx;
519
- line-height: 68rpx;
520
- text-align: center;
521
- font-size: 44rpx;
522
- color: #666;
523
- background: #f5f6f8;
524
- border-radius: 50%;
525
- margin-right: 16rpx;
526
- flex-shrink: 0;
527
- }
528
-
529
- .ly-ai-attach-btn.disabled {
530
- opacity: 0.5;
531
- }
532
-
533
- .ly-ai-chat-input {
534
- flex: 1;
535
- height: 72rpx;
536
- background: #f5f6f8;
537
- border-radius: 36rpx;
538
- padding: 0 28rpx;
539
- font-size: 28rpx;
540
- }
541
-
542
- .ly-ai-send-btn {
543
- margin-left: 16rpx;
544
- min-width: 120rpx;
545
- height: 72rpx;
546
- line-height: 72rpx;
547
- text-align: center;
548
- background: #f84616;
549
- color: #fff;
550
- border-radius: 36rpx;
551
- font-size: 28rpx;
552
- }
553
-
554
- .ly-ai-send-btn.disabled {
555
- opacity: 0.5;
556
- }
557
- </style>
1
+ <template>
2
+ <view>
3
+ <view v-if="visible" class="ly-ai-float" :style="floatStyle" @click="openChat">
4
+ <image v-if="floatIconUrl" class="ly-ai-float-icon" :src="floatIconUrl" mode="aspectFit" />
5
+ <view v-else class="ly-ai-float-text">AI</view>
6
+ </view>
7
+
8
+ <view v-if="opened" class="ly-ai-overlay" @click="onOverlayClick">
9
+ <view class="ly-ai-drawer" :style="drawerStyle" @click.stop>
10
+ <view class="ly-ai-header">
11
+ <text class="ly-ai-title">AI 智能客服</text>
12
+ <view class="ly-ai-close" @click="closeChat">×</view>
13
+ </view>
14
+
15
+ <view class="ly-ai-body">
16
+ <scroll-view class="ly-ai-list" scroll-y :scroll-into-view="scrollIntoView" scroll-with-animation
17
+ :show-scrollbar="false">
18
+ <view v-if="messages.length === 0" class="ly-ai-welcome">
19
+ <view class="ly-ai-welcome-title">AI 智能客服</view>
20
+ <text class="ly-ai-welcome-desc" selectable user-select>我可以帮你新增商品分类(一/二/三级)或新增商品。信息不全时会弹出表单让你填写,不会自动猜测名称或分类。长按消息可复制。</text>
21
+ <view v-for="(item, index) in quickQuestions" :key="index" class="ly-ai-quick" @click="sendQuick(item)">
22
+ {{ item }}
23
+ </view>
24
+ </view>
25
+
26
+ <view v-for="(item, index) in messages" :key="index" :id="'msg-' + index"
27
+ class="ly-ai-msg-row" :class="item.role === 'user' ? 'ly-ai-msg-row-user' : 'ly-ai-msg-row-ai'">
28
+ <view class="ly-ai-bubble" :class="item.role === 'user' ? 'ly-ai-bubble-user' : 'ly-ai-bubble-ai'">
29
+ <image v-for="(img, imgIndex) in item.imageUrls" :key="imgIndex" class="ly-ai-msg-image"
30
+ :src="img" mode="aspectFill" @click="previewImage(img, item.imageUrls)" />
31
+ <text v-if="item.content" class="ly-ai-msg-text" selectable user-select
32
+ @longpress="copyMessage(item.content)">{{ item.content }}</text>
33
+ </view>
34
+ </view>
35
+
36
+ <view v-if="loading" class="ly-ai-msg-row ly-ai-msg-row-ai">
37
+ <view class="ly-ai-bubble ly-ai-bubble-ai ly-ai-loading-bubble">
38
+ <text class="ly-ai-msg-text">正在思考中...</text>
39
+ </view>
40
+ </view>
41
+ </scroll-view>
42
+ </view>
43
+
44
+ <view class="ly-ai-footer">
45
+ <view v-if="renderConfig && renderConfig.type === 'confirm'" class="ly-ai-form-bar">
46
+ <view v-if="renderConfig.title" class="ly-ai-form-title">{{ renderConfig.title }}</view>
47
+ <view class="ly-ai-form-desc">{{ renderConfig.message || renderConfig.description || '请确认是否继续' }}</view>
48
+ <view class="ly-ai-form-actions">
49
+ <view class="ly-ai-form-btn confirm" :class="{ disabled: loading || uploading }" @click="submitConfirm(true)">确认</view>
50
+ <view class="ly-ai-form-btn cancel" :class="{ disabled: loading || uploading }" @click="submitConfirm(false)">取消</view>
51
+ </view>
52
+ </view>
53
+
54
+ <view v-else-if="renderConfig" class="ly-ai-form-bar">
55
+ <view v-if="renderConfig.title" class="ly-ai-form-title">{{ renderConfig.title }}</view>
56
+ <view v-if="renderConfig.description" class="ly-ai-form-desc">{{ renderConfig.description }}</view>
57
+ <view v-for="(field, fIndex) in renderConfig.fields" :key="fIndex" class="ly-ai-form-item" v-show="field.type !== 'hidden'">
58
+ <text class="ly-ai-form-label">{{ field.label || field.name }}</text>
59
+ <template v-if="field.type === 'input' || field.type === 'number'">
60
+ <input class="ly-ai-form-input" :type="field.type === 'number' ? 'number' : 'text'"
61
+ :value="formInputs[field.name]" :placeholder="field.placeholder || '请输入' + (field.label || field.name)"
62
+ :disabled="loading || uploading" @input="onFormInput($event, field.name)" />
63
+ </template>
64
+ <template v-else-if="field.type === 'select' || field.type === 'treeSelect'">
65
+ <picker class="ly-ai-form-picker" mode="selector" :range="getFieldOptions(field).labels"
66
+ :value="getFieldIndex(field)" :disabled="loading || uploading" @change="onFormSelect($event, field)">
67
+ <view class="ly-ai-form-picker-text">{{ getFieldSelectedLabel(field) }}</view>
68
+ </picker>
69
+ </template>
70
+ <template v-else-if="field.type === 'image'">
71
+ <view v-if="formInputs[field.name]" class="ly-ai-form-image-wrap">
72
+ <image class="ly-ai-form-image" :src="formInputs[field.name]" mode="aspectFill"
73
+ @click="previewImage(formInputs[field.name], [formInputs[field.name]])" />
74
+ <view class="ly-ai-form-image-remove" @click="onFormImageRemove(field.name)">×</view>
75
+ </view>
76
+ <view v-else class="ly-ai-form-image-btn" :class="{ disabled: loading || uploading }"
77
+ @click="onFormImageChoose(field.name)">拍照 / 相册</view>
78
+ </template>
79
+ </view>
80
+ <view class="ly-ai-form-actions">
81
+ <view class="ly-ai-form-btn confirm" :class="{ disabled: loading || uploading }" @click="submitForm">确认</view>
82
+ <view class="ly-ai-form-btn cancel" :class="{ disabled: loading || uploading }" @click="cancelForm">取消</view>
83
+ </view>
84
+ </view>
85
+
86
+ <view v-else-if="iconPickMode" class="ly-ai-icon-pick-bar">
87
+ <view class="ly-ai-icon-pick-title">请为「{{ pendingIconCategory && pendingIconCategory.name }}」选择分类图标</view>
88
+ <view class="ly-ai-icon-pick-actions">
89
+ <view class="ly-ai-icon-pick-btn" :class="{ disabled: loading || uploading }" @click="pickCategoryIcon('camera')">拍照</view>
90
+ <view class="ly-ai-icon-pick-btn" :class="{ disabled: loading || uploading }" @click="pickCategoryIcon('album')">从相册选择</view>
91
+ <view class="ly-ai-icon-pick-btn cancel" :class="{ disabled: loading || uploading }" @click="cancelIconPick">取消</view>
92
+ </view>
93
+ </view>
94
+
95
+ <template v-else>
96
+ <view v-if="pendingImageUrl" class="ly-ai-image-preview-bar">
97
+ <image class="ly-ai-preview-image" :src="pendingImageUrl" mode="aspectFill" />
98
+ <view class="ly-ai-preview-remove" @click="clearPendingImage">×</view>
99
+ </view>
100
+
101
+ <view class="ly-ai-input-bar">
102
+ <view class="ly-ai-attach-btn" :class="{ disabled: loading || uploading }" @click="chooseImage">+</view>
103
+ <input class="ly-ai-chat-input" :value="inputText" placeholder="请输入您的问题" confirm-type="send"
104
+ :disabled="loading || uploading" :adjust-position="false" :hold-keyboard="true" @input="onInput" @confirm="handleSend" />
105
+ <view class="ly-ai-send-btn" :class="{ disabled: loading || uploading || !canSend }" @click="handleSend">发送</view>
106
+ </view>
107
+ </template>
108
+ </view>
109
+ </view>
110
+ </view>
111
+ </view>
112
+ </template>
113
+
114
+ <script setup>
115
+ import {
116
+ ref,
117
+ computed,
118
+ nextTick,
119
+ onMounted,
120
+ onUnmounted
121
+ } from 'vue'
122
+ import {
123
+ onShow
124
+ } from '@dcloudio/uni-app'
125
+ import chatService from '../../src/chat/service.js'
126
+ import configModule from '../../src/config.js'
127
+
128
+ const visible = ref(false)
129
+ const opened = ref(false)
130
+ const messages = ref([])
131
+ const loading = ref(false)
132
+ const uploading = ref(false)
133
+ const iconPickMode = ref(false)
134
+ const pendingIconCategory = ref(null)
135
+ const pendingImageUrl = ref('')
136
+ const inputText = ref('')
137
+ const quickQuestions = ref([])
138
+ const scrollIntoView = ref('')
139
+ const floatIconUrl = ref('')
140
+ const keyboardHeight = ref(0)
141
+ const renderConfig = ref(null)
142
+ const formInputs = ref({})
143
+ let lastFormConfigKey = ''
144
+
145
+ function getRenderConfigKey(cfg) {
146
+ if (!cfg) {
147
+ return ''
148
+ }
149
+ return JSON.stringify({
150
+ type: cfg.type,
151
+ title: cfg.title,
152
+ message: cfg.message,
153
+ fields: cfg.fields
154
+ })
155
+ }
156
+
157
+ const canSend = computed(() => {
158
+ return !!(inputText.value && inputText.value.trim()) || !!pendingImageUrl.value
159
+ })
160
+
161
+ const floatStyle = computed(() => {
162
+ return {
163
+ right: '32rpx',
164
+ bottom: '180rpx'
165
+ }
166
+ })
167
+
168
+ const drawerStyle = computed(() => {
169
+ return {
170
+ bottom: keyboardHeight.value + 'px'
171
+ }
172
+ })
173
+
174
+ let updateHandler = null
175
+ let openHandler = null
176
+ let closeHandler = null
177
+ let keyboardHandler = null
178
+
179
+ function initFormInputs() {
180
+ var cfg = renderConfig.value
181
+ var values = {}
182
+ if (cfg && cfg.fields && cfg.fields.length) {
183
+ cfg.fields.forEach(function (field) {
184
+ if (field.name === 'confirmed') {
185
+ values[field.name] = field.value === true || field.value === 'true' ? 'true' : 'false'
186
+ } else if (field.value !== undefined && field.value !== '') {
187
+ values[field.name] = field.value
188
+ } else if ((field.type === 'select' || field.type === 'treeSelect') && field.options && field.options.length) {
189
+ values[field.name] = field.defaultValue !== undefined ? field.defaultValue : ''
190
+ } else {
191
+ values[field.name] = field.defaultValue !== undefined ? field.defaultValue : ''
192
+ }
193
+ })
194
+ }
195
+ formInputs.value = values
196
+ }
197
+
198
+ function syncState(snapshot) {
199
+ messages.value = snapshot.messages || []
200
+ loading.value = snapshot.loading || false
201
+ uploading.value = snapshot.uploading || false
202
+ iconPickMode.value = snapshot.iconPickMode || false
203
+ pendingIconCategory.value = snapshot.pendingIconCategory || null
204
+ pendingImageUrl.value = snapshot.pendingImageUrl || ''
205
+ inputText.value = snapshot.inputText || ''
206
+ var nextCfg = snapshot.renderConfig || null
207
+ var nextKey = getRenderConfigKey(nextCfg)
208
+ renderConfig.value = nextCfg
209
+ quickQuestions.value = snapshot.quickQuestions || []
210
+ if (!nextCfg) {
211
+ lastFormConfigKey = ''
212
+ formInputs.value = {}
213
+ } else if (nextKey !== lastFormConfigKey) {
214
+ lastFormConfigKey = nextKey
215
+ initFormInputs()
216
+ }
217
+ scrollToBottom()
218
+ }
219
+
220
+ function scrollToBottom() {
221
+ nextTick(() => {
222
+ scrollIntoView.value = 'msg-' + (messages.value.length - 1)
223
+ setTimeout(() => {
224
+ scrollIntoView.value = ''
225
+ }, 300)
226
+ })
227
+ }
228
+
229
+ function checkVisible() {
230
+ const config = configModule.getConfig()
231
+ const pages = getCurrentPages()
232
+ const current = pages[pages.length - 1]
233
+ const route = current ? current.route : ''
234
+ const visiblePages = config.visiblePages || []
235
+ const shouldShow = visiblePages.some(function (page) {
236
+ return route === page || route.indexOf(page) === 0
237
+ })
238
+ visible.value = shouldShow
239
+ }
240
+
241
+ function openChat() {
242
+ if (!chatService.openConversation()) {
243
+ return
244
+ }
245
+ opened.value = true
246
+ keyboardHeight.value = 0
247
+ scrollToBottom()
248
+ }
249
+
250
+ function closeChat() {
251
+ opened.value = false
252
+ keyboardHeight.value = 0
253
+ chatService.closeConversation()
254
+ }
255
+
256
+ function onOverlayClick(e) {
257
+ if (e && e.target && e.currentTarget && e.target !== e.currentTarget) {
258
+ return
259
+ }
260
+ closeChat()
261
+ }
262
+
263
+ function onInput(e) {
264
+ inputText.value = e.detail.value
265
+ chatService.setInputText(e.detail.value)
266
+ }
267
+
268
+ function handleSend() {
269
+ chatService.handleSend().then(() => {
270
+ scrollToBottom()
271
+ })
272
+ }
273
+
274
+ function sendQuick(text) {
275
+ chatService.sendQuick(text).then(() => {
276
+ scrollToBottom()
277
+ })
278
+ }
279
+
280
+ function chooseImage() {
281
+ chatService.chooseImage().catch(function (error) {
282
+ uni.showToast({
283
+ title: error && (error.message || error.msg) ? (error.message || error.msg) : '图片上传失败',
284
+ icon: 'none'
285
+ })
286
+ })
287
+ }
288
+
289
+ function pickCategoryIcon(source) {
290
+ chatService.pickCategoryIcon(source).catch(function (error) {
291
+ uni.showToast({
292
+ title: error && (error.message || error.msg) ? (error.message || error.msg) : '图片上传失败',
293
+ icon: 'none'
294
+ })
295
+ })
296
+ }
297
+
298
+ function cancelIconPick() {
299
+ chatService.cancelIconPick()
300
+ }
301
+
302
+ function flattenTreeOptions(options, prefix) {
303
+ var result = []
304
+ prefix = prefix || ''
305
+ ;(options || []).forEach(function (opt) {
306
+ var label = prefix ? (prefix + ' / ' + (opt.label || opt.value)) : (opt.label || opt.value)
307
+ var children = opt.children || opt.subCategories || []
308
+ if (children.length) {
309
+ result = result.concat(flattenTreeOptions(children, label))
310
+ } else {
311
+ result.push({
312
+ label: label,
313
+ value: opt.value
314
+ })
315
+ }
316
+ })
317
+ return result
318
+ }
319
+
320
+ function getFieldOptions(field) {
321
+ var options = field.type === 'treeSelect'
322
+ ? flattenTreeOptions(field.options || [])
323
+ : (field.options || [])
324
+ return {
325
+ labels: options.map(function (opt) {
326
+ return opt.label || opt.value
327
+ }),
328
+ values: options.map(function (opt) {
329
+ return opt.value
330
+ })
331
+ }
332
+ }
333
+
334
+ function isSameFieldValue(a, b) {
335
+ if (a === undefined || a === null || a === '') {
336
+ return false
337
+ }
338
+ return String(a) === String(b)
339
+ }
340
+
341
+ function getFieldIndex(field) {
342
+ var opts = getFieldOptions(field)
343
+ var current = formInputs.value[field.name]
344
+ for (var i = 0; i < opts.values.length; i++) {
345
+ if (isSameFieldValue(opts.values[i], current)) {
346
+ return i
347
+ }
348
+ }
349
+ return 0
350
+ }
351
+
352
+ function getFieldSelectedLabel(field) {
353
+ var opts = getFieldOptions(field)
354
+ var current = formInputs.value[field.name]
355
+ for (var i = 0; i < opts.values.length; i++) {
356
+ if (isSameFieldValue(opts.values[i], current)) {
357
+ return opts.labels[i] || '请选择'
358
+ }
359
+ }
360
+ return '请选择'
361
+ }
362
+
363
+ function onFormInput(e, name) {
364
+ formInputs.value[name] = e.detail.value
365
+ }
366
+
367
+ function onFormSelect(e, field) {
368
+ var index = e.detail.value
369
+ var opts = getFieldOptions(field)
370
+ formInputs.value[field.name] = opts.values[index]
371
+ }
372
+
373
+ function onFormImageChoose(name) {
374
+ chatService.chooseImage()
375
+ .then(function (imageUrl) {
376
+ formInputs.value[name] = imageUrl
377
+ })
378
+ .catch(function (error) {
379
+ uni.showToast({
380
+ title: error && (error.message || error.msg) ? (error.message || error.msg) : '图片上传失败',
381
+ icon: 'none'
382
+ })
383
+ })
384
+ }
385
+
386
+ function onFormImageRemove(name) {
387
+ formInputs.value[name] = ''
388
+ }
389
+
390
+ function submitForm() {
391
+ chatService.submitInputs(formInputs.value)
392
+ .then(function () {
393
+ scrollToBottom()
394
+ })
395
+ .catch(function (error) {
396
+ uni.showToast({
397
+ title: error && (error.message || error.msg) ? (error.message || error.msg) : '提交失败',
398
+ icon: 'none'
399
+ })
400
+ })
401
+ }
402
+
403
+ function cancelForm() {
404
+ chatService.cancelForm()
405
+ }
406
+
407
+ function submitConfirm(confirmed) {
408
+ chatService.submitConfirm(confirmed).then(function () {
409
+ scrollToBottom()
410
+ })
411
+ }
412
+
413
+ function clearPendingImage() {
414
+ chatService.clearPendingImage()
415
+ }
416
+
417
+ function copyMessage(content) {
418
+ var text = (content || '').trim()
419
+ if (!text) {
420
+ return
421
+ }
422
+ uni.setClipboardData({
423
+ data: text,
424
+ success: function () {
425
+ uni.showToast({ title: '已复制', icon: 'none' })
426
+ }
427
+ })
428
+ }
429
+
430
+ function previewImage(current, urls) {
431
+ const list = (urls && urls.length ? urls : [current]).filter(Boolean)
432
+ if (!list.length) {
433
+ return
434
+ }
435
+ uni.previewImage({
436
+ current: current,
437
+ urls: list
438
+ })
439
+ }
440
+
441
+ onMounted(() => {
442
+ const config = configModule.getConfig()
443
+ floatIconUrl.value = config.floatIcon || ''
444
+ updateHandler = function (snapshot) {
445
+ syncState(snapshot)
446
+ }
447
+ openHandler = function () {
448
+ opened.value = true
449
+ keyboardHeight.value = 0
450
+ scrollToBottom()
451
+ }
452
+ closeHandler = function () {
453
+ opened.value = false
454
+ keyboardHeight.value = 0
455
+ }
456
+ keyboardHandler = function (res) {
457
+ keyboardHeight.value = res.height || 0
458
+ if (keyboardHeight.value > 0) {
459
+ scrollToBottom()
460
+ }
461
+ }
462
+ chatService.onUpdate(updateHandler)
463
+ chatService.onOpen(openHandler)
464
+ chatService.onClose(closeHandler)
465
+ if (typeof uni.onKeyboardHeightChange === 'function') {
466
+ uni.onKeyboardHeightChange(keyboardHandler)
467
+ }
468
+ syncState(chatService.getSnapshot())
469
+ checkVisible()
470
+ })
471
+
472
+ onUnmounted(() => {
473
+ if (updateHandler) {
474
+ chatService.offUpdate(updateHandler)
475
+ }
476
+ if (openHandler) {
477
+ chatService.offOpen(openHandler)
478
+ }
479
+ if (closeHandler) {
480
+ chatService.offClose(closeHandler)
481
+ }
482
+ if (keyboardHandler && typeof uni.offKeyboardHeightChange === 'function') {
483
+ uni.offKeyboardHeightChange(keyboardHandler)
484
+ }
485
+ })
486
+
487
+ onShow(() => {
488
+ checkVisible()
489
+ })
490
+ </script>
491
+
492
+ <style scoped>
493
+ .ly-ai-float {
494
+ position: fixed;
495
+ z-index: 9999;
496
+ width: 96rpx;
497
+ height: 96rpx;
498
+ border-radius: 50%;
499
+ background: #f84616;
500
+ display: flex;
501
+ align-items: center;
502
+ justify-content: center;
503
+ box-shadow: 0 4rpx 16rpx rgba(248, 70, 22, 0.35);
504
+ }
505
+
506
+ .ly-ai-float-icon {
507
+ width: 56rpx;
508
+ height: 56rpx;
509
+ }
510
+
511
+ .ly-ai-float-text {
512
+ color: #fff;
513
+ font-size: 28rpx;
514
+ font-weight: 600;
515
+ }
516
+
517
+ .ly-ai-overlay {
518
+ position: fixed;
519
+ top: 0;
520
+ left: 0;
521
+ right: 0;
522
+ bottom: 0;
523
+ z-index: 10000;
524
+ background: rgba(0, 0, 0, 0.4);
525
+ }
526
+
527
+ .ly-ai-drawer {
528
+ position: absolute;
529
+ left: 0;
530
+ right: 0;
531
+ bottom: 0;
532
+ height: 82vh;
533
+ background: #f5f6f8;
534
+ border-radius: 32rpx 32rpx 0 0;
535
+ display: flex;
536
+ flex-direction: column;
537
+ overflow: hidden;
538
+ transition: bottom 0.2s ease;
539
+ }
540
+
541
+ .ly-ai-header {
542
+ display: flex;
543
+ align-items: center;
544
+ justify-content: center;
545
+ padding: 24rpx 32rpx;
546
+ background: #fff;
547
+ position: relative;
548
+ border-bottom: 1rpx solid #eee;
549
+ flex-shrink: 0;
550
+ }
551
+
552
+ .ly-ai-body {
553
+ flex: 1;
554
+ min-height: 0;
555
+ overflow: hidden;
556
+ }
557
+
558
+ .ly-ai-footer {
559
+ flex-shrink: 0;
560
+ background: #fff;
561
+ }
562
+
563
+ .ly-ai-title {
564
+ font-size: 32rpx;
565
+ font-weight: 600;
566
+ color: #333;
567
+ }
568
+
569
+ .ly-ai-close {
570
+ position: absolute;
571
+ right: 32rpx;
572
+ top: 50%;
573
+ transform: translateY(-50%);
574
+ width: 56rpx;
575
+ height: 56rpx;
576
+ line-height: 52rpx;
577
+ text-align: center;
578
+ font-size: 40rpx;
579
+ color: #999;
580
+ }
581
+
582
+ .ly-ai-list {
583
+ height: 100%;
584
+ padding: 24rpx;
585
+ box-sizing: border-box;
586
+ }
587
+
588
+ .ly-ai-welcome {
589
+ background: #fff;
590
+ border-radius: 16rpx;
591
+ padding: 32rpx;
592
+ margin-bottom: 24rpx;
593
+ }
594
+
595
+ .ly-ai-welcome-title {
596
+ font-size: 32rpx;
597
+ font-weight: 600;
598
+ color: #333;
599
+ margin-bottom: 12rpx;
600
+ }
601
+
602
+ .ly-ai-welcome-desc {
603
+ font-size: 26rpx;
604
+ color: #666;
605
+ line-height: 1.6;
606
+ margin-bottom: 20rpx;
607
+ }
608
+
609
+ .ly-ai-quick {
610
+ background: #fff5f2;
611
+ color: #f84616;
612
+ font-size: 24rpx;
613
+ padding: 18rpx 24rpx;
614
+ border-radius: 12rpx;
615
+ margin-top: 16rpx;
616
+ }
617
+
618
+ .ly-ai-msg-row {
619
+ display: flex;
620
+ margin-bottom: 20rpx;
621
+ }
622
+
623
+ .ly-ai-msg-row-user {
624
+ justify-content: flex-end;
625
+ }
626
+
627
+ .ly-ai-msg-row-ai {
628
+ justify-content: flex-start;
629
+ }
630
+
631
+ .ly-ai-bubble {
632
+ max-width: 78%;
633
+ padding: 20rpx 24rpx;
634
+ border-radius: 16rpx;
635
+ word-break: break-all;
636
+ }
637
+
638
+ .ly-ai-bubble-user {
639
+ background: #f84616;
640
+ color: #fff;
641
+ border-bottom-right-radius: 4rpx;
642
+ }
643
+
644
+ .ly-ai-bubble-ai {
645
+ background: #fff;
646
+ color: #333;
647
+ border-bottom-left-radius: 4rpx;
648
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
649
+ }
650
+
651
+ .ly-ai-loading-bubble {
652
+ color: #999;
653
+ }
654
+
655
+ .ly-ai-msg-image {
656
+ width: 200rpx;
657
+ height: 200rpx;
658
+ border-radius: 12rpx;
659
+ display: block;
660
+ margin-bottom: 12rpx;
661
+ }
662
+
663
+ .ly-ai-msg-text {
664
+ font-size: 28rpx;
665
+ line-height: 1.6;
666
+ white-space: pre-wrap;
667
+ word-break: break-word;
668
+ user-select: text;
669
+ -webkit-user-select: text;
670
+ }
671
+
672
+ .ly-ai-welcome-desc {
673
+ user-select: text;
674
+ -webkit-user-select: text;
675
+ }
676
+
677
+ .ly-ai-icon-pick-bar {
678
+ padding: 24rpx;
679
+ padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
680
+ background: #fff;
681
+ border-top: 1rpx solid #eee;
682
+ flex-shrink: 0;
683
+ }
684
+
685
+ .ly-ai-icon-pick-title {
686
+ font-size: 28rpx;
687
+ color: #333;
688
+ margin-bottom: 20rpx;
689
+ text-align: center;
690
+ }
691
+
692
+ .ly-ai-icon-pick-actions {
693
+ display: flex;
694
+ gap: 16rpx;
695
+ }
696
+
697
+ .ly-ai-icon-pick-btn {
698
+ flex: 1;
699
+ height: 80rpx;
700
+ line-height: 80rpx;
701
+ text-align: center;
702
+ background: #f84616;
703
+ color: #fff;
704
+ border-radius: 12rpx;
705
+ font-size: 28rpx;
706
+ }
707
+
708
+ .ly-ai-icon-pick-btn.cancel {
709
+ background: #f5f6f8;
710
+ color: #666;
711
+ }
712
+
713
+ .ly-ai-icon-pick-btn.disabled {
714
+ opacity: 0.5;
715
+ }
716
+
717
+ .ly-ai-image-preview-bar {
718
+ display: flex;
719
+ align-items: center;
720
+ padding: 12rpx 24rpx;
721
+ background: #fff;
722
+ border-top: 1rpx solid #eee;
723
+ flex-shrink: 0;
724
+ }
725
+
726
+ .ly-ai-preview-image {
727
+ width: 96rpx;
728
+ height: 96rpx;
729
+ border-radius: 12rpx;
730
+ }
731
+
732
+ .ly-ai-preview-remove {
733
+ margin-left: 16rpx;
734
+ width: 48rpx;
735
+ height: 48rpx;
736
+ line-height: 44rpx;
737
+ text-align: center;
738
+ font-size: 36rpx;
739
+ color: #999;
740
+ background: #f5f6f8;
741
+ border-radius: 50%;
742
+ }
743
+
744
+ .ly-ai-input-bar {
745
+ display: flex;
746
+ align-items: center;
747
+ padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
748
+ background: #fff;
749
+ border-top: 1rpx solid #eee;
750
+ flex-shrink: 0;
751
+ }
752
+
753
+ .ly-ai-attach-btn {
754
+ width: 72rpx;
755
+ height: 72rpx;
756
+ line-height: 68rpx;
757
+ text-align: center;
758
+ font-size: 44rpx;
759
+ color: #666;
760
+ background: #f5f6f8;
761
+ border-radius: 50%;
762
+ margin-right: 16rpx;
763
+ flex-shrink: 0;
764
+ }
765
+
766
+ .ly-ai-attach-btn.disabled {
767
+ opacity: 0.5;
768
+ }
769
+
770
+ .ly-ai-chat-input {
771
+ flex: 1;
772
+ height: 72rpx;
773
+ background: #f5f6f8;
774
+ border-radius: 36rpx;
775
+ padding: 0 28rpx;
776
+ font-size: 28rpx;
777
+ }
778
+
779
+ .ly-ai-send-btn {
780
+ margin-left: 16rpx;
781
+ min-width: 120rpx;
782
+ height: 72rpx;
783
+ line-height: 72rpx;
784
+ text-align: center;
785
+ background: #f84616;
786
+ color: #fff;
787
+ border-radius: 36rpx;
788
+ font-size: 28rpx;
789
+ }
790
+
791
+ .ly-ai-send-btn.disabled {
792
+ opacity: 0.5;
793
+ }
794
+
795
+ .ly-ai-form-bar {
796
+ padding: 24rpx;
797
+ padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
798
+ background: #fff;
799
+ border-top: 1rpx solid #eee;
800
+ flex-shrink: 0;
801
+ max-height: 50vh;
802
+ overflow-y: auto;
803
+ }
804
+
805
+ .ly-ai-form-title {
806
+ font-size: 30rpx;
807
+ font-weight: 600;
808
+ color: #333;
809
+ margin-bottom: 12rpx;
810
+ }
811
+
812
+ .ly-ai-form-desc {
813
+ font-size: 26rpx;
814
+ color: #666;
815
+ line-height: 1.5;
816
+ margin-bottom: 24rpx;
817
+ }
818
+
819
+ .ly-ai-form-item {
820
+ margin-bottom: 24rpx;
821
+ }
822
+
823
+ .ly-ai-form-label {
824
+ display: block;
825
+ font-size: 28rpx;
826
+ color: #333;
827
+ margin-bottom: 12rpx;
828
+ }
829
+
830
+ .ly-ai-form-input {
831
+ height: 72rpx;
832
+ background: #f5f6f8;
833
+ border-radius: 12rpx;
834
+ padding: 0 24rpx;
835
+ font-size: 28rpx;
836
+ color: #333;
837
+ }
838
+
839
+ .ly-ai-form-picker {
840
+ height: 72rpx;
841
+ background: #f5f6f8;
842
+ border-radius: 12rpx;
843
+ padding: 0 24rpx;
844
+ display: flex;
845
+ align-items: center;
846
+ }
847
+
848
+ .ly-ai-form-picker-text {
849
+ font-size: 28rpx;
850
+ color: #333;
851
+ }
852
+
853
+ .ly-ai-form-image-wrap {
854
+ display: flex;
855
+ align-items: center;
856
+ }
857
+
858
+ .ly-ai-form-image {
859
+ width: 160rpx;
860
+ height: 160rpx;
861
+ border-radius: 12rpx;
862
+ }
863
+
864
+ .ly-ai-form-image-remove {
865
+ margin-left: 16rpx;
866
+ width: 48rpx;
867
+ height: 48rpx;
868
+ line-height: 44rpx;
869
+ text-align: center;
870
+ font-size: 36rpx;
871
+ color: #999;
872
+ background: #f5f6f8;
873
+ border-radius: 50%;
874
+ }
875
+
876
+ .ly-ai-form-image-btn {
877
+ width: 200rpx;
878
+ height: 160rpx;
879
+ line-height: 160rpx;
880
+ text-align: center;
881
+ background: #f5f6f8;
882
+ color: #666;
883
+ border-radius: 12rpx;
884
+ font-size: 28rpx;
885
+ }
886
+
887
+ .ly-ai-form-actions {
888
+ display: flex;
889
+ gap: 16rpx;
890
+ margin-top: 12rpx;
891
+ }
892
+
893
+ .ly-ai-form-btn {
894
+ flex: 1;
895
+ height: 80rpx;
896
+ line-height: 80rpx;
897
+ text-align: center;
898
+ border-radius: 12rpx;
899
+ font-size: 28rpx;
900
+ }
901
+
902
+ .ly-ai-form-btn.confirm {
903
+ background: #f84616;
904
+ color: #fff;
905
+ }
906
+
907
+ .ly-ai-form-btn.cancel {
908
+ background: #f5f6f8;
909
+ color: #666;
910
+ }
911
+
912
+ .ly-ai-form-btn.disabled,
913
+ .ly-ai-form-image-btn.disabled {
914
+ opacity: 0.5;
915
+ }
916
+ </style>