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
5 changes: 4 additions & 1 deletion ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,11 @@ System 接口由资源管理服务提供;本地开源后端没有对应扩展

### 知识库文档

`workspace/knowledge/document.ts` 仅维护文档分页查询,路径为
`workspace/knowledge/document.ts` 维护文档分页查询,路径为
`/workspace/<workspaceId>/knowledge/<knowledgeId>/document/<currentPage>/<pageSize>`。
`putQuickCreateDocuments(knowledgeId, documents)` 通过 PUT `document/batch_create` 创建空白文档,
载荷为 `DocumentQuickCreatePayload[]`(仅含 `name`),返回 `DocumentItem[]`;名称上限 128 字符。
共享文档 API 不增加创建接口;文档页监听公共 MkQuickCreate 的 `create` 事件,管理创建请求和提交状态。
分页使用 `ParamsPage` / `ResponsePage<DocumentItem>`,支持 `name`、`create_user`、`status`、`task_type`、`is_active`、`hit_handling_method` 和 `tags` 筛选;
文件任务使用 `DOCUMENT_TASK_STATE` 的字符状态及 `DOCUMENT_TASK_TYPE` 的任务位置,
不使用通用 `STATE_TYPES`;选择全部时省略状态与任务类型,非任务专属状态省略任务类型。
Expand Down
2 changes: 1 addition & 1 deletion ui/src/api/admin/core/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { Dict } from '@/api/types'
import { MsgError } from '@/utils/message'
import { ADMIN_API_BASE_PATH } from '@/api/constants'

const DEFAULT_TIMEOUT = 30 * 60 * 1_000 // 30 minutes
const DEFAULT_TIMEOUT = 30 * 60 * 1000 // 30 minutes

