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:
wizardchen
2026-04-29 15:20:41 +08:00
committed by lyingbug
parent a40f191430
commit c62ff4c906
12 changed files with 395 additions and 219 deletions
+1 -1
View File
@@ -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';
};
+4 -4
View 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) => {
+1 -1
View File
@@ -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.',
+1 -1
View File
@@ -259,7 +259,7 @@ export default {
columnActions: "작업",
selectAll: "전체 선택",
selectedCount: "{count}개 선택됨",
clearSelection: "지우기",
clearSelection: "선택 해제",
batchDelete: "선택 삭제",
batchDeleteConfirmation: "일괄 삭제 확인",
confirmBatchDeleteDocument: "선택한 {count}개 문서를 삭제하시겠습니까? 삭제 후 복구할 수 없습니다.",
+1 -1
View File
@@ -227,7 +227,7 @@ export default {
columnActions: 'Действия',
selectAll: 'Выбрать все',
selectedCount: 'Выбрано: {count}',
clearSelection: 'Очистить',
clearSelection: 'Снять выделение',
batchDelete: 'Удалить выбранные',
batchDeleteConfirmation: 'Подтверждение пакетного удаления',
confirmBatchDeleteDocument: 'Удалить {count} выбранных документов? Это действие нельзя отменить.',
+1 -1
View File
@@ -256,7 +256,7 @@ export default {
columnActions: "操作",
selectAll: "全选",
selectedCount: "已选 {count} 项",
clearSelection: "清空",
clearSelection: "取消选择",
batchDelete: "批量删除",
batchDeleteConfirmation: "批量删除确认",
confirmBatchDeleteDocument: "确认删除选中的 {count} 个文档?删除后将无法恢复。",
+2 -1
View File
@@ -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';
};
+128 -71
View 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>
+15 -1
View File
@@ -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;