fix: add missing form validation rules and centralize regex patterns

- Add required validation for command type, call type, timeout, event type/level, menu type/level, temperature range
- Add phone (11-digit) and email format validation on user form
- Centralize NAME_PATTERN, AUTH_NAME_PATTERN, PHONE_PATTERN, EMAIL_PATTERN into formRuleUtil and align with backend patterns
- Apply centralized patterns across all settings edit forms (group, label, menu, resource, role, user, command, event, model config)
This commit is contained in:
pnoker
2026-05-27 00:28:00 +08:00
parent 76bd96cf07
commit ebf157b43a
12 changed files with 105 additions and 21 deletions
+10
View File
@@ -488,6 +488,9 @@ export default {
defaultValue: 'Default',
enabled: 'Enabled',
nameRequired: 'Command name is required',
commandTypeRequired: 'Please select command type',
callTypeRequired: 'Please select call type',
timeoutRequired: 'Please enter timeout',
codeRequired: 'Command code is required',
paramRequired: 'Command param name, code, direction, and type are required.',
paramNamePattern:
@@ -554,6 +557,8 @@ export default {
type: 'Type',
enabled: 'Enabled',
nameRequired: 'Event name is required',
eventTypeRequired: 'Please select event type',
eventLevelRequired: 'Please select event level',
codeRequired: 'Event code is required',
paramRequired: 'Event param name, code, and type are required.',
paramNamePattern:
@@ -971,7 +976,9 @@ export default {
userNamePlaceholder: 'Enter user name',
nickNamePlaceholder: 'Enter nickname',
phonePlaceholder: 'Enter phone number',
phoneFormat: 'Please enter a valid 11-digit phone number',
emailPlaceholder: 'Enter email',
emailFormat: 'Please enter a valid email format',
confirmDelete: 'Are you sure to delete this user? This action cannot be undone!',
assignSaved: 'Role assignment saved',
empty: 'No user data!',
@@ -1067,6 +1074,8 @@ export default {
titleZhPlaceholder: 'Enter Chinese display title',
titleEnPlaceholder: 'Enter English display title',
parentMenuIdPlaceholder: 'Optional, default top-level',
menuTypeRequired: 'Please select menu type',
menuLevelRequired: 'Please select menu level',
confirmDelete: 'Are you sure to delete this menu? This action cannot be undone!',
empty: 'No menu data!',
},
@@ -1129,6 +1138,7 @@ export default {
modelRequired: 'Model is required',
providerRequired: 'Provider is required',
maxTokensRequired: 'Max tokens is required',
temperatureRange: 'Temperature must be between 0.0 and 2.0',
nameRequired: 'Name is required',
baseUrlRequired: 'Base URL is required',
confirmDeleteModel: 'Delete model {name}?',
+10
View File
@@ -487,6 +487,9 @@ export default {
defaultValue: '默认值',
enabled: '启用',
nameRequired: '指令名称不能为空',
commandTypeRequired: '请选择指令类型',
callTypeRequired: '请选择调用类型',
timeoutRequired: '请输入超时时间',
codeRequired: '指令标识不能为空',
paramRequired: '指令参数名称、标识、方向和类型不能为空。',
paramNamePattern: '指令参数名称需2-32位,以字母、数字或中文开头,仅支持字母、数字、中文及-_#@/.|。',
@@ -552,6 +555,8 @@ export default {
type: '类型',
enabled: '启用',
nameRequired: '事件名称不能为空',
eventTypeRequired: '请选择事件类型',
eventLevelRequired: '请选择事件等级',
codeRequired: '事件标识不能为空',
paramRequired: '事件参数名称、标识和类型不能为空。',
paramNamePattern: '事件参数名称需2-32位,以字母、数字或中文开头,仅支持字母、数字、中文及-_#@/.|。',
@@ -968,7 +973,9 @@ export default {
userNamePlaceholder: '请输入用户名',
nickNamePlaceholder: '请输入昵称',
phonePlaceholder: '请输入手机号',
phoneFormat: '请输入正确的11位手机号',
emailPlaceholder: '请输入邮箱',
emailFormat: '请输入正确的邮箱格式',
confirmDelete: '是否确定删除该用户? 该操作不可恢复!',
assignSaved: '角色分配已保存',
empty: '暂无用户数据!',
@@ -1064,6 +1071,8 @@ export default {
titleZhPlaceholder: '请输入中文显示名称',
titleEnPlaceholder: '请输入英文显示名称',
parentMenuIdPlaceholder: '选填, 默认顶级菜单',
menuTypeRequired: '请选择菜单类型',
menuLevelRequired: '请选择菜单层级',
confirmDelete: '是否确定删除该菜单? 该操作不可恢复!',
empty: '暂无菜单数据!',
},
@@ -1126,6 +1135,7 @@ export default {
modelRequired: '模型不能为空',
providerRequired: '供应商不能为空',
maxTokensRequired: '最大词元不能为空',
temperatureRange: '温度值需在 0.0~2.0 之间',
nameRequired: '名称不能为空',
baseUrlRequired: 'Base URL 不能为空',
confirmDeleteModel: '是否删除模型 {name}?',
+8 -1
View File
@@ -17,7 +17,14 @@
import type { FormItemRule } from 'element-plus';
import type { ComposerTranslation } from 'vue-i18n';
export const NAME_PATTERN = /^[A-Za-z0-9\u4e00-\u9fa5][A-Za-z0-9\u4e00-\u9fa5-_]*$/;
/** Backend Pattern: ^[A-Za-z0-9\u4e00-\u9fa5][A-Za-z0-9\u4e00-\u9fa5-_#@/.|]{1,31}$ */
export const NAME_PATTERN = /^[A-Za-z0-9\u4e00-\u9fa5][A-Za-z0-9\u4e00-\u9fa5\-_#@/.|]{1,31}$/;
/** Backend Pattern: ^[A-Za-z0-9][A-Za-z0-9-_#@/.|]{1,31}$ (no Chinese) */
export const AUTH_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9\-_#@/.|]{1,31}$/;
/** Backend Pattern: ^1([3-9])\d{9}$ */
export const PHONE_PATTERN = /^1([3-9])\d{9}$/;
/** Backend Pattern: ^[A-Za-z0-9_.-]+@[A-Za-z0-9]+\.[A-Za-z0-9]+$ */
export const EMAIL_PATTERN = /^[A-Za-z0-9_.\-]+@[A-Za-z0-9]+\.[A-Za-z0-9]+$/;
export const DECIMAL_PATTERN = /^-?(([0-9]*(\.[0-9]{1,3})$)|([0-9]+$))/;
export function nameRules(t: ComposerTranslation, entityName: string): FormItemRule[] {
@@ -50,7 +50,7 @@
<el-checkbox v-model="form.reasoning">{{ $t('agentic.capReasoning') }}</el-checkbox>
</div>
</el-form-item>
<el-form-item :label="$t('settings.agentic.temperature')">
<el-form-item :label="$t('settings.agentic.temperature')" prop="temperature">
<el-slider v-model="form.temperature" :max="2" :min="0" :step="0.1" />
</el-form-item>
<el-form-item :label="$t('settings.agentic.maxTokens')" prop="maxTokens">
@@ -125,6 +125,15 @@
model: [{ required: true, message: t('settings.agentic.modelRequired'), trigger: 'blur' }],
providerId: [{ required: true, message: t('settings.agentic.providerRequired'), trigger: 'change' }],
maxTokens: [{ required: true, message: t('settings.agentic.maxTokensRequired'), trigger: 'blur' }],
temperature: [
{
type: 'number',
min: 0,
max: 2,
message: t('settings.agentic.temperatureRange'),
trigger: 'change',
},
],
}));
const show = () => {
@@ -157,6 +157,7 @@
POINT_TYPE_OPTIONS,
} from '@/config/constant/enums';
import type { CommandForm, CommandParamRecord, CommandRecord } from '@/config/types';
import { NAME_PATTERN } from '@/utils/formRuleUtil';
import { failMessage } from '@/utils/notificationUtil';
import {
callTypeValue,
@@ -210,10 +211,15 @@
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' }],
commandName: [
{ required: true, message: t('command.form.nameRequired'), trigger: 'blur' },
{ min: 2, max: 32, message: t('common.nameLength'), trigger: 'blur' },
{ pattern: NAME_PATTERN, message: t('common.nameFormat'), trigger: 'blur' },
],
commandTypeFlag: [{ required: true, message: t('command.form.commandTypeRequired'), trigger: 'change' }],
callTypeFlag: [{ required: true, message: t('command.form.callTypeRequired'), trigger: 'change' }],
timeout: [{ required: true, message: t('command.form.timeoutRequired'), trigger: 'blur' }],
};
type RowErrors = { paramName?: string; paramCode?: string };
@@ -227,7 +233,7 @@
const code = String(row.paramCode || '').trim();
if (!name) {
errors.paramName = t('command.form.paramRequired');
} else if (!PARAM_NAME_RE.test(name)) {
} else if (!NAME_PATTERN.test(name)) {
errors.paramName = t('command.form.paramNamePattern');
}
if (!code) {
@@ -301,7 +307,7 @@
failMessage(t('command.form.paramRequired'));
valid = false;
}
if (item.paramName && !PARAM_NAME_RE.test(item.paramName)) {
if (item.paramName && !NAME_PATTERN.test(item.paramName)) {
failMessage(t('command.form.paramNamePattern'));
valid = false;
}
@@ -127,6 +127,7 @@
import EnableFlagSegmented from '@/components/segmented/EnableFlagSegmented.vue';
import { EVENT_LEVEL_OPTIONS, EVENT_TYPE_OPTIONS, POINT_TYPE_OPTIONS } from '@/config/constant/enums';
import type { EventForm, EventParamRecord, EventRecord } from '@/config/types';
import { NAME_PATTERN } from '@/utils/formRuleUtil';
import { failMessage } from '@/utils/notificationUtil';
import { enableFlagValue, eventLevelValue, eventTypeValue, pointTypeValue } from '@/utils/thingModelFormatUtil';
@@ -172,10 +173,14 @@
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' }],
eventName: [
{ required: true, message: t('eventDefinition.form.nameRequired'), trigger: 'blur' },
{ min: 2, max: 32, message: t('common.nameLength'), trigger: 'blur' },
{ pattern: NAME_PATTERN, message: t('common.nameFormat'), trigger: 'blur' },
],
eventTypeFlag: [{ required: true, message: t('eventDefinition.form.eventTypeRequired'), trigger: 'change' }],
eventLevelFlag: [{ required: true, message: t('eventDefinition.form.eventLevelRequired'), trigger: 'change' }],
};
type RowErrors = { paramName?: string; paramCode?: string };
@@ -189,7 +194,7 @@
const code = String(row.paramCode || '').trim();
if (!name) {
errors.paramName = t('eventDefinition.form.paramRequired');
} else if (!PARAM_NAME_RE.test(name)) {
} else if (!NAME_PATTERN.test(name)) {
errors.paramName = t('eventDefinition.form.paramNamePattern');
}
if (!code) {
@@ -256,7 +261,7 @@
failMessage(t('eventDefinition.form.paramRequired'));
valid = false;
}
if (item.paramName && !PARAM_NAME_RE.test(item.paramName)) {
if (item.paramName && !NAME_PATTERN.test(item.paramName)) {
failMessage(t('eventDefinition.form.paramNamePattern'));
valid = false;
}
+6 -1
View File
@@ -22,6 +22,7 @@ import { useI18n } from 'vue-i18n';
import EnableFlagSegmented from '@/components/segmented/EnableFlagSegmented.vue';
import { ENTITY_TYPE_OPTIONS } from '@/config/constant/enums';
import type { GroupRecord } from '@/config/types/manager';
import { NAME_PATTERN } from '@/utils/formRuleUtil';
type FormMode = 'add' | 'edit';
@@ -85,7 +86,11 @@ export default defineComponent({
const rules: FormRules = {
groupTypeFlag: [{ required: true, message: t('settings.common.entityTypePlaceholder'), trigger: 'change' }],
groupName: [{ required: true, message: t('settings.group.groupNamePlaceholder'), trigger: 'blur' }],
groupName: [
{ required: true, message: t('settings.group.groupNamePlaceholder'), trigger: 'blur' },
{ min: 2, max: 32, message: t('common.nameLength'), trigger: 'blur' },
{ pattern: NAME_PATTERN, message: t('common.nameFormat'), trigger: 'blur' },
],
};
const excludedIds = computed(() => {
+6 -1
View File
@@ -21,6 +21,7 @@ import { useI18n } from 'vue-i18n';
import EnableFlagSegmented from '@/components/segmented/EnableFlagSegmented.vue';
import { ENTITY_TYPE_OPTIONS } from '@/config/constant/enums';
import type { LabelRecord } from '@/config/types/manager';
import { NAME_PATTERN } from '@/utils/formRuleUtil';
type FormMode = 'add' | 'edit';
@@ -53,7 +54,11 @@ export default defineComponent({
const rules: FormRules = {
entityTypeFlag: [{ required: true, message: t('settings.common.entityTypePlaceholder'), trigger: 'change' }],
labelName: [{ required: true, message: t('settings.label.labelNamePlaceholder'), trigger: 'blur' }],
labelName: [
{ required: true, message: t('settings.label.labelNamePlaceholder'), trigger: 'blur' },
{ min: 2, max: 32, message: t('common.nameLength'), trigger: 'blur' },
{ pattern: NAME_PATTERN, message: t('common.nameFormat'), trigger: 'blur' },
],
};
const reset = () => {
+9 -3
View File
@@ -22,6 +22,7 @@ import { useI18n } from 'vue-i18n';
import EnableFlagSegmented from '@/components/segmented/EnableFlagSegmented.vue';
import { MENU_LEVEL_OPTIONS, MENU_TYPE_OPTIONS } from '@/config/constant/enums';
import { iconMap, iconNames, resolveIcon } from '@/config/constant/icons';
import { AUTH_NAME_PATTERN } from '@/utils/formRuleUtil';
type FormMode = 'add' | 'edit';
@@ -65,12 +66,17 @@ export default defineComponent({
});
const rules: FormRules = {
menuName: [{ required: true, message: t('settings.menu.menuNamePlaceholder'), trigger: 'blur' }],
menuName: [
{ required: true, message: t('settings.menu.menuNamePlaceholder'), trigger: 'blur' },
{ min: 2, max: 32, message: t('common.nameLength'), trigger: 'blur' },
{ pattern: AUTH_NAME_PATTERN, message: t('common.nameFormat'), trigger: 'blur' },
],
menuCode: [{ required: true, message: t('settings.menu.menuCodePlaceholder'), trigger: 'blur' }],
titleZh: [{ required: true, message: t('settings.menu.titleZhPlaceholder'), trigger: 'blur' }],
titleEn: [{ required: true, message: t('settings.menu.titleEnPlaceholder'), trigger: 'blur' }],
menuTypeFlag: [{ required: true, trigger: 'change' }],
menuLevel: [{ required: true, trigger: 'change' }],
menuTypeFlag: [{ required: true, message: t('settings.menu.menuTypeRequired'), trigger: 'change' }],
menuLevel: [{ required: true, message: t('settings.menu.menuLevelRequired'), trigger: 'change' }],
parentMenuId: [{ required: true, message: t('settings.menu.parentMenuIdPlaceholder'), trigger: 'change' }],
};
const parentTreeOptions = computed(() => [{ id: 0, menuName: 'Root', children: props.treeData || [] }]);
+6 -1
View File
@@ -20,6 +20,7 @@ import type { FormInstance, FormRules } from 'element-plus';
import { useI18n } from 'vue-i18n';
import EnableFlagSegmented from '@/components/segmented/EnableFlagSegmented.vue';
import { AUTH_NAME_PATTERN } from '@/utils/formRuleUtil';
type FormMode = 'add' | 'edit';
@@ -75,7 +76,11 @@ export default defineComponent({
});
const rules: FormRules = {
resourceName: [{ required: true, message: t('settings.resource.resourceNamePlaceholder'), trigger: 'blur' }],
resourceName: [
{ required: true, message: t('settings.resource.resourceNamePlaceholder'), trigger: 'blur' },
{ min: 2, max: 32, message: t('common.nameLength'), trigger: 'blur' },
{ pattern: AUTH_NAME_PATTERN, message: t('common.nameFormat'), trigger: 'blur' },
],
parentResourceId: [
{ required: true, message: t('settings.resource.parentResourceIdPlaceholder'), trigger: 'change' },
],
+6 -1
View File
@@ -20,6 +20,7 @@ import type { FormInstance, FormRules } from 'element-plus';
import { useI18n } from 'vue-i18n';
import EnableFlagSegmented from '@/components/segmented/EnableFlagSegmented.vue';
import { AUTH_NAME_PATTERN } from '@/utils/formRuleUtil';
type FormMode = 'add' | 'edit';
@@ -56,7 +57,11 @@ export default defineComponent({
});
const rules: FormRules = {
roleName: [{ required: true, message: t('settings.role.roleNamePlaceholder'), trigger: 'blur' }],
roleName: [
{ required: true, message: t('settings.role.roleNamePlaceholder'), trigger: 'blur' },
{ min: 2, max: 32, message: t('common.nameLength'), trigger: 'blur' },
{ pattern: AUTH_NAME_PATTERN, message: t('common.nameFormat'), trigger: 'blur' },
],
parentRoleId: [{ required: true, message: t('settings.role.parentRoleIdPlaceholder'), trigger: 'change' }],
};
+13 -2
View File
@@ -19,6 +19,7 @@ import type { FormInstance, FormRules } from 'element-plus';
import { useI18n } from 'vue-i18n';
import EnableFlagSegmented from '@/components/segmented/EnableFlagSegmented.vue';
import { AUTH_NAME_PATTERN, EMAIL_PATTERN, NAME_PATTERN, PHONE_PATTERN } from '@/utils/formRuleUtil';
type FormMode = 'add' | 'edit';
@@ -49,8 +50,18 @@ export default defineComponent({
});
const rules: FormRules = {
userName: [{ required: true, message: t('settings.user.userNamePlaceholder'), trigger: 'blur' }],
nickName: [{ required: true, message: t('settings.user.nickNamePlaceholder'), trigger: 'blur' }],
userName: [
{ required: true, message: t('settings.user.userNamePlaceholder'), trigger: 'blur' },
{ min: 2, max: 32, message: t('common.nameLength'), trigger: 'blur' },
{ pattern: AUTH_NAME_PATTERN, message: t('common.nameFormat'), trigger: 'blur' },
],
nickName: [
{ required: true, message: t('settings.user.nickNamePlaceholder'), trigger: 'blur' },
{ min: 2, max: 32, message: t('common.nameLength'), trigger: 'blur' },
{ pattern: NAME_PATTERN, message: t('common.nameFormat'), trigger: 'blur' },
],
phone: [{ pattern: PHONE_PATTERN, message: t('settings.user.phoneFormat'), trigger: 'blur' }],
email: [{ pattern: EMAIL_PATTERN, message: t('settings.user.emailFormat'), trigger: 'blur' }],
};
const reset = () => {