fix: 防止 FAQ 表单重复提交

This commit is contained in:
zxr
2026-07-22 02:54:26 +08:00
parent e43ada5c7f
commit 51f938fbfc
2 changed files with 88 additions and 41 deletions

View File

@@ -3,9 +3,9 @@
:visible="visible"
:width="760"
:title="faq ? '编辑 FAQ' : '新建 FAQ'"
:closable="!submitting"
:mask-closable="!submitting"
:esc-to-close="!submitting"
:closable="!busy"
:mask-closable="!busy"
:esc-to-close="!busy"
:on-before-cancel="canClose"
unmount-on-close
@cancel="close"
@@ -55,6 +55,7 @@
<a-form-item label="子分类" field="sub_category">
<a-input v-model="form.sub_category" placeholder="请输入子分类或补充分类说明" :max-length="100" allow-clear />
</a-form-item>
<a-alert v-if="originalCategoryUnavailable" type="warning">原分类已停用或不可用请重新选择</a-alert>
</section>
<section class="form-section">
@@ -113,15 +114,15 @@
<template #footer>
<a-space>
<a-button :disabled="submitting" @click="close">取消</a-button>
<a-button type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit">保存</a-button>
<a-button :disabled="busy" @click="close">取消</a-button>
<a-button type="primary" :loading="busy" :disabled="busy" @click="handleSubmit">保存</a-button>
</a-space>
</template>
</a-drawer>
</template>
<script lang="ts" setup>
import { reactive, ref, watch } from 'vue'
import { computed, reactive, ref, watch } from 'vue'
import { Message, type FormInstance } from '@arco-design/web-vue'
import { faqPriorityOptions, faqProblemTypeOptions, type Faq, type FaqFormData } from '@/api/kb/faq'
@@ -149,10 +150,13 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<{
(event: 'update:visible', visible: boolean): void
(event: 'submit', values: FaqFormData): void
(event: 'submit', values: FaqFormData, done: () => void): void
}>()
const formRef = ref<FormInstance>()
const validating = ref(false)
const originalCategoryUnavailable = ref(false)
const busy = computed(() => validating.value || props.submitting)
type FaqEditorForm = Omit<FaqFormData, 'category_id'> & { category_id?: number }
const form = reactive<FaqEditorForm>(createEmptyForm())
@@ -180,6 +184,8 @@ function createEmptyForm(): FaqEditorForm {
/** 打开抽屉时载入当前 FAQ关闭后由 unmount-on-close 清理视图。 */
function resetForm() {
const faq = props.faq
const categoryAvailable = Boolean(faq && props.categories.some((category) => category.id === faq.category_id))
originalCategoryUnavailable.value = Boolean(faq && !categoryAvailable)
Object.assign(
form,
faq
@@ -187,7 +193,7 @@ function resetForm() {
question: faq.question,
answer: faq.answer,
priority: faq.priority || 'medium',
category_id: faq.category_id,
category_id: categoryAvailable ? faq.category_id : undefined,
sub_category: faq.sub_category || '',
problem_type: faq.problem_type || '',
solution: faq.solution || '',
@@ -201,6 +207,7 @@ function resetForm() {
}
: createEmptyForm()
)
validating.value = false
formRef.value?.clearValidate()
}
@@ -208,48 +215,84 @@ watch(
() => props.visible,
(visible) => {
if (visible) resetForm()
else validating.value = false
}
)
watch(
() => props.categories,
() => {
const faq = props.faq
if (!props.visible || !faq || !originalCategoryUnavailable.value) return
if (props.categories.some((category) => category.id === faq.category_id)) {
form.category_id = faq.category_id
originalCategoryUnavailable.value = false
}
},
{ deep: true }
)
watch(
() => form.category_id,
(categoryId) => {
if (categoryId && props.categories.some((category) => category.id === categoryId)) {
originalCategoryUnavailable.value = false
}
}
)
/** 校验通过后交由父页面保存,接口成功前不关闭抽屉。 */
async function handleSubmit() {
if (props.submitting) return
const errors = await formRef.value?.validate()
if (errors) return
if (busy.value) return
validating.value = true
let handedOff = false
try {
const errors = await formRef.value?.validate()
if (errors) return
const question = form.question.trim()
const answer = form.answer.trim()
const categoryId = form.category_id
if (!question || !answer || !categoryId) {
Message.warning('问题、答案和公共分类为必填项')
return
const question = form.question.trim()
const answer = form.answer.trim()
const categoryId = form.category_id
if (!question || !answer || !categoryId) {
Message.warning('问题、答案和公共分类为必填项')
return
}
handedOff = true
emit(
'submit',
{
...form,
question,
answer,
category_id: categoryId,
sub_category: form.sub_category.trim(),
problem_type: form.problem_type.trim(),
solution: form.solution.trim(),
process_steps: form.process_steps.trim(),
prerequisites: form.prerequisites.trim(),
keywords: form.keywords.trim(),
tags: form.tags.trim(),
detection_point_ids: form.detection_point_ids.trim(),
applicable_scope: form.applicable_scope.trim(),
remarks: form.remarks.trim(),
},
() => {
validating.value = false
}
)
} finally {
if (!handedOff) validating.value = false
}
emit('submit', {
...form,
question,
answer,
category_id: categoryId,
sub_category: form.sub_category.trim(),
problem_type: form.problem_type.trim(),
solution: form.solution.trim(),
process_steps: form.process_steps.trim(),
prerequisites: form.prerequisites.trim(),
keywords: form.keywords.trim(),
tags: form.tags.trim(),
detection_point_ids: form.detection_point_ids.trim(),
applicable_scope: form.applicable_scope.trim(),
remarks: form.remarks.trim(),
})
}
/** 提交中拒绝关闭,避免中断请求或丢失输入。 */
function close() {
if (!props.submitting) emit('update:visible', false)
if (!busy.value) emit('update:visible', false)
}
function canClose() {
return !props.submitting
return !busy.value
}
function handleVisibleChange(visible: boolean) {

View File

@@ -437,15 +437,18 @@ function openEditFromDetail(faq: Faq) {
}
/** 保存表单;仅接口成功后关闭抽屉。 */
async function saveFaq(values: FaqFormData) {
const target = editingFaq.value
if ((!target && !canCreate.value) || (target && !canEdit(target))) {
Message.error('无操作权限')
async function saveFaq(values: FaqFormData, done: () => void) {
if (formSubmitting.value) {
done()
return
}
formSubmitting.value = true
const target = editingFaq.value
try {
if ((!target && !canCreate.value) || (target && !canEdit(target))) {
Message.error('无操作权限')
return
}
const reply = target ? await updateFaq({ id: target.id, ...values }) : await createFaq(values)
if (!showReplyResult(reply, target ? '保存 FAQ 失败' : '创建 FAQ 失败')) return
Message.success(target ? 'FAQ 已保存' : 'FAQ 已创建')
@@ -458,6 +461,7 @@ async function saveFaq(values: FaqFormData) {
showRequestError(error, target ? '保存 FAQ 失败' : '创建 FAQ 失败')
} finally {
formSubmitting.value = false
done()
}
}