@flexem/chat-box 1.0.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.
Files changed (46) hide show
  1. package/README.md +638 -0
  2. package/miniprogram_dist/TEST_CASES.md +256 -0
  3. package/miniprogram_dist/assets/icons/icon-arrow-down.svg +1 -0
  4. package/miniprogram_dist/assets/icons/icon-arrow-up.svg +1 -0
  5. package/miniprogram_dist/assets/icons/icon-avatar-default.svg +1 -0
  6. package/miniprogram_dist/assets/icons/icon-back.svg +1 -0
  7. package/miniprogram_dist/assets/icons/icon-camera.svg +1 -0
  8. package/miniprogram_dist/assets/icons/icon-close.svg +1 -0
  9. package/miniprogram_dist/assets/icons/icon-copy.svg +1 -0
  10. package/miniprogram_dist/assets/icons/icon-delete.svg +1 -0
  11. package/miniprogram_dist/assets/icons/icon-edit-msg.svg +1 -0
  12. package/miniprogram_dist/assets/icons/icon-edit.svg +1 -0
  13. package/miniprogram_dist/assets/icons/icon-file.svg +1 -0
  14. package/miniprogram_dist/assets/icons/icon-image.svg +1 -0
  15. package/miniprogram_dist/assets/icons/icon-keyboard.svg +1 -0
  16. package/miniprogram_dist/assets/icons/icon-menu.svg +1 -0
  17. package/miniprogram_dist/assets/icons/icon-play-voice.svg +1 -0
  18. package/miniprogram_dist/assets/icons/icon-plus.svg +1 -0
  19. package/miniprogram_dist/assets/icons/icon-regenerate.svg +1 -0
  20. package/miniprogram_dist/assets/icons/icon-thinking.svg +1 -0
  21. package/miniprogram_dist/assets/icons/icon-voice.svg +1 -0
  22. package/miniprogram_dist/components/attachment/index.js +169 -0
  23. package/miniprogram_dist/components/attachment/index.json +4 -0
  24. package/miniprogram_dist/components/attachment/index.wxml +40 -0
  25. package/miniprogram_dist/components/attachment/index.wxss +119 -0
  26. package/miniprogram_dist/components/input-bar/index.js +934 -0
  27. package/miniprogram_dist/components/input-bar/index.json +6 -0
  28. package/miniprogram_dist/components/input-bar/index.wxml +132 -0
  29. package/miniprogram_dist/components/input-bar/index.wxss +324 -0
  30. package/miniprogram_dist/components/message/index.js +988 -0
  31. package/miniprogram_dist/components/message/index.json +4 -0
  32. package/miniprogram_dist/components/message/index.wxml +285 -0
  33. package/miniprogram_dist/components/message/index.wxss +575 -0
  34. package/miniprogram_dist/components/sidebar/index.js +506 -0
  35. package/miniprogram_dist/components/sidebar/index.json +4 -0
  36. package/miniprogram_dist/components/sidebar/index.wxml +137 -0
  37. package/miniprogram_dist/components/sidebar/index.wxss +264 -0
  38. package/miniprogram_dist/index.js +1316 -0
  39. package/miniprogram_dist/index.json +8 -0
  40. package/miniprogram_dist/index.wxml +172 -0
  41. package/miniprogram_dist/index.wxss +291 -0
  42. package/miniprogram_dist/package.json +5 -0
  43. package/miniprogram_dist/utils/api.js +474 -0
  44. package/miniprogram_dist/utils/audio.js +860 -0
  45. package/miniprogram_dist/utils/storage.js +168 -0
  46. package/package.json +27 -0
