Conversations 会话管理组件 📱
介绍
Conversations
是一个基于 Vue 3 和 Element Plus 开发的会话管理组件,支持分组展示、菜单交互、滚动加载、自定义样式等功能。适用于消息列表、文件管理、任务分组等场景,通过灵活的配置和插槽扩展,满足多样化的业务需求。
代码演示
基础使用
通过 @change
事件 获取选中的会话信息。 v-model:active
绑定当前选中的会话。
vue
<script setup lang="ts">
import type { ConversationItem } from 'vue-element-plus-x/types/Conversations'
const timeBasedItems = ref<ConversationItem<{ id: string, label: string }>[]>([
{
id: '1',
label: '今天的会话111111111111111111111111111',
group: 'today',
},
{
id: '2',
group: 'today',
label: '今天的会话2',
disabled: true,
},
{
id: '3',
group: 'yesterday',
label: '昨天的会话1',
},
{
id: '4',
label: '昨天的会话2',
},
{
id: '5',
label: '一周前的会话',
},
{
id: '6',
label: '一个月前的会话',
},
{
id: '7',
label: '很久以前的会话',
},
])
const activeKey1 = ref()
function handleChange(item: ConversationItem<{ id: string, label: string }>) {
ElMessage.success(`选中了: ${item.label}`)
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px; height: 420px;">
<Conversations
v-model:active="activeKey1"
:items="timeBasedItems"
:label-max-width="200"
:show-tooltip="true"
row-key="id"
@change="handleChange"
/>
</div>
</template>
<style scoped lang="less">
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
时间分组与吸顶效果
自动根据会话项的 group
字段分组,滚动时分组标题吸顶显示,提升导航体验。
vue
<script setup lang="ts">
import type { ConversationItem } from 'vue-element-plus-x/types/Conversations'
const timeBasedItems = ref<ConversationItem<{ id: string, label: string }>[]>([
{
id: '1',
label: '今天的会话111111111111111111111111111',
group: 'today',
disabled: true,
},
{
id: '2',
group: 'today',
label: '今天的会话2',
},
{
id: '3',
group: 'yesterday',
label: '昨天的会话1',
},
{
id: '4',
label: '昨天的会话2',
},
{
id: '5',
label: '一周前的会话',
},
{
id: '6',
label: '一个月前的会话',
},
{
id: '7',
label: '很久以前的会话',
},
])
const activeKey1 = ref('1')
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px; height: 420px;">
<Conversations
v-model:active="activeKey1"
:items="timeBasedItems"
groupable
:label-max-width="200"
:show-tooltip="false"
row-key="id"
/>
</div>
</template>
<style scoped lang="less">
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
自定义分组排序
通过 groupable
属性传入排序函数,自定义分组顺序(如:学习 > 工作 > 个人 > 未分组)。
vue
<script setup lang="ts">
import type { GroupableOptions } from 'vue-element-plus-x/types/Conversations'
const groupBasedItems = ref([
{
key: 'g1',
label: '工作文档1',
group: '工作',
},
{
key: 'g2',
label: '工作文档11111111111111111111111111111111111111111',
group: '工作',
},
{
key: 'g3',
label: '工作文档3',
group: '工作',
},
{
key: 'g4',
label: '工作文档4',
group: '工作',
},
{
key: 'g5',
label: '工作文档5',
group: '工作',
},
{
key: 'g6',
label: '工作文档6',
group: '工作',
},
{
key: 'g7',
label: '学习笔记1',
group: '学习',
},
{
key: 'g8',
label: '学习笔记2',
group: '学习',
},
{
key: 'g9',
label: '个人文档1',
group: '个人',
},
{
key: 'g10',
label: '未分组项目',
},
])
// 自定义分组选项
const customGroupOptions: GroupableOptions = {
// 自定义分组排序,学习 > 工作 > 个人 > 未分组
sort: (a: any, b: any) => {
const order: Record<string, number> = { 学习: 0, 工作: 1, 个人: 2, 未分组: 3 }
const orderA = order[a] !== undefined ? order[a] : 999
const orderB = order[b] !== undefined ? order[b] : 999
return orderA - orderB
},
}
const activeKey2 = ref('g1')
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px; height: 420px;">
<Conversations
v-model:active="activeKey2"
:items="groupBasedItems"
:groupable="customGroupOptions"
:label-max-width="200"
:show-tooltip="true"
show-to-top-btn
row-key="key"
>
<template #groupTitle="{ group }">
<div class="custom-group-title">
<!-- 为不同组添加不同的前缀 -->
<span v-if="group.title === '工作'">📊 </span>
<span v-else-if="group.title === '学习'">📚 </span>
<span v-else-if="group.title === '个人'">🏠 </span>
<span v-else>📁 </span>
{{ group.title }}
</div>
</template>
</Conversations>
</div>
</template>
<style scoped lang="less">
.custom-group-title {
display: flex;
align-items: center;
font-weight: 500;
color: #409EFF;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
内置下拉菜单
内置基础菜单功能(重命名、删除),支持菜单命令回调,轻松实现会话项的快捷操作。
@menu-command
触发内置的菜单点击事件。
vue
<script setup lang="ts">
import type { ConversationItem, ConversationMenuCommand } from 'vue-element-plus-x/types/Conversations'
const menuTestItems = ref([
{
key: 'm1',
label: '菜单测试项目 1 - 长文本效果演示文本长度溢出效果测试'.repeat(2),
},
{
key: 'm2',
label: '菜单测试项目 2',
disabled: true,
},
{
key: 'm3',
label: '菜单测试项目 3',
},
{
key: 'm4',
label: '菜单测试项目 4',
},
{
key: 'm5',
label: '菜单测试项目 5',
},
{
key: 'm6',
label: '菜单测试项目 6',
},
{
key: 'm7',
label: '菜单测试项目 7',
},
{
key: 'm8',
label: '菜单测试项目 8',
},
{
key: 'm9',
label: '菜单测试项目 9',
},
{
key: 'm10',
label: '菜单测试项目 10',
},
{
key: 'm11',
label: '菜单测试项目 11',
},
{
key: 'm12',
label: '菜单测试项目 12',
},
{
key: 'm13',
label: '菜单测试项目 13',
},
{
key: 'm14',
label: '菜单测试项目 14',
},
])
const activeKey4 = ref('m1')
// 内置菜单点击方法
function handleMenuCommand(command: ConversationMenuCommand, item: ConversationItem) {
console.log('内置菜单点击事件:', command, item)
// 直接修改 item 是否生效
if (command === 'delete') {
const index = menuTestItems.value.findIndex(itemSlef => itemSlef.key === item.key)
if (index !== -1) {
menuTestItems.value.splice(index, 1)
console.log('删除成功')
ElMessage.success('删除成功')
}
}
if (command === 'rename') {
item.label = '已修改'
console.log('重命名成功')
ElMessage.success('重命名成功')
}
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px; height: 420px;">
<Conversations
v-model:active="activeKey4"
:items="menuTestItems"
:label-max-width="200"
:show-tooltip="true"
row-key="key"
tooltip-placement="right"
:tooltip-offset="35"
show-to-top-btn
show-built-in-menu
@menu-command="handleMenuCommand"
/>
</div>
</template>
<style scoped lang="less">
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
自定义菜单交互
通过插槽扩展菜单内容,支持图标、文本及自定义命令,满足复杂业务逻辑。
vue
<script setup lang="ts">
import { Delete, Edit, EditPen, Share } from '@element-plus/icons-vue'
const menuTestItems = ref([
{
key: 'm1',
label: '菜单测试项目 1 - 长文本效果演示文本长度溢出效果测试'.repeat(2),
},
{
key: 'm2',
label: '菜单测试项目 2',
disabled: true,
},
{
key: 'm3',
label: '菜单测试项目 3',
},
{
key: 'm4',
label: '菜单测试项目 4',
},
{
key: 'm5',
label: '菜单测试项目 5',
},
{
key: 'm6',
label: '菜单测试项目 6',
},
{
key: 'm7',
label: '菜单测试项目 7',
},
{
key: 'm8',
label: '菜单测试项目 8',
},
{
key: 'm9',
label: '菜单测试项目 9',
},
{
key: 'm10',
label: '菜单测试项目 10',
},
{
key: 'm11',
label: '菜单测试项目 11',
},
{
key: 'm12',
label: '菜单测试项目 12',
},
{
key: 'm13',
label: '菜单测试项目 13',
},
{
key: 'm14',
label: '菜单测试项目 14',
},
])
const conversationMenuItems = [
{
key: 'edit',
label: '编辑',
icon: Edit,
command: {
self_id: '1',
self_message: '编辑',
self_type: 'text',
},
},
{
key: 'delete',
label: '删除',
icon: Delete,
disabled: true,
divided: true,
},
{
key: 'share',
label: '分享',
icon: Share,
command: 'share',
},
]
const activeKey4 = ref('m1')
// 处理菜单点击
function handleMenuClick(menuKey: string, item: any) {
console.log('菜单点击', menuKey, item)
switch (menuKey) {
case 'edit':
console.log(`编辑: ${item.label}`)
ElMessage.warning(`编辑: ${item.label}`)
break
case 'delete':
console.log(`删除: ${item.label}`)
ElMessage.error(`删除: ${item.label}`)
break
case 'share':
console.log(`分享: ${item.label}`)
ElMessage.success(`分享: ${item.label}`)
break
}
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px; height: 420px;">
<Conversations
v-model:active="activeKey4"
:items="menuTestItems"
row-key="key"
:label-max-width="200"
:show-tooltip="true"
show-to-top-btn
show-built-in-menu
>
<template #more-filled>
<el-icon>
<EditPen />
</el-icon>
</template>
<template #menu="{ item }">
<div class="menu-buttons">
<el-button
v-for="menuItem in conversationMenuItems"
:key="menuItem.key"
link
size="small"
@click.stop="handleMenuClick(menuItem.key, item)"
>
<el-icon v-if="menuItem.icon">
<component :is="menuItem.icon" />
</el-icon>
<span v-if="menuItem.label">{{ menuItem.label }}</span>
</el-button>
</div>
</template>
</Conversations>
</div>
</template>
<style scoped lang="less">
.menu-buttons {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
padding: 12px;
// 自定义菜单按钮-el-button样式
.el-button {
padding: 4px 8px;
margin-left: 0;
.el-icon {
margin-right: 8px;
}
}
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
懒加载功能
滚动至底部时自动触发加载更多数据,支持加载状态显示,优化大数据量场景性能。
vue
<script setup lang="ts">
import { ChatDotRound, ChatLineRound } from '@element-plus/icons-vue'
const lazyItems = shallowRef([
{
key: 'l1',
label: '初始项目1',
prefixIcon: ChatLineRound,
},
{
key: 'l2',
label: '初始项目2',
prefixIcon: ChatDotRound,
},
{
key: 'l3',
label: '初始项目3',
prefixIcon: ChatLineRound,
},
{
key: 'l4',
label: '初始项目1',
prefixIcon: ChatLineRound,
},
{
key: 'l5',
label: '初始项目2',
prefixIcon: ChatDotRound,
},
{
key: 'l6',
label: '初始项目3',
prefixIcon: ChatLineRound,
},
{
key: 'l7',
label: '初始项目1',
prefixIcon: ChatLineRound,
},
{
key: 'l8',
label: '初始项目2',
prefixIcon: ChatDotRound,
},
{
key: 'l9',
label: '初始项目3',
prefixIcon: ChatLineRound,
},
])
// 加载更多处理
const isLoading = ref(false)
function loadMoreItems() {
if (isLoading.value)
return
isLoading.value = true
console.log('加载更多数据...')
// 模拟异步加载
setTimeout(() => {
const newItems = [
{
key: `l${lazyItems.value.length + 1}`,
label: `加载的项目${lazyItems.value.length + 1}`,
prefixIcon: markRaw(ChatLineRound),
},
{
key: `l${lazyItems.value.length + 2}`,
label: `加载的项目${lazyItems.value.length + 2}`,
prefixIcon: markRaw(ChatDotRound),
},
]
lazyItems.value = [...lazyItems.value, ...newItems]
isLoading.value = false
}, 2000)
}
const activeKey6 = ref('l1')
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px; height: 420px;">
<Conversations
v-model:active="activeKey6"
:items="lazyItems"
:label-max-width="200"
row-key="key"
:show-tooltip="true"
:load-more="loadMoreItems"
:load-more-loading="isLoading"
show-to-top-btn
/>
</div>
</template>
<style scoped lang="less">
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
自定义样式与分组标题
通过插槽和样式属性自定义会话项外观及分组标题图标,支持悬停、激活、菜单打开状态的样式定制。
vue
<script setup lang="ts">
import type { GroupableOptions } from 'vue-element-plus-x/types/Conversations'
const menuTestItems1 = ref([
{
key: 'm1',
label: '菜单测试项目 1 - 长文本效果演示文本长度溢出效果测试'.repeat(2),
group: '工作',
},
{
key: 'm2',
label: '菜单测试项目 2',
disabled: true,
group: '工作',
},
{
key: 'm3',
label: '菜单测试项目 3',
group: '工作',
},
{
key: 'm4',
label: '菜单测试项目 4',
group: '学习',
},
{
key: 'm5',
label: '菜单测试项目 5',
group: '学习',
},
{
key: 'm6',
label: '菜单测试项目 6',
group: '学习',
},
{
key: 'm7',
label: '菜单测试项目 7',
group: '学习',
},
{
key: 'm8',
label: '菜单测试项目 8',
group: '个人',
},
{
key: 'm9',
label: '菜单测试项目 9',
group: '个人',
},
{
key: 'm10',
label: '菜单测试项目 10',
group: '个人',
},
{
key: 'm11',
label: '菜单测试项目 11',
group: '个人',
},
{
key: 'm12',
label: '菜单测试项目 12',
},
{
key: 'm13',
label: '菜单测试项目 13',
},
{
key: 'm14',
label: '菜单测试项目 14',
},
])
const conversationMenuItems1 = [
{
key: 'edit',
label: '编辑',
icon: '🍉',
command: {
self_id: '1',
self_message: '编辑',
self_type: 'text',
},
},
{
key: 'delete',
label: '删除',
icon: '🍎',
disabled: true,
divided: true,
},
{
key: 'share',
label: '分享',
icon: '🍆',
command: 'share',
},
]
const activeKey5 = ref('m1')
// 自定义分组选项
const customGroupOptions: GroupableOptions = {
// 自定义分组排序,学习 > 工作 > 个人 > 未分组
sort: (a: any, b: any) => {
const order: Record<string, number> = { 学习: 0, 工作: 1, 个人: 2, 未分组: 3 }
const orderA = order[a] !== undefined ? order[a] : 999
const orderB = order[b] !== undefined ? order[b] : 999
return orderA - orderB
},
}
// 处理菜单点击
function handleMenuClick(menuKey: string, item: any) {
console.log('菜单点击', menuKey, item)
switch (menuKey) {
case 'edit':
console.log(`编辑: ${item.label}`)
ElMessage.warning(`编辑: ${item.label}`)
break
case 'delete':
console.log(`删除: ${item.label}`)
ElMessage.error(`删除: ${item.label}`)
break
case 'share':
console.log(`分享: ${item.label}`)
ElMessage.success(`分享: ${item.label}`)
break
}
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px; height: 420px;">
<Conversations
v-model:active="activeKey5"
:items="menuTestItems1"
:label-max-width="200"
:show-tooltip="true"
tooltip-placement="right"
:tooltip-offset="35"
show-built-in-menu
:groupable="customGroupOptions"
row-key="key"
:items-style="{
padding: '10px 20px',
borderRadius: '10px',
fontSize: '16px',
fontWeight: 'bold',
textAlign: 'center',
boxShadow: '0 2px 12px 0 rgba(0, 0, 0, 0.1)',
transition: 'all 0.3s',
marginBottom: '20px',
border: '2px dashed transparent',
}"
:items-hover-style="{
background: '#FAFAD2',
border: '2px dashed #006400',
}"
:items-active-style="{
background: '#006400',
color: '#FFFAFA',
border: '2px dashed transparent',
}"
:items-menu-opened-style="{
border: '2px dashed transparent',
}"
:menu-style="{
backgroundColor: 'red',
boxShadow: '0 2px 12px 0 rgba(0, 0, 0, 0.1)',
padding: '10px 20px',
height: '200px',
}"
>
<template #label="{ item }">
<div class="custom-label">
{{ item.label }}
</div>
</template>
<template #groupTitle="{ group }">
<div class="custom-group-title">
<!-- 为不同组添加不同的前缀 -->
<span v-if="group.title === '工作'">📊 </span>
<span v-else-if="group.title === '学习'">📚 </span>
<span v-else-if="group.title === '个人'">🏠 </span>
<span v-else>📁 </span>
{{ group.title }}
</div>
</template>
<template #more-filled="{ item, isHovered, isActive, isMenuOpened, isDisabled }">
<span v-if="isHovered">✍️</span>
<span v-if="isActive">✅</span>
<span v-if="isMenuOpened">🥰</span>
<span
v-if="isDisabled"
:style="{
background: 'black',
padding: '5px',
borderRadius: '10px',
color: 'white',
fontSize: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}"
>
🫥是否禁用:{{ item?.disabled }}
</span>
</template>
<template #menu="{ item }">
<div class="menu-buttons">
<div
v-for="menuItem in conversationMenuItems1"
:key="menuItem.key"
class="menu-self-button"
@click.stop="handleMenuClick(menuItem.key, item)"
>
<span v-if="menuItem.icon">{{ menuItem.icon }}</span>
<span v-if="menuItem.label">{{ menuItem.label }}</span>
</div>
</div>
</template>
</Conversations>
</div>
</template>
<style scoped lang="less">
.custom-group-title {
display: flex;
align-items: center;
font-weight: 500;
color: #409EFF;
}
.menu-buttons {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
padding: 12px;
// 自定义菜单按钮-el-button样式
.el-button {
padding: 4px 8px;
margin-left: 0;
.el-icon {
margin-right: 8px;
}
}
// 自定义菜单按钮-自定义样式
.menu-self-button {
display: flex;
padding: 4px 8px;
align-items: center;
border-radius: 5px;
margin-left: 0;
cursor: pointer;
gap: 8px;
&:hover {
background-color: #f5f7fa;
color: #409EFF;
}
}
}
.custom-label {
display: flex;
align-items: center;
// 溢出隐藏
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
属性
属性名 | 类型 | 是否必填 | 默认值 | 描述 |
---|---|---|---|---|
items | ConversationItem<T>[] | 否 | [] | 会话项数据列表,包含 label 、group 、disabled 等字段 |
groupable | boolean | GroupableOptions | 否 | false | 是否启用分组功能,传入对象可自定义分组排序(sort 函数) |
showBuiltInMenu | boolean | 否 | false | 是否显示内置菜单(重命名、删除) |
loadMore | () => void | 否 | - | 懒加载回调函数,滚动至底部时触发 |
loadMoreLoading | boolean | 否 | false | 加载更多状态,控制加载动画显示 |
showToTopBtn | boolean | 否 | false | 是否显示返回顶部按钮 |
labelKey | string | 否 | 'label' | 会话项标签字段名 |
rowKey | string | 否 | 'id' | 会话项唯一标识字段名 |
itemsStyle | CSSProperties | 否 | {} | 会话项默认样式 |
itemsHoverStyle | CSSProperties | 否 | {} | 会话项悬停样式 |
itemsActiveStyle | CSSProperties | 否 | {} | 会话项激活样式 |
itemsMenuOpenedStyle | CSSProperties | 否 | {} | 会话项菜单打开时样式 |
插槽
插槽名 | 参数 | 描述 |
---|---|---|
#groupTitle | { group: GroupItem } | 自定义分组标题,支持添加图标或特殊样式 |
#label | { item: ConversationItem<T> } | 自定义会话项标签内容,支持文本溢出处理或富文本 |
#more-filled | { item, isHovered, isActive, isMenuOpened, isDisabled } | 会话项右侧附加内容,显示状态标识(如:禁用标记、操作图标) |
#menu | { item: ConversationItem<T> } | 自定义菜单内容,支持按钮、图标或复杂交互组件 |
#header | - | 容器头部插槽,用于添加搜索栏、筛选按钮等自定义内容 |
#footer | - | 容器底部插槽,用于添加分页、统计信息等自定义内容 |
事件
事件 | 参数 | 描述 |
---|---|---|
@menuCommand | (command: ConversationMenuCommand, item: ConversationItem): void | 菜单命令回调,支持重命名、删除等操作。如果你选择自定义菜单,这个方法失效,需要自行处点击菜单的逻辑。 |
:loadMore | -- | 绑定懒加载回调,滚动至底部时触发 |
功能特性
- 灵活分组管理
- 自动根据
group
字段分组,未分组项统一归至“未分组”标题下 - 支持自定义分组排序(通过
groupable.sort
函数),实现业务逻辑定制 - 分组标题吸顶显示,滚动时保持导航可见性
- 丰富的交互支持
- 内置基础菜单(重命名、删除),支持通过
@menu-command
监听命令回调 - 自定义菜单插槽,轻松扩展分享、编辑等复杂操作
- 会话项状态样式独立配置(默认、悬停、激活、菜单打开),视觉反馈清晰
- 性能优化
- 懒加载功能:滚动至底部自动加载更多数据,减少初始渲染压力
- 虚拟滚动(规划中):支持超大列表场景,提升内存使用效率
- 高度可定制
- 全量样式属性:通过
itemsStyle
系列属性自定义会话项外观 - 深度插槽扩展:标签、分组标题、菜单内容均可通过插槽完全自定义
- 响应式设计:支持自适应宽度和滚动条隐藏,适配不同容器尺寸