mirror of
https://gitee.com/pnoker/iot-dc3.git
synced 2026-09-19 10:11:36 +08:00
fix: prevent double-submit race and add inline param name validation
- Move `submitting = true` before `await validate()` in CommandEditForm, EventEditForm, ProviderEditForm, and ModelConfigEditForm to close the race window where two rapid clicks both pass validation and cause duplicate-entry R500 errors. - Add per-row inline validation for paramName (regex: 2-32 chars) and paramCode (required) on command/event param tables, with red border and tooltip error instead of only a console toast.
This commit is contained in:
@@ -490,6 +490,8 @@ export default {
|
||||
nameRequired: 'Command name is required',
|
||||
codeRequired: 'Command code is required',
|
||||
paramRequired: 'Command param name, code, direction, and type are required.',
|
||||
paramNamePattern:
|
||||
'Param name must be 2-32 chars, start with a letter/digit/CJK, and only contain letters, digits, CJK, and -_#@/.|.',
|
||||
paramCodeUnique: 'Command param code must be unique.',
|
||||
},
|
||||
detail: {
|
||||
@@ -554,6 +556,8 @@ export default {
|
||||
nameRequired: 'Event name is required',
|
||||
codeRequired: 'Event code is required',
|
||||
paramRequired: 'Event param name, code, and type are required.',
|
||||
paramNamePattern:
|
||||
'Param name must be 2-32 chars, start with a letter/digit/CJK, and only contain letters, digits, CJK, and -_#@/.|.',
|
||||
paramCodeUnique: 'Event param code must be unique.',
|
||||
},
|
||||
detail: {
|
||||
|
||||
@@ -489,6 +489,7 @@ export default {
|
||||
nameRequired: '指令名称不能为空',
|
||||
codeRequired: '指令标识不能为空',
|
||||
paramRequired: '指令参数名称、标识、方向和类型不能为空。',
|
||||
paramNamePattern: '指令参数名称需2-32位,以字母、数字或中文开头,仅支持字母、数字、中文及-_#@/.|。',
|
||||
paramCodeUnique: '指令参数标识不能重复。',
|
||||
},
|
||||
detail: {
|
||||
@@ -553,6 +554,7 @@ export default {
|
||||
nameRequired: '事件名称不能为空',
|
||||
codeRequired: '事件标识不能为空',
|
||||
paramRequired: '事件参数名称、标识和类型不能为空。',
|
||||
paramNamePattern: '事件参数名称需2-32位,以字母、数字或中文开头,仅支持字母、数字、中文及-_#@/.|。',
|
||||
paramCodeUnique: '事件参数标识不能重复。',
|
||||
},
|
||||
detail: {
|
||||
|
||||
@@ -153,8 +153,14 @@
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
await formRef.value?.validate();
|
||||
if (submitting.value) return;
|
||||
submitting.value = true;
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
submitting.value = false;
|
||||
return;
|
||||
}
|
||||
emit('save', { ...form }, () => {
|
||||
submitting.value = false;
|
||||
visible.value = false;
|
||||
|
||||
@@ -142,8 +142,14 @@
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
await formRef.value?.validate();
|
||||
if (submitting.value) return;
|
||||
submitting.value = true;
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
submitting.value = false;
|
||||
return;
|
||||
}
|
||||
emit('save', { ...form }, () => {
|
||||
submitting.value = false;
|
||||
visible.value = false;
|
||||
|
||||
@@ -59,13 +59,35 @@
|
||||
</div>
|
||||
<el-table :data="reactiveData.params" border max-height="260" size="small">
|
||||
<el-table-column :label="$t('common.name')" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.paramName" clearable />
|
||||
<template #default="{ row, $index }">
|
||||
<el-tooltip
|
||||
:content="paramErrors[$index]?.paramName || ''"
|
||||
:visible="!!paramErrors[$index]?.paramName"
|
||||
placement="top"
|
||||
>
|
||||
<el-input
|
||||
v-model="row.paramName"
|
||||
:class="{ 'is-error': !!paramErrors[$index]?.paramName }"
|
||||
clearable
|
||||
@blur="validateRow($index)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('command.form.code')" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.paramCode" clearable />
|
||||
<template #default="{ row, $index }">
|
||||
<el-tooltip
|
||||
:content="paramErrors[$index]?.paramCode || ''"
|
||||
:visible="!!paramErrors[$index]?.paramCode"
|
||||
placement="top"
|
||||
>
|
||||
<el-input
|
||||
v-model="row.paramCode"
|
||||
:class="{ 'is-error': !!paramErrors[$index]?.paramCode }"
|
||||
clearable
|
||||
@blur="validateRow($index)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('command.form.direction')" min-width="130">
|
||||
@@ -188,15 +210,42 @@
|
||||
paramLoading: false,
|
||||
});
|
||||
|
||||
const PARAM_NAME_RE = /^[A-Za-z0-9一-龥][A-Za-z0-9一-龥\-_#@/.|]{1,31}$/;
|
||||
|
||||
const rules: FormRules = {
|
||||
commandName: [{ required: true, message: t('command.form.nameRequired'), trigger: 'blur' }],
|
||||
};
|
||||
|
||||
type RowErrors = { paramName?: string; paramCode?: string };
|
||||
const paramErrors = reactive<RowErrors[]>([]);
|
||||
|
||||
const validateRow = (index: number) => {
|
||||
const row = reactiveData.params[index];
|
||||
if (!row) return;
|
||||
const errors: RowErrors = {};
|
||||
const name = String(row.paramName || '').trim();
|
||||
const code = String(row.paramCode || '').trim();
|
||||
if (!name) {
|
||||
errors.paramName = t('command.form.paramRequired');
|
||||
} else if (!PARAM_NAME_RE.test(name)) {
|
||||
errors.paramName = t('command.form.paramNamePattern');
|
||||
}
|
||||
if (!code) {
|
||||
errors.paramCode = t('command.form.paramRequired');
|
||||
}
|
||||
paramErrors[index] = errors;
|
||||
};
|
||||
|
||||
const clearParamErrors = () => {
|
||||
paramErrors.splice(0, paramErrors.length);
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
reactiveData.form = { ...reactiveData.originalForm };
|
||||
reactiveData.params = cloneParams(reactiveData.originalParams);
|
||||
reactiveData.submitting = false;
|
||||
formRef.value?.clearValidate();
|
||||
clearParamErrors();
|
||||
};
|
||||
|
||||
const rowKey = () => `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
@@ -239,21 +288,30 @@
|
||||
};
|
||||
});
|
||||
|
||||
const validateParams = (params: CommandParamRecord[]) => {
|
||||
const validateParams = (params: CommandParamRecord[]): boolean => {
|
||||
clearParamErrors();
|
||||
let valid = true;
|
||||
const codes = new Set<string>();
|
||||
for (let i = 0; i < reactiveData.params.length; i++) {
|
||||
validateRow(i);
|
||||
}
|
||||
for (const item of params) {
|
||||
const code = String(item.paramCode || '').trim();
|
||||
if (!item.paramName || !code || !item.paramDirectionFlag || !item.paramTypeFlag) {
|
||||
failMessage(t('command.form.paramRequired'));
|
||||
return false;
|
||||
valid = false;
|
||||
}
|
||||
if (item.paramName && !PARAM_NAME_RE.test(item.paramName)) {
|
||||
failMessage(t('command.form.paramNamePattern'));
|
||||
valid = false;
|
||||
}
|
||||
if (codes.has(code)) {
|
||||
failMessage(t('command.form.paramCodeUnique'));
|
||||
return false;
|
||||
valid = false;
|
||||
}
|
||||
codes.add(code);
|
||||
}
|
||||
return true;
|
||||
return valid;
|
||||
};
|
||||
|
||||
const addParamRow = () => {
|
||||
@@ -262,6 +320,7 @@
|
||||
|
||||
const removeParamRow = (index: number) => {
|
||||
reactiveData.params.splice(index, 1);
|
||||
paramErrors.splice(index, 1);
|
||||
};
|
||||
|
||||
const show = (profileId = '') => {
|
||||
@@ -271,6 +330,7 @@
|
||||
reactiveData.form = { ...emptyForm };
|
||||
reactiveData.originalParams = [];
|
||||
reactiveData.params = [];
|
||||
clearParamErrors();
|
||||
reactiveData.visible = true;
|
||||
};
|
||||
|
||||
@@ -290,6 +350,7 @@
|
||||
reactiveData.form = { ...initial };
|
||||
reactiveData.originalParams = [];
|
||||
reactiveData.params = [];
|
||||
clearParamErrors();
|
||||
reactiveData.visible = true;
|
||||
if (row.id) {
|
||||
reactiveData.paramLoading = true;
|
||||
@@ -312,9 +373,13 @@
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
const valid = await formRef.value?.validate().catch(() => false);
|
||||
if (!valid) return;
|
||||
if (reactiveData.submitting) return;
|
||||
reactiveData.submitting = true;
|
||||
const valid = await formRef.value?.validate().catch(() => false);
|
||||
if (!valid) {
|
||||
reactiveData.submitting = false;
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
...reactiveData.form,
|
||||
timeout: normalizeCommandTimeoutSeconds(reactiveData.form.timeout) ?? 30,
|
||||
@@ -333,3 +398,9 @@
|
||||
|
||||
defineExpose({ show, showEdit });
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.is-error .el-input__wrapper {
|
||||
box-shadow: 0 0 0 1px var(--el-color-danger) inset !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -56,13 +56,35 @@
|
||||
</div>
|
||||
<el-table :data="reactiveData.params" border max-height="260" size="small">
|
||||
<el-table-column :label="$t('common.name')" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.paramName" clearable />
|
||||
<template #default="{ row, $index }">
|
||||
<el-tooltip
|
||||
:content="paramErrors[$index]?.paramName || ''"
|
||||
:visible="!!paramErrors[$index]?.paramName"
|
||||
placement="top"
|
||||
>
|
||||
<el-input
|
||||
v-model="row.paramName"
|
||||
:class="{ 'is-error': !!paramErrors[$index]?.paramName }"
|
||||
clearable
|
||||
@blur="validateRow($index)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('eventDefinition.form.code')" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.paramCode" clearable />
|
||||
<template #default="{ row, $index }">
|
||||
<el-tooltip
|
||||
:content="paramErrors[$index]?.paramCode || ''"
|
||||
:visible="!!paramErrors[$index]?.paramCode"
|
||||
placement="top"
|
||||
>
|
||||
<el-input
|
||||
v-model="row.paramCode"
|
||||
:class="{ 'is-error': !!paramErrors[$index]?.paramCode }"
|
||||
clearable
|
||||
@blur="validateRow($index)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('eventDefinition.form.type')" min-width="140">
|
||||
@@ -150,15 +172,42 @@
|
||||
paramLoading: false,
|
||||
});
|
||||
|
||||
const PARAM_NAME_RE = /^[A-Za-z0-9一-龥][A-Za-z0-9一-龥\-_#@/.|]{1,31}$/;
|
||||
|
||||
const rules: FormRules = {
|
||||
eventName: [{ required: true, message: t('eventDefinition.form.nameRequired'), trigger: 'blur' }],
|
||||
};
|
||||
|
||||
type RowErrors = { paramName?: string; paramCode?: string };
|
||||
const paramErrors = reactive<RowErrors[]>([]);
|
||||
|
||||
const validateRow = (index: number) => {
|
||||
const row = reactiveData.params[index];
|
||||
if (!row) return;
|
||||
const errors: RowErrors = {};
|
||||
const name = String(row.paramName || '').trim();
|
||||
const code = String(row.paramCode || '').trim();
|
||||
if (!name) {
|
||||
errors.paramName = t('eventDefinition.form.paramRequired');
|
||||
} else if (!PARAM_NAME_RE.test(name)) {
|
||||
errors.paramName = t('eventDefinition.form.paramNamePattern');
|
||||
}
|
||||
if (!code) {
|
||||
errors.paramCode = t('eventDefinition.form.paramRequired');
|
||||
}
|
||||
paramErrors[index] = errors;
|
||||
};
|
||||
|
||||
const clearParamErrors = () => {
|
||||
paramErrors.splice(0, paramErrors.length);
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
reactiveData.form = { ...reactiveData.originalForm };
|
||||
reactiveData.params = cloneParams(reactiveData.originalParams);
|
||||
reactiveData.submitting = false;
|
||||
formRef.value?.clearValidate();
|
||||
clearParamErrors();
|
||||
};
|
||||
|
||||
const rowKey = () => `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
@@ -194,21 +243,30 @@
|
||||
};
|
||||
});
|
||||
|
||||
const validateParams = (params: EventParamRecord[]) => {
|
||||
const validateParams = (params: EventParamRecord[]): boolean => {
|
||||
clearParamErrors();
|
||||
let valid = true;
|
||||
const codes = new Set<string>();
|
||||
for (let i = 0; i < reactiveData.params.length; i++) {
|
||||
validateRow(i);
|
||||
}
|
||||
for (const item of params) {
|
||||
const code = String(item.paramCode || '').trim();
|
||||
if (!item.paramName || !code || !item.paramTypeFlag) {
|
||||
failMessage(t('eventDefinition.form.paramRequired'));
|
||||
return false;
|
||||
valid = false;
|
||||
}
|
||||
if (item.paramName && !PARAM_NAME_RE.test(item.paramName)) {
|
||||
failMessage(t('eventDefinition.form.paramNamePattern'));
|
||||
valid = false;
|
||||
}
|
||||
if (codes.has(code)) {
|
||||
failMessage(t('eventDefinition.form.paramCodeUnique'));
|
||||
return false;
|
||||
valid = false;
|
||||
}
|
||||
codes.add(code);
|
||||
}
|
||||
return true;
|
||||
return valid;
|
||||
};
|
||||
|
||||
const addParamRow = () => {
|
||||
@@ -217,6 +275,7 @@
|
||||
|
||||
const removeParamRow = (index: number) => {
|
||||
reactiveData.params.splice(index, 1);
|
||||
paramErrors.splice(index, 1);
|
||||
};
|
||||
|
||||
const show = (profileId = '') => {
|
||||
@@ -226,6 +285,7 @@
|
||||
reactiveData.form = { ...emptyForm };
|
||||
reactiveData.originalParams = [];
|
||||
reactiveData.params = [];
|
||||
clearParamErrors();
|
||||
reactiveData.visible = true;
|
||||
};
|
||||
|
||||
@@ -244,6 +304,7 @@
|
||||
reactiveData.form = { ...initial };
|
||||
reactiveData.originalParams = [];
|
||||
reactiveData.params = [];
|
||||
clearParamErrors();
|
||||
reactiveData.visible = true;
|
||||
if (row.id) {
|
||||
reactiveData.paramLoading = true;
|
||||
@@ -266,9 +327,13 @@
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
const valid = await formRef.value?.validate().catch(() => false);
|
||||
if (!valid) return;
|
||||
if (reactiveData.submitting) return;
|
||||
reactiveData.submitting = true;
|
||||
const valid = await formRef.value?.validate().catch(() => false);
|
||||
if (!valid) {
|
||||
reactiveData.submitting = false;
|
||||
return;
|
||||
}
|
||||
const payload = { ...reactiveData.form };
|
||||
const params = normalizeParams();
|
||||
if (!validateParams(params)) {
|
||||
@@ -284,3 +349,9 @@
|
||||
|
||||
defineExpose({ show, showEdit });
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.is-error .el-input__wrapper {
|
||||
box-shadow: 0 0 0 1px var(--el-color-danger) inset !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user