Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,23 @@ Dialog、Drawer、Popover、嵌套区域等其他大、小表格均禁止开启
`clearSelection()`。操作栏与 `MkViewLayout` 复用 `LayoutBatchFooter`,统一全选、半选、数量和
取消行为;它在主内容滚动区域内吸附于页面底部,不随表格内容滚出可视区域。

`selectionGroupKey` 可按需开启同组选择,接受字段路径或返回字符串/数字的函数;不传时保持
原生逐行选择。点击复选框时,同步当前 `data` 中同组记录的选中或取消状态,遵守选择列的
`selectable` 禁用规则;分组值为 null、undefined 或非字符串/数字时,该行保持独立选择。
联动完成后只通过 `selection-change` 通知一次最终结果,底栏仍按实际选中行数统计,不按组计数。
`rowKey` 仍须唯一,不能用重复的分组字段替代;`span-method` 只控制展示合并,不自动开启联动。
该能力用于平面分组表格,不与树形/懒加载选择混用,不主动加载或联动未传入的跨页记录。
原生 `select` 事件仍透传;业务批量操作使用 `selection-change` 的最终结果。
搜索、分页、切换业务对象后的清理以及提交 ID 的业务去重由页面负责。

```vue
<MkTable :data="tagRows" selection-group-key="key" :span-method="spanMethod" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="40" />
<el-table-column prop="key" label="标签" />
<el-table-column prop="value" label="标签值" />
</MkTable>
```

### MkAction

