| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097 |
- <template>
- <div class="qa-manage">
- <div class="action-bar">
- <div class="action-bar__left">
- <el-input
- v-model="keyword"
- clearable
- placeholder="搜索问题"
- style="width: 280px"
- @keyup.enter="refreshFaqList"
- @clear="refreshFaqList"
- />
- <el-button @click="refreshFaqList">
- <el-icon><Search /></el-icon>
- 搜索
- </el-button>
- </div>
- <div class="flex">
- <el-button @click="openImportModal">
- <el-icon><Upload /></el-icon>
- 模版导入
- </el-button>
- <el-button type="primary" @click="openCreateDrawer">
- <el-icon><Plus /></el-icon>
- 新增问答
- </el-button>
- </div>
- </div>
- <el-card>
- <template #header>
- <div class="card-header">
- <span>问答列表</span>
- <!-- <span class="card-header__meta">面向 FAQ 召回和问答命中的问答管理</span> -->
- </div>
- </template>
- <el-table v-if="faqList.length || loading" :data="faqList" v-loading="loading" border>
- <el-table-column prop="standard_question" label="标准问题" min-width="260" />
- <el-table-column label="答案" min-width="260">
- <template #default="{ row }">
- <span class="answers-preview">{{ formatAnswers(row.answers) }}</span>
- </template>
- </el-table-column>
- <el-table-column label="相似问数量" width="110">
- <template #default="{ row }">
- {{ row.similar_questions?.length || 0 }}
- </template>
- </el-table-column>
- <el-table-column label="反例数量" width="110">
- <template #default="{ row }">
- {{ row.negative_questions?.length || 0 }}
- </template>
- </el-table-column>
- <el-table-column label="启用" width="90">
- <template #default="{ row }">
- <el-switch :model-value="!!row.is_enabled" @change="toggleFaqStatus(row, $event)" />
- </template>
- </el-table-column>
- <el-table-column prop="creationTime" label="创建时间" width="180" />
- <el-table-column label="操作" width="160" fixed="right">
- <template #default="{ row }">
- <el-button link type="primary" @click="openDetailDialog(row.id)">详情</el-button>
- <el-button link type="primary" @click="openEditDrawer(row)">编辑</el-button>
- <el-button link type="danger" @click="removeFaq(row.id)">删除</el-button>
- </template>
- </el-table-column>
- <template #empty>暂无问答条目</template>
- </el-table>
- <el-empty v-else description="暂无问答条目" class="page-empty" />
- <div v-if="faqList.length || pagination.totalCount" class="pagination-wrap">
- <el-pagination
- background
- layout="total, prev, pager, next"
- :current-page="pagination.pageIndex"
- :page-size="pagination.pageSize"
- :total="pagination.totalCount"
- @current-change="handlePageChange"
- />
- </div>
- </el-card>
- <!-- 编辑抽屉 -->
- <el-drawer
- v-model="drawerVisible"
- :title="form.id ? '编辑问答' : '新增问答'"
- direction="rtl"
- size="680px"
- >
- <el-form ref="formRef" :model="form" :rules="rules" label-position="top" label-width="100%">
- <el-form-item label="标准问法" prop="standard_question">
- <el-input v-model="form.standard_question" placeholder="请输入标准问题" />
- </el-form-item>
- <el-form-item>
- <template #label>
- <div class="array-field__header">
- <span>相似问法</span>
- <el-button link type="primary" @click="addArrayItem('similar_questions')">
- <el-icon><Plus /></el-icon>添加
- </el-button>
- </div>
- <div class="field-tip">
- 添加与标准问意思相同但表述不同的问题,帮助系统更好地匹配用户查询。
- </div>
- </template>
- <div class="array-field">
- <div
- v-for="(_item, index) in form.similar_questions"
- :key="`similar-${index}`"
- class="array-field__row"
- >
- <el-input
- v-model="form.similar_questions[index]"
- placeholder="请输入与标准问语义相同但表述不同的问题"
- />
- <el-button
- link
- type="danger"
- :disabled="form.similar_questions.length <= 1"
- @click="removeArrayItem('similar_questions', index)"
- >
- 删除
- </el-button>
- </div>
- </div>
- </el-form-item>
- <el-form-item>
- <template #label>
- <div class="array-field__header">
- <span>反例</span>
- <el-button link type="primary" @click="addArrayItem('negative_questions')">
- <el-icon><Plus /></el-icon>添加
- </el-button>
- </div>
- <div class="field-tip">添加不应匹配此答案的问题,用于排除误匹配。</div>
- </template>
- <div class="array-field">
- <div
- v-for="(_item, index) in form.negative_questions"
- :key="`negative-${index}`"
- class="array-field__row"
- >
- <el-input
- v-model="form.negative_questions[index]"
- placeholder="请输入不应匹配此答案的问题"
- />
- <el-button
- link
- type="danger"
- :disabled="form.negative_questions.length <= 1"
- @click="removeArrayItem('negative_questions', index)"
- >
- 删除
- </el-button>
- </div>
- </div>
- </el-form-item>
- <el-form-item prop="answers">
- <template #label>
- <div class="array-field__header">
- <span>答案</span>
- <el-button link type="primary" @click="addArrayItem('answers')">
- <el-icon><Plus /></el-icon>添加
- </el-button>
- </div>
- <div class="field-tip">提供完整准确的答案内容,可添加多个答案以覆盖不同场景。</div>
- </template>
- <div class="array-field">
- <div
- v-for="(_item, index) in form.answers"
- :key="`answer-${index}`"
- class="array-field__row"
- >
- <el-input
- v-model="form.answers[index]"
- type="textarea"
- :rows="3"
- placeholder="请输入答案内容"
- />
- <el-button
- link
- type="danger"
- :disabled="form.answers.length <= 1"
- @click="removeArrayItem('answers', index)"
- >
- 删除
- </el-button>
- </div>
- </div>
- </el-form-item>
- <el-form-item label="启用">
- <el-switch v-model="form.is_enabled" />
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="drawer-footer">
- <el-button @click="drawerVisible = false">取消</el-button>
- <el-button type="primary" :loading="submitLoading" @click="submitForm">保存</el-button>
- </div>
- </template>
- </el-drawer>
- <!-- 详情弹窗 -->
- <el-dialog v-model="detailVisible" title="问答详情" width="720px">
- <div v-loading="detailLoading" class="detail-panel">
- <template v-if="detailData">
- <div class="detail-item">
- <div class="detail-item__label">标准问题</div>
- <div class="detail-item__value">
- <el-tag type="primary" effect="plain">
- {{ detailData.standard_question || '-' }}
- </el-tag>
- </div>
- </div>
- <div class="detail-item">
- <div class="detail-item__label">相似问</div>
- <div class="detail-tag-list">
- <el-tag
- v-for="item in detailData.similar_questions || []"
- :key="`similar-${item}`"
- effect="plain"
- >
- {{ item }}
- </el-tag>
- <span v-if="!(detailData.similar_questions || []).length" class="detail-empty"
- >-</span
- >
- </div>
- </div>
- <div class="detail-item">
- <div class="detail-item__label">反例</div>
- <div class="detail-tag-list">
- <el-tag
- v-for="item in detailData.negative_questions || []"
- :key="`negative-${item}`"
- effect="plain"
- type="info"
- >
- {{ item }}
- </el-tag>
- <span v-if="!(detailData.negative_questions || []).length" class="detail-empty"
- >-</span
- >
- </div>
- </div>
- <div class="detail-item">
- <div class="detail-item__label">答案</div>
- <div class="detail-answer-list">
- <div
- v-for="(item, index) in detailData.answers || []"
- :key="`answer-${index}`"
- class="detail-answer-item"
- >
- {{ item }}
- </div>
- <span v-if="!(detailData.answers || []).length" class="detail-empty">-</span>
- </div>
- </div>
- <div class="detail-grid">
- <div class="detail-item">
- <div class="detail-item__label">启用状态</div>
- <div class="detail-item__value">
- <el-tag :type="detailData.is_enabled ? 'success' : 'warning'">{{
- detailData.is_enabled ? '启用' : '停用'
- }}</el-tag>
- </div>
- </div>
- <div class="detail-item">
- <div class="detail-item__label">索引方式</div>
- <div class="detail-item__value">
- {{ detailData.index_mode }}
- </div>
- </div>
- <div class="detail-item">
- <div class="detail-item__label">创建时间</div>
- <div class="detail-item__value">{{ detailData.creationTime || '-' }}</div>
- </div>
- </div>
- </template>
- </div>
- </el-dialog>
- <!-- 导入弹窗 -->
- <el-dialog v-model="importDialogVisible" title="模版导入" width="500px">
- <el-form> </el-form>
- <div class="import-modal-content">
- <div class="import-step">
- <div class="step-title">上传文件</div>
- <div class="step-desc">上传填写好的 Excel 文件。</div>
- <FileUploadInput
- v-model="importFile"
- :fileExtensions="['.xlsx', '.xls']"
- placeholder="点击或拖拽上传 Excel 文件"
- />
- </div>
- <div class="import-step">
- <div class="step-title">导入模式</div>
- <el-select v-model="importFormData.mode" placeholder="请选择">
- <el-option label="追加" value="append"></el-option>
- <el-option label="覆盖" value="replace"></el-option>
- </el-select>
- </div>
- <div class="import-step">
- <div class="step-title">下载模版</div>
- <div class="step-desc">请下载标准模版,并按照格式填写问答数据。</div>
- <div>
- <el-button type="primary" link @click="downloadTemplate">
- <el-icon><Download /></el-icon>
- 下载 faq_example.xlsx
- </el-button>
- </div>
- </div>
- </div>
- <template #footer>
- <div class="drawer-footer">
- <el-button @click="importDialogVisible = false">取消</el-button>
- <el-button
- type="primary"
- :loading="importLoading"
- :disabled="!importFile"
- @click="submitImport"
- >
- 开始导入
- </el-button>
- </div>
- </template>
- </el-dialog>
- <el-dialog
- v-model="importTaskDialogVisible"
- title="导入任务状态"
- width="560px"
- :close-on-click-modal="false"
- >
- <div class="import-task">
- <div v-if="importTaskInfo" class="import-task__meta">
- <div class="import-task__row">
- <span class="import-task__label">任务编号</span>
- <span class="import-task__value">{{ importTaskInfo.code || '-' }}</span>
- </div>
- <div class="import-task__row">
- <span class="import-task__label">创建时间</span>
- <span class="import-task__value">{{ importTaskInfo.creationTime || '-' }}</span>
- </div>
- </div>
- <div class="import-task__status">
- <el-tag :type="getImportTaskStatusTag(importTaskInfo?.status)">
- {{ getImportTaskStatusText(importTaskInfo?.status) }}
- </el-tag>
- <span class="import-task__progress"> 进度:{{ importTaskInfo?.progress || '-' }} </span>
- </div>
- </div>
- <template #footer>
- <div class="drawer-footer">
- <el-button @click="handleImportTaskDialogClose">关闭</el-button>
- </div>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts">
- import { onBeforeUnmount, reactive, ref, watch } from 'vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { Plus, Search, Upload, Download } from '@element-plus/icons-vue'
- import { knowledge } from '@repo/api-service'
- import FileUploadInput from '@/features/fileUpload/FileUploadInput.vue'
- import type { FaqForm, FaqItem } from './types'
- import type { WorkflowUploadFile } from '@/features/fileUpload/shared'
- interface FaqDetail extends FaqItem {
- answer_strategy?: string
- chunk_id?: string
- chunk_type?: string
- index_mode?: string
- is_recommended?: boolean
- knowledge_base_id?: string
- knowledge_id?: string
- tag_id?: string
- }
- interface AsyncTaskInfo {
- code?: string
- creationTime?: string
- htmlStatusInfo?: string
- id: string
- progress?: string
- status?: number
- updateTime?: string
- }
- const props = defineProps<{ currentBaseId: string }>()
- const loading = ref(false)
- const submitLoading = ref(false)
- const drawerVisible = ref(false)
- const detailVisible = ref(false)
- const detailLoading = ref(false)
- const keyword = ref('')
- const faqList = ref<FaqItem[]>([])
- const detailData = ref<FaqDetail | null>(null)
- const formRef = ref()
- const pagination = reactive({
- pageIndex: 1,
- pageSize: 20,
- totalCount: 0
- })
- const importDialogVisible = ref(false)
- const importLoading = ref(false)
- const importTaskDialogVisible = ref(false)
- const importTaskLoading = ref(false)
- const importTaskInfo = ref<AsyncTaskInfo | null>(null)
- const importFormData = reactive<{
- knowledge_base_id: string
- fileId: string
- mode: 'replace' | 'append'
- }>({
- knowledge_base_id: '',
- fileId: '',
- mode: 'append' // 默认追加,可根据需求调整
- })
- const importFile = ref<WorkflowUploadFile>()
- let importTaskTimer: number | null = null
- type ArrayFieldKey = 'similar_questions' | 'negative_questions' | 'answers'
- function normalizeStringArray(value?: string[]) {
- return (value || []).map((item) => item.trim()).filter(Boolean)
- }
- const createDefaultForm = (): FaqForm => ({
- id: '',
- standard_question: '',
- similar_questions: [''],
- negative_questions: [''],
- answers: [''],
- is_enabled: true
- })
- const form = reactive<FaqForm>(createDefaultForm())
- const rules = {
- standard_question: [{ required: true, message: '请输入标准问题', trigger: 'blur' }],
- answers: [
- {
- validator: (_rule: unknown, value: string[], callback: (error?: Error) => void) => {
- if (normalizeStringArray(value).length) {
- callback()
- return
- }
- callback(new Error('请至少填写一个答案'))
- },
- trigger: 'change'
- }
- ]
- }
- function addArrayItem(field: ArrayFieldKey) {
- form[field].push('')
- }
- function removeArrayItem(field: ArrayFieldKey, index: number) {
- if (form[field].length <= 1) return
- form[field].splice(index, 1)
- }
- function formatAnswers(value?: string[]) {
- const list = normalizeStringArray(value)
- return list.length ? list.join(' / ') : '-'
- }
- function resetForm() {
- Object.assign(form, createDefaultForm())
- }
- function applyFaqFormData(data: Partial<FaqDetail>) {
- const answers = normalizeStringArray(data.answers)
- Object.assign(form, {
- id: data.id || '',
- standard_question: data.standard_question || '',
- similar_questions: normalizeStringArray(data.similar_questions).length
- ? normalizeStringArray(data.similar_questions)
- : [''],
- negative_questions: normalizeStringArray(data.negative_questions).length
- ? normalizeStringArray(data.negative_questions)
- : [''],
- answers: answers.length ? answers : [''],
- is_enabled: data.is_enabled ?? true
- })
- }
- async function fetchFaqDetail(id?: string) {
- if (!id) return null
- const res = await knowledge.postAiFaqInfo({ id })
- if (!res?.isSuccess || !res.result) return null
- return (res.result || null) as FaqDetail | null
- }
- async function fetchFaqList() {
- if (!props.currentBaseId) return
- loading.value = true
- try {
- const res = await knowledge.postAiFaqPageList({
- knowledge_base_id: props.currentBaseId,
- pageIndex: pagination.pageIndex,
- pageSize: pagination.pageSize,
- keyword: keyword.value
- })
- if (res?.isSuccess) {
- faqList.value = (res.result?.model || []) as FaqItem[]
- pagination.totalCount = res.result?.totalCount || 0
- }
- } finally {
- loading.value = false
- }
- }
- async function refreshFaqList() {
- pagination.pageIndex = 1
- await fetchFaqList()
- }
- async function handlePageChange(page: number) {
- pagination.pageIndex = page
- await fetchFaqList()
- }
- function openCreateDrawer() {
- resetForm()
- drawerVisible.value = true
- }
- async function openDetailDialog(id?: string) {
- if (!id) return
- detailVisible.value = true
- detailLoading.value = true
- detailData.value = null
- try {
- detailData.value = await fetchFaqDetail(id)
- } finally {
- detailLoading.value = false
- }
- }
- async function openEditDrawer(row: FaqItem) {
- const detail = await fetchFaqDetail(row.id)
- if (!detail) {
- ElMessage.error('问答详情加载失败')
- return
- }
- applyFaqFormData(detail)
- drawerVisible.value = true
- }
- async function submitForm() {
- form.similar_questions = normalizeStringArray(form.similar_questions)
- form.negative_questions = normalizeStringArray(form.negative_questions)
- form.answers = normalizeStringArray(form.answers)
- const valid = await formValidate(formRef.value)
- if (!valid) {
- if (!form.similar_questions.length) form.similar_questions = ['']
- if (!form.negative_questions.length) form.negative_questions = ['']
- if (!form.answers.length) form.answers = ['']
- return
- }
- submitLoading.value = true
- try {
- const payload = {
- standard_question: form.standard_question.trim(),
- similar_questions: normalizeStringArray(form.similar_questions),
- negative_questions: normalizeStringArray(form.negative_questions),
- answers: normalizeStringArray(form.answers),
- is_enabled: form.is_enabled
- }
- if (form.id) {
- await knowledge.postAiFaqUpdate({ id: form.id, ...payload })
- ElMessage.success('问答已更新')
- } else {
- await knowledge.postAiFaqCreate({
- knowledge_base_id: props.currentBaseId,
- ...payload
- })
- ElMessage.success('问答已创建')
- }
- drawerVisible.value = false
- await refreshFaqList()
- } catch {
- ElMessage.error('保存失败')
- } finally {
- if (!form.similar_questions.length) form.similar_questions = ['']
- if (!form.negative_questions.length) form.negative_questions = ['']
- if (!form.answers.length) form.answers = ['']
- submitLoading.value = false
- }
- }
- async function toggleFaqStatus(row: FaqItem, value: string | number | boolean) {
- if (!row.id) return
- const detail = await fetchFaqDetail(row.id)
- if (!detail) {
- ElMessage.error('问答详情加载失败')
- return
- }
- await knowledge.postAiFaqUpdate({
- id: row.id,
- standard_question: detail.standard_question || '',
- similar_questions: normalizeStringArray(detail.similar_questions),
- negative_questions: normalizeStringArray(detail.negative_questions),
- answers: normalizeStringArray(detail.answers),
- is_enabled: Boolean(value)
- })
- ElMessage.success('状态已更新')
- await fetchFaqList()
- }
- async function removeFaq(id?: string) {
- if (!id) return
- const confirmed = await ElMessageBox.confirm('确定删除该问答吗?删除后不可恢复。', '删除确认', {
- type: 'warning'
- })
- .then(() => true)
- .catch(() => false)
- if (!confirmed) return
- await knowledge.postAiFaqOpenApiDelete({ id })
- ElMessage.success('问答已删除')
- await refreshFaqList()
- }
- async function formValidate(formInstance: any) {
- return formInstance
- ?.validate()
- .then(() => true)
- .catch(() => false)
- }
- // --- 导入相关逻辑 ---
- function openImportModal() {
- importFormData.fileId = ''
- importFormData.knowledge_base_id = props.currentBaseId
- importDialogVisible.value = true
- }
- function resetImportForm() {
- importFile.value = undefined
- importFormData.fileId = ''
- importFormData.mode = 'append'
- }
- function clearImportTaskTimer() {
- if (importTaskTimer) {
- window.clearTimeout(importTaskTimer)
- importTaskTimer = null
- }
- }
- function getImportTaskStatusText(status?: number) {
- switch (status) {
- case 0:
- return '已创建'
- case 1:
- return '运行中'
- case 2:
- return '成功'
- case 3:
- return '失败'
- case 4:
- return '挂起'
- default:
- return '处理中'
- }
- }
- function getImportTaskStatusTag(status?: number) {
- switch (status) {
- case 2:
- return 'success'
- case 3:
- return 'danger'
- case 4:
- return 'warning'
- default:
- return 'primary'
- }
- }
- function extractAsyncTaskId(payload: any) {
- if (!payload) return ''
- return (
- payload.id ||
- payload.result?.id ||
- payload.appAsynTaskId ||
- payload.result?.appAsynTaskId ||
- payload.taskId ||
- payload.result?.taskId ||
- ''
- )
- }
- async function fetchImportTaskInfo(taskId: string) {
- const res = await knowledge.postBpmGetAsynTaskInfo({ id: taskId })
- if (!res?.isSuccess || !res.result) {
- throw new Error((res as any)?.error || '异步任务状态查询失败')
- }
- importTaskInfo.value = res.result as AsyncTaskInfo
- return importTaskInfo.value
- }
- async function pollImportTask(taskId: string) {
- importTaskLoading.value = true
- try {
- const taskInfo = await fetchImportTaskInfo(taskId)
- const status = taskInfo?.status
- if (status === 2) {
- clearImportTaskTimer()
- ElMessage.success('导入成功')
- await refreshFaqList()
- importTaskDialogVisible.value = false
- importTaskInfo.value = null
- return
- }
- if (status === 3) {
- clearImportTaskTimer()
- ElMessage.error('导入失败,请查看任务状态信息')
- return
- }
- clearImportTaskTimer()
- importTaskTimer = window.setTimeout(() => {
- void pollImportTask(taskId)
- }, 1000)
- } catch (error) {
- clearImportTaskTimer()
- console.error('Fetch import task failed:', error)
- ElMessage.error('异步任务状态查询失败,请稍后重试')
- } finally {
- importTaskLoading.value = false
- }
- }
- function handleImportTaskDialogClose() {
- clearImportTaskTimer()
- importTaskDialogVisible.value = false
- importTaskInfo.value = null
- }
- function downloadTemplate() {
- window.open('/Content/Template/faq_example.xlsx', '_blank')
- }
- async function submitImport() {
- if (!importFile.value) {
- ElMessage.warning('请先上传文件')
- return
- }
- if (!props.currentBaseId) {
- ElMessage.error('知识库ID缺失')
- return
- }
- importLoading.value = true
- try {
- const payload: any = {
- knowledge_base_id: props.currentBaseId,
- fileId: importFile.value.id,
- mode: importFormData.mode
- }
- const res = await knowledge.postAiFaqBatchImport(payload)
- if (!res.isSuccess) {
- ElMessage.error(res.error)
- return
- }
- const taskId = extractAsyncTaskId(res)
- importDialogVisible.value = false
- resetImportForm()
- if (!taskId) {
- ElMessage.success('导入成功')
- await refreshFaqList()
- return
- }
- importTaskInfo.value = {
- id: taskId,
- status: 0,
- progress: '0%'
- }
- importTaskDialogVisible.value = true
- await pollImportTask(taskId)
- } catch (error) {
- console.error('Import failed:', error)
- ElMessage.error('导入失败,请检查文件格式或联系管理员')
- } finally {
- importLoading.value = false
- }
- }
- watch(
- () => props.currentBaseId,
- () => {
- clearImportTaskTimer()
- keyword.value = ''
- faqList.value = []
- pagination.pageIndex = 1
- pagination.totalCount = 0
- detailVisible.value = false
- detailData.value = null
- importTaskDialogVisible.value = false
- importTaskInfo.value = null
- resetImportForm()
- if (props.currentBaseId) {
- void fetchFaqList()
- }
- },
- { immediate: true }
- )
- onBeforeUnmount(() => {
- clearImportTaskTimer()
- })
- </script>
- <style scoped lang="less">
- .qa-manage {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
- .action-bar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- flex-wrap: wrap;
- }
- .action-bar__left {
- display: flex;
- align-items: center;
- gap: 12px;
- flex-wrap: wrap;
- }
- .card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- font-weight: 600;
- }
- .card-header__meta {
- font-size: 12px;
- color: #6b7280;
- }
- .answers-preview {
- display: inline-block;
- max-width: 320px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- .pagination-wrap {
- margin-top: 16px;
- display: flex;
- justify-content: flex-end;
- }
- .page-empty {
- min-height: calc(100vh - 360px);
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .array-field__header {
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- }
- .array-field {
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: 10px;
- border: 1px solid #eee;
- border-radius: 4px;
- padding: 12px;
- box-sizing: border-box;
- }
- .array-field__row {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: 12px;
- align-items: start;
- }
- .array-field__empty {
- padding: 12px;
- border: 1px dashed #d1d5db;
- border-radius: 8px;
- font-size: 12px;
- color: #9ca3af;
- background: #f9fafb;
- }
- .field-tip {
- margin-top: 8px;
- font-size: 12px;
- line-height: 1.5;
- color: #a6aab3;
- font-weight: 400;
- }
- .drawer-footer {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- }
- .detail-panel {
- min-height: 180px;
- }
- .detail-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 16px;
- margin-bottom: 16px;
- }
- .detail-item {
- margin-bottom: 16px;
- }
- .detail-item__label {
- margin-bottom: 8px;
- font-size: 13px;
- font-weight: 600;
- color: #374151;
- }
- .detail-item__value {
- line-height: 1.6;
- color: #111827;
- word-break: break-all;
- }
- .detail-tag-list {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
- .detail-answer-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .detail-answer-item {
- padding: 10px 12px;
- border-radius: 8px;
- background: #f8fafc;
- color: #111827;
- line-height: 1.6;
- }
- .detail-empty {
- color: #9ca3af;
- }
- :deep(.el-form-item__label) {
- width: 100%;
- color: #333;
- font-weight: 600;
- }
- .import-modal-content {
- display: flex;
- flex-direction: column;
- gap: 24px;
- padding: 10px 0;
- }
- .import-step {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .step-title {
- font-weight: 600;
- font-size: 14px;
- color: #303133;
- }
- .step-desc {
- font-size: 13px;
- color: #909399;
- line-height: 1.4;
- }
- .import-task {
- display: flex;
- flex-direction: column;
- gap: 16px;
- min-height: 180px;
- }
- .import-task__status {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .import-task__progress {
- font-size: 13px;
- color: #6b7280;
- }
- .import-task__meta {
- display: grid;
- gap: 10px;
- padding: 14px;
- border-radius: 10px;
- background: #f8fafc;
- }
- .import-task__row {
- display: grid;
- grid-template-columns: 72px minmax(0, 1fr);
- gap: 10px;
- font-size: 13px;
- }
- .import-task__label {
- color: #6b7280;
- }
- .import-task__value {
- color: #111827;
- word-break: break-all;
- }
- .import-task__detail {
- padding: 14px;
- border: 1px solid #e5e7eb;
- border-radius: 10px;
- background: #fff;
- }
- .import-task__detail-title {
- margin-bottom: 8px;
- font-size: 13px;
- font-weight: 600;
- color: #111827;
- }
- .import-task__detail-body {
- font-size: 13px;
- line-height: 1.7;
- color: #111827;
- word-break: break-word;
- }
- .import-task__detail-body :deep(*) {
- max-width: 100%;
- }
- </style>
|