mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-19 02:18:25 +08:00
fix(frontend): knowledge document list layout, batch bar, and selection
- Platform route outlet: flex min-height so document area is not collapsed - Card grid: stable :key=item.id; clamp selection anchor after list reload - DocumentListView: TDesign checkbox, sticky header, neutral file icons - DocumentBatchBar: column-footer placement; toned style vs floating pill - Clear selection i18n: less ambiguous wording (e.g. 取消选择) - Batch delete: poll list refresh after async queue; getKnowled returns Promise - files.ts: use file icon for text types; align chat/upload icon usage
This commit is contained in:
@@ -116,7 +116,7 @@ const getFileIcon = (fileName: string): string => {
|
||||
if (['doc', 'docx'].includes(ext || '')) return 'file-word';
|
||||
if (['xls', 'xlsx'].includes(ext || '')) return 'file-excel';
|
||||
if (['ppt', 'pptx'].includes(ext || '')) return 'file-powerpoint';
|
||||
if (['txt', 'md'].includes(ext || '')) return 'file-text';
|
||||
if (['txt', 'md'].includes(ext || '')) return 'file';
|
||||
if (['mp3', 'wav', 'm4a', 'flac', 'ogg', 'aac'].includes(ext || '')) return 'sound';
|
||||
return 'file';
|
||||
};
|
||||
|
||||
@@ -38,11 +38,11 @@ export default function (knowledgeBaseId?: string) {
|
||||
const getKnowled = (
|
||||
query: { page: number; page_size: number; tag_id?: string; keyword?: string; file_type?: string } = { page: 1, page_size: 35 },
|
||||
kbId?: string,
|
||||
) => {
|
||||
): Promise<void> => {
|
||||
const targetKbId = kbId || knowledgeBaseId;
|
||||
if (!targetKbId) return;
|
||||
|
||||
listKnowledgeFiles(targetKbId, query)
|
||||
if (!targetKbId) return Promise.resolve();
|
||||
|
||||
return listKnowledgeFiles(targetKbId, query)
|
||||
.then((result: any) => {
|
||||
const { data, total: totalResult } = result;
|
||||
const cardList_ = data.map((item: any) => {
|
||||
|
||||
@@ -255,7 +255,7 @@ export default {
|
||||
columnActions: 'Actions',
|
||||
selectAll: 'Select all',
|
||||
selectedCount: '{count} selected',
|
||||
clearSelection: 'Clear',
|
||||
clearSelection: 'Deselect all',
|
||||
batchDelete: 'Delete selected',
|
||||
batchDeleteConfirmation: 'Confirm Batch Delete',
|
||||
confirmBatchDeleteDocument: 'Delete {count} selected documents? This action cannot be undone.',
|
||||
|
||||
@@ -259,7 +259,7 @@ export default {
|
||||
columnActions: "작업",
|
||||
selectAll: "전체 선택",
|
||||
selectedCount: "{count}개 선택됨",
|
||||
clearSelection: "지우기",
|
||||
clearSelection: "선택 해제",
|
||||
batchDelete: "선택 삭제",
|
||||
batchDeleteConfirmation: "일괄 삭제 확인",
|
||||
confirmBatchDeleteDocument: "선택한 {count}개 문서를 삭제하시겠습니까? 삭제 후 복구할 수 없습니다.",
|
||||
|
||||
@@ -227,7 +227,7 @@ export default {
|
||||
columnActions: 'Действия',
|
||||
selectAll: 'Выбрать все',
|
||||
selectedCount: 'Выбрано: {count}',
|
||||
clearSelection: 'Очистить',
|
||||
clearSelection: 'Снять выделение',
|
||||
batchDelete: 'Удалить выбранные',
|
||||
batchDeleteConfirmation: 'Подтверждение пакетного удаления',
|
||||
confirmBatchDeleteDocument: 'Удалить {count} выбранных документов? Это действие нельзя отменить.',
|
||||
|
||||
@@ -256,7 +256,7 @@ export default {
|
||||
columnActions: "操作",
|
||||
selectAll: "全选",
|
||||
selectedCount: "已选 {count} 项",
|
||||
clearSelection: "清空",
|
||||
clearSelection: "取消选择",
|
||||
batchDelete: "批量删除",
|
||||
batchDeleteConfirmation: "批量删除确认",
|
||||
confirmBatchDeleteDocument: "确认删除选中的 {count} 个文档?删除后将无法恢复。",
|
||||
|
||||
@@ -31,7 +31,8 @@ export function getFileIcon(input: string | { type?: string; file_type?: string;
|
||||
if (['doc', 'docx'].includes(ext)) return 'file-word';
|
||||
if (['xls', 'xlsx', 'csv'].includes(ext)) return 'file-excel';
|
||||
if (['ppt', 'pptx'].includes(ext)) return 'file-powerpoint';
|
||||
if (['txt', 'md', 'markdown'].includes(ext)) return 'file-text';
|
||||
// TDesign 无稳定 `file-text` 字形时会导致空白,纯文类统一用 `file`
|
||||
if (['txt', 'md', 'markdown', 'json', 'log', 'yaml', 'yml', 'xml'].includes(ext)) return 'file';
|
||||
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].includes(ext)) return 'image';
|
||||
if (['mp3', 'wav', 'm4a', 'flac', 'ogg', 'aac'].includes(ext)) return 'sound';
|
||||
return 'file';
|
||||
|
||||
@@ -115,7 +115,7 @@ const getAttachmentIcon = (fileNameOrType) => {
|
||||
if (['doc', 'docx'].includes(ext)) return 'file-word';
|
||||
if (['xls', 'xlsx'].includes(ext)) return 'file-excel';
|
||||
if (['ppt', 'pptx'].includes(ext)) return 'file-powerpoint';
|
||||
if (['txt', 'md'].includes(ext)) return 'file-text';
|
||||
if (['txt', 'md'].includes(ext)) return 'file';
|
||||
if (['mp3', 'wav', 'm4a', 'flac', 'ogg', 'aac'].includes(ext)) return 'sound';
|
||||
return 'file';
|
||||
};
|
||||
|
||||
@@ -413,9 +413,9 @@ const getKnowledgeType = (item: any) => {
|
||||
return '--';
|
||||
}
|
||||
|
||||
const loadKnowledgeFiles = (kbIdValue: string) => {
|
||||
if (!kbIdValue) return;
|
||||
getKnowled(
|
||||
const loadKnowledgeFiles = (kbIdValue: string): Promise<void> => {
|
||||
if (!kbIdValue) return Promise.resolve();
|
||||
return getKnowled(
|
||||
{
|
||||
page: 1,
|
||||
page_size: pageSize,
|
||||
@@ -1632,6 +1632,11 @@ const toggleSelectRow = (id: string, checked: boolean, shiftKey?: boolean) => {
|
||||
lastSelectedIndex = idx;
|
||||
};
|
||||
|
||||
const onCardGridCheckboxChange = (id: string, checked: boolean, ctx?: { e?: Event }) => {
|
||||
const me = ctx?.e as MouseEvent | undefined;
|
||||
toggleSelectRow(id, checked, !!me?.shiftKey);
|
||||
};
|
||||
|
||||
const toggleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
for (const item of cardList.value || []) selectedIds.value.add(item.id);
|
||||
@@ -1653,6 +1658,7 @@ const openBatchDeleteDialog = () => {
|
||||
const confirmBatchDelete = async () => {
|
||||
if (batchDeleting.value || selectedIds.value.size === 0) return;
|
||||
const ids = Array.from(selectedIds.value);
|
||||
const deletedIdSet = new Set(ids);
|
||||
batchDeleting.value = true;
|
||||
try {
|
||||
const res: any = await batchDeleteKnowledge(kbId.value, ids);
|
||||
@@ -1661,7 +1667,15 @@ const confirmBatchDelete = async () => {
|
||||
clearSelection();
|
||||
batchDeleteDialog.value = false;
|
||||
page = 1;
|
||||
loadKnowledgeFiles(kbId.value);
|
||||
// 后端将批量删除放入异步队列,立刻拉列表仍可能包含待删项;短轮询直到列表与后端一致或超时
|
||||
const maxPolls = 30;
|
||||
const delayMs = 400;
|
||||
for (let i = 0; i < maxPolls; i++) {
|
||||
await loadKnowledgeFiles(kbId.value);
|
||||
const stillPresent = (cardList.value || []).some((c: KnowledgeCard) => deletedIdSet.has(c.id));
|
||||
if (!stillPresent) break;
|
||||
await new Promise<void>((r) => setTimeout(r, delayMs));
|
||||
}
|
||||
loadTags(kbId.value);
|
||||
} else {
|
||||
MessagePlugin.error(res?.message || t('knowledgeBase.batchDeleteFailed'));
|
||||
@@ -1690,10 +1704,18 @@ watch([selectedTagId, docSearchKeyword, selectedFileType, kbId], () => {
|
||||
clearSelection();
|
||||
});
|
||||
|
||||
// After cardList reloads, drop ids that are no longer present.
|
||||
// After cardList reloads: stable keys rely on correct indices for shift-range; clamp anchor index.
|
||||
watch(cardList, () => {
|
||||
const items = cardList.value || [];
|
||||
const n = items.length;
|
||||
if (lastSelectedIndex >= n) {
|
||||
lastSelectedIndex = n > 0 ? n - 1 : -1;
|
||||
}
|
||||
if (moreIndex.value >= n) {
|
||||
moreIndex.value = -1;
|
||||
}
|
||||
if (selectedIds.value.size === 0) return;
|
||||
const visible = new Set((cardList.value || []).map((i: KnowledgeCard) => i.id));
|
||||
const visible = new Set(items.map((i: KnowledgeCard) => i.id));
|
||||
for (const id of selectedIds.value) {
|
||||
if (!visible.has(id)) selectedIds.value.delete(id);
|
||||
}
|
||||
@@ -2144,27 +2166,28 @@ async function createNewSession(value: string): Promise<void> {
|
||||
class="knowledge-card"
|
||||
:class="{ 'is-selected': selectedIds.has(item.id), 'has-selection': selectedIds.size > 0 }"
|
||||
v-for="(item, index) in cardList"
|
||||
:key="index"
|
||||
:key="item.id"
|
||||
@click="openCardDetails(item)"
|
||||
@mouseenter="onCardMouseEnter($event, item)"
|
||||
@mousemove="onCardMouseMove($event)"
|
||||
@mouseleave="onCardMouseLeave"
|
||||
>
|
||||
<label
|
||||
v-if="canEdit"
|
||||
class="card-select-overlay"
|
||||
:class="{ active: selectedIds.has(item.id) }"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedIds.has(item.id)"
|
||||
@click.stop="(e: MouseEvent) => toggleSelectRow(item.id, !selectedIds.has(item.id), e.shiftKey)"
|
||||
:aria-label="item.file_name"
|
||||
/>
|
||||
</label>
|
||||
<div class="card-content">
|
||||
<div class="card-content-nav">
|
||||
<div
|
||||
v-if="canEdit"
|
||||
class="card-nav-check"
|
||||
:class="{ active: selectedIds.has(item.id) }"
|
||||
@click.stop
|
||||
>
|
||||
<t-checkbox
|
||||
class="card-select-checkbox"
|
||||
size="small"
|
||||
:checked="selectedIds.has(item.id)"
|
||||
:title="item.file_name"
|
||||
@change="(checked, ctx) => onCardGridCheckboxChange(item.id, checked, ctx)"
|
||||
/>
|
||||
</div>
|
||||
<span class="card-content-title" :title="item.file_name">{{ item.file_name }}</span>
|
||||
<t-popup
|
||||
v-if="canEdit"
|
||||
@@ -2388,6 +2411,8 @@ async function createNewSession(value: string): Promise<void> {
|
||||
<EmptyKnowledge />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="doc-batch-bar-anchor" v-show="selectedIds.size > 0">
|
||||
<DocumentBatchBar
|
||||
:count="selectedIds.size"
|
||||
:loading="batchDeleting"
|
||||
@@ -2544,7 +2569,7 @@ async function createNewSession(value: string): Promise<void> {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 24px 32px 32px;
|
||||
padding: 24px 32px 0px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -3112,6 +3137,12 @@ async function createNewSession(value: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/* 批量条在滚动区外,始终贴主内容列底部,不随列表高度在列内上下漂移 */
|
||||
.doc-batch-bar-anchor {
|
||||
flex-shrink: 0;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
// Header 样式(无底部分割线,留更多空间给下方内容区)
|
||||
.document-header {
|
||||
display: flex;
|
||||
@@ -3388,6 +3419,7 @@ async function createNewSession(value: string): Promise<void> {
|
||||
|
||||
.faq-manager-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 24px 32px;
|
||||
overflow-y: auto;
|
||||
margin: 0 16px 0 4px;
|
||||
@@ -3442,7 +3474,7 @@ async function createNewSession(value: string): Promise<void> {
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
|
||||
gap: 14px;
|
||||
gap: 16px;
|
||||
align-content: flex-start;
|
||||
width: 100%;
|
||||
|
||||
@@ -3453,14 +3485,24 @@ async function createNewSession(value: string): Promise<void> {
|
||||
|
||||
.knowledge-card-skeleton {
|
||||
cursor: default;
|
||||
.card-content { padding: 15px 17px 13px; }
|
||||
.card-content-nav { margin-bottom: 14px; }
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 16px 8px;
|
||||
}
|
||||
|
||||
.card-content-nav {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
flex-shrink: 0;
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
padding: 0 17px;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
@@ -3747,6 +3789,7 @@ async function createNewSession(value: string): Promise<void> {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-draft-tip {
|
||||
@@ -3756,65 +3799,77 @@ async function createNewSession(value: string): Promise<void> {
|
||||
|
||||
.knowledge-card {
|
||||
min-width: 248px;
|
||||
border: 1px solid var(--td-component-stroke);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid color-mix(in srgb, var(--td-component-stroke) 82%, var(--td-bg-color-secondarycontainer));
|
||||
height: 148px;
|
||||
border-radius: 9px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.035);
|
||||
background: var(--td-bg-color-container);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--td-brand-color, #0052d9);
|
||||
box-shadow: 0 0 0 1px var(--td-brand-color, #0052d9), 0 4px 12px rgba(0, 82, 217, 0.08);
|
||||
background: var(--td-brand-color-1, #f0f6ff);
|
||||
}
|
||||
|
||||
.card-select-overlay {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
/* 初始态略可见,悬停/多选/已选时再强调,避免「凭空多一列」的突兀感 */
|
||||
.card-nav-check {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--td-bg-color-container, #fff);
|
||||
border: 1px solid var(--td-component-border, #e0e0e0);
|
||||
border-radius: 6px;
|
||||
width: 22px;
|
||||
height: 29px;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s ease;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
|
||||
z-index: 1;
|
||||
|
||||
input[type='checkbox'] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
accent-color: var(--td-brand-color, #0052d9);
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:focus-within {
|
||||
opacity: 1;
|
||||
border-color: var(--td-brand-color, #0052d9);
|
||||
}
|
||||
|
||||
.card-select-checkbox {
|
||||
margin: 0;
|
||||
line-height: 0;
|
||||
|
||||
:deep(.t-checkbox) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.t-checkbox__label) {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
min-width: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.t-checkbox__input) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.t-checkbox__input-wrapper) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .card-select-overlay,
|
||||
&.has-selection .card-select-overlay {
|
||||
&:hover .card-nav-check,
|
||||
&.has-selection .card-nav-check {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 15px 17px 13px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 16px 8px;
|
||||
}
|
||||
|
||||
.card-analyze {
|
||||
flex-shrink: 0;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
}
|
||||
@@ -3838,11 +3893,11 @@ async function createNewSession(value: string): Promise<void> {
|
||||
}
|
||||
|
||||
.card-content-nav {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 11px;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-content-title {
|
||||
@@ -3886,6 +3941,8 @@ async function createNewSession(value: string): Promise<void> {
|
||||
}
|
||||
|
||||
.card-content-txt {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
@@ -3899,9 +3956,9 @@ async function createNewSession(value: string): Promise<void> {
|
||||
}
|
||||
|
||||
.card-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 0 17px;
|
||||
flex-shrink: 0;
|
||||
margin-top: auto;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
@@ -3920,19 +3977,19 @@ async function createNewSession(value: string): Promise<void> {
|
||||
}
|
||||
|
||||
.card-type {
|
||||
color: var(--td-text-color-secondary);
|
||||
color: var(--td-text-color-placeholder);
|
||||
font-family: "PingFang SC";
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 3px 8px;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-card:hover {
|
||||
border-color: var(--td-brand-color);
|
||||
box-shadow: 0 2px 8px rgba(7, 192, 95, 0.12);
|
||||
border-color: color-mix(in srgb, var(--td-component-stroke) 55%, var(--td-brand-color));
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
/* 悬停知识卡片时跟随鼠标的详情气泡 */
|
||||
|
||||
@@ -17,23 +17,25 @@ const { t } = useI18n();
|
||||
<template>
|
||||
<transition name="batch-bar-fade">
|
||||
<div v-if="count > 0" class="doc-batch-bar" role="region" :aria-label="t('knowledgeBase.selectedCount', { count })">
|
||||
<div class="batch-bar-info">
|
||||
<span class="batch-bar-count">{{ t('knowledgeBase.selectedCount', { count }) }}</span>
|
||||
<button class="batch-bar-link" type="button" @click="emit('clear')">
|
||||
{{ t('knowledgeBase.clearSelection') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="batch-bar-actions">
|
||||
<t-button
|
||||
theme="danger"
|
||||
variant="base"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="emit('delete')"
|
||||
>
|
||||
<template #icon><t-icon name="delete" size="14px" /></template>
|
||||
{{ t('knowledgeBase.batchDelete') }}
|
||||
</t-button>
|
||||
<div class="batch-bar-inner">
|
||||
<div class="batch-bar-left">
|
||||
<span class="batch-bar-count">{{ t('knowledgeBase.selectedCount', { count }) }}</span>
|
||||
<t-button variant="text" theme="default" size="small" class="batch-bar-clear" @click="emit('clear')">
|
||||
{{ t('knowledgeBase.clearSelection') }}
|
||||
</t-button>
|
||||
</div>
|
||||
<div class="batch-bar-actions">
|
||||
<t-button
|
||||
theme="danger"
|
||||
variant="outline"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="emit('delete')"
|
||||
>
|
||||
<template #icon><t-icon name="delete" size="14px" /></template>
|
||||
{{ t('knowledgeBase.batchDelete') }}
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -41,63 +43,69 @@ const { t } = useI18n();
|
||||
|
||||
<style scoped lang="less">
|
||||
.doc-batch-bar {
|
||||
position: sticky;
|
||||
bottom: 12px;
|
||||
align-self: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 8px 12px 8px 16px;
|
||||
margin: 12px auto 4px;
|
||||
min-width: 320px;
|
||||
max-width: 720px;
|
||||
background: var(--td-bg-color-container, #fff);
|
||||
border: 1px solid var(--td-component-border, #e7e7e7);
|
||||
border-radius: 999px;
|
||||
box-shadow:
|
||||
0 6px 20px rgba(0, 0, 0, 0.08),
|
||||
0 2px 6px rgba(0, 0, 0, 0.06);
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
padding: 0 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.batch-bar-info {
|
||||
.batch-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
background: var(--td-bg-color-container);
|
||||
border: 1px solid var(--td-component-stroke);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.batch-bar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.batch-bar-count {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--td-text-color-primary, #232323);
|
||||
color: var(--td-text-color-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.batch-bar-link {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 2px 4px;
|
||||
.batch-bar-clear {
|
||||
flex-shrink: 0;
|
||||
padding: 0 6px !important;
|
||||
height: 28px !important;
|
||||
font-size: 12px;
|
||||
color: var(--td-brand-color, #0052d9);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
color: var(--td-text-color-secondary) !important;
|
||||
|
||||
&:hover { background: var(--td-brand-color-1, #f0f6ff); }
|
||||
&:hover {
|
||||
color: var(--td-brand-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.batch-bar-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.batch-bar-fade-enter-active,
|
||||
.batch-bar-fade-leave-active {
|
||||
transition: transform 0.18s ease, opacity 0.18s ease;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.batch-bar-fade-enter-from,
|
||||
.batch-bar-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
transform: translateY(6px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -110,14 +110,13 @@ const someSelected = computed(() => {
|
||||
return props.items.some(i => props.selectedIds.has(i.id)) && !allSelected.value;
|
||||
});
|
||||
|
||||
const onHeaderToggle = (e: Event) => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
const onHeaderCheckboxChange = (checked: boolean) => {
|
||||
emit('toggle-all', checked);
|
||||
};
|
||||
|
||||
const onRowToggle = (item: KnowledgeItem, e: MouseEvent) => {
|
||||
const checked = !props.selectedIds.has(item.id);
|
||||
emit('toggle-row', item.id, checked, e.shiftKey);
|
||||
const onRowCheckboxChange = (item: KnowledgeItem, checked: boolean, ctx?: { e?: Event }) => {
|
||||
const me = ctx?.e as MouseEvent | undefined;
|
||||
emit('toggle-row', item.id, checked, !!me?.shiftKey);
|
||||
};
|
||||
|
||||
const moreOpen = ref<string | null>(null);
|
||||
@@ -130,22 +129,22 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know
|
||||
item.isMore = false;
|
||||
emit('action', action, item);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="doc-list-view" :class="{ 'is-loading': loading }">
|
||||
<div class="doc-list-header" role="row">
|
||||
<div class="cell cell-check" role="columnheader">
|
||||
<label class="checkbox-wrap" @click.stop>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allSelected"
|
||||
:indeterminate.prop="someSelected"
|
||||
:disabled="!items.length"
|
||||
@change="onHeaderToggle"
|
||||
:aria-label="t('knowledgeBase.selectAll')"
|
||||
/>
|
||||
</label>
|
||||
<div class="cell cell-check" role="columnheader" @click.stop>
|
||||
<t-checkbox
|
||||
class="doc-list-check"
|
||||
size="small"
|
||||
:checked="allSelected"
|
||||
:indeterminate="someSelected"
|
||||
:disabled="!items.length"
|
||||
:title="t('knowledgeBase.selectAll')"
|
||||
@change="onHeaderCheckboxChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="cell cell-name" role="columnheader">{{ t('knowledgeBase.columnName') }}</div>
|
||||
<div class="cell cell-tag" role="columnheader">{{ t('knowledgeBase.columnTag') }}</div>
|
||||
@@ -166,18 +165,19 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know
|
||||
@click="emit('open', item)"
|
||||
>
|
||||
<div class="cell cell-check" @click.stop>
|
||||
<label class="checkbox-wrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedIds.has(item.id)"
|
||||
@click="onRowToggle(item, $event as unknown as MouseEvent)"
|
||||
:aria-label="item.file_name"
|
||||
/>
|
||||
</label>
|
||||
<t-checkbox
|
||||
class="doc-list-check"
|
||||
size="small"
|
||||
:checked="selectedIds.has(item.id)"
|
||||
:title="item.file_name"
|
||||
@change="(c, ctx) => onRowCheckboxChange(item, c, ctx)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="cell cell-name">
|
||||
<t-icon :name="getFileIcon(item)" class="row-file-icon" />
|
||||
<span class="row-file-icon-wrap">
|
||||
<t-icon :name="getFileIcon(item)" />
|
||||
</span>
|
||||
<span class="row-file-name" :title="item.file_name">{{ item.file_name }}</span>
|
||||
</div>
|
||||
|
||||
@@ -203,7 +203,7 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know
|
||||
v-if="statusByRow.get(item.id)!.label !== '--'"
|
||||
size="small"
|
||||
:theme="statusByRow.get(item.id)!.theme"
|
||||
variant="light"
|
||||
variant="light-outline"
|
||||
class="row-status-tag"
|
||||
>
|
||||
<template v-if="statusByRow.get(item.id)!.icon" #icon>
|
||||
@@ -239,16 +239,20 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know
|
||||
class="row-menu-item"
|
||||
@click.stop="handleAction('edit', item)"
|
||||
>
|
||||
<t-icon name="edit" /> <span>{{ t('knowledgeBase.editDocument') }}</span>
|
||||
<t-icon class="icon" name="edit" />
|
||||
<span>{{ t('knowledgeBase.editDocument') }}</span>
|
||||
</div>
|
||||
<div class="row-menu-item" @click.stop="handleAction('reparse', item)">
|
||||
<t-icon name="refresh" /> <span>{{ t('knowledgeBase.rebuildDocument') }}</span>
|
||||
<t-icon class="icon" name="refresh" />
|
||||
<span>{{ t('knowledgeBase.rebuildDocument') }}</span>
|
||||
</div>
|
||||
<div class="row-menu-item" @click.stop="handleAction('move', item)">
|
||||
<t-icon name="swap" /> <span>{{ t('knowledgeBase.moveDocument') }}</span>
|
||||
<t-icon class="icon" name="swap" />
|
||||
<span>{{ t('knowledgeBase.moveDocument') }}</span>
|
||||
</div>
|
||||
<div class="row-menu-item danger" @click.stop="handleAction('delete', item)">
|
||||
<t-icon name="delete" /> <span>{{ t('knowledgeBase.deleteDocument') }}</span>
|
||||
<t-icon class="icon" name="delete" />
|
||||
<span>{{ t('knowledgeBase.deleteDocument') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -260,14 +264,28 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
@keyframes doc-list-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.doc-list-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
background: var(--td-bg-color-container, #fff);
|
||||
border: 1px solid var(--td-component-stroke, #f0f0f0);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--td-bg-color-container);
|
||||
border: 1px solid var(--td-component-stroke);
|
||||
border-radius: 9px;
|
||||
/* 不能用 overflow:hidden,否则表头 position:sticky 相对外层滚动区失效 */
|
||||
overflow: visible;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
animation: doc-list-fade-in 0.32s ease-out;
|
||||
}
|
||||
|
||||
.doc-list-header,
|
||||
@@ -284,59 +302,54 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know
|
||||
48px; // actions
|
||||
align-items: center;
|
||||
column-gap: 0;
|
||||
padding: 0 12px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.doc-list-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
height: 36px;
|
||||
z-index: 3;
|
||||
height: 40px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--td-text-color-placeholder, #a6a6a6);
|
||||
background: var(--td-bg-color-page, #fafbfc);
|
||||
border-bottom: 1px solid var(--td-component-stroke, #f0f0f0);
|
||||
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
color: var(--td-text-color-secondary);
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-bottom: 1px solid var(--td-component-stroke);
|
||||
border-radius: 8px 8px 0 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.doc-list-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.doc-list-row {
|
||||
position: relative;
|
||||
height: 48px;
|
||||
min-height: 52px;
|
||||
font-size: 13px;
|
||||
color: var(--td-text-color-primary, #232323);
|
||||
border-bottom: 1px solid var(--td-component-stroke, #f3f3f3);
|
||||
color: var(--td-text-color-primary);
|
||||
border-bottom: 1px solid var(--td-component-stroke);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.12s ease, box-shadow 0.12s ease;
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:last-child { border-bottom: 0; }
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover:not(.selected),
|
||||
&.menu-open:not(.selected) {
|
||||
background: var(--td-bg-color-page, #f7f8fa);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--td-brand-color-1, #f2f5fc);
|
||||
box-shadow: inset 3px 0 0 var(--td-brand-color, #0052d9);
|
||||
|
||||
// brand-color-light alias maps back to brand-color-1, so a plain
|
||||
// var() swap produces no visible hover delta. Mix in a touch of
|
||||
// brand-color so the hover state is perceptible in both light and
|
||||
// dark themes without falling back to the saturated brand-color-2.
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--td-brand-color-1) 75%, var(--td-brand-color));
|
||||
}
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
}
|
||||
|
||||
&:hover .row-more-btn,
|
||||
&.menu-open .row-more-btn,
|
||||
&.selected .row-more-btn { opacity: 1; }
|
||||
&.selected .row-more-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.cell {
|
||||
@@ -354,8 +367,8 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know
|
||||
}
|
||||
|
||||
.cell-name {
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
gap: 10px;
|
||||
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.cell-size,
|
||||
@@ -367,23 +380,42 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.checkbox-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
input[type='checkbox'] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: var(--td-brand-color, #0052d9);
|
||||
cursor: pointer;
|
||||
/* TDesign 勾选框:去掉空白 label、与表格行对齐 */
|
||||
.doc-list-check {
|
||||
margin: 0;
|
||||
|
||||
:deep(.t-checkbox) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.t-checkbox__label) {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
min-width: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.t-checkbox__input) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.t-checkbox__input-wrapper) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.row-file-icon {
|
||||
.row-file-icon-wrap {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: var(--td-text-color-secondary, #888);
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
.row-file-name {
|
||||
@@ -392,6 +424,10 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
|
||||
.row-tag {
|
||||
@@ -411,7 +447,8 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know
|
||||
.row-mono {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 12px;
|
||||
color: var(--td-text-color-secondary, #666);
|
||||
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
.row-status-tag :deep(.t-icon) {
|
||||
@@ -432,35 +469,94 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
color: var(--td-text-color-secondary, #666);
|
||||
border-radius: 5px;
|
||||
color: var(--td-text-color-secondary);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.12s ease, background-color 0.12s ease;
|
||||
transition: opacity 0.15s ease, background-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover { background: var(--td-bg-color-component-hover, #ececec); }
|
||||
&.active { opacity: 1; background: var(--td-bg-color-component-active, #e0e0e0); }
|
||||
&:hover {
|
||||
background: var(--td-component-stroke);
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
background: var(--td-component-stroke);
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.row-menu {
|
||||
min-width: 160px;
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 140px;
|
||||
gap: 2px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.row-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: var(--td-text-color-primary, #232323);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--td-text-color-primary);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.12s ease;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.15s cubic-bezier(0.2, 0, 0, 1), transform 0.12s ease;
|
||||
|
||||
&:hover { background: var(--td-bg-color-component-hover, #f5f5f5); }
|
||||
&.danger { color: var(--td-error-color, #d54941); }
|
||||
&.danger:hover { background: var(--td-error-color-1, #fff1f0); }
|
||||
&:hover {
|
||||
background: var(--td-bg-color-container-hover);
|
||||
}
|
||||
|
||||
.t-icon { font-size: 14px; }
|
||||
&:active {
|
||||
background: var(--td-bg-color-container-active);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
color: var(--td-text-color-secondary);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover .icon {
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: var(--td-error-color-6);
|
||||
margin-top: 4px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
height: 1px;
|
||||
background: var(--td-component-stroke);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--td-error-color-6);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--td-error-color-1);
|
||||
color: var(--td-error-color-6);
|
||||
|
||||
.icon {
|
||||
color: var(--td-error-color-6);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--td-error-color-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div class="main" ref="dropzone">
|
||||
<Menu></Menu>
|
||||
<RouterView v-if="isRouterAlive" />
|
||||
<div v-if="isRouterAlive" class="platform-route-outlet">
|
||||
<RouterView />
|
||||
</div>
|
||||
<div class="upload-mask" v-show="ismask">
|
||||
<input type="file" style="display: none" ref="uploadInput" accept=".pdf,.docx,.doc,.pptx,.ppt,.txt,.md,.jpg,.jpeg,.png,.csv,.xls,.xlsx" />
|
||||
<UploadMask></UploadMask>
|
||||
@@ -177,13 +179,25 @@ onUnmounted(() => {
|
||||
<style lang="less">
|
||||
.main {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
min-height: 0;
|
||||
/* 统一整页背景,让左侧菜单与右侧内容区视觉连贯 */
|
||||
background: var(--td-bg-color-container);
|
||||
}
|
||||
|
||||
/* 右侧路由区:占满剩余宽度与整列高度,并把 min-height:0 传给子页面以便内部 flex 滚动 */
|
||||
.platform-route-outlet {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.upload-mask {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
position: fixed;
|
||||
|
||||
Reference in New Issue
Block a user