Skip to content

Commit 438bf37

Browse files
committed
feat(shared): 新增 useSelection 函数
1 parent f72ad76 commit 438bf37

File tree

3 files changed

+338
-0
lines changed

3 files changed

+338
-0
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
<script setup lang="ts">
2+
import { computed, reactive, ref } from 'vue'
3+
import { SectionItem, SectionLayout, SectionMain, useSelection } from '@/shared'
4+
5+
interface User {
6+
id: number
7+
name: string
8+
role: string
9+
disabled?: boolean
10+
}
11+
12+
const config = reactive({
13+
singleSelect: false,
14+
ignoreSelectable: false,
15+
})
16+
17+
const users = ref<User[]>([
18+
{ id: 1, name: '张三', role: 'Admin' },
19+
{ id: 2, name: '李四', role: 'Editor' },
20+
{ id: 3, name: '王五', role: 'User', disabled: true },
21+
{ id: 4, name: '赵六', role: 'User' },
22+
{ id: 5, name: '孙七', role: 'Editor', disabled: true },
23+
{ id: 6, name: '周八', role: 'Admin' },
24+
])
25+
26+
const selection = useSelection<User, number>({
27+
singleSelect: computed(() => config.singleSelect),
28+
toKey: (row) => row.id,
29+
selectable: (row) => !row.disabled,
30+
})
31+
32+
const selectedNames = computed(() =>
33+
selection.selection.value.map((r) => r.name),
34+
)
35+
36+
const selectableRows = computed(() =>
37+
users.value.filter(
38+
(r) => config.ignoreSelectable || selection.isSelectable(r),
39+
),
40+
)
41+
const selectedSelectableCount = computed(
42+
() => selectableRows.value.filter((r) => selection.isSelected(r)).length,
43+
)
44+
const allSelected = computed(
45+
() =>
46+
selectableRows.value.length > 0 &&
47+
selectedSelectableCount.value === selectableRows.value.length,
48+
)
49+
const isIndeterminate = computed(
50+
() =>
51+
selectedSelectableCount.value > 0 &&
52+
selectedSelectableCount.value < selectableRows.value.length,
53+
)
54+
55+
function handleToggleAll(val: any) {
56+
const checked = !!val
57+
selectableRows.value.forEach((row) =>
58+
selection.toggleSelection(row, checked, config.ignoreSelectable),
59+
)
60+
}
61+
62+
function selectAllowed() {
63+
if (config.singleSelect) {
64+
const row = users.value.find((r) => selection.isSelectable(r))
65+
if (row) {
66+
selection.toggleSelection(row, true)
67+
}
68+
}
69+
else {
70+
users.value.forEach((row) => {
71+
if (selection.isSelectable(row)) {
72+
selection.toggleSelection(row, true)
73+
}
74+
})
75+
}
76+
}
77+
78+
function toggleThird() {
79+
const row = users.value[2]
80+
if (row) {
81+
selection.toggleSelection(row, undefined, config.ignoreSelectable)
82+
}
83+
}
84+
</script>
85+
86+
<template>
87+
<SectionLayout height="100%">
88+
<SectionItem card>
89+
<div style="padding: 12px">
90+
<ElSpace
91+
wrap
92+
:size="12"
93+
alignment="center"
94+
>
95+
<ElSwitch
96+
v-model="config.singleSelect"
97+
active-text="单选模式"
98+
@change="() => selection.clearSelection()"
99+
/>
100+
<ElSwitch
101+
v-model="config.ignoreSelectable"
102+
active-text="忽略 selectable(编程操作)"
103+
/>
104+
<ElDivider direction="vertical" />
105+
<ElButton @click="selectAllowed">
106+
选择可选项
107+
</ElButton>
108+
<ElButton @click="toggleThird">
109+
切换第 3 行
110+
</ElButton>
111+
<ElButton
112+
type="danger"
113+
@click="selection.clearSelection()"
114+
>
115+
清空选择
116+
</ElButton>
117+
</ElSpace>
118+
</div>
119+
</SectionItem>
120+
121+
<SectionMain card>
122+
<div style="padding: 16px">
123+
<ElSpace
124+
direction="vertical"
125+
fill
126+
style="width: 100%"
127+
>
128+
<ElText tag="b">
129+
1. 基础用法
130+
</ElText>
131+
132+
<ElTable
133+
:data="users"
134+
border
135+
style="width: 100%"
136+
>
137+
<ElTableColumn
138+
label="选择"
139+
width="80"
140+
align="center"
141+
>
142+
<template #header>
143+
<ElCheckbox
144+
v-if="!config.singleSelect"
145+
:model-value="allSelected"
146+
:indeterminate="isIndeterminate"
147+
@change="handleToggleAll"
148+
/>
149+
<ElText v-else>
150+
单选
151+
</ElText>
152+
</template>
153+
<template #default="{ row }">
154+
<ElCheckbox
155+
:model-value="selection.isSelected(row)"
156+
:disabled="!selection.isSelectable(row)"
157+
@change="(val) => selection.toggleSelection(row, !!val)"
158+
/>
159+
</template>
160+
</ElTableColumn>
161+
<ElTableColumn
162+
prop="id"
163+
label="ID"
164+
width="80"
165+
align="center"
166+
/>
167+
<ElTableColumn
168+
prop="name"
169+
label="姓名"
170+
/>
171+
<ElTableColumn
172+
prop="role"
173+
label="角色"
174+
width="120"
175+
align="center"
176+
/>
177+
<ElTableColumn
178+
label="可选"
179+
width="100"
180+
align="center"
181+
>
182+
<template #default="{ row }">
183+
<ElTag :type="row.disabled ? 'danger' : 'success'">
184+
{{ row.disabled ? '不可选' : '可选' }}
185+
</ElTag>
186+
</template>
187+
</ElTableColumn>
188+
</ElTable>
189+
190+
<ElAlert
191+
type="info"
192+
:closable="false"
193+
>
194+
<template #title>
195+
当前选择
196+
</template>
197+
<div>
198+
<div>
199+
选中键(ids):
200+
{{ selection.selectedKeys.value.join(', ') || '无' }}
201+
</div>
202+
<div>选中项: {{ selectedNames.join(', ') || '无' }}</div>
203+
</div>
204+
</ElAlert>
205+
206+
<ElAlert
207+
type="success"
208+
:closable="false"
209+
>
210+
<template #title>
211+
说明
212+
</template>
213+
<ul style="margin: 4px 0; padding-left: 18px">
214+
<li>singleSelect 为真时,每次选择前会清空已有选择</li>
215+
<li>
216+
selectable 控制行是否可选;示例中标记为“不可选”的行将禁用勾选
217+
</li>
218+
<li>
219+
toggleSelection(row, selected?, ignoreSelectable?)
220+
支持忽略可选逻辑进行编程式切换
221+
</li>
222+
</ul>
223+
</ElAlert>
224+
</ElSpace>
225+
</div>
226+
</SectionMain>
227+
</SectionLayout>
228+
</template>

