509 lines
24 KiB
Vue
509 lines
24 KiB
Vue
<script setup lang="ts">
|
||
import { ElMessage, ElMessageBox } from "element-plus";
|
||
import { onMounted, reactive, ref } from "vue";
|
||
|
||
import { api } from "../services/api";
|
||
import KnowledgeContentPreview from "./KnowledgeContentPreview.vue";
|
||
import TableRowActions from "./TableRowActions.vue";
|
||
import AdminPagination from "./AdminPagination.vue";
|
||
import type {
|
||
KnowledgeDetail,
|
||
KnowledgeBatchSyncResult,
|
||
KnowledgeItem,
|
||
KnowledgeSyncJob,
|
||
MigrationSummary,
|
||
} from "../types/api";
|
||
|
||
const emit = defineEmits<{ preview: [knowledgeId: number] }>();
|
||
|
||
const rows = ref<KnowledgeItem[]>([]);
|
||
const migration = ref<MigrationSummary | null>(null);
|
||
const loading = ref(false);
|
||
const operatingId = ref<number | null>(null);
|
||
const detailOpen = ref(false);
|
||
const detail = ref<KnowledgeDetail | null>(null);
|
||
const jobs = ref<KnowledgeSyncJob[]>([]);
|
||
const previewVersionId = ref<number | null>(null);
|
||
const activeTab = ref("overview");
|
||
const selectedRows = ref<KnowledgeItem[]>([]);
|
||
const editOpen = ref(false);
|
||
const editSaving = ref(false);
|
||
const batchOpen = ref(false);
|
||
const batchSaving = ref(false);
|
||
const batchSyncing = ref(false);
|
||
const batchSyncResultOpen = ref(false);
|
||
const batchSyncResult = ref<KnowledgeBatchSyncResult | null>(null);
|
||
const resolvingNode = ref(false);
|
||
const nodeResolved = ref(false);
|
||
const sourceTitle = ref("");
|
||
const filter = reactive({ keyword: "", type: "", open: "", source: "" });
|
||
const pager = reactive({ page: 1, pageSize: 20, total: 0 });
|
||
const form = reactive({
|
||
name: "",
|
||
feishuSpaceId: "",
|
||
feishuNodeId: "",
|
||
knowledgeType: "general",
|
||
status: 0,
|
||
remark: "",
|
||
});
|
||
const editForm = reactive({ id: 0, name: "", knowledgeType: "general" });
|
||
const batchForm = reactive({ knowledgeType: "" });
|
||
|
||
onMounted(load);
|
||
|
||
async function load(page = pager.page, pageSize = pager.pageSize) {
|
||
loading.value = true;
|
||
try {
|
||
const result = await api.knowledge({
|
||
keyword: filter.keyword, knowledgeType: filter.type, openStatus: filter.open,
|
||
sourceStatus: filter.source,
|
||
page, pageSize,
|
||
});
|
||
rows.value = result.items;
|
||
Object.assign(pager, { page: result.page, pageSize: result.pageSize, total: result.total });
|
||
if (!migration.value) migration.value = await api.migrationSummary();
|
||
} catch (error) {
|
||
ElMessage.error(errorMessage(error, "知识库列表加载失败"));
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function openEdit() {
|
||
if (!detail.value) return;
|
||
Object.assign(editForm, {
|
||
id: detail.value.knowledge.id,
|
||
name: detail.value.knowledge.name,
|
||
knowledgeType: detail.value.knowledge.knowledgeType,
|
||
});
|
||
editOpen.value = true;
|
||
}
|
||
|
||
async function saveMetadata() {
|
||
if (!editForm.name.trim()) {
|
||
ElMessage.warning("请输入后台知识库名称");
|
||
return;
|
||
}
|
||
editSaving.value = true;
|
||
try {
|
||
await api.updateKnowledgeMetadata(editForm.id, {
|
||
name: editForm.name.trim(), knowledgeType: editForm.knowledgeType,
|
||
});
|
||
ElMessage.success("知识库基本信息已更新");
|
||
editOpen.value = false;
|
||
await Promise.all([load(), refreshDetail(editForm.id)]);
|
||
} catch (error) {
|
||
ElMessage.error(errorMessage(error, "基本信息保存失败"));
|
||
} finally {
|
||
editSaving.value = false;
|
||
}
|
||
}
|
||
|
||
function openBatchEdit() {
|
||
if (!selectedRows.value.length) {
|
||
ElMessage.warning("请先勾选需要修改的知识库");
|
||
return;
|
||
}
|
||
Object.assign(batchForm, { knowledgeType: "" });
|
||
batchOpen.value = true;
|
||
}
|
||
|
||
async function saveBatchMetadata() {
|
||
if (!batchForm.knowledgeType) {
|
||
ElMessage.warning("请选择知识库类型");
|
||
return;
|
||
}
|
||
if (!await confirmAction(
|
||
`将修改已选择的 ${selectedRows.value.length} 个知识库。该操作不影响正式版本和开放状态。`,
|
||
"确认批量修改",
|
||
{ confirmButtonText: "确认修改", cancelButtonText: "取消", type: "warning" },
|
||
)) return;
|
||
batchSaving.value = true;
|
||
try {
|
||
const result = await api.batchUpdateKnowledgeMetadata({
|
||
knowledgeIds: selectedRows.value.map((item) => item.id),
|
||
knowledgeType: batchForm.knowledgeType || undefined,
|
||
});
|
||
ElMessage[result.failed ? "warning" : "success"](`批量修改完成:成功 ${result.success} 个,失败 ${result.failed} 个`);
|
||
batchOpen.value = false;
|
||
await load();
|
||
} catch (error) {
|
||
ElMessage.error(errorMessage(error, "批量修改失败"));
|
||
} finally {
|
||
batchSaving.value = false;
|
||
}
|
||
}
|
||
|
||
async function syncSelected() {
|
||
if (!selectedRows.value.length) {
|
||
ElMessage.warning("请先勾选需要同步的知识库");
|
||
return;
|
||
}
|
||
if (!await confirmAction(
|
||
`将依次同步已选择的 ${selectedRows.value.length} 个知识库。同步成功后最新内容立即生效,单个失败不会中断其他知识库。`,
|
||
"确认批量同步",
|
||
{ confirmButtonText: "开始同步", cancelButtonText: "取消", type: "warning" },
|
||
)) return;
|
||
batchSyncing.value = true;
|
||
try {
|
||
const result = await api.batchSyncKnowledge(selectedRows.value.map((item) => item.id));
|
||
batchSyncResult.value = result;
|
||
batchSyncResultOpen.value = true;
|
||
ElMessage[result.failed ? "warning" : "success"](
|
||
`批量同步完成:成功 ${result.success} 个,失败 ${result.failed} 个`,
|
||
);
|
||
await load();
|
||
} catch (error) {
|
||
ElMessage.error(errorMessage(error, "批量同步失败"));
|
||
} finally {
|
||
batchSyncing.value = false;
|
||
}
|
||
}
|
||
|
||
async function handleMore(command: string, item: KnowledgeItem) {
|
||
try {
|
||
if (command === "detail") return await openDetail(item);
|
||
if (command === "preview") return emit("preview", item.id);
|
||
if (command === "archive") return await archive(item);
|
||
if (command === "restore") return await restore(item);
|
||
if (command === "delete") return await remove(item);
|
||
} catch (error) {
|
||
if (error !== "cancel" && error !== "close") ElMessage.error(errorMessage(error, "操作失败"));
|
||
}
|
||
}
|
||
|
||
async function restore(item: KnowledgeItem) {
|
||
if (!await confirmAction("恢复后知识库仍保持关闭,需要单独确认开放。", "恢复知识库", {
|
||
confirmButtonText: "确认恢复", cancelButtonText: "取消", type: "warning",
|
||
})) return;
|
||
await api.setKnowledgeLifecycle(item.id, "active");
|
||
ElMessage.success("知识库已恢复,当前仍为关闭状态");
|
||
await load();
|
||
}
|
||
|
||
async function createKnowledge() {
|
||
if (!nodeResolved.value || !form.name || !form.feishuSpaceId || !form.feishuNodeId) {
|
||
ElMessage.warning("请先通过 Node ID 获取并确认知识库信息");
|
||
return;
|
||
}
|
||
try {
|
||
await api.createKnowledge({ ...form });
|
||
Object.assign(form, { name: "", feishuSpaceId: "", feishuNodeId: "", knowledgeType: "general", status: 0, remark: "" });
|
||
nodeResolved.value = false; sourceTitle.value = "";
|
||
ElMessage.success("知识库已新增,首次同步成功后将自动生效");
|
||
await load();
|
||
} catch (error) {
|
||
ElMessage.error(errorMessage(error, "知识库新增失败"));
|
||
}
|
||
}
|
||
|
||
async function resolveNode() {
|
||
const nodeId = form.feishuNodeId.trim();
|
||
if (!nodeId) return ElMessage.warning("请输入 Node ID");
|
||
resolvingNode.value = true;
|
||
try {
|
||
const info = await api.resolveKnowledgeNode(nodeId);
|
||
Object.assign(form, { feishuNodeId: info.nodeId, feishuSpaceId: info.spaceId, name: info.name, remark: info.remark || "" });
|
||
sourceTitle.value = info.sourceTitle; nodeResolved.value = true;
|
||
ElMessage.success("已获取飞书知识库信息");
|
||
} catch (error) {
|
||
nodeResolved.value = false; sourceTitle.value = "";
|
||
ElMessage.error(errorMessage(error, "Node ID 信息获取失败"));
|
||
} finally { resolvingNode.value = false; }
|
||
}
|
||
|
||
async function openDetail(item: KnowledgeItem) {
|
||
detailOpen.value = true;
|
||
detail.value = null;
|
||
activeTab.value = "overview";
|
||
previewVersionId.value = null;
|
||
try {
|
||
const refreshedDetail = await refreshDetail(item.id);
|
||
previewVersionId.value = refreshedDetail.currentVersion?.id ?? null;
|
||
} catch (error) {
|
||
detailOpen.value = false;
|
||
ElMessage.error(errorMessage(error, "知识库详情加载失败"));
|
||
}
|
||
}
|
||
|
||
async function refreshDetail(id: number) {
|
||
const [nextDetail, nextJobs] = await Promise.all([
|
||
api.knowledgeDetail(id), api.knowledgeSyncJobs(id),
|
||
]);
|
||
detail.value = nextDetail;
|
||
jobs.value = nextJobs;
|
||
return nextDetail;
|
||
}
|
||
|
||
async function sync(item: KnowledgeItem) {
|
||
operatingId.value = item.id;
|
||
try {
|
||
const result = await api.syncKnowledge(item.id);
|
||
const message = result.status === "unchanged"
|
||
? "飞书内容未变化,未生成新版本"
|
||
: result.status === "published"
|
||
? "同步完成,最新版本已自动生效"
|
||
: "同步完成";
|
||
ElMessage.success(message);
|
||
await load();
|
||
if (detailOpen.value) await refreshDetail(item.id);
|
||
} catch (error) {
|
||
ElMessage.error(errorMessage(error, "知识库同步失败"));
|
||
} finally {
|
||
operatingId.value = null;
|
||
}
|
||
}
|
||
|
||
async function runMigration() {
|
||
if (!await confirmAction(
|
||
"将依次重新读取全部飞书文章。同步成功后仅保留最新版,旧版本和旧内容会被清理。",
|
||
"开始知识迁移",
|
||
{ confirmButtonText: "开始迁移", cancelButtonText: "取消", type: "warning" },
|
||
)) return;
|
||
loading.value = true;
|
||
try {
|
||
const result = await api.runKnowledgeMigration();
|
||
ElMessage.success(`迁移处理完成:${result.success}/${result.total} 成功`);
|
||
await load();
|
||
} catch (error) {
|
||
ElMessage.error(errorMessage(error, "知识迁移失败"));
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
async function toggleOpen(item: KnowledgeItem): Promise<boolean> {
|
||
const next = item.status !== 1;
|
||
if (!await confirmAction(
|
||
next ? "开放后,该知识库将从用户下一条消息开始进入 Agent 动态知识目录。" : "关闭后,当前生成不受影响,从用户下一条消息开始无法检索该知识库。",
|
||
next ? "开放知识库" : "关闭知识库",
|
||
{ confirmButtonText: next ? "确认开放" : "确认关闭", cancelButtonText: "取消", type: "warning" },
|
||
)) return false;
|
||
operatingId.value = item.id;
|
||
try {
|
||
await api.setKnowledgeOpen(item.id, next);
|
||
ElMessage.success(next ? "知识库已开放" : "知识库已关闭");
|
||
if (detailOpen.value && detail.value?.knowledge.id === item.id) await refreshDetail(item.id);
|
||
return true;
|
||
} catch (error) {
|
||
ElMessage.error(errorMessage(error, next ? "知识库开放失败" : "知识库关闭失败"));
|
||
return false;
|
||
} finally {
|
||
operatingId.value = null;
|
||
}
|
||
}
|
||
|
||
async function archive(item: KnowledgeItem) {
|
||
if (!await confirmAction("归档后知识库会立即关闭,当前最新版和检索记录仍会保留。", "归档知识库", {
|
||
confirmButtonText: "确认归档", cancelButtonText: "取消", type: "warning",
|
||
})) return;
|
||
await api.setKnowledgeLifecycle(item.id, "archived");
|
||
ElMessage.success("知识库已归档");
|
||
await load();
|
||
}
|
||
|
||
async function remove(item: KnowledgeItem) {
|
||
if (!await confirmAction(
|
||
`将物理删除“${item.name}”及关联数据。该操作不可恢复,并可能影响历史检索记录定位。`,
|
||
"物理删除知识库",
|
||
{ confirmButtonText: "确认删除", cancelButtonText: "取消", type: "error" },
|
||
)) return;
|
||
await api.deleteKnowledge(item.id);
|
||
ElMessage.success("知识库已删除");
|
||
await load();
|
||
}
|
||
|
||
function statusType(status: string) {
|
||
if (["published", "passed", "normal", "completed"].includes(status)) return "success";
|
||
if (["failed", "blocked", "error", "rejected"].includes(status)) return "danger";
|
||
if (["pending_review", "processing", "pending"].includes(status)) return "warning";
|
||
return "info";
|
||
}
|
||
|
||
function typeLabel(value: string) {
|
||
return { course: "课程知识库", qa: "答疑知识库", general: "通用知识库" }[value] || value;
|
||
}
|
||
|
||
function errorMessage(error: unknown, fallback: string) {
|
||
return error instanceof Error ? error.message : fallback;
|
||
}
|
||
|
||
async function confirmAction(
|
||
message: string,
|
||
title: string,
|
||
options: Parameters<typeof ElMessageBox.confirm>[2],
|
||
) {
|
||
try {
|
||
await ElMessageBox.confirm(message, title, options);
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page-head inline">
|
||
<div><h2>知识库管理</h2><p>一篇飞书文章对应一个知识库;同步成功后自动生效,并且只保留最新版。</p></div>
|
||
<el-button type="primary" :loading="loading" @click="runMigration">迁移现有知识库</el-button>
|
||
</div>
|
||
|
||
<section class="migration-overview">
|
||
<div><span>知识库总数</span><strong>{{ migration?.total ?? 0 }}</strong></div>
|
||
<div><span>待迁移</span><strong>{{ migration?.counts.pending ?? 0 }}</strong></div>
|
||
<div><span>迁移完成</span><strong>{{ migration?.counts.completed ?? 0 }}</strong></div>
|
||
<div><span>失败</span><strong>{{ migration?.counts.failed ?? 0 }}</strong></div>
|
||
</section>
|
||
|
||
<section class="knowledge-work-panel">
|
||
<h3>通过 Node ID 快速新增</h3>
|
||
<div class="node-resolve-row">
|
||
<el-input v-model="form.feishuNodeId" placeholder="请输入飞书 Node ID" :disabled="resolvingNode" @input="nodeResolved = false" />
|
||
<el-button type="primary" :loading="resolvingNode" @click="resolveNode">获取知识库信息</el-button>
|
||
</div>
|
||
<el-alert v-if="nodeResolved" :title="`已获取:${sourceTitle}`" type="success" :closable="false" show-icon />
|
||
<div v-if="nodeResolved" class="knowledge-create-grid">
|
||
<el-input v-model="form.feishuSpaceId" placeholder="Space ID" disabled />
|
||
<el-input :model-value="sourceTitle" placeholder="飞书原标题" disabled />
|
||
<el-input v-model="form.name" placeholder="后台显示名称" />
|
||
<el-select v-model="form.knowledgeType">
|
||
<el-option label="课程知识库" value="course" /><el-option label="答疑知识库" value="qa" /><el-option label="通用知识库" value="general" />
|
||
</el-select>
|
||
<el-input v-model="form.remark" placeholder="备注(可修改)" />
|
||
<el-button type="primary" @click="createKnowledge">保存知识库</el-button>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="knowledge-filters">
|
||
<el-input v-model="filter.keyword" placeholder="搜索后台名称或飞书标题" clearable @change="load(1)" />
|
||
<el-select v-model="filter.type" placeholder="知识库类型" clearable @change="load(1)"><el-option label="课程" value="course" /><el-option label="答疑" value="qa" /><el-option label="通用" value="general" /></el-select>
|
||
<el-select v-model="filter.open" placeholder="开放状态" clearable @change="load(1)"><el-option label="已开放" value="1" /><el-option label="已关闭" value="0" /></el-select>
|
||
<el-select v-model="filter.source" placeholder="源状态" clearable @change="load(1)"><el-option label="正常" value="normal" /><el-option label="源异常" value="error" /></el-select>
|
||
</div>
|
||
|
||
<div class="knowledge-batch-bar">
|
||
<span>已选择 {{ selectedRows.length }} 个知识库</span>
|
||
<div class="knowledge-batch-actions">
|
||
<el-button type="primary" plain :disabled="selectedRows.length === 0 || batchSaving" :loading="batchSyncing" @click="syncSelected">批量同步</el-button>
|
||
<el-button :disabled="selectedRows.length === 0 || batchSyncing" @click="openBatchEdit">批量修改类型</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-table v-loading="loading" :data="rows" stripe @selection-change="selectedRows = $event">
|
||
<el-table-column type="selection" width="48" />
|
||
<el-table-column label="知识库" min-width="240"><template #default="{ row }"><strong>{{ row.name }}</strong><small class="table-subline">{{ row.sourceTitle || "尚未同步源标题" }}</small></template></el-table-column>
|
||
<el-table-column label="类型" width="140"><template #default="{ row }">{{ typeLabel(row.knowledgeType) }}</template></el-table-column>
|
||
<el-table-column label="当前版本" width="130"><template #default="{ row }"><span>{{ row.currentVersionId ? `#${row.currentVersionId}` : '尚未同步' }}</span></template></el-table-column>
|
||
<el-table-column label="状态" width="210"><template #default="{ row }"><div class="knowledge-status-cell"><el-switch v-model="row.status" :active-value="1" :inactive-value="0" active-text="开放" inactive-text="关闭" inline-prompt :loading="operatingId === row.id" :disabled="row.lifecycleStatus === 'archived'" :before-change="() => toggleOpen(row)" /><el-tag :type="statusType(row.sourceStatus)">{{ row.sourceStatus === 'normal' ? '源正常' : '源异常' }}</el-tag></div></template></el-table-column>
|
||
<el-table-column prop="lastSyncedAt" label="最近同步" width="180" />
|
||
<el-table-column label="操作" width="220" fixed="right"><template #default="{ row }"><TableRowActions><el-button link type="primary" @click="openDetail(row)">详情</el-button><el-button link type="primary" :disabled="row.lifecycleStatus === 'archived'" :loading="operatingId === row.id" @click="sync(row)">同步最新版</el-button><el-dropdown trigger="click" :teleported="true" @command="handleMore($event, row)"><el-button link type="primary">更多</el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="detail">查看详情</el-dropdown-item><el-dropdown-item command="preview">进入 Agent 预览</el-dropdown-item><el-dropdown-item v-if="row.lifecycleStatus === 'active'" command="archive" divided>归档</el-dropdown-item><el-dropdown-item v-else command="restore" divided>恢复</el-dropdown-item><el-dropdown-item command="delete" :disabled="row.lifecycleStatus !== 'archived'" divided>删除</el-dropdown-item></el-dropdown-menu></template></el-dropdown></TableRowActions></template></el-table-column>
|
||
</el-table>
|
||
<AdminPagination :page="pager.page" :page-size="pager.pageSize" :total="pager.total" @change="load" />
|
||
|
||
<el-drawer v-model="detailOpen" size="76%" title="知识库详情" destroy-on-close>
|
||
<template v-if="detail">
|
||
<div class="knowledge-detail-head">
|
||
<div><h3>{{ detail.knowledge.name }}</h3><p>{{ typeLabel(detail.knowledge.knowledgeType) }} · 同步后自动生效</p></div>
|
||
<div class="knowledge-detail-head-actions"><el-button type="primary" plain @click="activeTab = 'content'">预览内容</el-button><el-tag :type="detail.knowledge.status === 1 ? 'success' : 'info'">{{ detail.knowledge.status === 1 ? '已开放' : '已关闭' }}</el-tag><el-tag :type="statusType(detail.knowledge.sourceStatus)">{{ detail.knowledge.sourceStatus === 'normal' ? '源文章正常' : '源文章异常' }}</el-tag></div>
|
||
</div>
|
||
<el-alert v-if="detail.knowledge.sourceError" :title="detail.knowledge.sourceError" type="error" :closable="false" />
|
||
<el-tabs v-model="activeTab">
|
||
<el-tab-pane label="基本信息" name="overview">
|
||
<div class="knowledge-section-head"><h4>基本信息</h4><el-button type="primary" plain @click="openEdit">编辑基本信息</el-button></div>
|
||
<dl class="knowledge-meta"><dt>后台名称</dt><dd>{{ detail.knowledge.name }}</dd><dt>知识库类型</dt><dd>{{ typeLabel(detail.knowledge.knowledgeType) }}</dd><dt>版本更新</dt><dd>同步成功后自动替换</dd><dt>SpaceID</dt><dd>{{ detail.knowledge.feishuSpaceId }}</dd><dt>NodeID</dt><dd>{{ detail.knowledge.feishuNodeId }}</dd><dt>当前版本</dt><dd>{{ detail.currentVersion ? `V${detail.currentVersion.versionNo}` : '暂无' }}</dd></dl>
|
||
<section v-if="detail.manifest" class="manifest-panel"><h4>Knowledge Manifest</h4><p><strong>用途:</strong>{{ detail.manifest.purpose }}</p><p><strong>适用:</strong>{{ detail.manifest.applicableQuestions }}</p><p><strong>不适用:</strong>{{ detail.manifest.inapplicableQuestions }}</p><p><strong>核心主题:</strong>{{ detail.manifest.coreTopics }}</p><p><strong>边界:</strong>{{ detail.manifest.boundaries }}</p></section>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="内容预览" name="content" lazy>
|
||
<KnowledgeContentPreview :version-id="previewVersionId" />
|
||
</el-tab-pane>
|
||
<el-tab-pane label="同步记录" name="jobs"><el-table :data="jobs"><el-table-column prop="id" label="任务" width="80" /><el-table-column prop="status" label="状态" width="130" /><el-table-column prop="stage" label="阶段" width="130" /><el-table-column label="进度" width="180"><template #default="{ row }"><el-progress :percentage="row.progress" /></template></el-table-column><el-table-column prop="sourceError" label="失败原因" min-width="260" /></el-table></el-tab-pane>
|
||
</el-tabs>
|
||
</template>
|
||
</el-drawer>
|
||
|
||
<el-dialog v-model="editOpen" title="编辑知识库基本信息" width="min(520px, 92vw)" append-to-body destroy-on-close>
|
||
<el-form label-position="top">
|
||
<el-form-item label="后台知识库名称" required><el-input v-model="editForm.name" maxlength="100" show-word-limit /></el-form-item>
|
||
<el-form-item label="知识库类型" required><el-select v-model="editForm.knowledgeType"><el-option label="课程知识库" value="course" /><el-option label="答疑知识库" value="qa" /><el-option label="通用知识库" value="general" /></el-select></el-form-item>
|
||
<el-alert title="本次修改不会重新同步飞书,也不会改变当前正式版本和开放状态。" type="info" :closable="false" />
|
||
</el-form>
|
||
<template #footer><el-button @click="editOpen = false">取消</el-button><el-button type="primary" :loading="editSaving" @click="saveMetadata">保存</el-button></template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="batchOpen" title="批量修改知识库" width="min(520px, 92vw)" append-to-body destroy-on-close>
|
||
<el-alert :title="`已选择 ${selectedRows.length} 个知识库,未选择的字段将保持原值。`" type="info" :closable="false" />
|
||
<el-form label-position="top" class="batch-edit-form">
|
||
<el-form-item label="知识库类型"><el-select v-model="batchForm.knowledgeType" clearable placeholder="不修改"><el-option label="课程知识库" value="course" /><el-option label="答疑知识库" value="qa" /><el-option label="通用知识库" value="general" /></el-select></el-form-item>
|
||
</el-form>
|
||
<template #footer><el-button @click="batchOpen = false">取消</el-button><el-button type="primary" :loading="batchSaving" @click="saveBatchMetadata">下一步</el-button></template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="batchSyncResultOpen" title="批量同步结果" width="min(760px, 94vw)" append-to-body destroy-on-close>
|
||
<el-alert
|
||
v-if="batchSyncResult"
|
||
:title="`共处理 ${batchSyncResult.total} 个:成功 ${batchSyncResult.success} 个,失败 ${batchSyncResult.failed} 个`"
|
||
:type="batchSyncResult.failed ? 'warning' : 'success'"
|
||
:closable="false"
|
||
show-icon
|
||
/>
|
||
<el-table v-if="batchSyncResult" :data="batchSyncResult.results" max-height="440" class="batch-sync-result-table">
|
||
<el-table-column label="知识库" min-width="220">
|
||
<template #default="{ row }">{{ row.name || `知识库 #${row.knowledgeId}` }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="结果" width="100">
|
||
<template #default="{ row }"><el-tag :type="row.ok ? 'success' : 'danger'">{{ row.ok ? '成功' : '失败' }}</el-tag></template>
|
||
</el-table-column>
|
||
<el-table-column label="状态 / 原因" min-width="300">
|
||
<template #default="{ row }">{{ row.ok ? (row.status === 'unchanged' ? '内容无变化' : '最新内容已生效') : row.error }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<template #footer><el-button type="primary" @click="batchSyncResultOpen = false">完成</el-button></template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.knowledge-batch-bar,
|
||
.knowledge-section-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
}
|
||
|
||
.knowledge-batch-bar {
|
||
min-height: 48px;
|
||
padding: 8px 0;
|
||
color: #66756f;
|
||
}
|
||
|
||
.knowledge-batch-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.knowledge-section-head h4 {
|
||
margin: 0;
|
||
}
|
||
|
||
.knowledge-status-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.knowledge-detail-head-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.batch-edit-form {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.batch-sync-result-table {
|
||
margin-top: 16px;
|
||
}
|
||
</style>
|