fix: 防止 FAQ 表单重复提交
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user