Skip to content

Commit cad24a6

Browse files
committed
feat: selection group key for mktable
1 parent 6f87f2b commit cad24a6

5 files changed

Lines changed: 88 additions & 13 deletions

File tree

‎ui/src/components/COMPONENT_README.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,23 @@ Dialog、Drawer、Popover、嵌套区域等其他大、小表格均禁止开启
461461
`clearSelection()`。操作栏与 `MkViewLayout` 复用 `LayoutBatchFooter`,统一全选、半选、数量和
462462
取消行为;它在主内容滚动区域内吸附于页面底部,不随表格内容滚出可视区域。
463463

464+
`selectionGroupKey` 可按需开启同组选择,接受字段路径或返回字符串/数字的函数;不传时保持
465+
原生逐行选择。点击复选框时,同步当前 `data` 中同组记录的选中或取消状态,遵守选择列的
466+
`selectable` 禁用规则;分组值为 null、undefined 或非字符串/数字时,该行保持独立选择。
467+
联动完成后只通过 `selection-change` 通知一次最终结果,底栏仍按实际选中行数统计,不按组计数。
468+
`rowKey` 仍须唯一,不能用重复的分组字段替代;`span-method` 只控制展示合并,不自动开启联动。
469+
该能力用于平面分组表格,不与树形/懒加载选择混用,不主动加载或联动未传入的跨页记录。
470+
原生 `select` 事件仍透传;业务批量操作使用 `selection-change` 的最终结果。
471+
搜索、分页、切换业务对象后的清理以及提交 ID 的业务去重由页面负责。
472+
473+
```vue
474+
<MkTable :data="tagRows" selection-group-key="key" :span-method="spanMethod" @selection-change="handleSelectionChange">
475+
<el-table-column type="selection" width="40" />
476+
<el-table-column prop="key" label="标签" />
477+
<el-table-column prop="value" label="标签值" />
478+
</MkTable>
479+
```
480+
464481
### MkAction
465482

466483
`global/mk-table/MkAction.vue` 统一单个操作的两种外观,通过 `display="menu"`(默认)或