interface ExportRequestConfig extends AxiosRequestConfig {
skipGlobalErrorMessage?: boolean
Expand Down
10 changes: 7 additions & 3 deletions ui/src/api/admin/workspace/knowledge/document.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { get } from '../../core/request'
import { get, put } from '../../core/request'
import type { ParamsPage, ResponsePage } from '../../core/types'
import type { Dict, DocumentItem } from '@/api/types'
import type { Dict, DocumentItem, DocumentQuickCreatePayload } from '@/api/types'
import { getWorkspaceId } from '@/utils/resource-context'

const getPrefix = (knowledgeId: string) => `/workspace/${getWorkspaceId()}/knowledge/${knowledgeId}/document`
Expand All @@ -9,4 +9,8 @@ const getPrefix = (knowledgeId: string) => `/workspace/${getWorkspaceId()}/knowl
const getDocumentPage = (knowledgeId: string, page: ParamsPage, query?: Dict<unknown>) =>
get<ResponsePage<DocumentItem>>(`${getPrefix(knowledgeId)}/${page.currentPage}/${page.pageSize}`, query)

export default { getDocumentPage }
/** 批量创建仅包含名称的空白文档。 */
const putQuickCreateDocuments = (knowledgeId: string, documents: DocumentQuickCreatePayload[]) =>
put<DocumentQuickCreatePayload[], DocumentItem[]>(`${getPrefix(knowledgeId)}/batch_create`, documents)

export default { getDocumentPage, putQuickCreateDocuments }
5 changes: 5 additions & 0 deletions ui/src/api/types/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ export type DocumentHitHandling = (typeof DOCUMENT_HIT_HANDLING)[keyof typeof DO
export type DocumentTaskState = (typeof DOCUMENT_TASK_STATE)[keyof typeof DOCUMENT_TASK_STATE]
export type DocumentTaskType = (typeof DOCUMENT_TASK_TYPE)[keyof typeof DOCUMENT_TASK_TYPE]

/** 快速创建空白文档的请求数据。 */
export interface DocumentQuickCreatePayload {
name: string
}

/** 文件各任务的分段计数及状态发生时间。 */
export interface DocumentStatusMeta {
aggs?: { status: string | null; count: number }[]
Expand Down
2 changes: 2 additions & 0 deletions ui/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ declare module 'vue' {
MkIcon: typeof import('./components/global/mk-icon/index.vue')['default']
MkInfiniteScroll: typeof import('./components/global/mk-infinite-scroll/index.vue')['default']
MkListItem: typeof import('./components/global/mk-list-item/index.vue')['default']
MkQuickCreate: typeof import('./components/global/mk-table/MkQuickCreate.vue')['default']
MkSearchInput: typeof import('./components/global/mk-search-input/index.vue')['default']
MkSlider: typeof import('./components/global/mk-slider/index.vue')['default']
MkSourceCard: typeof import('./components/global/mk-source-card/index.vue')['default']
Expand Down Expand Up @@ -80,6 +81,7 @@ declare global {
const MkIcon: typeof import('./components/global/mk-icon/index.vue')['default']
const MkInfiniteScroll: typeof import('./components/global/mk-infinite-scroll/index.vue')['default']
const MkListItem: typeof import('./components/global/mk-list-item/index.vue')['default']
const MkQuickCreate: typeof import('./components/global/mk-table/MkQuickCreate.vue')['default']
const MkSearchInput: typeof import('./components/global/mk-search-input/index.vue')['default']
const MkSlider: typeof import('./components/global/mk-slider/index.vue')['default']
const MkSourceCard: typeof import('./components/global/mk-source-card/index.vue')['default']
Expand Down
58 changes: 58 additions & 0 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,31 @@ Element Plus 的 `v-infinite-scroll`。组件通过 `v-model` 管理已经加载
`paginationConfig` 由组件接收,其余 Table 属性和事件通过 `$attrs` 传入,列继续使用
`el-table-column`。

`body-prepend` 插槽用于在表头下、数据行之前插入独立操作区,例如快速创建。内部借用
Element Plus `append`,仅在传入该插槽时将直属表体滚动容器设为纵向 Flex,并将插槽排到前面;
内容自然占位,不写固定高度,不向 `data` 插入占位记录,不影响选择、排序、分页总数和空状态。
该插槽用于默认的 `table-layout="fixed"` 表格;不与 `table-layout="auto"` 混用。
操作区通过 sticky 固定在表头下方,纵向、横向滚动均保持原位,背景遮挡经过的普通列和固定列。
表体使用 inline-size 容器,操作区宽度通过 `100cqw` 跟随表体可视宽度,滚动内容保留完整列宽;
表单展开和错误提示仍自然占位。表单状态、请求与成功后的刷新由调用方维护,MkTable 不内置业务创建逻辑。

`MkQuickCreate` 位于 `global/mk-table/MkQuickCreate.vue`,自动注册,供 `body-prepend` 插槽组合。
组件只提供行内快速创建交互,不依赖文档 API 或业务 ID。点击创建只发出 `create(name)` 事件,
由页面管理请求、`loading`、成功提示及列表刷新;成功后通过组件 ref 调用 `close()` 清空并收起,失败保留输入。
`text` 默认为“快速创建”,`placeholder` 默认为“请输入名称”,`maxlength` 默认为 256。
组件不使用 Form 或表单校验;空名称、纯空白及提交期间禁用创建按钮,提交前 trim。
Esc 或取消清理草稿;`loading` 默认为 false,为 true 时禁止创建和取消。组件不等待 Promise,不自动收起。

```vue
<MkTable :data="documentData">
<template #body-prepend>
<!-- 快速创建空白文档 -->
<MkQuickCreate ref="quickCreateRef" text="快速创建空白文档" placeholder="请输入文档名称" :loading="creatingDocument" @create="handleCreateDocument" />
</template>
<el-table-column prop="name" label="文档名称" />
</MkTable>
```

`paginationConfig` 包含 `currentPage`、`pageSize`、`total` 和可选 `pageSizes`;不传则隐藏分页器,
`pageSizes` 默认 `[10, 20, 50, 100]`。

Expand Down Expand Up @@ -438,6 +463,39 @@ Dialog、Drawer、Popover、嵌套区域等其他大、小表格均禁止开启

## 手动导入的共享 UI

### MkEditName(行内编辑名称)

手动导入 `@/components/mk-edit-name/index.vue`,不全局注册。必填 `v-model` 保存已生效名称,
必填 `save(name): Promise<string>` 由调用方执行业务请求并返回最终保存的名称;失败必须 reject,
不可吞掉错误后当作成功返回。组件不调用业务 API,仅在保存成功后回写模型。

默认展示名称,`prefix` 插槽放文档等业务图标;长名称省略并提供原生 title。悬停或内部获得焦点时
显示编辑按钮,点击后进入输入态并选中名称。Enter 或失焦保存,Esc 取消;输入法选词不提交。
保存前清理首尾空格,拒绝空名称和纯空白;`maxlength` 可限制长度,`validate(name)` 可返回
业务错误文案,返回 `undefined` 表示通过。名称未变化直接退出,不请求。
保存期间只读并显示 loading,失败保留草稿及错误提示,允许重新聚焦后修改、重试或取消。
名称展示区域保留父级点击行为,编辑按钮和输入区域阻止点击冒泡。

```vue
<script setup lang="ts">
import { ref } from 'vue'
import MkEditName from '@/components/mk-edit-name/index.vue'

const documentName = ref('DataEase 快速入门')
// 示例:真实使用时在这里等待业务 API,返回服务端最终名称。
async function saveDocumentName(name: string): Promise<string> {
return name
}
</script>

<template>
<MkEditName v-model="documentName" :save="saveDocumentName" />
</template>
```

表格通过列默认插槽使用,列表以资源 ID 为 key;该列不再启用 `show-overflow-tooltip`,避免编辑时
叠加表格提示。单元格可用宽度控制组件宽度,业务图标在编辑态隐藏。

### MkEditAvatar(修改头像)

手动导入 `@/components/mk-edit-avatar/index.vue`,不参与全局注册。字符串 `v-model` 为当前
Expand Down
71 changes: 71 additions & 0 deletions ui/src/components/global/mk-table/MkQuickCreate.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script setup lang="ts">
import { computed, nextTick, ref } from 'vue'
import type { ButtonInstance, InputInstance } from 'element-plus'

defineOptions({ name: 'MkQuickCreate' })

const props = withDefaults(
defineProps<{
loading?: boolean
text?: string
placeholder?: string
maxlength?: number
}>(),
{ loading: false, text: '快速创建', placeholder: '请输入名称', maxlength: 256 },
)
const emit = defineEmits<{ create: [name: string] }>()

/* 行内快速创建 */
const showCreateInput = ref(false)
const createName = ref('')
const createDisabled = computed(() => props.loading || !createName.value.trim())
const inputRef = ref<InputInstance>()
const triggerRef = ref<ButtonInstance>()

function handleShowCreateInput() {
showCreateInput.value = true
void nextTick(() => inputRef.value?.focus())
}

function resetDraft() {
showCreateInput.value = false
createName.value = ''
void nextTick(() => triggerRef.value?.$el.focus())
}

function handleCancel() {
if (!props.loading) resetDraft()
}

function handleCreate() {
if (createDisabled.value) return
emit('create', createName.value.trim())
}

defineExpose({ close: resetDraft })
</script>

<template>
<div class="p-3">
<div v-if="showCreateInput" class="flex-align-center" @keydown.esc.prevent="handleCancel">
<el-input
ref="inputRef"
v-model="createName"
class="mr-3 w-80!"
:placeholder="props.placeholder"
:maxlength="props.maxlength"
show-word-limit
clearable
/>
<!-- 创建 -->
<el-button type="primary" :loading="props.loading" :disabled="createDisabled" @click="handleCreate">创建</el-button>
<!-- 取消创建 -->
<el-button plain :disabled="props.loading" @click="handleCancel">取消</el-button>
</div>
<!-- 展开快速创建 -->
<el-button v-else ref="triggerRef" type="primary" link @click="handleShowCreateInput">
<MkIcon name="icon_add_outlined" />
<span class="ml-1">{{ props.text }}</span>
</el-button>
</div>
</template>
48 changes: 46 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 } from 'vue'
import { computed, 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 Down Expand Up @@ -40,6 +40,12 @@ const emit = defineEmits<{

const tableRef = ref<TableInstance>()

defineSlots<{
default?: () => VNode[]
'body-prepend'?: () => VNode[]
'footer-batch-actions'?: () => VNode[]
}>()

/** 行排序:只处理当前传入的平面数据,保持显示顺序与数组索引一致。 */
const sortableRows = computed({
get: () => props.data,
Expand Down Expand Up @@ -126,7 +132,11 @@ defineExpose({ clearSelection, tableRef })
<div class="mk-table relative flex-column w-full min-h-0 flex-1">
<el-table
ref="tableRef"
:class="{ 'mk-table__resizable--borderless': props.resizable, small: props.size === 'small' }"
:class="{
'mk-table__resizable--borderless': props.resizable,
'mk-table__body-prepend': !!$slots['body-prepend'],
small: props.size === 'small',
}"
:data="props.data"
:max-height="tableHeight"
:row-key="props.rowKey"
Expand All @@ -135,6 +145,9 @@ defineExpose({ clearSelection, tableRef })
@selection-change="handleSelectionChange"
>
<slot />
<template v-if="$slots['body-prepend']" #append>
<slot name="body-prepend" />
</template>
</el-table>

<div class="mt-4 flex justify-end" v-if="props.paginationConfig">
Expand Down Expand Up @@ -166,6 +179,37 @@ defineExpose({ clearSelection, tableRef })
</template>

<style scoped lang="scss">
/* 表体顶部插槽:自然占位并固定在滚动区域顶部,不影响嵌套表格。 */
:deep(.mk-table__body-prepend) {
> .el-table__inner-wrapper > .el-table__body-wrapper {
container-type: inline-size;

> .el-scrollbar > .el-scrollbar__wrap > .el-scrollbar__view {
display: flex !important;
flex-direction: column;
min-width: 100%;
width: max-content;

> .el-table__append-wrapper {
background: var(--el-bg-color);
border-bottom: var(--el-table-border);
flex-shrink: 0;
left: 0;
order: -1;
position: sticky;
top: 0;
width: 100cqw;
z-index: 3;
}

> .el-table__body,
> .el-table__empty-block {
flex-shrink: 0;
}
}
}
}

:deep(.mk-table__resizable--borderless) {
border: none !important;

Expand Down
Loading
Loading