@syfei49/mini-dynamic-renderer 1.0.0 → 1.0.3

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.
@@ -0,0 +1,523 @@
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.self="closeChat">
9
+ <view class="ly-ai-drawer">
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" @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
+
94
+ const canSend = computed(() => {
95
+ return !!(inputText.value && inputText.value.trim()) || !!pendingImageUrl.value
96
+ })
97
+
98
+ const floatStyle = computed(() => {
99
+ return {
100
+ right: '32rpx',
101
+ bottom: '180rpx'
102
+ }
103
+ })
104
+
105
+ let updateHandler = null
106
+ let openHandler = null
107
+ let closeHandler = null
108
+
109
+ function syncState(snapshot) {
110
+ messages.value = snapshot.messages || []
111
+ loading.value = snapshot.loading || false
112
+ uploading.value = snapshot.uploading || false
113
+ iconPickMode.value = snapshot.iconPickMode || false
114
+ pendingIconCategory.value = snapshot.pendingIconCategory || null
115
+ pendingImageUrl.value = snapshot.pendingImageUrl || ''
116
+ inputText.value = snapshot.inputText || ''
117
+ quickQuestions.value = snapshot.quickQuestions || []
118
+ scrollToBottom()
119
+ }
120
+
121
+ function scrollToBottom() {
122
+ nextTick(() => {
123
+ scrollIntoView.value = 'msg-' + (messages.value.length - 1)
124
+ setTimeout(() => {
125
+ scrollIntoView.value = ''
126
+ }, 300)
127
+ })
128
+ }
129
+
130
+ function checkVisible() {
131
+ const config = configModule.getConfig()
132
+ const pages = getCurrentPages()
133
+ const current = pages[pages.length - 1]
134
+ const route = current ? current.route : ''
135
+ const visiblePages = config.visiblePages || []
136
+ const shouldShow = visiblePages.some(function (page) {
137
+ return route === page || route.indexOf(page) === 0
138
+ })
139
+ visible.value = shouldShow
140
+ }
141
+
142
+ function openChat() {
143
+ if (!chatService.openConversation()) {
144
+ return
145
+ }
146
+ opened.value = true
147
+ scrollToBottom()
148
+ }
149
+
150
+ function closeChat() {
151
+ opened.value = false
152
+ chatService.closeConversation()
153
+ }
154
+
155
+ function onInput(e) {
156
+ inputText.value = e.detail.value
157
+ chatService.setInputText(e.detail.value)
158
+ }
159
+
160
+ function handleSend() {
161
+ chatService.handleSend().then(() => {
162
+ scrollToBottom()
163
+ })
164
+ }
165
+
166
+ function sendQuick(text) {
167
+ chatService.sendQuick(text).then(() => {
168
+ scrollToBottom()
169
+ })
170
+ }
171
+
172
+ function chooseImage() {
173
+ chatService.chooseImage().catch(function (error) {
174
+ uni.showToast({
175
+ title: error && (error.message || error.msg) ? (error.message || error.msg) : '图片上传失败',
176
+ icon: 'none'
177
+ })
178
+ })
179
+ }
180
+
181
+ function pickCategoryIcon(source) {
182
+ chatService.pickCategoryIcon(source).catch(function (error) {
183
+ uni.showToast({
184
+ title: error && (error.message || error.msg) ? (error.message || error.msg) : '图片上传失败',
185
+ icon: 'none'
186
+ })
187
+ })
188
+ }
189
+
190
+ function cancelIconPick() {
191
+ chatService.cancelIconPick()
192
+ }
193
+
194
+ function clearPendingImage() {
195
+ chatService.clearPendingImage()
196
+ }
197
+
198
+ function previewImage(current, urls) {
199
+ const list = (urls && urls.length ? urls : [current]).filter(Boolean)
200
+ if (!list.length) {
201
+ return
202
+ }
203
+ uni.previewImage({
204
+ current: current,
205
+ urls: list
206
+ })
207
+ }
208
+
209
+ onMounted(() => {
210
+ const config = configModule.getConfig()
211
+ floatIconUrl.value = config.floatIcon || ''
212
+ updateHandler = function (snapshot) {
213
+ syncState(snapshot)
214
+ }
215
+ openHandler = function () {
216
+ opened.value = true
217
+ scrollToBottom()
218
+ }
219
+ closeHandler = function () {
220
+ opened.value = false
221
+ }
222
+ chatService.onUpdate(updateHandler)
223
+ chatService.onOpen(openHandler)
224
+ chatService.onClose(closeHandler)
225
+ syncState(chatService.getSnapshot())
226
+ checkVisible()
227
+ })
228
+
229
+ onUnmounted(() => {
230
+ if (updateHandler) {
231
+ chatService.offUpdate(updateHandler)
232
+ }
233
+ if (openHandler) {
234
+ chatService.offOpen(openHandler)
235
+ }
236
+ if (closeHandler) {
237
+ chatService.offClose(closeHandler)
238
+ }
239
+ })
240
+
241
+ onShow(() => {
242
+ checkVisible()
243
+ })
244
+ </script>
245
+
246
+ <style scoped>
247
+ .ly-ai-float {
248
+ position: fixed;
249
+ z-index: 9999;
250
+ width: 96rpx;
251
+ height: 96rpx;
252
+ border-radius: 50%;
253
+ background: #f84616;
254
+ display: flex;
255
+ align-items: center;
256
+ justify-content: center;
257
+ box-shadow: 0 4rpx 16rpx rgba(248, 70, 22, 0.35);
258
+ }
259
+
260
+ .ly-ai-float-icon {
261
+ width: 56rpx;
262
+ height: 56rpx;
263
+ }
264
+
265
+ .ly-ai-float-text {
266
+ color: #fff;
267
+ font-size: 28rpx;
268
+ font-weight: 600;
269
+ }
270
+
271
+ .ly-ai-overlay {
272
+ position: fixed;
273
+ top: 0;
274
+ left: 0;
275
+ right: 0;
276
+ bottom: 0;
277
+ z-index: 10000;
278
+ background: rgba(0, 0, 0, 0.4);
279
+ display: flex;
280
+ flex-direction: column;
281
+ justify-content: flex-end;
282
+ }
283
+
284
+ .ly-ai-drawer {
285
+ height: 82vh;
286
+ background: #f5f6f8;
287
+ border-radius: 32rpx 32rpx 0 0;
288
+ display: flex;
289
+ flex-direction: column;
290
+ overflow: hidden;
291
+ }
292
+
293
+ .ly-ai-header {
294
+ display: flex;
295
+ align-items: center;
296
+ justify-content: center;
297
+ padding: 24rpx 32rpx;
298
+ background: #fff;
299
+ position: relative;
300
+ border-bottom: 1rpx solid #eee;
301
+ }
302
+
303
+ .ly-ai-title {
304
+ font-size: 32rpx;
305
+ font-weight: 600;
306
+ color: #333;
307
+ }
308
+
309
+ .ly-ai-close {
310
+ position: absolute;
311
+ right: 32rpx;
312
+ top: 50%;
313
+ transform: translateY(-50%);
314
+ width: 56rpx;
315
+ height: 56rpx;
316
+ line-height: 52rpx;
317
+ text-align: center;
318
+ font-size: 40rpx;
319
+ color: #999;
320
+ }
321
+
322
+ .ly-ai-list {
323
+ flex: 1;
324
+ padding: 24rpx;
325
+ box-sizing: border-box;
326
+ }
327
+
328
+ .ly-ai-welcome {
329
+ background: #fff;
330
+ border-radius: 16rpx;
331
+ padding: 32rpx;
332
+ margin-bottom: 24rpx;
333
+ }
334
+
335
+ .ly-ai-welcome-title {
336
+ font-size: 32rpx;
337
+ font-weight: 600;
338
+ color: #333;
339
+ margin-bottom: 12rpx;
340
+ }
341
+
342
+ .ly-ai-welcome-desc {
343
+ font-size: 26rpx;
344
+ color: #666;
345
+ line-height: 1.6;
346
+ margin-bottom: 20rpx;
347
+ }
348
+
349
+ .ly-ai-quick {
350
+ background: #fff5f2;
351
+ color: #f84616;
352
+ font-size: 24rpx;
353
+ padding: 18rpx 24rpx;
354
+ border-radius: 12rpx;
355
+ margin-top: 16rpx;
356
+ }
357
+
358
+ .ly-ai-msg-row {
359
+ display: flex;
360
+ margin-bottom: 20rpx;
361
+ }
362
+
363
+ .ly-ai-msg-row-user {
364
+ justify-content: flex-end;
365
+ }
366
+
367
+ .ly-ai-msg-row-ai {
368
+ justify-content: flex-start;
369
+ }
370
+
371
+ .ly-ai-bubble {
372
+ max-width: 78%;
373
+ padding: 20rpx 24rpx;
374
+ border-radius: 16rpx;
375
+ word-break: break-all;
376
+ }
377
+
378
+ .ly-ai-bubble-user {
379
+ background: #f84616;
380
+ color: #fff;
381
+ border-bottom-right-radius: 4rpx;
382
+ }
383
+
384
+ .ly-ai-bubble-ai {
385
+ background: #fff;
386
+ color: #333;
387
+ border-bottom-left-radius: 4rpx;
388
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
389
+ }
390
+
391
+ .ly-ai-loading-bubble {
392
+ color: #999;
393
+ }
394
+
395
+ .ly-ai-msg-image {
396
+ width: 200rpx;
397
+ height: 200rpx;
398
+ border-radius: 12rpx;
399
+ display: block;
400
+ margin-bottom: 12rpx;
401
+ }
402
+
403
+ .ly-ai-msg-text {
404
+ font-size: 28rpx;
405
+ line-height: 1.6;
406
+ white-space: pre-wrap;
407
+ }
408
+
409
+ .ly-ai-icon-pick-bar {
410
+ padding: 24rpx;
411
+ padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
412
+ background: #fff;
413
+ border-top: 1rpx solid #eee;
414
+ }
415
+
416
+ .ly-ai-icon-pick-title {
417
+ font-size: 28rpx;
418
+ color: #333;
419
+ margin-bottom: 20rpx;
420
+ text-align: center;
421
+ }
422
+
423
+ .ly-ai-icon-pick-actions {
424
+ display: flex;
425
+ gap: 16rpx;
426
+ }
427
+
428
+ .ly-ai-icon-pick-btn {
429
+ flex: 1;
430
+ height: 80rpx;
431
+ line-height: 80rpx;
432
+ text-align: center;
433
+ background: #f84616;
434
+ color: #fff;
435
+ border-radius: 12rpx;
436
+ font-size: 28rpx;
437
+ }
438
+
439
+ .ly-ai-icon-pick-btn.cancel {
440
+ background: #f5f6f8;
441
+ color: #666;
442
+ }
443
+
444
+ .ly-ai-icon-pick-btn.disabled {
445
+ opacity: 0.5;
446
+ }
447
+
448
+ .ly-ai-image-preview-bar {
449
+ display: flex;
450
+ align-items: center;
451
+ padding: 12rpx 24rpx;
452
+ background: #fff;
453
+ border-top: 1rpx solid #eee;
454
+ }
455
+
456
+ .ly-ai-preview-image {
457
+ width: 96rpx;
458
+ height: 96rpx;
459
+ border-radius: 12rpx;
460
+ }
461
+
462
+ .ly-ai-preview-remove {
463
+ margin-left: 16rpx;
464
+ width: 48rpx;
465
+ height: 48rpx;
466
+ line-height: 44rpx;
467
+ text-align: center;
468
+ font-size: 36rpx;
469
+ color: #999;
470
+ background: #f5f6f8;
471
+ border-radius: 50%;
472
+ }
473
+
474
+ .ly-ai-input-bar {
475
+ display: flex;
476
+ align-items: center;
477
+ padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
478
+ background: #fff;
479
+ border-top: 1rpx solid #eee;
480
+ }
481
+
482
+ .ly-ai-attach-btn {
483
+ width: 72rpx;
484
+ height: 72rpx;
485
+ line-height: 68rpx;
486
+ text-align: center;
487
+ font-size: 44rpx;
488
+ color: #666;
489
+ background: #f5f6f8;
490
+ border-radius: 50%;
491
+ margin-right: 16rpx;
492
+ flex-shrink: 0;
493
+ }
494
+
495
+ .ly-ai-attach-btn.disabled {
496
+ opacity: 0.5;
497
+ }
498
+
499
+ .ly-ai-chat-input {
500
+ flex: 1;
501
+ height: 72rpx;
502
+ background: #f5f6f8;
503
+ border-radius: 36rpx;
504
+ padding: 0 28rpx;
505
+ font-size: 28rpx;
506
+ }
507
+
508
+ .ly-ai-send-btn {
509
+ margin-left: 16rpx;
510
+ min-width: 120rpx;
511
+ height: 72rpx;
512
+ line-height: 72rpx;
513
+ text-align: center;
514
+ background: #f84616;
515
+ color: #fff;
516
+ border-radius: 36rpx;
517
+ font-size: 28rpx;
518
+ }
519
+
520
+ .ly-ai-send-btn.disabled {
521
+ opacity: 0.5;
522
+ }
523
+ </style>
package/index.js CHANGED
@@ -1,14 +1,16 @@
1
- var initModule = require('./src/init');
2
- var interceptor = require('./src/interceptor');
3
- var handler = require('./src/handler');
4
-
5
- module.exports = {
6
- version: '1.0.0',
7
- init: initModule.init,
8
- getConfig: initModule.getConfig,
9
- isInitialized: initModule.isInitialized,
10
- uninstall: interceptor.uninstallInterceptor,
11
- parseRenderConfig: handler.parseRenderConfig
12
- };
13
-
14
- initModule.init();
1
+ var initModule = require('./src/init');
2
+ var interceptor = require('./src/interceptor');
3
+ var handler = require('./src/handler');
4
+ var chatService = require('./src/chat/service');
5
+ var configModule = require('./src/config');
6
+
7
+ module.exports = {
8
+ version: '1.0.1',
9
+ init: initModule.init,
10
+ getConfig: configModule.getConfig,
11
+ isInitialized: initModule.isInitialized,
12
+ uninstall: interceptor.uninstallInterceptor,
13
+ parseRenderConfig: handler.parseRenderConfig,
14
+ openChat: chatService.openConversation,
15
+ closeChat: chatService.closeConversation
16
+ };
package/package.json CHANGED
@@ -1,18 +1,27 @@
1
- {
2
- "name": "@syfei49/mini-dynamic-renderer",
3
- "version": "1.0.0",
4
- "description": "微信小程序对话接口动态渲染 SDK",
5
- "main": "index.js",
6
- "miniprogram": ".",
7
- "files": [
8
- "index.js",
9
- "src"
10
- ],
11
- "keywords": [
12
- "wechat",
13
- "miniprogram",
14
- "dynamic-renderer"
15
- ],
16
- "author": "syfei49",
17
- "license": "MIT"
18
- }
1
+ {
2
+ "name": "@syfei49/mini-dynamic-renderer",
3
+ "version": "1.0.3",
4
+ "description": "uni-app 浮层 AI 客服插件",
5
+ "main": "index.js",
6
+ "files": [
7
+ "index.js",
8
+ "src/**/*.js",
9
+ "components/**/*.vue",
10
+ "README.md"
11
+ ],
12
+ "scripts": {
13
+ "test": "echo \"Error: no test specified\" && exit 1"
14
+ },
15
+ "keywords": [
16
+ "uni-app",
17
+ "mini-program",
18
+ "ai",
19
+ "chat",
20
+ "float"
21
+ ],
22
+ "author": "syfei49",
23
+ "license": "MIT",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ }
27
+ }
@@ -0,0 +1,106 @@
1
+ var configModule = require('../config');
2
+
3
+ function getFullUrl(path) {
4
+ var config = configModule.getConfig();
5
+ var baseUrl = config.baseUrl || '';
6
+ var normalizedPath = (path || '').replace(/^\/+/, '/');
7
+ if (baseUrl.slice(-1) === '/' && normalizedPath.indexOf('/') === 0) {
8
+ return baseUrl + normalizedPath.slice(1);
9
+ }
10
+ if (baseUrl && normalizedPath.indexOf('/') !== 0) {
11
+ normalizedPath = '/' + normalizedPath;
12
+ }
13
+ return baseUrl + normalizedPath;
14
+ }
15
+
16
+ function getHeaders(extraHeaders) {
17
+ var config = configModule.getConfig();
18
+ var headers = Object.assign({}, config.getHeaders() || {});
19
+ var token = typeof config.getToken === 'function' ? config.getToken() : '';
20
+ if (token) {
21
+ headers.Authorization = token;
22
+ }
23
+ if (extraHeaders) {
24
+ Object.assign(headers, extraHeaders);
25
+ }
26
+ return headers;
27
+ }
28
+
29
+ function sendChat(payload) {
30
+ var config = configModule.getConfig();
31
+ var url = getFullUrl(config.chatApiPath);
32
+ var headers = getHeaders({
33
+ 'Content-Type': 'application/json'
34
+ });
35
+
36
+ if (config.debug) {
37
+ console.log('[mini-dynamic-renderer] sendChat', url, payload);
38
+ }
39
+
40
+ return new Promise(function (resolve, reject) {
41
+ uni.request({
42
+ url: url,
43
+ method: 'POST',
44
+ data: payload,
45
+ header: headers,
46
+ timeout: 120000,
47
+ success: function (res) {
48
+ var data = res.data || {};
49
+ if (res.statusCode >= 200 && res.statusCode < 300 && data.code !== 500) {
50
+ resolve(data);
51
+ } else {
52
+ reject(new Error(data.msg || data.message || ('请求失败 ' + res.statusCode)));
53
+ }
54
+ },
55
+ fail: function (err) {
56
+ reject(err || new Error('网络请求失败'));
57
+ }
58
+ });
59
+ });
60
+ }
61
+
62
+ function uploadImage(filePath) {
63
+ var config = configModule.getConfig();
64
+ var url = getFullUrl(config.uploadApiPath);
65
+ var headers = getHeaders();
66
+
67
+ if (config.debug) {
68
+ console.log('[mini-dynamic-renderer] uploadImage', url, filePath);
69
+ }
70
+
71
+ return new Promise(function (resolve, reject) {
72
+ uni.uploadFile({
73
+ url: url,
74
+ filePath: filePath,
75
+ name: 'file',
76
+ header: headers,
77
+ success: function (res) {
78
+ var data = res.data || '{}';
79
+ try {
80
+ data = typeof data === 'string' ? JSON.parse(data) : data;
81
+ } catch (e) {
82
+ reject(new Error('上传响应解析失败'));
83
+ return;
84
+ }
85
+ var dataObj = data.data || {};
86
+ var urlValue = data.url || dataObj.url || dataObj.fileName || dataObj || '';
87
+ if (urlValue && typeof urlValue === 'object' && urlValue.url) {
88
+ urlValue = urlValue.url;
89
+ }
90
+ if (urlValue) {
91
+ resolve(urlValue);
92
+ } else {
93
+ reject(new Error(data.msg || data.message || '上传失败'));
94
+ }
95
+ },
96
+ fail: function (err) {
97
+ reject(err || new Error('图片上传失败'));
98
+ }
99
+ });
100
+ });
101
+ }
102
+
103
+ module.exports = {
104
+ sendChat: sendChat,
105
+ uploadImage: uploadImage
106
+ };