feat: 支持FAQ Excel导入导出并优化管理端交互
- 新增 FAQ Excel 导入、导出、模板下载接口及导入结果 VO,支持按分类路径+问题 upsert 与逐行容错 - 模板增加填写说明与更宽列宽,导出列与模板保持一致 - 管理端新增导入弹窗与结果展示,FAQ 列表操作栏精简为"添加 + 更多操作"并去除多余外层框 - 修复导出前校验顺序,避免非 FAQ 知识库触发默认分类写入
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
import { downloadFileFromBlob } from '@easyflow/utils';
|
||||
|
||||
import {
|
||||
Bottom,
|
||||
@@ -12,6 +13,8 @@ import {
|
||||
FolderAdd,
|
||||
MoreFilled,
|
||||
Plus,
|
||||
RefreshRight,
|
||||
Search,
|
||||
Top,
|
||||
Upload,
|
||||
} from '@element-plus/icons-vue';
|
||||
@@ -22,17 +25,18 @@ import {
|
||||
ElDropdownMenu,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElInput,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTree,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
|
||||
import FaqCategoryDialog from './FaqCategoryDialog.vue';
|
||||
import FaqEditDialog from './FaqEditDialog.vue';
|
||||
import FaqImportDialog from './FaqImportDialog.vue';
|
||||
|
||||
const props = defineProps({
|
||||
knowledgeId: {
|
||||
@@ -53,20 +57,14 @@ const searchKeyword = ref('');
|
||||
const categoryTree = ref<any[]>([]);
|
||||
const categoryParentOptions = ref<any[]>([]);
|
||||
const categoryActionLoading = ref(false);
|
||||
const importDialogVisible = ref(false);
|
||||
const templateDownloadLoading = ref(false);
|
||||
const exportLoading = ref(false);
|
||||
|
||||
const baseQueryParams = ref({
|
||||
collectionId: props.knowledgeId,
|
||||
});
|
||||
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'add',
|
||||
text: $t('button.add'),
|
||||
icon: Plus,
|
||||
type: 'primary',
|
||||
},
|
||||
];
|
||||
|
||||
const treeData = computed(() => [
|
||||
{
|
||||
id: 'all',
|
||||
@@ -134,8 +132,13 @@ const hasCategoryId = (nodes: any[], id: string): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleSearch = (keyword: string) => {
|
||||
searchKeyword.value = keyword || '';
|
||||
const handleSearch = () => {
|
||||
searchKeyword.value = searchKeyword.value.trim();
|
||||
refreshList();
|
||||
};
|
||||
|
||||
const handleResetSearch = () => {
|
||||
searchKeyword.value = '';
|
||||
refreshList();
|
||||
};
|
||||
|
||||
@@ -150,9 +153,57 @@ const openAddDialog = () => {
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleButtonClick = (event: any) => {
|
||||
if (event.key === 'add') {
|
||||
openAddDialog();
|
||||
const downloadImportTemplate = async () => {
|
||||
if (templateDownloadLoading.value) {
|
||||
return;
|
||||
}
|
||||
templateDownloadLoading.value = true;
|
||||
try {
|
||||
const blob = await api.download(
|
||||
`/api/v1/faqItem/downloadImportTemplate?collectionId=${props.knowledgeId}`,
|
||||
);
|
||||
downloadFileFromBlob({
|
||||
fileName: 'faq_import_template.xlsx',
|
||||
source: blob,
|
||||
});
|
||||
} finally {
|
||||
templateDownloadLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const exportFaqExcel = async () => {
|
||||
if (exportLoading.value) {
|
||||
return;
|
||||
}
|
||||
exportLoading.value = true;
|
||||
try {
|
||||
const blob = await api.download(
|
||||
`/api/v1/faqItem/exportExcel?collectionId=${props.knowledgeId}`,
|
||||
);
|
||||
downloadFileFromBlob({
|
||||
fileName: 'faq_export.xlsx',
|
||||
source: blob,
|
||||
});
|
||||
} finally {
|
||||
exportLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportSuccess = () => {
|
||||
refreshList();
|
||||
};
|
||||
|
||||
const handleMoreActionCommand = (command: string) => {
|
||||
if (command === 'import') {
|
||||
importDialogVisible.value = true;
|
||||
return;
|
||||
}
|
||||
if (command === 'downloadTemplate') {
|
||||
downloadImportTemplate();
|
||||
return;
|
||||
}
|
||||
if (command === 'export') {
|
||||
exportFaqExcel();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -740,11 +791,58 @@ onMounted(() => {
|
||||
|
||||
<div class="faq-content-pane">
|
||||
<div class="faq-header">
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
@search="handleSearch"
|
||||
@button-click="handleButtonClick"
|
||||
/>
|
||||
<div class="faq-toolbar">
|
||||
<div class="faq-search-actions">
|
||||
<ElInput
|
||||
v-model="searchKeyword"
|
||||
clearable
|
||||
class="faq-search-input"
|
||||
:placeholder="$t('common.searchPlaceholder')"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<ElButton type="primary" :icon="Search" @click="handleSearch">
|
||||
{{ $t('button.query') }}
|
||||
</ElButton>
|
||||
<ElButton :icon="RefreshRight" @click="handleResetSearch">
|
||||
{{ $t('button.reset') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="faq-primary-actions">
|
||||
<ElButton type="primary" :icon="Plus" @click="openAddDialog">
|
||||
{{ $t('button.add') }}
|
||||
</ElButton>
|
||||
<ElDropdown
|
||||
trigger="click"
|
||||
@command="handleMoreActionCommand"
|
||||
>
|
||||
<ElButton :icon="MoreFilled">
|
||||
{{ $t('documentCollection.faq.import.moreActions') }}
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem command="import" :icon="Upload">
|
||||
{{ $t('button.import') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
command="downloadTemplate"
|
||||
:icon="Download"
|
||||
:disabled="templateDownloadLoading"
|
||||
>
|
||||
{{ $t('documentCollection.faq.import.downloadTemplate') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
command="export"
|
||||
:icon="Download"
|
||||
:disabled="exportLoading"
|
||||
>
|
||||
{{ $t('button.export') }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PageData
|
||||
@@ -817,6 +915,12 @@ onMounted(() => {
|
||||
:parent-options="categoryParentOptions"
|
||||
@submit="saveCategory"
|
||||
/>
|
||||
|
||||
<FaqImportDialog
|
||||
v-model="importDialogVisible"
|
||||
:knowledge-id="knowledgeId"
|
||||
@success="handleImportSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -898,6 +1002,65 @@ onMounted(() => {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.faq-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.faq-search-actions {
|
||||
flex: 1;
|
||||
min-width: 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.faq-search-input {
|
||||
width: min(460px, 100%);
|
||||
}
|
||||
|
||||
.faq-primary-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:deep(.faq-toolbar .el-button) {
|
||||
height: 38px;
|
||||
padding: 0 16px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.faq-toolbar .el-button:not(.el-button--primary):hover) {
|
||||
color: hsl(var(--primary));
|
||||
border-color: hsl(var(--primary) / 45%);
|
||||
background: hsl(var(--primary) / 7%);
|
||||
}
|
||||
|
||||
:deep(.faq-search-input .el-input__wrapper) {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 0 1px var(--el-border-color) inset;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
:deep(.faq-search-input .el-input__wrapper:hover) {
|
||||
box-shadow: 0 0 0 1px hsl(var(--primary) / 30%) inset;
|
||||
}
|
||||
|
||||
:deep(.faq-search-input .el-input__wrapper.is-focus) {
|
||||
box-shadow: 0 0 0 1px hsl(var(--primary) / 60%) inset;
|
||||
}
|
||||
|
||||
:deep(
|
||||
.faq-category-tree > .el-tree-node > .el-tree-node__content > .el-tree-node__expand-icon
|
||||
) {
|
||||
@@ -940,4 +1103,20 @@ onMounted(() => {
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 1360px) {
|
||||
.faq-toolbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.faq-search-actions,
|
||||
.faq-primary-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faq-primary-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user