表单项添加红色边框的错误提示

This commit is contained in:
icssoa
2025-08-11 16:25:25 +08:00
parent 81fa708e0a
commit a5fde75b8a
7 changed files with 76 additions and 18 deletions

View File

@@ -27,7 +27,7 @@
</view>
</view>
<view class="cl-form-item__error" v-if="hasError && showMessage">
<view class="cl-form-item__error" v-if="isError && showMessage">
<slot name="error" :error="errorText">
<cl-text
color="error"
@@ -121,7 +121,7 @@ const errorText = computed<string>(() => {
});
// 是否有错误
const hasError = computed<boolean>(() => {
const isError = computed<boolean>(() => {
return errorText.value != "";
});
@@ -194,6 +194,10 @@ onMounted(() => {
onUnmounted(() => {
removeField(props.prop);
});
defineExpose({
prop: props.prop
});
</script>
<style lang="scss" scoped>

View File

@@ -15,7 +15,7 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch, type PropType } from "vue";
import { isEmpty, parsePt, parseToObject } from "@/cool";
import { isEmpty, isString, parsePt, parseToObject } from "@/cool";
import type { ClFormLabelPosition, ClFormRule, ClFormValidateError } from "../../types";
import { $t, t } from "@/locale";
@@ -167,13 +167,17 @@ function getRule(prop: string): ClFormRule[] {
function validateRule(value: any | null, rule: ClFormRule): null | string {
// 必填验证
if (rule.required == true) {
if (value == null || value == "" || (Array.isArray(value) && value.length == 0)) {
if (
value == null ||
(value == "" && isString(value)) ||
(Array.isArray(value) && value.length == 0)
) {
return rule.message ?? t("此字段为必填项");
}
}
// 如果值为空且不是必填,直接通过
if ((value == null || value == "") && rule.required != true) {
if ((value == null || (value == "" && isString(value))) && rule.required != true) {
return null;
}

View File

@@ -7,7 +7,8 @@
'is-dark': isDark,
'cl-input--border': border,
'cl-input--focus': isFocus,
'cl-input--disabled': isDisabled
'cl-input--disabled': isDisabled,
'cl-input--error': isError
}
]"
@tap="onTap"
@@ -85,7 +86,7 @@ import type { ClInputType, PassThroughProps } from "../../types";
import { isDark, parseClass, parsePt } from "@/cool";
import type { ClIconProps } from "../cl-icon/props";
import { t } from "@/locale";
import { useForm } from "../../hooks";
import { useForm, useFormItem } from "../../hooks";
defineOptions({
name: "cl-input"
@@ -205,6 +206,9 @@ const emit = defineEmits([
// cl-form 上下文
const { disabled } = useForm();
// cl-form-item 上下文
const { isError } = useFormItem();
// 是否禁用
const isDisabled = computed(() => {
return disabled.value || props.disabled;
@@ -365,6 +369,10 @@ defineExpose({
}
}
&--error {
@apply border-red-500;
}
&.is-dark {
@apply bg-surface-800;

View File

@@ -5,7 +5,8 @@
{
'is-dark': isDark,
'cl-select-trigger--disabled': isDisabled,
'cl-select-trigger--focus': focus
'cl-select-trigger--focus': focus,
'cl-select-trigger--error': isError
},
pt.className
]"
@@ -62,7 +63,7 @@ import type { ClIconProps } from "../cl-icon/props";
import { isDark, parseClass, parsePt } from "@/cool";
import { t } from "@/locale";
import type { PassThroughProps } from "../../types";
import { useForm } from "../../hooks";
import { useForm, useFormItem } from "../../hooks";
defineOptions({
name: "cl-select-trigger"
@@ -104,8 +105,12 @@ const props = defineProps({
const emit = defineEmits(["open", "clear"]);
// cl-form 上下文
const { disabled } = useForm();
// cl-form-item 上下文
const { isError } = useFormItem();
// 是否禁用
const isDisabled = computed(() => {
return disabled.value || props.disabled;
@@ -172,6 +177,10 @@ function open() {
}
}
&--error {
@apply border-red-500;
}
&.is-dark {
@apply border-surface-700 bg-surface-800;

View File

@@ -7,7 +7,8 @@
'is-dark': isDark,
'cl-textarea--border': border,
'cl-textarea--focus': isFocus,
'cl-textarea--disabled': isDisabled
'cl-textarea--disabled': isDisabled,
'cl-textarea--error': isError
}
]"
@tap="onTap"
@@ -63,7 +64,7 @@ import { parsePt, parseRpx } from "@/cool";
import type { PassThroughProps } from "../../types";
import { isDark } from "@/cool";
import { t } from "@/locale";
import { useForm } from "../../hooks";
import { useForm, useFormItem } from "../../hooks";
defineOptions({
name: "cl-textarea"
@@ -225,6 +226,9 @@ const emit = defineEmits([
// cl-form 上下文
const { disabled } = useForm();
// cl-form-item 上下文
const { isError } = useFormItem();
// 是否禁用
const isDisabled = computed(() => {
return disabled.value || props.disabled;
@@ -360,6 +364,10 @@ defineExpose({
@apply bg-surface-100 opacity-70;
}
&--error {
@apply border-red-500;
}
&.is-dark {
@apply bg-surface-800;

View File

@@ -2,7 +2,7 @@ import { computed, ref, type ComputedRef } from "vue";
import type { ClFormRule, ClFormValidateError } from "../types";
import { useParent } from "@/cool";
export class FormValidate {
export class Form {
public formRef = ref<ClFormComponentPublicInstance | null>(null);
public disabled: ComputedRef<boolean>;
@@ -85,8 +85,35 @@ export class FormValidate {
validate = (callback: (valid: boolean, errors: ClFormValidateError[]) => void): void => {
this.formRef.value!.validate(callback);
};
// 检查字段是否存在错误
isError = (prop: string): boolean => {
return this.formRef.value!.getError(prop) != "";
};
}
export const useForm = (): FormValidate => {
return new FormValidate();
class FormItem {
public isError: ComputedRef<boolean>;
constructor() {
const { isError } = new Form();
const ClFormItem = useParent<ClFormItemComponentPublicInstance>("cl-form-item");
// 监听表单字段是否验证错误
this.isError = computed<boolean>(() => {
if (ClFormItem == null) {
return false;
}
return isError(ClFormItem.prop);
});
}
}
export const useForm = (): Form => {
return new Form();
};
export const useFormItem = (): FormItem => {
return new FormItem();
};

View File

@@ -189,8 +189,6 @@ declare type ClFormComponentPublicInstance = {
};
declare type ClFormItemComponentPublicInstance = {
validate: () => Promise<boolean>;
clearValidate: () => void;
hasError: boolean;
currentError: string;
prop: string;
isError: boolean;
};