‎ui/src/components/global/mk-table/index.vue‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
<script setup lang="ts" generic="T">
2-
import { computed, onBeforeUnmount, onMounted, ref, type VNode } from 'vue'
1+
<script setup lang="ts" generic="T extends object">
2+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, type VNode } from 'vue'
33
import type { TableInstance } from 'element-plus'
44
import LayoutBatchFooter from '../mk-view-layout/LayoutBatchFooter.vue'
55
import { useSortable, type SortableChange } from '@/utils/use-sortable'
@@ -24,6 +24,8 @@ const props = withDefaults(
2424
resizable?: boolean // 非指定表格禁止开启
2525
size?: 'small'
2626
rowKey?: string | ((row: T) => string | number)
27+
/** 同组记录联动选择;不传时保持逐行选择。 */
28+
selectionGroupKey?: string | ((row: T) => string | number | null | undefined)
2729
sortable?: boolean
2830
}>(),
2931
{ data: () => [], maxTableHeight: 250, resizable: false, rowKey: 'id', sortable: false },
@@ -82,8 +84,36 @@ function updateTableHeight() {
8284
/** 选择操作栏 */
8385
const selectedRows = ref<unknown[]>([])
8486
const isAllRowsSelected = computed(() => tableRef.value?.store.states.isAllSelected.value ?? false)
87+
let groupSelectionPending = false
88+
89+
function getSelectionGroupKey(row: T): unknown {
90+
const key = props.selectionGroupKey
91+
return typeof key === 'function' ? key(row) : key ? get(row, key) : undefined
92+
}
93+
94+
function handleSelect(selection: T[], row: T) {
95+
const groupKey = getSelectionGroupKey(row)
96+
if ((typeof groupKey !== 'string' && typeof groupKey !== 'number') || !tableRef.value) return
97+
98+
// 等整组勾选完成后再更新选择结果,保证已选数量和批量操作包含组内所有记录。
99+
if (!groupSelectionPending) {
100+
groupSelectionPending = true
101+
nextTick(() => {
102+
groupSelectionPending = false
103+
if (tableRef.value) handleSelectionChange(tableRef.value.getSelectionRows())
104+
})
105+
}
106+
107+
const checked = selection.includes(row)
108+
props.data.forEach((groupRow) => {
109+
if (getSelectionGroupKey(groupRow) === groupKey) {
110+
tableRef.value?.toggleRowSelection(groupRow, checked, false)
111+
}
112+
})
113+
}
85114
86115
function handleSelectionChange(selection: unknown[]) {
116+
if (groupSelectionPending) return
87117
selectedRows.value = selection
88118
emit('selection-change', selection)
89119
}
@@ -142,6 +172,7 @@ defineExpose({ clearSelection, tableRef })
142172
:row-key="props.rowKey"
143173
v-bind="$attrs"
144174
:border="props.resizable"
175+
@select="handleSelect"
145176
@selection-change="handleSelectionChange"
146177
>
147178
<slot />

‎ui/src/views/VIEW_README.md‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,12 @@ src/views/system/identity/users/
442442
或选中用户 ID 数组。行操作菜单开启 `persistent`,避免菜单关闭时卸载其内部的配额弹窗。
443443
两套用户页面保留各自业务 API 和导入成功后的查询刷新方式。
444444

445+
`system/identity/workspaces/index.vue` 的成员表按用户合并选择框、姓名和用户名,
446+
通过 `MkTable.selectionGroupKey="user_id"` 联动当前页同一用户的角色记录;
447+
行标识仍使用 `user_relation_id:role_id`,底栏按选中记录行数统计,不按用户数统计。
448+
批量移除确认说明记录数与涉及用户数,请求按 `user_relation_id` 去重,不按用户 ID 丢弃角色记录。
449+
列表加载前清空选择,覆盖搜索、分页、工作空间切换及操作后的刷新;不新增跨页选择或修改接口协议。
450+
445451
操作日志页面的清除策略入口与弹窗统一放在 `system/operate-logs/clean-strategy/`:
446452
`ButtonCleanStrategy.vue` 管理打开动作和弹窗 Ref,`CleanStrategyDialog.vue` 负责策略查询与保存,
447453
`index.vue` 只组合入口组件。
@@ -869,8 +875,9 @@ library 的接口调整已撤回,文档和标签沿用原有调用方式,Sys
869875
维护搜索、导入入口、创建入口和标签表格,不嵌套页面布局或修改路由。
870876
通过 `workspace/knowledge/knowledge.ts` 查询全量分组,前端按标签名或值搜索并保留完整命中分组;
871877
先按标签组分页,再展开各组标签值,分页总数为标签组数。
872-
`MkTable` 合并选择列和标签名,选择行为与工作空间成员表格一致,不额外联动同组记录;
873-
批量底栏按实际选中记录数统计,删除确认明确影响所选记录所属标签及其全部值。
878+
`MkTable` 合并选择列和标签名,通过 `selection-group-key="key"` 联动当前页同组标签值;
879+
批量底栏按实际选中标签值行数/当前页标签值总行数统计,不按标签组计数。
880+
删除提交全部选中标签值 ID,确认框同时说明标签值数量、标签组数量及整组删除的影响。
874881
标签名旁提供新增值、编辑标签、删除标签;行末提供编辑值和删除值,
875882
数量链接打开同目录 `TagDocumentDrawer.vue`。
876883

‎ui/src/views/knowledge-detail/library/tags/index.vue‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function handleSelectionChange(selection: unknown[]) {
8383
selectedTags.value = selection as TagTableRow[]
8484
}
8585
86-
// 与工作空间成员表格一致:仅合并单元格,不另外联动同组勾选。
86+
// 这里只合并展示单元格,同组选择由 MkTable 的 selectionGroupKey 处理。
8787
function spanMethod({ column, row }: { column: TableColumnCtx<TagTableRow>; row: TagTableRow }) {
8888
if (column.type === 'selection' || column.property === 'key') return row.rowspan ? [row.rowspan, 1] : [0, 0]
8989
}
@@ -150,7 +150,7 @@ function handleBatchDelete() {
150150
const tagIds = selectedTags.value.map(({ id }) => id)
151151
const groupCount = new Set(selectedTags.value.map(({ key }) => key)).size
152152
return MsgConfirm(
153-
`是否删除选中的 ${groupCount} 个标签?`,
153+
`是否删除选中的 ${tagIds.length} 个标签值(涉及 ${groupCount} 个标签)?`,
154154
'将删除选中记录所属的标签及其全部标签值,并移除文档上的对应标签;文档本身不会删除,请谨慎操作!',
155155
).then(
156156
() => {
@@ -194,6 +194,7 @@ onMounted(() => {
194194
v-model:pagination-config="paginationConfig"
195195
v-loading="loading || operationLoading"
196196
:data="tagRows"
197+
selection-group-key="key"
197198
:span-method="spanMethod"
198199
:max-table-height="210"
199200
@selection-change="handleSelectionChange"

‎ui/src/views/system/identity/workspaces/index.vue‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,20 @@ const memberSearchFields: OptionItem<string>[] = [
7070
{ label: '姓名', value: 'name' },
7171
]
7272
const workspaceMembers = ref<WorkspaceMemberItem[]>([])
73+
const workspaceMemberTableRef = useTemplateRef<{ clearSelection: () => void }>('workspaceMemberTableRef')
7374
function handleMemberSearch(query?: Dict<unknown>) {
7475
memberSearchQuery.value = query
7576
paginationConfig.value.currentPage = 1
7677
return loadWorkspaceMembers()
7778
}
7879
80+
function clearMemberSelection() {
81+
workspaceMemberTableRef.value?.clearSelection()
82+
batchSelectedMembers.value = []
83+
}
84+
7985
function loadWorkspaceMembers() {
86+
clearMemberSelection()
8087
const workspaceId = currentWorkspace.value?.id
8188
if (!workspaceId) {
8289
workspaceMembers.value = []
@@ -95,7 +102,16 @@ function loadWorkspaceMembers() {
95102
}
96103
97104
/** 同一用户的选择框、姓名和用户名按连续记录合并。 */
98-
function objectSpanMethod({ column, row, rowIndex }: { row: WorkspaceMemberItem; column: TableColumnCtx<WorkspaceMemberItem>; rowIndex: number; columnIndex: number }) {
105+
function objectSpanMethod({
106+
column,
107+
row,
108+
rowIndex,
109+
}: {
110+
row: WorkspaceMemberItem
111+
column: TableColumnCtx<WorkspaceMemberItem>
112+
rowIndex: number
113+
columnIndex: number
114+
}) {
99115
const shouldMerge = column.type === 'selection' || column.property === 'nick_name' || column.property === 'username'
100116
if (!shouldMerge) return
101117
@@ -139,17 +155,18 @@ function handleRemoveMember(member: WorkspaceMemberItem) {
139155
140156
/* 批量删除成员 */
141157
const batchSelectedMembers = ref<WorkspaceMemberItem[]>([])
158+
142159
function handleBatchDelete() {
143160
const workspaceId = currentWorkspace.value?.id
144-
if (!workspaceId) return
161+
if (!workspaceId || workspaceLoading.value || workspaceMembersLoading.value || !batchSelectedMembers.value.length) return
145162
146-
MsgConfirm(`是否删除选中的 ${batchSelectedMembers.value.length} 个成员?`, '')
163+
const selectedMembers = [...batchSelectedMembers.value]
164+
const userCount = new Set(selectedMembers.map(({ user_id }) => user_id)).size
165+
const userRelationIds = [...new Set(selectedMembers.map(({ user_relation_id }) => user_relation_id))]
166+
MsgConfirm('是否移除选中记录对应的成员?', `涉及 ${userCount} 位用户,共 ${selectedMembers.length} 条记录。`)
147167
.then(() => {
148168
workspaceMembersLoading.value = true
149-
return WorkspaceApi.postBatchRemoveWorkspaceMembers(
150-
workspaceId,
151-
batchSelectedMembers.value.map(({ user_relation_id }) => user_relation_id),
152-
).then(({ success_count, failed_count }) => {
169+
return WorkspaceApi.postBatchRemoveWorkspaceMembers(workspaceId, userRelationIds).then(({ success_count, failed_count }) => {
153170
if (failed_count > 0) {
154171
MsgSuccess(`移除成功 ${success_count} 个成员,失败 ${failed_count} 个`)
155172
} else {
@@ -231,8 +248,10 @@ onMounted(() => loadWorkspaceOptions())
231248
</div>
232249

233250
<MkTable
251+
ref="workspaceMemberTableRef"
234252
v-model:pagination-config="paginationConfig"
235253
:data="workspaceMembers"
254+
selection-group-key="user_id"
236255
v-loading="workspaceMembersLoading"
237256
@current-change="loadWorkspaceMembers()"
238257
@size-change="loadWorkspaceMembers()"

0 commit comments

Comments
 (0)