feat: 前端初始调整及知识库模块添加
parent
121d515402
commit
3e1c7283ce
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,340 @@
|
||||
<!-- 创建知识库页 —— 对应源代码 CreateKnowledge.tsx -->
|
||||
<template>
|
||||
<div :class="styles.page">
|
||||
<ElSteps :active="currentStep" :class="styles.steps" class="create_knowledge_steps">
|
||||
<ElStep title="向量数据库" />
|
||||
<ElStep title="文本分段与清洗" />
|
||||
<ElStep title="处理并完成" />
|
||||
</ElSteps>
|
||||
|
||||
<ElForm class="form-body" label-position="top">
|
||||
<KnowledgeInfo
|
||||
v-if="currentStep === 0"
|
||||
:file-list="fileList"
|
||||
:allowed-extensions="allowedExtensions"
|
||||
:on-file-list-change="setFileList"
|
||||
:set-file-ids="setFileIds"
|
||||
:knowledge-base-info="knowledgeBaseInfo"
|
||||
:set-knowledge-base-info="setKnowledgeBaseInfo"
|
||||
/>
|
||||
<SegmentSetting
|
||||
v-else-if="currentStep === 1"
|
||||
:file-list="fileList"
|
||||
:file-ids="fileIds"
|
||||
:rerank-model="rerankModel"
|
||||
:embedding-model="embeddingModel"
|
||||
:indexing-estimate="indexingEstimate"
|
||||
:set-indexing-estimate="setIndexingEstimate"
|
||||
:set-rerank-model="setRerankModel"
|
||||
:set-embedding-model="setEmbeddingModel"
|
||||
:rerank-enabled="rerankEnabled"
|
||||
:top-k="topK"
|
||||
:score-threshold="scoreThreshold"
|
||||
:score-threshold-enabled="scoreThresholdEnabled"
|
||||
:set-rerank-enabled="setRerankEnabled"
|
||||
:set-top-k="setTopK"
|
||||
:set-score-threshold="setScoreThreshold"
|
||||
:set-score-threshold-enabled="setScoreThresholdEnabled"
|
||||
/>
|
||||
<InformationVerification
|
||||
v-else-if="currentStep === 2"
|
||||
:file-list="fileList"
|
||||
:knowledge-base-info="knowledgeBaseInfo"
|
||||
:rerank-model="rerankModel"
|
||||
:embedding-model="embeddingModel"
|
||||
:indexing-estimate="indexingEstimate"
|
||||
:rerank-enabled="rerankEnabled"
|
||||
:top-k="topK"
|
||||
:score-threshold="scoreThreshold"
|
||||
:score-threshold-enabled="scoreThresholdEnabled"
|
||||
:indexing-status-list="indexingStatusList"
|
||||
/>
|
||||
</ElForm>
|
||||
|
||||
<div :class="styles.footer">
|
||||
<ElButton
|
||||
v-if="!isClickComplete && currentStep > 0"
|
||||
:disabled="loading"
|
||||
@click="handlePrev"
|
||||
>
|
||||
上一步
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="!isClickComplete && currentStep < stepItems.length - 1"
|
||||
type="primary"
|
||||
:disabled="isNextDisabled()"
|
||||
:loading="loading"
|
||||
@click="handleNext"
|
||||
>
|
||||
下一步
|
||||
</ElButton>
|
||||
|
||||
<template v-if="currentStep === 2">
|
||||
<ElButton
|
||||
v-if="!isClickComplete"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
@click="createKnowledge"
|
||||
>
|
||||
创建并保存
|
||||
</ElButton>
|
||||
<ElButton v-else type="primary" @click="goBack">
|
||||
完成
|
||||
</ElButton>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: "CreateKnowledge" });
|
||||
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import KnowledgeInfo from "./components/KnowledgeInfo.vue";
|
||||
import SegmentSetting from "./components/SegmentSetting.vue";
|
||||
import InformationVerification from "./components/InformationVerification.vue";
|
||||
import { useDifyGet, useDifyPost, useRequest } from "./useDifyApis";
|
||||
import { knowledgeBase } from "./api";
|
||||
import {
|
||||
decodeInputValue,
|
||||
getInitKnowledgeQueryData,
|
||||
getQueryIndexingEstimateJson,
|
||||
initIndexingEstimate,
|
||||
type IndexingEstimate,
|
||||
type IndexingStatusItem,
|
||||
type KnowledgeBaseInfoType,
|
||||
type ModelType,
|
||||
type UploadFile as UploadFileType,
|
||||
} from "./types";
|
||||
import styles from "./style/createKnowledge.module.scss";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const stepItems = [
|
||||
{ title: "向量数据库" },
|
||||
{ title: "文本分段与清洗" },
|
||||
{ title: "处理并完成" },
|
||||
];
|
||||
|
||||
const currentStep = ref(0);
|
||||
const knowledgeBaseInfo = ref<KnowledgeBaseInfoType>({
|
||||
name: "",
|
||||
description: "",
|
||||
});
|
||||
const indexingEstimate = ref<IndexingEstimate>(initIndexingEstimate);
|
||||
const fileList = ref<UploadFileType[]>([]);
|
||||
const fileIds = ref<string[]>([]);
|
||||
const rerankModel = ref<ModelType>({
|
||||
currentProvider: "",
|
||||
currentModel: "",
|
||||
});
|
||||
const embeddingModel = ref<ModelType>({
|
||||
currentProvider: "",
|
||||
currentModel: "",
|
||||
});
|
||||
const rerankEnabled = ref(true);
|
||||
const topK = ref(3);
|
||||
const scoreThreshold = ref(0.5);
|
||||
const scoreThresholdEnabled = ref(false);
|
||||
const indexingStatusList = ref<IndexingStatusItem[]>([]);
|
||||
const isClickComplete = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const { doRequest } = useRequest();
|
||||
|
||||
let pollingTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// 线上临时文件列表
|
||||
const { result: supportTypeList, doGet } = useDifyGet({
|
||||
url: knowledgeBase.supportType,
|
||||
});
|
||||
|
||||
// 创建知识库
|
||||
const { doPost: initKnowledgeQueryDataPost } = useDifyPost({
|
||||
url: knowledgeBase.initKnowledgeQueryData,
|
||||
});
|
||||
|
||||
// 文档解析循环
|
||||
const { doGet: indexingStatusGet } = useDifyGet();
|
||||
|
||||
const finishedIndexingStatus = ["completed", "error", "paused", "stopped"];
|
||||
|
||||
const allowedExtensions = computed(
|
||||
() => (supportTypeList.value as any)?.allowed_extensions || []
|
||||
);
|
||||
|
||||
function handlePrev() {
|
||||
currentStep.value = Math.max(currentStep.value - 1, 0);
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
currentStep.value = Math.min(currentStep.value + 1, stepItems.length - 1);
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push("/platform/ai/knowledge");
|
||||
}
|
||||
|
||||
function setFileList(list: UploadFileType[]) {
|
||||
fileList.value = list;
|
||||
}
|
||||
|
||||
function setFileIds(ids: string[]) {
|
||||
fileIds.value = ids;
|
||||
}
|
||||
|
||||
function setKnowledgeBaseInfo(info: KnowledgeBaseInfoType) {
|
||||
knowledgeBaseInfo.value = info;
|
||||
}
|
||||
|
||||
function setIndexingEstimate(estimate: IndexingEstimate) {
|
||||
indexingEstimate.value = estimate;
|
||||
}
|
||||
|
||||
function setRerankModel(model: ModelType) {
|
||||
rerankModel.value = model;
|
||||
}
|
||||
|
||||
function setEmbeddingModel(model: ModelType) {
|
||||
embeddingModel.value = model;
|
||||
}
|
||||
|
||||
function setRerankEnabled(value: boolean) {
|
||||
rerankEnabled.value = value;
|
||||
}
|
||||
|
||||
function setTopK(value: number) {
|
||||
topK.value = value;
|
||||
}
|
||||
|
||||
function setScoreThreshold(value: number) {
|
||||
scoreThreshold.value = value;
|
||||
}
|
||||
|
||||
function setScoreThresholdEnabled(value: boolean) {
|
||||
scoreThresholdEnabled.value = value;
|
||||
}
|
||||
|
||||
function isNextDisabled() {
|
||||
if (currentStep.value === 0 && fileIds.value.length === 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 轮询文件解析状态
|
||||
async function pollIndexingStatus(url: string) {
|
||||
const resp = await indexingStatusGet({ url });
|
||||
const statusList = ((resp as any)?.data || []) as IndexingStatusItem[];
|
||||
indexingStatusList.value = statusList;
|
||||
|
||||
const isFinished =
|
||||
statusList.length > 0 &&
|
||||
statusList.every((item) => finishedIndexingStatus.includes(item.indexing_status));
|
||||
|
||||
if (!isFinished) {
|
||||
pollingTimer = setTimeout(() => {
|
||||
pollIndexingStatus(url);
|
||||
}, 1000);
|
||||
} else {
|
||||
isClickComplete.value = true;
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createKnowledge() {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const queryIndexingEstimateJsonResult = getQueryIndexingEstimateJson(
|
||||
fileIds.value,
|
||||
[
|
||||
{
|
||||
id: "remove_extra_spaces",
|
||||
enabled: indexingEstimate.value.pre_processing_rules.includes(
|
||||
"remove_extra_spaces"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "remove_urls_emails",
|
||||
enabled: indexingEstimate.value.pre_processing_rules.includes(
|
||||
"remove_urls_emails"
|
||||
),
|
||||
},
|
||||
],
|
||||
{
|
||||
separator: decodeInputValue(indexingEstimate.value.segmentIdentifier),
|
||||
max_tokens: indexingEstimate.value.maximumSegmentLength,
|
||||
chunk_overlap: indexingEstimate.value.segmentOverlapLength,
|
||||
}
|
||||
);
|
||||
|
||||
const data = getInitKnowledgeQueryData({
|
||||
queryIndexingEstimateJsonResult,
|
||||
top_k: topK.value,
|
||||
score_threshold_enabled: scoreThresholdEnabled.value,
|
||||
score_threshold: scoreThreshold.value,
|
||||
embedding_model: embeddingModel.value.currentModel,
|
||||
embedding_model_provider: embeddingModel.value.currentProvider,
|
||||
reranking_provider_name: rerankModel.value.currentProvider,
|
||||
reranking_model_name: rerankModel.value.currentModel,
|
||||
});
|
||||
|
||||
const resp: any = await initKnowledgeQueryDataPost({
|
||||
data,
|
||||
});
|
||||
|
||||
if (resp?.dataset && resp.dataset?.id && resp.batch) {
|
||||
const datasetId = resp.dataset.id;
|
||||
const batchId = resp.batch;
|
||||
|
||||
indexingStatusList.value = [];
|
||||
const statusUrl = knowledgeBase.indexingStatus(datasetId, batchId);
|
||||
pollIndexingStatus(statusUrl);
|
||||
await doRequest({
|
||||
url: knowledgeBase.queryKnowledgeRule(datasetId),
|
||||
method: "patch",
|
||||
data: {
|
||||
name: knowledgeBaseInfo.value.name,
|
||||
description: knowledgeBaseInfo.value.description,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
loading.value = false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
doGet();
|
||||
});
|
||||
|
||||
watch(fileList, (list) => {
|
||||
if (!knowledgeBaseInfo.value.name && list.length > 0) {
|
||||
knowledgeBaseInfo.value = {
|
||||
...knowledgeBaseInfo.value,
|
||||
name: list[0]?.name ?? "",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollingTimer) {
|
||||
clearTimeout(pollingTimer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.create_knowledge_steps {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,416 @@
|
||||
<!-- 文档列表页 —— 对应源代码 Documents.tsx -->
|
||||
<template>
|
||||
<div :class="styles.page">
|
||||
<div :class="styles.header">
|
||||
<!-- <ElButton text :icon="ArrowLeft" @click="handleBack" /> -->
|
||||
<div>
|
||||
<h4 :class="styles.title">文档</h4>
|
||||
<div :class="styles.description">知识库的所有文件都在这里显示</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加文件 -->
|
||||
<AddDocument
|
||||
v-if="addDocumentModalOpen === 'add'"
|
||||
:set-add-document-modal-open="setAddDocumentModalOpen"
|
||||
:dataset-id="datasetId"
|
||||
:fetch-document-list="() => fetchDocumentList()"
|
||||
/>
|
||||
|
||||
<!-- 文档预览 -->
|
||||
<PreviewDocument
|
||||
v-else-if="addDocumentModalOpen === 'preview'"
|
||||
:set-add-document-modal-open="setAddDocumentModalOpen"
|
||||
:dataset-id="datasetId"
|
||||
:record="previewRecord"
|
||||
/>
|
||||
|
||||
<!-- 文档列表 -->
|
||||
<template v-else>
|
||||
<div :class="styles.toolbar">
|
||||
<div :class="styles.toolbarLeft">
|
||||
<ElSelect :class="styles.typeSelect" :model-value="'all'" disabled>
|
||||
<ElOption label="全部" value="all" />
|
||||
</ElSelect>
|
||||
<ElInput
|
||||
v-model="keyword"
|
||||
:class="styles.searchInput"
|
||||
clearable
|
||||
placeholder="搜索"
|
||||
@clear="() => { keyword = ''; debounceFetch(''); }"
|
||||
@input="(val: string) => debounceFetch(val)"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon><Search /></ElIcon>
|
||||
</template>
|
||||
</ElInput>
|
||||
<ElButton :icon="Refresh" :class="styles.syncIcon" @click="() => fetchDocumentList()" />
|
||||
</div>
|
||||
<div :class="styles.toolbarRight">
|
||||
<ElButton type="primary" :icon="Plus" @click="() => setAddDocumentModalOpen('add')">
|
||||
添加文件
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="styles.tableCard">
|
||||
<ElTable
|
||||
v-loading="documentListLoading || doDocumentsStatusLoading"
|
||||
:data="documentList"
|
||||
row-key="id"
|
||||
height="100%"
|
||||
>
|
||||
<ElTableColumn type="selection" width="42" />
|
||||
<ElTableColumn label="#" width="60" prop="position">
|
||||
<template #default="{ row, $index }">
|
||||
{{ row.position || $index + 1 }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="名称" prop="name" min-width="260" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div :class="styles.nameCell">
|
||||
<span :class="styles.fileIcon">
|
||||
<ElIcon>
|
||||
<Document v-if="!isMarkdown(row)" />
|
||||
<Memo v-else />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<span
|
||||
:class="styles.nameText"
|
||||
@click="() => openPreview(row)"
|
||||
>
|
||||
{{ row.name || "-" }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="分段模式" prop="doc_form" width="140">
|
||||
<template #default="{ row }">
|
||||
<ElTag :class="styles.modeTag" effect="plain" size="small">
|
||||
{{ DOC_FORM_LABEL[row.doc_form as string] || row.doc_form || "-" }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="字符数" prop="word_count" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ formatCount(row.word_count) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="召回次数" prop="hit_count" width="130">
|
||||
<template #default="{ row }">
|
||||
{{ formatCount(row.hit_count) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="上传时间" prop="created_at" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="display_status" width="120">
|
||||
<template #default="{ row }">
|
||||
<span :class="[styles.statusAvailable, !row.enabled ? styles.statusAvailableDisabled : '']">
|
||||
<span :class="[styles.statusDot, !row.enabled ? styles.statusDotDisabled : '']" />
|
||||
{{ STATUS_LABEL[row.display_status as string] || STATUS_LABEL[row.indexing_status as string] || row.display_status || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="160">
|
||||
<template #default="{ row }">
|
||||
<ElSwitch
|
||||
size="small"
|
||||
:model-value="row.enabled"
|
||||
@change="(val: any) => handleStatusChange(row, Boolean(val))"
|
||||
/>
|
||||
<ElDropdown class="action-dropdown" trigger="click" @command="(cmd: string) => handleActionCommand(cmd, row)">
|
||||
<ElIcon :class="styles.actionIcon"><MoreFilled /></ElIcon>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem command="rename">重命名</ElDropdownItem>
|
||||
<ElDropdownItem command="delete" divided>删除</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<div :class="styles.footer">
|
||||
<ElPagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.limit"
|
||||
:total="total"
|
||||
:page-sizes="[10, 25, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="(page: number) => fetchDocumentList(page, pagination.limit)"
|
||||
@size-change="(size: number) => fetchDocumentList(1, size)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElDialog v-model="renameModalOpen" title="重命名文档" width="480px">
|
||||
<ElInput
|
||||
v-model="renameValue"
|
||||
placeholder="请输入新名称"
|
||||
@keyup.enter="submitRename"
|
||||
/>
|
||||
<template #footer>
|
||||
<ElButton @click="() => (renameModalOpen = false)">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="doRenameDocumentLoading"
|
||||
:disabled="!renameValue.trim() || renameValue.trim() === String(renameRecord?.name || '')"
|
||||
@click="submitRename"
|
||||
>
|
||||
确定
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: "Documents" });
|
||||
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import {
|
||||
Search,
|
||||
Refresh,
|
||||
Plus,
|
||||
MoreFilled,
|
||||
Document,
|
||||
Memo,
|
||||
ArrowLeft,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import AddDocument from "./components/AddDocument.vue";
|
||||
import PreviewDocument from "./components/docPreview/index.vue";
|
||||
import {
|
||||
useDifyDelete,
|
||||
useDifyGet,
|
||||
useDifyPatch,
|
||||
useDifyPost,
|
||||
} from "./useDifyApis";
|
||||
import { knowledgeBase } from "./api";
|
||||
import type { DocumentModalOpenType } from "./types";
|
||||
import styles from "./style/documents.module.scss";
|
||||
|
||||
type DocumentItem = {
|
||||
id: string;
|
||||
position: number;
|
||||
name: string;
|
||||
word_count: number;
|
||||
hit_count: number;
|
||||
created_at: number;
|
||||
indexing_status: string;
|
||||
display_status: string;
|
||||
enabled: boolean;
|
||||
doc_form: string;
|
||||
data_source_detail_dict?: {
|
||||
upload_file?: {
|
||||
extension?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
available: "可用",
|
||||
disabled: "已禁用",
|
||||
indexing: "索引中",
|
||||
error: "异常",
|
||||
};
|
||||
|
||||
const DOC_FORM_LABEL: Record<string, string> = {
|
||||
text_model: "通用 · QA",
|
||||
hierarchical_model: "父子分段",
|
||||
qa_model: "问答模式",
|
||||
};
|
||||
|
||||
function formatDateTime(timestamp: number) {
|
||||
if (!timestamp) return "-";
|
||||
const date = new Date(timestamp * 1000);
|
||||
const pad = (value: number) => String(value).padStart(2, "0");
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
|
||||
date.getDate()
|
||||
)} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
function formatCount(value: number) {
|
||||
if (!Number.isFinite(value)) return "0";
|
||||
if (value >= 1000) {
|
||||
const text = (value / 1000).toFixed(value % 1000 === 0 ? 0 : 1);
|
||||
return `${text}k`;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const datasetId = computed(() => String(route.params.dataset_id || ""));
|
||||
|
||||
const pagination = ref({ page: 1, limit: 10 });
|
||||
const keyword = ref("");
|
||||
const addDocumentModalOpen = ref<DocumentModalOpenType>("documents");
|
||||
const previewRecord = ref<DocumentItem | null>(null);
|
||||
|
||||
// 获取文档列表
|
||||
const {
|
||||
result: documentListResult,
|
||||
loading: documentListLoading,
|
||||
doGet: doGetDocumentList,
|
||||
} = useDifyGet();
|
||||
|
||||
// 禁用文档
|
||||
const { doPatch: doDocumentsStatus, loading: doDocumentsStatusLoading } = useDifyPatch();
|
||||
|
||||
// 重命名文档
|
||||
const { doPost: doRenameDocument, loading: doRenameDocumentLoading } = useDifyPost();
|
||||
|
||||
// 删除文档
|
||||
const { doDelete: doDeleteDocument } = useDifyDelete({
|
||||
url: knowledgeBase.documents(datasetId.value),
|
||||
});
|
||||
|
||||
const renameModalOpen = ref(false);
|
||||
const renameValue = ref("");
|
||||
const renameRecord = ref<DocumentItem | null>(null);
|
||||
|
||||
const documentList = computed<DocumentItem[]>(() => {
|
||||
return Array.isArray((documentListResult.value as any)?.data)
|
||||
? (documentListResult.value as any).data
|
||||
: [];
|
||||
});
|
||||
|
||||
const total = computed(() => Number((documentListResult.value as any)?.total) || 0);
|
||||
|
||||
function setAddDocumentModalOpen(open: DocumentModalOpenType) {
|
||||
addDocumentModalOpen.value = open;
|
||||
}
|
||||
|
||||
function openPreview(record: DocumentItem) {
|
||||
previewRecord.value = record;
|
||||
setAddDocumentModalOpen("preview");
|
||||
}
|
||||
|
||||
function openRenameModal(record: DocumentItem) {
|
||||
renameRecord.value = record;
|
||||
renameValue.value = record.name || "";
|
||||
renameModalOpen.value = true;
|
||||
}
|
||||
|
||||
function confirmDeleteDocument(record: DocumentItem) {
|
||||
if (!datasetId.value || !record?.id) return;
|
||||
ElMessageBox.confirm("此操作不可逆,请谨慎操作。", "确定要删除该文档吗?", {
|
||||
type: "warning",
|
||||
confirmButtonText: "删除",
|
||||
cancelButtonText: "取消",
|
||||
confirmButtonClass: "el-button--danger",
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
await doDeleteDocument({
|
||||
config: {
|
||||
params: {
|
||||
document_id: record.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
ElMessage.success("删除成功");
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
} finally {
|
||||
fetchDocumentList();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
async function submitRename() {
|
||||
const nextName = renameValue.value.trim();
|
||||
if (!datasetId.value || !renameRecord.value?.id || !nextName) return;
|
||||
try {
|
||||
await doRenameDocument({
|
||||
url: knowledgeBase.renameDocument(datasetId.value, renameRecord.value.id),
|
||||
data: {
|
||||
name: nextName,
|
||||
},
|
||||
});
|
||||
renameModalOpen.value = false;
|
||||
renameRecord.value = null;
|
||||
} catch {
|
||||
ElMessage.error("操作失败");
|
||||
} finally {
|
||||
fetchDocumentList();
|
||||
}
|
||||
}
|
||||
|
||||
function handleStatusChange(record: DocumentItem, enabled: boolean) {
|
||||
if (!datasetId.value) return;
|
||||
doDocumentsStatus({
|
||||
url: knowledgeBase.documentStatus(datasetId.value, enabled ? "enable" : "disable"),
|
||||
config: {
|
||||
params: {
|
||||
document_id: record.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("操作失败");
|
||||
})
|
||||
.finally(() => {
|
||||
fetchDocumentList();
|
||||
});
|
||||
}
|
||||
|
||||
function handleActionCommand(cmd: string, record: DocumentItem) {
|
||||
if (cmd === "rename") openRenameModal(record);
|
||||
if (cmd === "delete") confirmDeleteDocument(record);
|
||||
}
|
||||
|
||||
function isMarkdown(row: DocumentItem): boolean {
|
||||
const extension =
|
||||
row.data_source_detail_dict?.upload_file?.extension ||
|
||||
row.name?.split(".").pop() ||
|
||||
"";
|
||||
return ["md", "markdown"].includes(String(extension).toLowerCase());
|
||||
}
|
||||
|
||||
function fetchDocumentList(
|
||||
page = pagination.value.page,
|
||||
limit = pagination.value.limit,
|
||||
key = keyword.value.trim()
|
||||
) {
|
||||
if (!datasetId.value) return;
|
||||
keyword.value = key;
|
||||
pagination.value = { page, limit };
|
||||
doGetDocumentList({
|
||||
url: knowledgeBase.documents(datasetId.value),
|
||||
params: { page, limit, keyword: key },
|
||||
});
|
||||
}
|
||||
|
||||
const debounceFetch = useDebounceFn((val: string) => {
|
||||
fetchDocumentList(pagination.value.page, pagination.value.limit, val);
|
||||
}, 500);
|
||||
|
||||
watch(
|
||||
datasetId,
|
||||
() => {
|
||||
fetchDocumentList(1, pagination.value.limit);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handleBack = () => {
|
||||
router.back();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.action-dropdown {
|
||||
margin-left: 12px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,42 @@
|
||||
// 知识库
|
||||
export const knowledgeBase = {
|
||||
pageList: "/console/api/datasets",
|
||||
login: "/console/api/login",
|
||||
supportType: "/console/api/files/support-type",
|
||||
upload: "/console/api/files/upload",
|
||||
indexingEstimate: "/console/api/datasets/indexing-estimate",
|
||||
// 文本嵌入模型
|
||||
textEmbedding: "/console/api/workspaces/current/models/model-types/text-embedding",
|
||||
// 排序模型
|
||||
rerank: "/console/api/workspaces/current/models/model-types/rerank",
|
||||
// 初始化知识库查询数据
|
||||
initKnowledgeQueryData: "/console/api/datasets/init",
|
||||
|
||||
// 文件创建解析
|
||||
indexingStatus: (dataset_id: string, batch_id: string) =>
|
||||
`/console/api/datasets/${dataset_id}/batch/${batch_id}/indexing-status`,
|
||||
|
||||
// 文档
|
||||
documents: (dataset_id: string) => `/console/api/datasets/${dataset_id}/documents`,
|
||||
|
||||
// 文件状态设置
|
||||
documentStatus: (dataset_id: string, status: "disable" | "enable") =>
|
||||
`/console/api/datasets/${dataset_id}/documents/status/${status}/batch`,
|
||||
|
||||
// 文件重命名
|
||||
renameDocument: (dataset_id: string, document_id: string) =>
|
||||
`/console/api/datasets/${dataset_id}/documents/${document_id}/rename`,
|
||||
|
||||
// 读取文档规则
|
||||
readCurrentDocumentRule: "/console/api/datasets/process-rule",
|
||||
|
||||
// 新增文档保存文档
|
||||
createAndSaveDocument: (dataset_id: string) => `/console/api/datasets/${dataset_id}/documents`,
|
||||
|
||||
// 查询当前知识库规则
|
||||
queryKnowledgeRule: (dataset_id: string) => `/console/api/datasets/${dataset_id}`,
|
||||
|
||||
// 获取完整文件
|
||||
downloadDocument: (dataset_id: string, document_id: string) =>
|
||||
`/console/api/datasets/${dataset_id}/documents/${document_id}/download`,
|
||||
};
|
||||
@ -0,0 +1,74 @@
|
||||
<!-- 文本分段预览组件 —— 对应源代码 SegmentedPreview.tsx -->
|
||||
<template>
|
||||
<div :class="styles.segmentRight">
|
||||
<div :class="styles.previewHeader">
|
||||
<div :class="styles.previewTitle">
|
||||
<template v-if="selectedFilename">
|
||||
<span :class="styles.previewFileIcon">
|
||||
<FileIcon
|
||||
:extension="selectedExt || 'file'"
|
||||
v-bind="selectedIconStyle"
|
||||
/>
|
||||
</span>
|
||||
<span :class="styles.previewFileName" :title="selectedFilename">
|
||||
{{ selectedFilename }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="styles.previewBody">
|
||||
<div v-loading="loadingIndexingEstimate" class="preview-loading-wrapper">
|
||||
<div v-if="indexingEstimatePreviewChunks.length" :class="styles.previewChunkList">
|
||||
<div v-for="(chunk, index) in indexingEstimatePreviewChunks" :key="index">
|
||||
<div :class="styles.previewChunkHeader">
|
||||
<ElIcon :class="styles.previewChunkIcon"><Grid /></ElIcon>
|
||||
<div :class="styles.previewChunkMeta">
|
||||
Chunk-{{ index + 1 }} · {{ chunk.content?.length ?? 0 }} 字符
|
||||
</div>
|
||||
</div>
|
||||
<div :class="styles.previewChunkContent">{{ chunk.content ?? "" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="styles.previewEmptyWrapper">
|
||||
<div :class="styles.previewEmpty">
|
||||
<ElIcon :class="styles.previewEmptyIcon"><Search /></ElIcon>
|
||||
<div :class="styles.previewEmptyText">点击左侧的"预览块"按钮来加载预览</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: "SegmentedPreview" });
|
||||
|
||||
import { Grid, Search } from "@element-plus/icons-vue";
|
||||
import FileIcon from "./FileIcon.vue";
|
||||
import type { FileIconStyle } from "./fileStyles";
|
||||
import styles from "../style/common.module.scss";
|
||||
|
||||
type IndexingEstimatePreviewChunk = {
|
||||
content?: string;
|
||||
summary?: string | null;
|
||||
child_chunks?: unknown;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
selectedFilename?: string;
|
||||
selectedExt: string;
|
||||
selectedIconStyle: FileIconStyle;
|
||||
loadingIndexingEstimate: boolean;
|
||||
indexingEstimatePreviewChunks: IndexingEstimatePreviewChunk[];
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview-loading-wrapper {
|
||||
height: 100%;
|
||||
min-height: 320px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,36 @@
|
||||
<!-- docx 文件预览 —— 对应源代码 docPreview/DocxPreview.tsx -->
|
||||
<template>
|
||||
<div :class="styles.docxPreviewContainer">
|
||||
<div ref="containerRef" :class="styles.docxPreviewBody" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { renderAsync } from "docx-preview";
|
||||
import styles from "../../style/doc.module.scss";
|
||||
|
||||
interface Props {
|
||||
file: Blob | ArrayBuffer | null | undefined;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const containerRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
async function renderDocx() {
|
||||
if (!props.file || !containerRef.value) return;
|
||||
containerRef.value.innerHTML = "";
|
||||
await renderAsync(props.file, containerRef.value);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderDocx();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.file,
|
||||
() => {
|
||||
renderDocx();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@ -0,0 +1,80 @@
|
||||
<!-- Excel / CSV 文件预览 —— 对应源代码 docPreview/ExcelPreview.tsx -->
|
||||
<template>
|
||||
<div v-if="error" :class="styles.docPreviewLoading">
|
||||
<ElEmpty :description="error" />
|
||||
</div>
|
||||
<div v-else-if="!sheetNames.length" :class="styles.docPreviewLoading">
|
||||
<ElEmpty description="暂无内容" />
|
||||
</div>
|
||||
<div v-else :class="styles.excelPreviewContainer">
|
||||
<ElTabs v-model="activeSheet">
|
||||
<ElTabPane
|
||||
v-for="name in sheetNames"
|
||||
:key="name"
|
||||
:label="name"
|
||||
:name="name"
|
||||
>
|
||||
<div :class="styles.excelSheet" v-html="htmlMap[name] || ''" />
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import * as XLSX from "xlsx";
|
||||
import styles from "../../style/doc.module.scss";
|
||||
|
||||
interface Props {
|
||||
file: ArrayBuffer;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const htmlMap = ref<Record<string, string>>({});
|
||||
const sheetNames = ref<string[]>([]);
|
||||
const activeSheet = ref<string>("");
|
||||
const error = ref<string>("");
|
||||
|
||||
function parseWorkbook() {
|
||||
error.value = "";
|
||||
try {
|
||||
const ext = String(props.extension || "").toLowerCase();
|
||||
const workbook =
|
||||
ext === "csv"
|
||||
? XLSX.read(new TextDecoder().decode(props.file), { type: "string" })
|
||||
: XLSX.read(props.file, { type: "array" });
|
||||
const names = Array.isArray(workbook.SheetNames) ? workbook.SheetNames : [];
|
||||
const nextHtmlMap: Record<string, string> = {};
|
||||
names.forEach((name) => {
|
||||
const sheet = workbook.Sheets[name];
|
||||
if (sheet) {
|
||||
nextHtmlMap[name] = XLSX.utils.sheet_to_html(sheet);
|
||||
}
|
||||
});
|
||||
sheetNames.value = names;
|
||||
htmlMap.value = nextHtmlMap;
|
||||
activeSheet.value =
|
||||
activeSheet.value && names.includes(activeSheet.value)
|
||||
? activeSheet.value
|
||||
: names[0] || "";
|
||||
} catch {
|
||||
sheetNames.value = [];
|
||||
htmlMap.value = {};
|
||||
activeSheet.value = "";
|
||||
error.value = "解析失败";
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
parseWorkbook();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.file, props.extension],
|
||||
() => {
|
||||
parseWorkbook();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@ -0,0 +1,36 @@
|
||||
<!-- HTML 文件预览 —— 对应源代码 docPreview/HtmlPreview.tsx -->
|
||||
<template>
|
||||
<div :class="styles.htmlPreviewContainer">
|
||||
<div :class="styles.htmlPreviewBody" v-html="html" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import DOMPurify from "dompurify";
|
||||
import styles from "../../style/doc.module.scss";
|
||||
|
||||
interface Props {
|
||||
file: ArrayBuffer;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const html = ref<string>("");
|
||||
|
||||
function sanitizeHtml() {
|
||||
const content = new TextDecoder().decode(props.file);
|
||||
html.value = DOMPurify.sanitize(content);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
sanitizeHtml();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.file,
|
||||
() => {
|
||||
sanitizeHtml();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@ -0,0 +1,33 @@
|
||||
<!-- PDF 文件预览 —— 基于 @file-viewer/vue3 渲染 -->
|
||||
<template>
|
||||
<div :class="styles.pdfPreviewContainer">
|
||||
<FileViewer
|
||||
v-if="file"
|
||||
:file="file"
|
||||
:filename="filename"
|
||||
:options="options"
|
||||
style="height: 100%"
|
||||
/>
|
||||
<ElSkeleton v-else :rows="8" animated style="padding: 24px" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import allPreset from "@file-viewer/preset-all";
|
||||
import { FileViewer } from "@file-viewer/vue3";
|
||||
import styles from "../../style/doc.module.scss";
|
||||
|
||||
interface Props {
|
||||
file: ArrayBuffer | null | undefined;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const options = {
|
||||
preset: allPreset,
|
||||
rendererMode: "replace" as const,
|
||||
theme: "light" as const,
|
||||
toolbar: { position: "bottom-right" as const },
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,114 @@
|
||||
// 文件图标默认样式表 —— 对应 react-file-icon 的 defaultStyles
|
||||
// 提供各类文件扩展名对应的颜色配置
|
||||
|
||||
export type FileIconStyle = {
|
||||
type?: string;
|
||||
color?: string;
|
||||
gradient?: string;
|
||||
labelColor?: string;
|
||||
labelTextColor?: string;
|
||||
};
|
||||
|
||||
export const defaultStyles: Record<string, FileIconStyle> = {
|
||||
// PDF
|
||||
pdf: { type: "pdf", color: "#D32F2F", labelColor: "#D32F2F", labelTextColor: "#fff" },
|
||||
|
||||
// Word
|
||||
doc: { type: "doc", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
docx: { type: "docx", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
docm: { type: "docm", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
dot: { type: "dot", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
dotx: { type: "dotx", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
dotm: { type: "dotm", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
pages: { type: "pages", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
|
||||
// PowerPoint
|
||||
ppt: { type: "ppt", color: "#EF6C00", labelColor: "#EF6C00", labelTextColor: "#fff" },
|
||||
pptx: { type: "pptx", color: "#EF6C00", labelColor: "#EF6C00", labelTextColor: "#fff" },
|
||||
pptm: { type: "pptm", color: "#EF6C00", labelColor: "#EF6C00", labelTextColor: "#fff" },
|
||||
pot: { type: "pot", color: "#EF6C00", labelColor: "#EF6C00", labelTextColor: "#fff" },
|
||||
potx: { type: "potx", color: "#EF6C00", labelColor: "#EF6C00", labelTextColor: "#fff" },
|
||||
potm: { type: "potm", color: "#EF6C00", labelColor: "#EF6C00", labelTextColor: "#fff" },
|
||||
key: { type: "key", color: "#EF6C00", labelColor: "#EF6C00", labelTextColor: "#fff" },
|
||||
|
||||
// Excel
|
||||
xls: { type: "xls", color: "#2E7D32", labelColor: "#2E7D32", labelTextColor: "#fff" },
|
||||
xlsx: { type: "xlsx", color: "#2E7D32", labelColor: "#2E7D32", labelTextColor: "#fff" },
|
||||
xlsm: { type: "xlsm", color: "#2E7D32", labelColor: "#2E7D32", labelTextColor: "#fff" },
|
||||
xlt: { type: "xlt", color: "#2E7D32", labelColor: "#2E7D32", labelTextColor: "#fff" },
|
||||
xltx: { type: "xltx", color: "#2E7D32", labelColor: "#2E7D32", labelTextColor: "#fff" },
|
||||
xltm: { type: "xltm", color: "#2E7D32", labelColor: "#2E7D32", labelTextColor: "#fff" },
|
||||
numbers: { type: "numbers", color: "#2E7D32", labelColor: "#2E7D32", labelTextColor: "#fff" },
|
||||
|
||||
// 压缩
|
||||
zip: { type: "zip", color: "#F9A825", labelColor: "#F9A825", labelTextColor: "#fff" },
|
||||
rar: { type: "rar", color: "#F9A825", labelColor: "#F9A825", labelTextColor: "#fff" },
|
||||
"7z": { type: "7z", color: "#F9A825", labelColor: "#F9A825", labelTextColor: "#fff" },
|
||||
gz: { type: "gz", color: "#F9A825", labelColor: "#F9A825", labelTextColor: "#fff" },
|
||||
tar: { type: "tar", color: "#F9A825", labelColor: "#F9A825", labelTextColor: "#fff" },
|
||||
|
||||
// 代码
|
||||
js: { type: "js", color: "#FBC02D", labelColor: "#FBC02D", labelTextColor: "#fff" },
|
||||
jsx: { type: "jsx", color: "#FBC02D", labelColor: "#FBC02D", labelTextColor: "#fff" },
|
||||
ts: { type: "ts", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
tsx: { type: "tsx", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
json: { type: "json", color: "#F9A825", labelColor: "#F9A825", labelTextColor: "#fff" },
|
||||
html: { type: "html", color: "#E53935", labelColor: "#E53935", labelTextColor: "#fff" },
|
||||
css: { type: "css", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
scss: { type: "scss", color: "#C2185B", labelColor: "#C2185B", labelTextColor: "#fff" },
|
||||
less: { type: "less", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
vue: { type: "vue", color: "#41B883", labelColor: "#41B883", labelTextColor: "#fff" },
|
||||
py: { type: "py", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
java: { type: "java", color: "#E53935", labelColor: "#E53935", labelTextColor: "#fff" },
|
||||
c: { type: "c", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
cpp: { type: "cpp", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
h: { type: "h", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
cs: { type: "cs", color: "#6A1B9A", labelColor: "#6A1B9A", labelTextColor: "#fff" },
|
||||
go: { type: "go", color: "#00ACC1", labelColor: "#00ACC1", labelTextColor: "#fff" },
|
||||
rs: { type: "rs", color: "#E53935", labelColor: "#E53935", labelTextColor: "#fff" },
|
||||
rb: { type: "rb", color: "#C2185B", labelColor: "#C2185B", labelTextColor: "#fff" },
|
||||
php: { type: "php", color: "#6A1B9A", labelColor: "#6A1B9A", labelTextColor: "#fff" },
|
||||
sh: { type: "sh", color: "#37474F", labelColor: "#37474F", labelTextColor: "#fff" },
|
||||
|
||||
// 文本/文档
|
||||
txt: { type: "txt", color: "#9E9E9E", labelColor: "#9E9E9E", labelTextColor: "#fff" },
|
||||
md: { type: "md", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
markdown: { type: "markdown", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
rtf: { type: "rtf", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
csv: { type: "csv", color: "#2E7D32", labelColor: "#2E7D32", labelTextColor: "#fff" },
|
||||
log: { type: "log", color: "#9E9E9E", labelColor: "#9E9E9E", labelTextColor: "#fff" },
|
||||
|
||||
// 图片
|
||||
png: { type: "png", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
jpg: { type: "jpg", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
jpeg: { type: "jpeg", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
gif: { type: "gif", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
bmp: { type: "bmp", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
svg: { type: "svg", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
webp: { type: "webp", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
ico: { type: "ico", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
tiff: { type: "tiff", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
|
||||
// 音视频
|
||||
mp3: { type: "mp3", color: "#E91E63", labelColor: "#E91E63", labelTextColor: "#fff" },
|
||||
wav: { type: "wav", color: "#E91E63", labelColor: "#E91E63", labelTextColor: "#fff" },
|
||||
flac: { type: "flac", color: "#E91E63", labelColor: "#E91E63", labelTextColor: "#fff" },
|
||||
mp4: { type: "mp4", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
avi: { type: "avi", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
mov: { type: "mov", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
wmv: { type: "wmv", color: "#7B1FA2", labelColor: "#7B1FA2", labelTextColor: "#fff" },
|
||||
|
||||
// 其他
|
||||
xml: { type: "xml", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
yaml: { type: "yaml", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
yml: { type: "yml", color: "#1565C0", labelColor: "#1565C0", labelTextColor: "#fff" },
|
||||
sql: { type: "sql", color: "#6A1B9A", labelColor: "#6A1B9A", labelTextColor: "#fff" },
|
||||
db: { type: "db", color: "#6A1B9A", labelColor: "#6A1B9A", labelTextColor: "#fff" },
|
||||
file: { type: "file", color: "#9E9E9E", labelColor: "#9E9E9E", labelTextColor: "#fff" },
|
||||
};
|
||||
|
||||
// 获取扩展名对应的图标样式,找不到时使用 txt 作为默认
|
||||
export function getFileIconStyle(ext: string): FileIconStyle {
|
||||
const key = String(ext || "").toLowerCase();
|
||||
return defaultStyles[key] ?? defaultStyles.txt ?? { color: "#9E9E9E", labelColor: "#9E9E9E", labelTextColor: "#fff" };
|
||||
}
|
||||
@ -0,0 +1,651 @@
|
||||
<!-- 知识库列表页 —— 对应源代码 aiKnowledge/index.tsx -->
|
||||
<template>
|
||||
<div class="knowledge-page">
|
||||
<div :class="styles.toolbar">
|
||||
<ElInput
|
||||
v-model="keyword"
|
||||
clearable
|
||||
placeholder="请输入知识库名称"
|
||||
class="search-input"
|
||||
@clear="() => { keyword = ''; fetchPageList(1, pagination.limit, ''); }"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
|
||||
<ElButton :icon="Refresh" :class="styles.syncIcon" @click="() => fetchPageList()" />
|
||||
</div>
|
||||
|
||||
<div v-loading="pageListLoading" class="list-wrapper">
|
||||
<div v-if="safePageList.length > 0" :class="styles.container">
|
||||
<div :class="styles.grid">
|
||||
<div v-if="knowledgeBaseIndexAfterRequest === 1">
|
||||
<button type="button" :class="styles.createCard" @click="goCreate">
|
||||
<span :class="styles.createIcon">
|
||||
<ElIcon><Plus /></ElIcon>
|
||||
</span>
|
||||
<span :class="styles.createTitle">创建知识库</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="item in safePageList"
|
||||
:key="item.id"
|
||||
>
|
||||
<ElCard
|
||||
shadow="hover"
|
||||
:class="styles.card"
|
||||
class="knowledge-card"
|
||||
@click="goDocuments(item.id)"
|
||||
>
|
||||
<ElDropdown
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
@command="(cmd: string) => handleCardCommand(cmd, item)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
:class="styles.cardMore"
|
||||
:disabled="deleteSubmittingId === item.id"
|
||||
@click.stop
|
||||
>
|
||||
<ElIcon><MoreFilled /></ElIcon>
|
||||
</button>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem command="edit">
|
||||
<ElIcon><Edit /></ElIcon>
|
||||
编辑
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem command="delete" divided>
|
||||
<ElIcon><Delete /></ElIcon>
|
||||
删除
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
|
||||
<div :class="styles.cardHeader">
|
||||
<div :class="styles.iconWrapper" :style="{ background: getBgColor(item) }">
|
||||
📚
|
||||
<span :class="styles.iconBadge">
|
||||
<ElIcon><Reading /></ElIcon>
|
||||
</span>
|
||||
</div>
|
||||
<div :class="styles.cardTitle">
|
||||
<span :class="styles.cardName" :title="item.name">{{ item.name }}</span>
|
||||
<div :class="styles.cardMeta">
|
||||
{{ item.authorName }}
|
||||
<span v-if="item.updatedAt > 0"> · 编辑于 {{ timeAgo(item.updatedAt) }}</span>
|
||||
</div>
|
||||
<div :class="styles.badgeRow">
|
||||
<span v-if="getPermissionLabel(item.permission)" :class="styles.permissionBadge">
|
||||
{{ getPermissionLabel(item.permission) }}
|
||||
</span>
|
||||
<span v-if="getTechniqueLabel(item.indexingTechnique)" :class="styles.techniqueBadge">
|
||||
{{ getTechniqueLabel(item.indexingTechnique) }}
|
||||
<span v-if="getSearchLabel(item.searchMethod)"> · {{ getSearchLabel(item.searchMethod) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="styles.description">
|
||||
<span v-if="item.description" :title="item.description">{{ item.description }}</span>
|
||||
<span v-else :class="styles.descriptionEmpty">暂无描述</span>
|
||||
</div>
|
||||
|
||||
<div :class="styles.cardFooter">
|
||||
<ElIcon><Reading /></ElIcon>
|
||||
<span>{{ item.documentCount }}</span>
|
||||
<ElIcon :class="styles.footerAppCount"><Monitor /></ElIcon>
|
||||
<span>{{ item.appCount }}</span>
|
||||
<template v-if="item.updatedAt > 0">
|
||||
<span :class="styles.footerDivider">/</span>
|
||||
<span>更新于 {{ timeAgo(item.updatedAt) }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="styles.pagination">
|
||||
<ElPagination
|
||||
:current-page="pagination.page"
|
||||
:total="total"
|
||||
:page-size="pagination.limit"
|
||||
:page-sizes="[12, 24, 32, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="(page: number) => fetchPageList(page, pagination.limit)"
|
||||
@size-change="(size: number) => fetchPageList(1, size)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ElEmpty v-else description="暂无知识库数据" />
|
||||
</div>
|
||||
|
||||
<ElDialog
|
||||
v-model="editModalOpen"
|
||||
title="编辑知识库"
|
||||
width="520px"
|
||||
destroy-on-close
|
||||
>
|
||||
<ElForm ref="editFormRef" :model="editForm" label-position="top">
|
||||
<ElFormItem label="名称" prop="name" :rules="nameRules">
|
||||
<ElInput v-model="editForm.name" placeholder="请输入知识库名称" :maxlength="50" show-word-limit />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="description" :rules="descRules">
|
||||
<ElInput
|
||||
v-model="editForm.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入知识库描述"
|
||||
:maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="closeEditModal">取消</ElButton>
|
||||
<ElButton type="primary" :loading="editSubmitting" @click="submitEdit">保存</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: "AiKnowledge" });
|
||||
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import {
|
||||
Plus,
|
||||
MoreFilled,
|
||||
Edit,
|
||||
Delete,
|
||||
Reading,
|
||||
Monitor,
|
||||
Refresh,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ElMessage, ElMessageBox, type FormInstance } from "element-plus";
|
||||
import { useDifyGet, useDifyPost, useRequest } from "./useDifyApis";
|
||||
import { knowledgeBase } from "./api";
|
||||
import { encryptPassword, getCookie } from "./types";
|
||||
import styles from "./style/index.module.scss";
|
||||
|
||||
const PAGE_SIZE = 12;
|
||||
|
||||
type KnowledgeCardItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
authorName: string;
|
||||
permission: string;
|
||||
indexingTechnique: string;
|
||||
searchMethod: string;
|
||||
documentCount: number;
|
||||
appCount: number;
|
||||
updatedAt: number;
|
||||
tags: Array<string | number>;
|
||||
iconBackground: string | null;
|
||||
};
|
||||
|
||||
const INDEXING_TECHNIQUE_LABEL: Record<string, string> = {
|
||||
high_quality: "高质量",
|
||||
economy: "经济",
|
||||
};
|
||||
|
||||
const SEARCH_METHOD_LABEL: Record<string, string> = {
|
||||
semantic_search: "语义检索",
|
||||
keyword_search: "关键词检索",
|
||||
full_text_search: "全文检索",
|
||||
hybrid_search: "混合检索",
|
||||
};
|
||||
|
||||
const PERMISSION_LABEL: Record<string, string> = {
|
||||
only_me: "仅自己",
|
||||
all_team_members: "所有成员",
|
||||
partial_members: "部分成员",
|
||||
};
|
||||
|
||||
const ICON_BG_COLORS = [
|
||||
"#e8f4ff",
|
||||
"#fff3e0",
|
||||
"#e8f5e9",
|
||||
"#fce4ec",
|
||||
"#ede7f6",
|
||||
"#e0f2f1",
|
||||
"#fff9c4",
|
||||
"#fbe9e7",
|
||||
];
|
||||
|
||||
function timeAgo(ts: number): string {
|
||||
if (!ts) return "-";
|
||||
const diff = Math.floor(Date.now() / 1000 - ts);
|
||||
if (diff < 60) return "刚刚";
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)} 小时前`;
|
||||
return `${Math.floor(diff / 86400)} 天前`;
|
||||
}
|
||||
|
||||
function hashColor(id: string): string {
|
||||
let h = 0;
|
||||
for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) & 0xffff;
|
||||
return ICON_BG_COLORS[h % ICON_BG_COLORS.length] ?? "#e8f4ff";
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const pagination = ref({ page: 1, limit: PAGE_SIZE });
|
||||
const keyword = ref("");
|
||||
const knowledgeBaseIndexAfterRequest = ref(1);
|
||||
const editModalOpen = ref(false);
|
||||
const editingItem = ref<KnowledgeCardItem | null>(null);
|
||||
const editSubmitting = ref(false);
|
||||
const deleteSubmittingId = ref<string | null>(null);
|
||||
const editForm = reactive({ name: "", description: "" });
|
||||
const editFormRef = ref<FormInstance | null>(null);
|
||||
|
||||
const nameRules = [
|
||||
{ required: true, message: "请输入知识库名称", trigger: "blur" },
|
||||
{ max: 50, message: "最多 50 个字符", trigger: "blur" },
|
||||
];
|
||||
const descRules = [{ max: 200, message: "最多 200 个字符", trigger: "blur" }];
|
||||
|
||||
const { doRequest } = useRequest();
|
||||
|
||||
// 登录
|
||||
const { doPost: loginDo } = useDifyPost({
|
||||
url: knowledgeBase.login,
|
||||
data: {
|
||||
email: "gdut@gdut.com",
|
||||
password: encryptPassword("gdut1234"),
|
||||
language: "zh-Hans",
|
||||
remember_me: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 列表
|
||||
const { doGet: pageList, loading: pageListLoading, result: pageListResult } = useDifyGet({
|
||||
url: knowledgeBase.pageList,
|
||||
params: { limit: PAGE_SIZE, page: 1 },
|
||||
});
|
||||
|
||||
const safePageList = computed<KnowledgeCardItem[]>(() => {
|
||||
const rawList = Array.isArray((pageListResult.value as any)?.data)
|
||||
? (pageListResult.value as any).data
|
||||
: [];
|
||||
|
||||
if ((pageListResult.value as any)?.page) {
|
||||
knowledgeBaseIndexAfterRequest.value =
|
||||
(pageListResult.value as any).page || pagination.value.page || 1;
|
||||
}
|
||||
|
||||
return rawList
|
||||
.filter((item: any) => item && typeof item === "object" && item.id && item.name)
|
||||
.map((item: any) => ({
|
||||
id: String(item.id),
|
||||
name: String(item.name),
|
||||
description:
|
||||
typeof item.description === "string" && item.description.trim()
|
||||
? item.description
|
||||
: "",
|
||||
authorName:
|
||||
typeof item.author_name === "string" && item.author_name.trim()
|
||||
? item.author_name
|
||||
: "-",
|
||||
permission: typeof item.permission === "string" ? item.permission : "",
|
||||
indexingTechnique:
|
||||
typeof item.indexing_technique === "string" ? item.indexing_technique : "",
|
||||
searchMethod:
|
||||
typeof item.retrieval_model_dict?.search_method === "string"
|
||||
? item.retrieval_model_dict.search_method
|
||||
: "",
|
||||
documentCount: Number.isFinite(Number(item.document_count))
|
||||
? Number(item.document_count)
|
||||
: 0,
|
||||
appCount: Number.isFinite(Number(item.app_count)) ? Number(item.app_count) : 0,
|
||||
updatedAt: Number.isFinite(Number(item.updated_at)) ? Number(item.updated_at) : 0,
|
||||
tags: Array.isArray(item.tags) ? item.tags.filter(Boolean) : [],
|
||||
iconBackground: item.icon_info?.icon_background ?? null,
|
||||
}));
|
||||
});
|
||||
|
||||
const total = computed(() => Number((pageListResult.value as any)?.total) || 0);
|
||||
|
||||
async function fetchPageList(
|
||||
page = pagination.value.page,
|
||||
limit = pagination.value.limit,
|
||||
searchKeyword = keyword.value
|
||||
) {
|
||||
pagination.value = { page, limit };
|
||||
const trimmedKeyword = searchKeyword.trim();
|
||||
await pageList({
|
||||
params: {
|
||||
page,
|
||||
limit,
|
||||
...(trimmedKeyword ? { keyword: trimmedKeyword } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
fetchPageList(1, pagination.value.limit);
|
||||
}
|
||||
|
||||
function goCreate() {
|
||||
router.push("/platform/ai/knowledge/createKnowledge");
|
||||
}
|
||||
|
||||
function goDocuments(id: string) {
|
||||
router.push(`/platform/ai/knowledge/documents/${id}`);
|
||||
}
|
||||
|
||||
function getBgColor(item: KnowledgeCardItem): string {
|
||||
return item.iconBackground ?? hashColor(item.id);
|
||||
}
|
||||
|
||||
function getTechniqueLabel(value: string): string {
|
||||
return INDEXING_TECHNIQUE_LABEL[value] ?? value;
|
||||
}
|
||||
|
||||
function getSearchLabel(value: string): string {
|
||||
return SEARCH_METHOD_LABEL[value] ?? value;
|
||||
}
|
||||
|
||||
function getPermissionLabel(value: string): string {
|
||||
return PERMISSION_LABEL[value] ?? value;
|
||||
}
|
||||
|
||||
function openEditModal(item: KnowledgeCardItem) {
|
||||
editingItem.value = item;
|
||||
editForm.name = item.name;
|
||||
editForm.description = item.description;
|
||||
editModalOpen.value = true;
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
editModalOpen.value = false;
|
||||
editingItem.value = null;
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
if (!editingItem.value) return;
|
||||
try {
|
||||
await editFormRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
name: String(editForm.name ?? "").trim(),
|
||||
description: String(editForm.description ?? "").trim(),
|
||||
};
|
||||
editSubmitting.value = true;
|
||||
try {
|
||||
await doRequest({
|
||||
url: knowledgeBase.queryKnowledgeRule(editingItem.value.id),
|
||||
method: "patch",
|
||||
data: payload,
|
||||
});
|
||||
ElMessage.success("编辑成功");
|
||||
closeEditModal();
|
||||
await fetchPageList(pagination.value.page, pagination.value.limit);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || "编辑失败");
|
||||
} finally {
|
||||
editSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(item: KnowledgeCardItem) {
|
||||
ElMessageBox.confirm("此操作不可逆,请谨慎操作。", "确定要删除该知识库吗?", {
|
||||
type: "warning",
|
||||
confirmButtonText: "删除",
|
||||
cancelButtonText: "取消",
|
||||
confirmButtonClass: "el-button--danger",
|
||||
})
|
||||
.then(async () => {
|
||||
deleteSubmittingId.value = item.id;
|
||||
try {
|
||||
await doRequest({
|
||||
url: knowledgeBase.queryKnowledgeRule(item.id),
|
||||
method: "delete",
|
||||
});
|
||||
ElMessage.success("删除成功");
|
||||
await fetchPageList(1, pagination.value.limit);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
} finally {
|
||||
deleteSubmittingId.value = null;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function handleCardCommand(cmd: string, item: KnowledgeCardItem) {
|
||||
if (cmd === "edit") openEditModal(item);
|
||||
if (cmd === "delete") confirmDelete(item);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const DIFY_TIMEOUT_KEY = "currentDifyTimeOut";
|
||||
const EXPIRE_TIME = 60 * 60 * 1000;
|
||||
|
||||
const refreshExpireTime = () => {
|
||||
localStorage.setItem(DIFY_TIMEOUT_KEY, String(Date.now() + EXPIRE_TIME));
|
||||
};
|
||||
|
||||
const loginAndLoad = async (page = 1, limit = PAGE_SIZE) => {
|
||||
const res = await loginDo();
|
||||
if ((res as any).result === "success") {
|
||||
refreshExpireTime();
|
||||
await fetchPageList(page, limit);
|
||||
}
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
const csrfToken = getCookie("csrf_token");
|
||||
const timeout = Number(localStorage.getItem(DIFY_TIMEOUT_KEY));
|
||||
const needLogin =
|
||||
!csrfToken || !timeout || Number.isNaN(timeout) || Date.now() >= timeout;
|
||||
try {
|
||||
if (needLogin) {
|
||||
await loginAndLoad();
|
||||
return;
|
||||
}
|
||||
await fetchPageList(1, PAGE_SIZE);
|
||||
} catch {
|
||||
try {
|
||||
await loginAndLoad();
|
||||
} catch (loginError) {
|
||||
console.error("Dify登录失败", loginError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.search-input {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
min-height: 200px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.knowledge-card {
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
height: 100%;
|
||||
border: 1px solid #e5e7eb;
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
transition: all 0.2s;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-card:hover {
|
||||
:deep(.cardMore) {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.cardMore {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(229, 231, 235, 0.9);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 18px;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.iconBadge {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #3b82f6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 9px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cardName {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.cardMeta {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.badgeRow {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.permissionBadge {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
|
||||
.techniqueBadge {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.descriptionEmpty {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.cardFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.footerAppCount {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.footerDivider {
|
||||
color: #d1d5db;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,676 @@
|
||||
.page {
|
||||
height: calc(100vh - 100px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px 18px 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.formBody {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.steps {
|
||||
max-width: 430px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 30px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.titleBlock {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin: 0 0 6px;
|
||||
color: #1f2937;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sectionDesc {
|
||||
margin: 0;
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.fieldCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 12px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 12px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.iconBox {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
flex: 0 0 58px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.04);
|
||||
color: #4096ff;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.iconBoxOrange {
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.fullFormItem {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.fullFormItem :deep(.el-form-item__label) {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.uploadBlock {
|
||||
// margin-top: 28px;
|
||||
}
|
||||
|
||||
.uploadTitle {
|
||||
margin: 0 0 14px;
|
||||
color: #344054;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.uploadBlock :deep(.el-upload),
|
||||
.uploadBlock :deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.uploadBlock :deep(.el-upload-dragger) {
|
||||
min-height: 128px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px dashed #d9dfe8;
|
||||
border-radius: 10px;
|
||||
background: #fbfcfe;
|
||||
}
|
||||
|
||||
.uploadBlock :deep(.el-upload-dragger:hover) {
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.uploadMain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: #344054;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.uploadIcon {
|
||||
color: #344054;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.uploadLink {
|
||||
color: #155eef;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.uploadDesc {
|
||||
color: #667085;
|
||||
line-height: 1.55;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.segmentContent {
|
||||
max-width: 1220px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.segmentTitle {
|
||||
margin: 0 0 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.segmentLayout {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 18px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.segmentLeft {
|
||||
flex: 0 0 520px;
|
||||
max-width: 520px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.segmentRight {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.previewHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.previewTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
color: #1f2937;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.previewFileName {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.previewFileIcon {
|
||||
width: 18px;
|
||||
height: 22px;
|
||||
flex: 0 0 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.previewFileIcon :deep(svg) {
|
||||
width: 18px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.previewBody {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: block;
|
||||
padding: 18px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.previewEmptyWrapper {
|
||||
height: 100%;
|
||||
min-height: 320px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.previewEmpty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.previewEmptyIcon {
|
||||
font-size: 34px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.previewEmptyText {
|
||||
color: #9ca3af;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.previewChunkList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.previewChunkHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.previewChunkIcon {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.previewChunkMeta {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.previewChunkContent {
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.settingCard {
|
||||
padding: 20px 22px 24px;
|
||||
border: 2px solid #4096ff;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.settingHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.settingIcon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
background: #4096ff;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settingName {
|
||||
color: #1f2937;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.settingDesc {
|
||||
margin-top: 14px;
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.formRow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.compactFormItem {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 20px 0;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.ruleTitle {
|
||||
margin-bottom: 10px;
|
||||
color: #374151;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.urlCheckbox {
|
||||
margin-left: 28px;
|
||||
}
|
||||
|
||||
.segmentActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.resetBtn {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.embeddingItem {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.embeddingSelect {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.embeddingGroupLabel {
|
||||
color: #6b7280;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.embeddingOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.embeddingOptionIcon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex: 0 0 22px;
|
||||
border-radius: 6px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.embeddingOptionLabel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.retrievalSection {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.retrievalForm {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.retrievalRerankRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.retrievalLabel {
|
||||
color: #374151;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.retrievalModelSelect {
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.retrievalRow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.retrievalControl {
|
||||
padding: 12px 12px 10px;
|
||||
border-radius: 10px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.retrievalControlHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
color: #374151;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.retrievalControlBody {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.retrievalNumberInput {
|
||||
width: 72px;
|
||||
flex: 0 0 72px;
|
||||
}
|
||||
|
||||
.retrievalSlider {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.filePreviewSection {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.filePreviewTitle {
|
||||
margin-bottom: 8px;
|
||||
color: #374151;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filePreviewEmpty {
|
||||
padding: 14px 12px;
|
||||
border: 1px dashed #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #9ca3af;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filePreviewList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.verificationBlock {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.verificationBlockTitle {
|
||||
margin-bottom: 10px;
|
||||
color: #1f2937;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.filePreviewItemStatic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.fileStatusIcon {
|
||||
width: 18px;
|
||||
flex: 0 0 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.fileStatusIconDone {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.fileStatusIconPending {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.verificationGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px 22px;
|
||||
padding: 12px 12px;
|
||||
border-radius: 10px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.verificationItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.verificationLabel {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.verificationValue {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: right;
|
||||
color: #111827;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filePreviewItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease,
|
||||
background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.filePreviewItem:hover {
|
||||
border-color: #4096ff;
|
||||
background: #f5faff;
|
||||
}
|
||||
|
||||
.filePreviewItemSelected {
|
||||
border: 2px solid #4096ff;
|
||||
background: #f5faff;
|
||||
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.filePreviewItemSelected:hover {
|
||||
border-color: #4096ff;
|
||||
}
|
||||
|
||||
.filePreviewIcon {
|
||||
width: 22px;
|
||||
height: 28px;
|
||||
flex: 0 0 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.filePreviewIcon :deep(svg) {
|
||||
width: 22px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.filePreviewMeta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filePreviewName {
|
||||
color: #111827;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.filePreviewSub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.fileIndexingProgress {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.filePreviewSize {
|
||||
color: #9ca3af;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.emptyStep {
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 1060px) {
|
||||
.segmentLayout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.segmentLeft {
|
||||
flex: 1;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.retrievalRow {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,370 @@
|
||||
// 文档预览页样式 —— 对应源代码 style/doc.module.less
|
||||
|
||||
.docPreviewCard {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #edf0f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.docPreviewHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
background: #fafbfc;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.docPreviewHeaderTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.docPreviewFileName {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.docPreviewFileExt {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.docPreviewBody {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.docPreviewLoading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 80px 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.docPreviewLoadingTip {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.docPreviewError {
|
||||
color: #ef4444;
|
||||
font-size: 13px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.docPreviewBackTop {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
// ============ docx 预览 ============
|
||||
.docxPreviewContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background: #f5f7fa;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.docxPreviewBody {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
padding: 48px 64px;
|
||||
min-height: 100%;
|
||||
|
||||
:deep(.docx-wrapper) {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
|
||||
.docx {
|
||||
box-shadow: none;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ PDF 预览 ============
|
||||
.pdfPreviewContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #525659;
|
||||
// padding: 16px;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Excel 预览 ============
|
||||
.excelPreviewContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-tabs) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.el-tab-pane) {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.excelSheet {
|
||||
padding: 12px 16px;
|
||||
overflow: auto;
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid #e4e7ed;
|
||||
padding: 4px 8px;
|
||||
white-space: nowrap;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 文本 / Markdown 预览 ============
|
||||
.textPreviewContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background: #f5f7fa;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.textPreviewBody {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
padding: 32px 48px;
|
||||
min-height: 100%;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
}
|
||||
|
||||
.markdownPreviewBody {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
padding: 32px 48px;
|
||||
min-height: 100%;
|
||||
|
||||
// Markdown 渲染样式
|
||||
:deep(h1) {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 24px 0 16px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
:deep(h2) {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin: 20px 0 14px;
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
:deep(h3) {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
margin: 18px 0 12px;
|
||||
color: #3a3a3a;
|
||||
}
|
||||
|
||||
:deep(h4) {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin: 14px 0 10px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
:deep(h5) {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 12px 0 8px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
:deep(h6) {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin: 10px 0 8px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
margin: 0 0 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
:deep(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(th) {
|
||||
border: 1px solid #d0d5dd;
|
||||
padding: 8px 12px;
|
||||
background: #f9fafb;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(td) {
|
||||
border: 1px solid #e4e7ed;
|
||||
padding: 8px 12px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
:deep(ul),
|
||||
:deep(ol) {
|
||||
margin: 0 0 12px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
:deep(li) {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
:deep(blockquote) {
|
||||
margin: 12px 0;
|
||||
padding: 8px 16px;
|
||||
border-left: 4px solid #d1d5db;
|
||||
background: #f9fafb;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
:deep(code) {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
}
|
||||
|
||||
:deep(:not(pre) > code) {
|
||||
background: #f3f4f6;
|
||||
color: #dc2626;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: #1677ff;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(hr) {
|
||||
border: none;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ HTML 预览 ============
|
||||
.htmlPreviewContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background: #f5f7fa;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.htmlPreviewBody {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
padding: 32px 48px;
|
||||
min-height: 100%;
|
||||
}
|
||||
@ -0,0 +1,196 @@
|
||||
.page {
|
||||
min-height: calc(100vh - 90px);
|
||||
padding: 22px 20px 14px;
|
||||
background: #f7f9fc;
|
||||
color: #1f2937;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 8px !important;
|
||||
color: #111827 !important;
|
||||
font-size: 18px !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.learnMore {
|
||||
height: auto;
|
||||
padding: 0 0 0 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.toolbarLeft,
|
||||
.toolbarRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.toolbarLeft {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.typeSelect {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.sortButton {
|
||||
min-width: 144px;
|
||||
color: #5f6b7a;
|
||||
}
|
||||
|
||||
.tableCard {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #edf0f5;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.03);
|
||||
|
||||
:deep(.el-table-wrapper),
|
||||
:deep(.el-scrollbar),
|
||||
:deep(.el-table__inner-wrapper),
|
||||
:deep(.el-table),
|
||||
:deep(.el-table__body-wrapper) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
color: #334155;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper th) {
|
||||
height: 42px;
|
||||
padding: 8px 12px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
color: #69758a;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.el-table__body-wrapper td) {
|
||||
height: 38px;
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid #f0f3f8;
|
||||
color: #40506a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:hover > td) {
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
:deep(.el-table-column--selection) {
|
||||
width: 42px;
|
||||
min-width: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
.nameCell {
|
||||
max-width: 100%;
|
||||
color: #243247;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nameText {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
}
|
||||
|
||||
.fileIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
background: #2f80ed;
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.modeTag {
|
||||
margin: 0;
|
||||
height: 22px;
|
||||
line-height: 20px;
|
||||
border-color: #d8dee8;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
color: #59677e;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.statusAvailable {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: #16a34a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #35d27f;
|
||||
}
|
||||
|
||||
.statusAvailableDisabled {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.statusDotDisabled {
|
||||
background: #999999;
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
color: #4b5b73;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
|
||||
&:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-top: 14px;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
@ -0,0 +1,225 @@
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
grid-auto-rows: 202px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 25px 20px 0 25px;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
.createCard {
|
||||
width: 100%;
|
||||
min-height: 202px;
|
||||
height: 100%;
|
||||
border: 2px dashed #d8dee9;
|
||||
border-radius: 20px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 24px rgba(31, 41, 55, 0.04);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
color: #7f89a6;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #9aa6c1;
|
||||
color: #5f6b89;
|
||||
}
|
||||
}
|
||||
|
||||
.createIcon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 24px;
|
||||
background: #fafafa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 42px;
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.createTitle {
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 12px;
|
||||
height: 100%;
|
||||
border: 1px solid #e5e7eb;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.cardMore {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cardMore {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(229, 231, 235, 0.9);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 18px;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.iconBadge {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #3b82f6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 9px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cardName {
|
||||
font-size: 15px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cardMeta {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.badgeRow {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.permissionBadge {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
|
||||
.techniqueBadge {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.descriptionEmpty {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.cardFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.footerAppCount {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.footerDivider {
|
||||
color: #d1d5db;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
Loading…
Reference in New Issue