@@ -0,0 +1,506 @@
1
+ /**
2
+ * 侧边栏组件
3
+ * 对话列表管理、编辑模式、用户信息显示
4
+ */
5
+
6
+ const api = require('../../utils/api.js');
7
+
8
+ Component({
9
+ properties: {
10
+ // 是否显示侧边栏
11
+ visible: {
12
+ type: Boolean,
13
+ value: false
14
+ },
15
+ // API 基础地址
16
+ baseUrl: {
17
+ type: String,
18
+ value: ''
19
+ },
20
+ // 用户 Token
21
+ token: {
22
+ type: String,
23
+ value: ''
24
+ },
25
+ // 当前会话 ID
26
+ currentSessionId: {
27
+ type: String,
28
+ value: ''
29
+ },
30
+ // 用户名
31
+ username: {
32
+ type: String,
33
+ value: ''
34
+ },
35
+ // 用户头像
36
+ userAvatar: {
37
+ type: String,
38
+ value: ''
39
+ }
40
+ },
41
+
42
+ data: {
43
+ sessions: [], // 所有会话列表
44
+ todaySessions: [], // 今天的会话
45
+ thisWeekSessions: [], // 本周的会话
46
+ earlierSessions: [], // 更早的会话
47
+ isEditMode: false, // 是否编辑模式
48
+ isLoading: false, // 是否加载中
49
+ hasMore: true, // 是否有更多数据
50
+ currentPage: 1, // 当前页码
51
+ pageSize: 20, // 每页数量
52
+ searchKeyword: '' // 搜索关键词
53
+ },
54
+
55
+ observers: {
56
+ 'visible': function(visible) {
57
+ if (visible && this.data.sessions.length === 0) {
58
+ this.loadSessions();
59
+ }
60
+ }
61
+ },
62
+
63
+ methods: {
64
+ /**
65
+ * 加载会话列表
66
+ */
67
+ async loadSessions(isRefresh = true) {
68
+ if (this.data.isLoading) return;
69
+ if (!this.properties.baseUrl || !this.properties.token) {
70
+ console.error('缺少 baseUrl 或 token');
71
+ return;
72
+ }
73
+
74
+ this.setData({ isLoading: true });
75
+
76
+ try {
77
+ const page = isRefresh ? 1 : this.data.currentPage;
78
+ const result = await api.getChatSessions(
79
+ this.properties.baseUrl,
80
+ this.properties.token,
81
+ page,
82
+ this.data.pageSize
83
+ );
84
+
85
+ // 支持多种 API 返回格式: items, data.items, records, data, 直接数组
86
+ const resultData = result.items || result.data?.items || result.records || result.data || result || [];
87
+ const sessions = this.processSessionData(Array.isArray(resultData) ? resultData : []);
88
+ const grouped = this.groupSessionsByDate(sessions);
89
+
90
+ if (isRefresh) {
91
+ this.setData({
92
+ sessions: sessions,
93
+ todaySessions: grouped.today,
94
+ thisWeekSessions: grouped.thisWeek,
95
+ earlierSessions: grouped.earlier,
96
+ currentPage: 2,
97
+ hasMore: sessions.length >= this.data.pageSize
98
+ });
99
+ } else {
100
+ const allSessions = [...this.data.sessions, ...sessions];
101
+ const allGrouped = this.groupSessionsByDate(allSessions);
102
+ this.setData({
103
+ sessions: allSessions,
104
+ todaySessions: allGrouped.today,
105
+ thisWeekSessions: allGrouped.thisWeek,
106
+ earlierSessions: allGrouped.earlier,
107
+ currentPage: this.data.currentPage + 1,
108
+ hasMore: sessions.length >= this.data.pageSize
109
+ });
110
+ }
111
+ } catch (error) {
112
+ console.error('加载会话列表失败', error);
113
+ if (error.code === 401) {
114
+ this.triggerEvent('login');
115
+ } else {
116
+ wx.showToast({
117
+ title: error.message || '加载失败',
118
+ icon: 'none'
119
+ });
120
+ }
121
+ } finally {
122
+ this.setData({ isLoading: false });
123
+ }
124
+ },
125
+
126
+ /**
127
+ * 处理会话数据
128
+ */
129
+ processSessionData(sessions) {
130
+ return sessions.map(session => {
131
+ // 支持多种时间字段格式
132
+ const createTime = new Date(session.created_at || session.create_time || session.createTime);
133
+ return {
134
+ id: session.id,
135
+ title: session.title || '新对话',
136
+ createTime: createTime,
137
+ timeText: this.formatTimeText(createTime),
138
+ configs: session.configs || {}
139
+ };
140
+ });
141
+ },
142
+
143
+ /**
144
+ * 按日期分组会话
145
+ */
146
+ groupSessionsByDate(sessions) {
147
+ const now = new Date();
148
+ const today = new Date(now);
149
+ today.setHours(0, 0, 0, 0);
150
+
151
+ // 获取本周一的日期
152
+ const weekStart = new Date(today);
153
+ const dayOfWeek = weekStart.getDay();
154
+ const diff = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // 周日为0,需要特殊处理
155
+ weekStart.setDate(weekStart.getDate() - diff);
156
+
157
+ const grouped = {
158
+ today: [],
159
+ thisWeek: [],
160
+ earlier: []
161
+ };
162
+
163
+ sessions.forEach(session => {
164
+ const sessionDate = new Date(session.createTime);
165
+ sessionDate.setHours(0, 0, 0, 0);
166
+
167
+ if (sessionDate.getTime() === today.getTime()) {
168
+ grouped.today.push(session);
169
+ } else if (sessionDate.getTime() >= weekStart.getTime()) {
170
+ grouped.thisWeek.push(session);
171
+ } else {
172
+ grouped.earlier.push(session);
173
+ }
174
+ });
175
+
176
+ return grouped;
177
+ },
178
+
179
+ /**
180
+ * 格式化时间文本
181
+ */
182
+ formatTimeText(date) {
183
+ const now = new Date();
184
+ const diff = now.getTime() - date.getTime();
185
+ const hours = date.getHours().toString().padStart(2, '0');
186
+ const minutes = date.getMinutes().toString().padStart(2, '0');
187
+
188
+ // 今天
189
+ if (diff < 24 * 60 * 60 * 1000 && now.getDate() === date.getDate()) {
190
+ return `${hours}:${minutes}`;
191
+ }
192
+
193
+ // 本周(非今天)- 显示星期几
194
+ const today = new Date(now);
195
+ today.setHours(0, 0, 0, 0);
196
+ const weekStart = new Date(today);
197
+ const dayOfWeek = weekStart.getDay();
198
+ const weekDiff = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
199
+ weekStart.setDate(weekStart.getDate() - weekDiff);
200
+
201
+ const sessionDate = new Date(date);
202
+ sessionDate.setHours(0, 0, 0, 0);
203
+
204
+ if (sessionDate.getTime() >= weekStart.getTime()) {
205
+ const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
206
+ return `${weekdays[date.getDay()]} ${hours}:${minutes}`;
207
+ }
208
+
209
+ // 更早 - 显示年月日
210
+ const year = date.getFullYear();
211
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
212
+ const day = date.getDate().toString().padStart(2, '0');
213
+ return `${year}-${month}-${day}`;
214
+ },
215
+
216
+ /**
217
+ * 加载更多
218
+ */
219
+ loadMore() {
220
+ if (!this.data.hasMore || this.data.isLoading) return;
221
+ this.loadSessions(false);
222
+ },
223
+
224
+ /**
225
+ * 滚动到底部自动加载更多
226
+ */
227
+ onScrollToLower() {
228
+ this.loadMore();
229
+ },
230
+
231
+ /**
232
+ * 切换编辑模式
233
+ */
234
+ toggleEditMode() {
235
+ this.setData({
236
+ isEditMode: !this.data.isEditMode
237
+ });
238
+ },
239
+
240
+ /**
241
+ * 选择会话
242
+ */
243
+ onSelectSession(e) {
244
+ if (this.data.isEditMode) return;
245
+ const session = e.currentTarget.dataset.session;
246
+ this.triggerEvent('select', { session });
247
+ this.onClose();
248
+ },
249
+
250
+ /**
251
+ * 新建对话
252
+ */
253
+ async onNewChat() {
254
+ if (!this.properties.baseUrl || !this.properties.token) {
255
+ this.triggerEvent('login');
256
+ return;
257
+ }
258
+
259
+ wx.showLoading({ title: '创建中...' });
260
+
261
+ try {
262
+ const result = await api.createChatSession(
263
+ this.properties.baseUrl,
264
+ this.properties.token,
265
+ '新对话'
266
+ );
267
+
268
+ const newSession = {
269
+ id: result.id,
270
+ title: result.title || '新对话',
271
+ createTime: new Date(),
272
+ timeText: this.formatTimeText(new Date()),
273
+ configs: result.configs || {}
274
+ };
275
+
276
+ // 添加到列表顶部
277
+ const todaySessions = [newSession, ...this.data.todaySessions];
278
+ const sessions = [newSession, ...this.data.sessions];
279
+
280
+ this.setData({
281
+ sessions,
282
+ todaySessions
283
+ });
284
+
285
+ this.triggerEvent('select', { session: newSession });
286
+ this.onClose();
287
+ } catch (error) {
288
+ console.error('创建对话失败', error);
289
+ if (error.code === 401) {
290
+ this.triggerEvent('login');
291
+ } else {
292
+ wx.showToast({
293
+ title: error.message || '创建失败',
294
+ icon: 'none'
295
+ });
296
+ }
297
+ } finally {
298
+ wx.hideLoading();
299
+ }
300
+ },
301
+
302
+ /**
303
+ * 重命名对话
304
+ */
305
+ onRename(e) {
306
+ const session = e.currentTarget.dataset.session;
307
+
308
+ wx.showModal({
309
+ title: '重命名',
310
+ editable: true,
311
+ placeholderText: '请输入新标题',
312
+ content: session.title,
313
+ success: async (res) => {
314
+ if (res.confirm && res.content && res.content.trim()) {
315
+ try {
316
+ await api.renameChatSession(
317
+ this.properties.baseUrl,
318
+ this.properties.token,
319
+ session.id,
320
+ res.content.trim()
321
+ );
322
+
323
+ // 更新本地数据
324
+ this.updateSessionTitle(session.id, res.content.trim());
325
+
326
+ wx.showToast({
327
+ title: '重命名成功',
328
+ icon: 'success'
329
+ });
330
+ } catch (error) {
331
+ console.error('重命名失败', error);
332
+ wx.showToast({
333
+ title: error.message || '重命名失败',
334
+ icon: 'none'
335
+ });
336
+ }
337
+ }
338
+ }
339
+ });
340
+ },
341
+
342
+ /**
343
+ * 更新会话标题
344
+ */
345
+ updateSessionTitle(sessionId, newTitle) {
346
+ const updateList = (list) => {
347
+ return list.map(item => {
348
+ if (item.id === sessionId) {
349
+ return { ...item, title: newTitle };
350
+ }
351
+ return item;
352
+ });
353
+ };
354
+
355
+ this.setData({
356
+ sessions: updateList(this.data.sessions),
357
+ todaySessions: updateList(this.data.todaySessions),
358
+ thisWeekSessions: updateList(this.data.thisWeekSessions),
359
+ earlierSessions: updateList(this.data.earlierSessions)
360
+ });
361
+ },
362
+
363
+ /**
364
+ * 删除对话
365
+ */
366
+ onDelete(e) {
367
+ const session = e.currentTarget.dataset.session;
368
+
369
+ wx.showModal({
370
+ title: '确认删除',
371
+ content: `确定要删除「${session.title}」吗?`,
372
+ confirmColor: '#f44336',
373
+ success: async (res) => {
374
+ if (res.confirm) {
375
+ try {
376
+ await api.deleteChatSession(
377
+ this.properties.baseUrl,
378
+ this.properties.token,
379
+ session.id
380
+ );
381
+
382
+ // 从本地列表中移除
383
+ this.removeSession(session.id);
384
+
385
+ wx.showToast({
386
+ title: '删除成功',
387
+ icon: 'success'
388
+ });
389
+
390
+ // 如果删除的是当前会话,通知父组件
391
+ if (session.id === this.properties.currentSessionId) {
392
+ this.triggerEvent('delete', { sessionId: session.id });
393
+ }
394
+ } catch (error) {
395
+ console.error('删除失败', error);
396
+ wx.showToast({
397
+ title: error.message || '删除失败',
398
+ icon: 'none'
399
+ });
400
+ }
401
+ }
402
+ }
403
+ });
404
+ },
405
+
406
+ /**
407
+ * 从列表中移除会话
408
+ */
409
+ removeSession(sessionId) {
410
+ const filterList = (list) => list.filter(item => item.id !== sessionId);
411
+
412
+ this.setData({
413
+ sessions: filterList(this.data.sessions),
414
+ todaySessions: filterList(this.data.todaySessions),
415
+ thisWeekSessions: filterList(this.data.thisWeekSessions),
416
+ earlierSessions: filterList(this.data.earlierSessions)
417
+ });
418
+ },
419
+
420
+ /**
421
+ * 搜索输入
422
+ */
423
+ onSearchInput(e) {
424
+ this.setData({
425
+ searchKeyword: e.detail.value
426
+ });
427
+ },
428
+
429
+ /**
430
+ * 执行搜索
431
+ */
432
+ async onSearch() {
433
+ const keyword = this.data.searchKeyword.trim();
434
+ if (!keyword) {
435
+ this.loadSessions();
436
+ return;
437
+ }
438
+
439
+ this.setData({ isLoading: true });
440
+
441
+ try {
442
+ const result = await api.searchChats(
443
+ this.properties.baseUrl,
444
+ this.properties.token,
445
+ keyword
446
+ );
447
+
448
+ const sessions = this.processSessionData(result || []);
449
+ const grouped = this.groupSessionsByDate(sessions);
450
+
451
+ this.setData({
452
+ sessions: sessions,
453
+ todaySessions: grouped.today,
454
+ thisWeekSessions: grouped.thisWeek,
455
+ earlierSessions: grouped.earlier,
456
+ hasMore: false
457
+ });
458
+ } catch (error) {
459
+ console.error('搜索失败', error);
460
+ wx.showToast({
461
+ title: error.message || '搜索失败',
462
+ icon: 'none'
463
+ });
464
+ } finally {
465
+ this.setData({ isLoading: false });
466
+ }
467
+ },
468
+
469
+ /**
470
+ * 清除搜索
471
+ */
472
+ clearSearch() {
473
+ this.setData({ searchKeyword: '' });
474
+ this.loadSessions();
475
+ },
476
+
477
+ /**
478
+ * 关闭侧边栏
479
+ */
480
+ onClose() {
481
+ this.setData({ isEditMode: false });
482
+ this.triggerEvent('close');
483
+ },
484
+
485
+ /**
486
+ * 点击遮罩关闭
487
+ */
488
+ onMaskTap() {
489
+ this.onClose();
490
+ },
491
+
492
+ /**
493
+ * 阻止冒泡
494
+ */
495
+ preventBubble() {
496
+ // 阻止事件冒泡
497
+ },
498
+
499
+ /**
500
+ * 刷新列表
501
+ */
502
+ refresh() {
503
+ this.loadSessions(true);
504
+ }
505
+ }
506
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "component": true,
3
+ "usingComponents": {}
4
+ }
@@ -0,0 +1,137 @@
1
+ <view class="sidebar-wrapper {{visible ? 'show' : ''}}">
2
+ <view class="sidebar-container" catchtap="preventBubble">
3
+ <!-- 顶部标题栏(只有关闭按钮) -->
4
+ <view class="sidebar-header">
5
+ <view class="close-btn" bindtap="onClose">
6
+ <image class="close-icon" src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-close.svg" mode="aspectFit" />
7
+ </view>
8
+ </view>
9
+
10
+ <!-- 标题行(对话列表 + 编辑按钮) -->
11
+ <view class="title-row">
12
+ <text class="sidebar-title">对话列表</text>
13
+ <text class="edit-btn" bindtap="toggleEditMode">{{isEditMode ? '完成' : '编辑'}}</text>
14
+ </view>
15
+ <!-- 新建对话按钮(暂时隐藏)
16
+ <view class="new-chat-btn" bindtap="onNewChat">
17
+ <text class="new-chat-icon">+</text>
18
+ <text class="new-chat-text">开始新对话</text>
19
+ </view>
20
+ -->
21
+
22
+ <!-- 搜索框(暂时隐藏)
23
+ <view class="search-box">
24
+ <input
25
+ class="search-input"
26
+ placeholder="搜索对话..."
27
+ value="{{searchKeyword}}"
28
+ bindinput="onSearchInput"
29
+ bindconfirm="onSearch"
30
+ />
31
+ <view wx:if="{{searchKeyword}}" class="search-clear" bindtap="clearSearch">
32
+ <text>×</text>
33
+ </view>
34
+ </view>
35
+ -->
36
+
37
+ <!-- 对话列表 -->
38
+ <scroll-view class="session-list" scroll-y="true" enhanced="true" show-scrollbar="false" bindscrolltolower="onScrollToLower" lower-threshold="100">
39
+ <!-- 今天的对话 -->
40
+ <view wx:if="{{todaySessions.length > 0}}" class="session-group">
41
+ <text class="group-title">今天</text>
42
+ <view
43
+ wx:for="{{todaySessions}}"
44
+ wx:key="id"
45
+ class="session-item {{currentSessionId === item.id ? 'active' : ''}}"
46
+ bindtap="onSelectSession"
47
+ data-session="{{item}}"
48
+ >
49
+ <view class="session-content">
50
+ <text class="session-title">{{item.title || '新对话'}}</text>
51
+ <text class="session-time">{{item.timeText}}</text>
52
+ </view>
53
+ <view wx:if="{{isEditMode}}" class="session-actions">
54
+ <view class="action-btn rename-btn" catchtap="onRename" data-session="{{item}}">
55
+ <image class="action-icon" src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-edit.svg" mode="aspectFit" />
56
+ </view>
57
+ <view class="action-btn delete-btn" catchtap="onDelete" data-session="{{item}}">
58
+ <image class="action-icon" src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-delete.svg" mode="aspectFit" />
59
+ </view>
60
+ </view>
61
+ </view>
62
+ </view>
63
+
64
+ <!-- 本周的对话 -->
65
+ <view wx:if="{{thisWeekSessions.length > 0}}" class="session-group">
66
+ <text class="group-title">本周</text>
67
+ <view
68
+ wx:for="{{thisWeekSessions}}"
69
+ wx:key="id"
70
+ class="session-item {{currentSessionId === item.id ? 'active' : ''}}"
71
+ bindtap="onSelectSession"
72
+ data-session="{{item}}"
73
+ >
74
+ <view class="session-content">
75
+ <text class="session-title">{{item.title || '新对话'}}</text>
76
+ <text class="session-time">{{item.timeText}}</text>
77
+ </view>
78
+ <view wx:if="{{isEditMode}}" class="session-actions">
79
+ <view class="action-btn rename-btn" catchtap="onRename" data-session="{{item}}">
80
+ <image class="action-icon" src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-edit.svg" mode="aspectFit" />
81
+ </view>
82
+ <view class="action-btn delete-btn" catchtap="onDelete" data-session="{{item}}">
83
+ <image class="action-icon" src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-delete.svg" mode="aspectFit" />
84
+ </view>
85
+ </view>
86
+ </view>
87
+ </view>
88
+
89
+ <!-- 更早的对话 -->
90
+ <view wx:if="{{earlierSessions.length > 0}}" class="session-group">
91
+ <text class="group-title">更早</text>
92
+ <view
93
+ wx:for="{{earlierSessions}}"
94
+ wx:key="id"
95
+ class="session-item {{currentSessionId === item.id ? 'active' : ''}}"
96
+ bindtap="onSelectSession"
97
+ data-session="{{item}}"
98
+ >
99
+ <view class="session-content">
100
+ <text class="session-title">{{item.title || '新对话'}}</text>
101
+ <text class="session-time">{{item.timeText}}</text>
102
+ </view>
103
+ <view wx:if="{{isEditMode}}" class="session-actions">
104
+ <view class="action-btn rename-btn" catchtap="onRename" data-session="{{item}}">
105
+ <image class="action-icon" src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-edit.svg" mode="aspectFit" />
106
+ </view>
107
+ <view class="action-btn delete-btn" catchtap="onDelete" data-session="{{item}}">
108
+ <image class="action-icon" src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-delete.svg" mode="aspectFit" />
109
+ </view>
110
+ </view>
111
+ </view>
112
+ </view>
113
+
114
+ <!-- 空状态 -->
115
+ <view wx:if="{{!todaySessions.length && !thisWeekSessions.length && !earlierSessions.length}}" class="empty-state">
116
+ <text class="empty-text">暂无对话记录</text>
117
+ </view>
118
+
119
+ <!-- 加载更多 -->
120
+ <view wx:if="{{hasMore}}" class="load-more" bindtap="loadMore">
121
+ <text>{{isLoading ? '加载中...' : '加载更多'}}</text>
122
+ </view>
123
+ </scroll-view>
124
+
125
+ <!-- 底部用户信息 -->
126
+ <view class="sidebar-footer">
127
+ <view class="user-info">
128
+ <image
129
+ class="user-avatar"
130
+ src="{{userAvatar || '/miniprogram_npm/@flexem/chat-box/assets/icons/icon-avatar-default.svg'}}"
131
+ mode="aspectFill"
132
+ />
133
+ <text class="username">{{username || '未登录'}}</text>
134
+ </view>
135
+ </view>
136
+ </view>
137
+ </view>