`global/mk-table/MkAction.vue` 统一单个操作的两种外观,通过 `display="menu"`(默认)或
Expand Down
35 changes: 33 additions & 2 deletions ui/src/components/global/mk-table/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts" generic="T">
import { computed, onBeforeUnmount, onMounted, ref, type VNode } from 'vue'
<script setup lang="ts" generic="T extends object">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, type VNode } from 'vue'
import type { TableInstance } from 'element-plus'
import LayoutBatchFooter from '../mk-view-layout/LayoutBatchFooter.vue'
import { useSortable, type SortableChange } from '@/utils/use-sortable'
Expand All @@ -24,6 +24,8 @@ const props = withDefaults(
resizable?: boolean // 非指定表格禁止开启
size?: 'small'
rowKey?: string | ((row: T) => string | number)
/** 同组记录联动选择;不传时保持逐行选择。 */
selectionGroupKey?: string | ((row: T) => string | number | null | undefined)
sortable?: boolean
}>(),
{ data: () => [], maxTableHeight: 250, resizable: false, rowKey: 'id', sortable: false },
Expand Down Expand Up @@ -82,8 +84,36 @@ function updateTableHeight() {
/** 选择操作栏 */
const selectedRows = ref<unknown[]>([])
const isAllRowsSelected = computed(() => tableRef.value?.store.states.isAllSelected.value ?? false)
let groupSelectionPending = false

function getSelectionGroupKey(row: T): unknown {
const key = props.selectionGroupKey
return typeof key === 'function' ? key(row) : key ? get(row, key) : undefined
}

function handleSelect(selection: T[], row: T) {
const groupKey = getSelectionGroupKey(row)
if ((typeof groupKey !== 'string' && typeof groupKey !== 'number') || !tableRef.value) return

// 等整组勾选完成后再更新选择结果,保证已选数量和批量操作包含组内所有记录。
if (!groupSelectionPending) {
groupSelectionPending = true
nextTick(() => {
groupSelectionPending = false
if (tableRef.value) handleSelectionChange(tableRef.value.getSelectionRows())
})
}

const checked = selection.includes(row)
props.data.forEach((groupRow) => {
if (getSelectionGroupKey(groupRow) === groupKey) {
tableRef.value?.toggleRowSelection(groupRow, checked, false)
}
})
}

function handleSelectionChange(selection: unknown[]) {
if (groupSelectionPending) return
selectedRows.value = selection
emit('selection-change', selection)
}
Expand Down Expand Up @@ -142,6 +172,7 @@ defineExpose({ clearSelection, tableRef })
:row-key="props.rowKey"
v-bind="$attrs"
:border="props.resizable"
@select="handleSelect"
@selection-change="handleSelectionChange"
>
<slot />
Expand Down
11 changes: 9 additions & 2 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,12 @@ src/views/system/identity/users/
或选中用户 ID 数组。行操作菜单开启 `persistent`,避免菜单关闭时卸载其内部的配额弹窗。
两套用户页面保留各自业务 API 和导入成功后的查询刷新方式。

`system/identity/workspaces/index.vue` 的成员表按用户合并选择框、姓名和用户名,
通过 `MkTable.selectionGroupKey="user_id"` 联动当前页同一用户的角色记录;
行标识仍使用 `user_relation_id:role_id`,底栏按选中记录行数统计,不按用户数统计。
批量移除确认说明记录数与涉及用户数,请求按 `user_relation_id` 去重,不按用户 ID 丢弃角色记录。
列表加载前清空选择,覆盖搜索、分页、工作空间切换及操作后的刷新;不新增跨页选择或修改接口协议。

操作日志页面的清除策略入口与弹窗统一放在 `system/operate-logs/clean-strategy/`:
`ButtonCleanStrategy.vue` 管理打开动作和弹窗 Ref,`CleanStrategyDialog.vue` 负责策略查询与保存,
`index.vue` 只组合入口组件。
Expand Down Expand Up @@ -869,8 +875,9 @@ library 的接口调整已撤回,文档和标签沿用原有调用方式,Sys
维护搜索、导入入口、创建入口和标签表格,不嵌套页面布局或修改路由。
通过 `workspace/knowledge/knowledge.ts` 查询全量分组,前端按标签名或值搜索并保留完整命中分组;
先按标签组分页,再展开各组标签值,分页总数为标签组数。
`MkTable` 合并选择列和标签名,选择行为与工作空间成员表格一致,不额外联动同组记录;
批量底栏按实际选中记录数统计,删除确认明确影响所选记录所属标签及其全部值。
`MkTable` 合并选择列和标签名,通过 `selection-group-key="key"` 联动当前页同组标签值;
批量底栏按实际选中标签值行数/当前页标签值总行数统计,不按标签组计数。
删除提交全部选中标签值 ID,确认框同时说明标签值数量、标签组数量及整组删除的影响。
标签名旁提供新增值、编辑标签、删除标签;行末提供编辑值和删除值,
数量链接打开同目录 `TagDocumentDrawer.vue`。

Expand Down
5 changes: 3 additions & 2 deletions ui/src/views/knowledge-detail/library/tags/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function handleSelectionChange(selection: unknown[]) {
selectedTags.value = selection as TagTableRow[]
}

// 与工作空间成员表格一致:仅合并单元格,不另外联动同组勾选。
// 这里只合并展示单元格,同组选择由 MkTable 的 selectionGroupKey 处理。
function spanMethod({ column, row }: { column: TableColumnCtx<TagTableRow>; row: TagTableRow }) {
if (column.type === 'selection' || column.property === 'key') return row.rowspan ? [row.rowspan, 1] : [0, 0]
}
Expand Down Expand Up @@ -150,7 +150,7 @@ function handleBatchDelete() {
const tagIds = selectedTags.value.map(({ id }) => id)
const groupCount = new Set(selectedTags.value.map(({ key }) => key)).size
return MsgConfirm(
`是否删除选中的 ${groupCount} 个标签?`,
`是否删除选中的 ${tagIds.length} 个标签值(涉及 ${groupCount} 个标签)?`,
'将删除选中记录所属的标签及其全部标签值,并移除文档上的对应标签;文档本身不会删除,请谨慎操作!',
).then(
() => {
Expand Down Expand Up @@ -194,6 +194,7 @@ onMounted(() => {
v-model:pagination-config="paginationConfig"
v-loading="loading || operationLoading"
:data="tagRows"
selection-group-key="key"
:span-method="spanMethod"
:max-table-height="210"
@selection-change="handleSelectionChange"
Expand Down
33 changes: 26 additions & 7 deletions ui/src/views/system/identity/workspaces/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,20 @@ const memberSearchFields: OptionItem<string>[] = [
{ label: '姓名', value: 'name' },
]
const workspaceMembers = ref<WorkspaceMemberItem[]>([])
const workspaceMemberTableRef = useTemplateRef<{ clearSelection: () => void }>('workspaceMemberTableRef')
function handleMemberSearch(query?: Dict<unknown>) {
memberSearchQuery.value = query
paginationConfig.value.currentPage = 1
return loadWorkspaceMembers()
}

function clearMemberSelection() {
workspaceMemberTableRef.value?.clearSelection()
batchSelectedMembers.value = []
}

function loadWorkspaceMembers() {
clearMemberSelection()
const workspaceId = currentWorkspace.value?.id
if (!workspaceId) {
workspaceMembers.value = []
Expand All @@ -95,7 +102,16 @@ function loadWorkspaceMembers() {
}

/** 同一用户的选择框、姓名和用户名按连续记录合并。 */
function objectSpanMethod({ column, row, rowIndex }: { row: WorkspaceMemberItem; column: TableColumnCtx<WorkspaceMemberItem>; rowIndex: number; columnIndex: number }) {
function objectSpanMethod({
column,
row,
rowIndex,
}: {
row: WorkspaceMemberItem
column: TableColumnCtx<WorkspaceMemberItem>
rowIndex: number
columnIndex: number
}) {
const shouldMerge = column.type === 'selection' || column.property === 'nick_name' || column.property === 'username'
if (!shouldMerge) return

Expand Down Expand Up @@ -139,17 +155,18 @@ function handleRemoveMember(member: WorkspaceMemberItem) {

/* 批量删除成员 */
const batchSelectedMembers = ref<WorkspaceMemberItem[]>([])

function handleBatchDelete() {
const workspaceId = currentWorkspace.value?.id
if (!workspaceId) return
if (!workspaceId || workspaceLoading.value || workspaceMembersLoading.value || !batchSelectedMembers.value.length) return

MsgConfirm(`是否删除选中的 ${batchSelectedMembers.value.length} 个成员?`, '')
const selectedMembers = [...batchSelectedMembers.value]
const userCount = new Set(selectedMembers.map(({ user_id }) => user_id)).size
const userRelationIds = [...new Set(selectedMembers.map(({ user_relation_id }) => user_relation_id))]
MsgConfirm('是否移除选中记录对应的成员?', `涉及 ${userCount} 位用户,共 ${selectedMembers.length} 条记录。`)
.then(() => {
workspaceMembersLoading.value = true
return WorkspaceApi.postBatchRemoveWorkspaceMembers(
workspaceId,
batchSelectedMembers.value.map(({ user_relation_id }) => user_relation_id),
).then(({ success_count, failed_count }) => {
return WorkspaceApi.postBatchRemoveWorkspaceMembers(workspaceId, userRelationIds).then(({ success_count, failed_count }) => {
if (failed_count > 0) {
MsgSuccess(`移除成功 ${success_count} 个成员,失败 ${failed_count} 个`)
} else {
Expand Down Expand Up @@ -231,8 +248,10 @@ onMounted(() => loadWorkspaceOptions())
</div>

<MkTable
ref="workspaceMemberTableRef"
v-model:pagination-config="paginationConfig"
:data="workspaceMembers"
selection-group-key="user_id"
v-loading="workspaceMembersLoading"
@current-change="loadWorkspaceMembers()"
@size-change="loadWorkspaceMembers()"
Expand Down
Loading