feat(ai): add three-level FAQ category management
- add FAQ category table/sql migration and initialize ddl updates - add category service/controller with validation, default category rules, and sorting - support faq item category binding and category-based filtering (include descendants) - redesign FAQ page with category tree actions and UI polish
This commit is contained in:
@@ -1,19 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, ref} from 'vue';
|
||||
import {useRoute, useRouter} from 'vue-router';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import {$t} from '@easyflow/locales';
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import {ArrowLeft, Plus} from '@element-plus/icons-vue';
|
||||
import {ElIcon, ElImage} from 'element-plus';
|
||||
import { ArrowLeft, Plus } from '@element-plus/icons-vue';
|
||||
import { ElIcon, ElImage } from 'element-plus';
|
||||
|
||||
import {api} from '#/api/request';
|
||||
import { api } from '#/api/request';
|
||||
import bookIcon from '#/assets/ai/knowledge/book.svg';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import ChunkDocumentTable from '#/views/ai/documentCollection/ChunkDocumentTable.vue';
|
||||
import DocumentCollectionDataConfig
|
||||
from '#/views/ai/documentCollection/DocumentCollectionDataConfig.vue';
|
||||
import DocumentCollectionDataConfig from '#/views/ai/documentCollection/DocumentCollectionDataConfig.vue';
|
||||
import DocumentTable from '#/views/ai/documentCollection/DocumentTable.vue';
|
||||
import FaqTable from '#/views/ai/documentCollection/FaqTable.vue';
|
||||
import ImportKnowledgeDocFile from '#/views/ai/documentCollection/ImportKnowledgeDocFile.vue';
|
||||
@@ -27,11 +25,10 @@ const knowledgeId = ref<string>((route.query.id as string) || '');
|
||||
const activeMenu = ref<string>((route.query.activeMenu as string) || '');
|
||||
const knowledgeInfo = ref<any>({});
|
||||
const selectedCategory = ref('');
|
||||
const defaultSelectedMenu = ref('');
|
||||
|
||||
const resolveDefaultMenu = (collectionType: string, menuKey: string) => {
|
||||
const faqMenus = new Set(['faqList', 'knowledgeSearch', 'config']);
|
||||
const documentMenus = new Set(['documentList', 'knowledgeSearch', 'config']);
|
||||
const faqMenus = new Set(['config', 'faqList', 'knowledgeSearch']);
|
||||
const documentMenus = new Set(['config', 'documentList', 'knowledgeSearch']);
|
||||
const fallbackMenu = collectionType === 'FAQ' ? 'faqList' : 'documentList';
|
||||
|
||||
if (!menuKey) {
|
||||
@@ -53,7 +50,6 @@ const getKnowledge = () => {
|
||||
res.data.collectionType || 'DOCUMENT',
|
||||
activeMenu.value,
|
||||
);
|
||||
defaultSelectedMenu.value = initialMenu;
|
||||
selectedCategory.value = initialMenu;
|
||||
}
|
||||
});
|
||||
@@ -64,18 +60,26 @@ onMounted(() => {
|
||||
const back = () => {
|
||||
router.push({ path: '/ai/documentCollection' });
|
||||
};
|
||||
const isFaqCollection = computed(() => knowledgeInfo.value.collectionType === 'FAQ');
|
||||
const isFaqCollection = computed(
|
||||
() => knowledgeInfo.value.collectionType === 'FAQ',
|
||||
);
|
||||
const categoryData = computed(() => {
|
||||
if (isFaqCollection.value) {
|
||||
return [
|
||||
{ key: 'faqList', name: $t('documentCollection.faq.faqList') },
|
||||
{ key: 'knowledgeSearch', name: $t('documentCollection.knowledgeRetrieval') },
|
||||
{
|
||||
key: 'knowledgeSearch',
|
||||
name: $t('documentCollection.knowledgeRetrieval'),
|
||||
},
|
||||
{ key: 'config', name: $t('documentCollection.config') },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ key: 'documentList', name: $t('documentCollection.documentList') },
|
||||
{ key: 'knowledgeSearch', name: $t('documentCollection.knowledgeRetrieval') },
|
||||
{
|
||||
key: 'knowledgeSearch',
|
||||
name: $t('documentCollection.knowledgeRetrieval'),
|
||||
},
|
||||
{ key: 'config', name: $t('documentCollection.config') },
|
||||
];
|
||||
});
|
||||
@@ -106,8 +110,8 @@ const handleButtonClick = (event: any) => {
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleCategoryClick = (category: any) => {
|
||||
selectedCategory.value = category.key;
|
||||
const handleCategoryClick = (menuKey: string) => {
|
||||
selectedCategory.value = menuKey;
|
||||
viewDocVisible.value = false;
|
||||
};
|
||||
const viewDocVisible = ref(false);
|
||||
@@ -138,17 +142,19 @@ const backDoc = () => {
|
||||
{{ knowledgeInfo.description || '' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-top-menu">
|
||||
<button
|
||||
v-for="item in categoryData"
|
||||
:key="item.key"
|
||||
class="doc-menu-item"
|
||||
:class="{ active: selectedCategory === item.key }"
|
||||
@click="handleCategoryClick(item.key)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-content">
|
||||
<div>
|
||||
<PageSide
|
||||
label-key="name"
|
||||
value-key="key"
|
||||
:menus="categoryData"
|
||||
:default-selected="defaultSelectedMenu"
|
||||
@change="handleCategoryClick"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="doc-table-content menu-container border border-[var(--el-border-color)]"
|
||||
>
|
||||
@@ -230,11 +236,53 @@ const backDoc = () => {
|
||||
}
|
||||
.doc-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
.doc-top-menu {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
}
|
||||
.doc-menu-item {
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
padding: 7px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
color 0.2s,
|
||||
border-color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
.doc-menu-item:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.doc-menu-item.active {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary-light-7);
|
||||
box-shadow: 0 2px 10px rgb(64 158 255 / 16%);
|
||||
font-weight: 600;
|
||||
}
|
||||
.doc-menu-item:focus-visible {
|
||||
outline: 2px solid var(--el-color-primary-light-5);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.doc-table {
|
||||
background-color: var(--el-bg-color);
|
||||
@@ -260,6 +308,7 @@ const backDoc = () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 160px;
|
||||
}
|
||||
.title {
|
||||
font-weight: 500;
|
||||
|
||||
Reference in New Issue
Block a user