src/shared/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './section-layout'
22
export * from './toolbar'
33
export * from './use-pagination'
4+
export * from './use-selection'

src/shared/use-selection/index.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { ComputedRef, MaybeRefOrGetter } from 'vue'
2+
import { computed, shallowReactive, toValue } from 'vue'
3+
4+
export interface UseSelectionOptions<T = any, K = any> {
5+
/**
6+
* 是否为单选模式,开启后每次选择前会清空之前的选择
7+
*/
8+
singleSelect?: MaybeRefOrGetter<boolean>
9+
/**
10+
* 获取数据的 key
11+
*/
12+
toKey: (data: T) => K
13+
/**
14+
* 是否可选
15+
* @default
16+
* ```ts
17+
* () => true
18+
* ```
19+
*/
20+
selectable?: (data: T) => boolean
21+
}
22+
23+
export interface UseSelectionReturn<T = any, K = any> {
24+
/**
25+
* 选中的数据列表
26+
*/
27+
selection: ComputedRef<T[]>
28+
/**
29+
* 选中的数据 key 列表
30+
*/
31+
selectedKeys: ComputedRef<K[]>
32+
/**
33+
* 是否为单选模式
34+
*/
35+
isSingleSelect: ComputedRef<boolean>
36+
/**
37+
* 获取数据的 key
38+
*/
39+
toKey: UseSelectionOptions<T, K>['toKey']
40+
/**
41+
* 是否选中某条数据
42+
*/
43+
isSelected: (data: T) => boolean
44+
/**
45+
* 是否可选某条数据
46+
*/
47+
isSelectable: (data: T) => boolean
48+
/**
49+
* 切换选中项
50+
* @param data 数据
51+
* @param selected 是否选中
52+
* @param ignoreSelectable 是否忽略 selectable 选项,默认为 false
53+
*/
54+
toggleSelection: (
55+
data: T,
56+
selected?: boolean,
57+
ignoreSelectable?: boolean,
58+
) => void
59+
/**
60+
* 清空选中项
61+
*/
62+
clearSelection: () => void
63+
}
64+
65+
export function useSelection<T, K = any>({
66+
singleSelect = false,
67+
toKey,
68+
selectable = () => true,
69+
}: UseSelectionOptions<T, K>): UseSelectionReturn<T, K> {
70+
const selection = shallowReactive(new Map<any, T>())
71+
72+
const returned: UseSelectionReturn<T> = {
73+
selection: computed(() => Array.from(selection.values())),
74+
selectedKeys: computed(() => Array.from(selection.keys())),
75+
isSingleSelect: computed(() => toValue(singleSelect)),
76+
toKey,
77+
isSelected(data) {
78+
return selection.has(toKey(data))
79+
},
80+
isSelectable(data) {
81+
return selectable(data)
82+
},
83+
toggleSelection(
84+
data,
85+
selected = !returned.isSelected(data),
86+
ignoreSelectable,
87+
) {
88+
if (!ignoreSelectable && !selectable(data)) {
89+
return
90+
}
91+
92+
if (toValue(singleSelect)) {
93+
returned.clearSelection()
94+
}
95+
96+
if (selected) {
97+
selection.set(toKey(data), data)
98+
}
99+
else {
100+
selection.delete(toKey(data))
101+
}
102+
},
103+
clearSelection() {
104+
selection.clear()
105+
},
106+
}
107+
108+
return returned
109+
}

0 commit comments

Comments
 (0)