添加 cl-form 组件

This commit is contained in:
icssoa
2025-08-06 16:30:33 +08:00
parent 42fd445248
commit ae566bf919
5 changed files with 250 additions and 64 deletions

View File

@@ -595,6 +595,22 @@ export function base64ToBlob(data: string, type: string = "image/jpeg"): Blob {
// #endif
}
/**
* 检查两个值是否相等
* @param a 值1
* @param b 值2
* @returns 是否相等
*/
export function isEqual(a: any, b: any): boolean {
if (isObject(a) && isObject(b)) {
return isEqual(JSON.stringify(a), JSON.stringify(b));
} else if (isArray(a) && isArray(b)) {
return isEqual(JSON.stringify(a), JSON.stringify(b));
}
return a == b;
}
/**
* 检查是否为小程序环境
* @returns 是否为小程序环境

View File

@@ -111,11 +111,11 @@ export const parseClass = (data: any): string => {
* @returns 转换后的UTSJSONObject对象
*/
export function parseToObject<T>(data: T): UTSJSONObject {
// #ifdef APP
// #ifdef APP-ANDROID
return JSON.parseObject(JSON.stringify(data)!)!;
// #endif
// #ifndef APP
// #ifndef APP-ANDROID
return JSON.parse(JSON.stringify(data)) as UTSJSONObject;
// #endif
}

View File

@@ -12,45 +12,93 @@
:disabled="saving"
label-position="top"
>
<cl-form-item label="头像" prop="avatarUrl" required>
<cl-upload v-model="formData.avatarUrl"></cl-upload>
<cl-form-item prop="avatarUrl">
<cl-upload v-model="formData.avatarUrl" test></cl-upload>
</cl-form-item>
<cl-form-item label="用户名" prop="nickName" required>
<cl-input v-model="formData.nickName" placeholder="请输入用户名"></cl-input>
<cl-form-item :label="t('用户名')" prop="nickName" required>
<cl-input
v-model="formData.nickName"
:placeholder="t('请输入用户名')"
clearable
></cl-input>
</cl-form-item>
<cl-form-item label="邮箱" prop="email">
<cl-input v-model="formData.email" placeholder="请输入邮箱地址"></cl-input>
<cl-form-item :label="t('邮箱')" prop="email" required>
<cl-input
v-model="formData.email"
:placeholder="t('请输入邮箱地址')"
></cl-input>
</cl-form-item>
<cl-form-item label="年龄" prop="age">
<cl-input-number
v-model="formData.age"
:min="18"
:max="50"
></cl-input-number>
<cl-form-item :label="t('身高cm')" prop="height" required>
<cl-slider v-model="formData.height" :max="220" show-value>
<template #value="{ value }">
<cl-text
:pt="{
className: 'text-center w-[120rpx]'
}"
>{{ value }} cm</cl-text
>
</template>
</cl-slider>
</cl-form-item>
<cl-form-item label="性别" prop="gender">
<cl-select
v-model="formData.gender"
:options="options['gender']"
></cl-select>
<cl-form-item :label="t('体重kg')" prop="weight" required>
<cl-slider v-model="formData.weight" :max="150" show-value>
<template #value="{ value }">
<cl-text
:pt="{
className: 'text-center w-[120rpx]'
}"
>{{ value }} kg</cl-text
>
</template>
</cl-slider>
</cl-form-item>
<cl-form-item label="个人简介" prop="description">
<cl-form-item :label="t('标签')" prop="tags" required>
<view class="flex flex-row flex-wrap">
<cl-checkbox
v-model="formData.tags"
v-for="(item, index) in tagsOptions"
:value="index"
:pt="{
className: 'mr-5 mt-2'
}"
>{{ item.label }}</cl-checkbox
>
</view>
</cl-form-item>
<cl-form-item :label="t('性别')" prop="gender" required>
<cl-select v-model="formData.gender" :options="genderOptions"></cl-select>
</cl-form-item>
<cl-form-item :label="t('所在地区')" prop="pca" required>
<cl-cascader v-model="formData.pca" :options="pcaOptions"></cl-cascader>
</cl-form-item>
<cl-form-item :label="t('出生年月')" prop="birthday" required>
<cl-select-date v-model="formData.birthday" type="date"></cl-select-date>
</cl-form-item>
<cl-form-item :label="t('个人简介')" prop="description">
<cl-textarea
v-model="formData.description"
placeholder="请输入个人简介"
:placeholder="t('请输入个人简介')"
:maxlength="200"
></cl-textarea>
</cl-form-item>
<cl-form-item :label="t('公开状态')">
<cl-switch v-model="formData.isPublic"></cl-switch>
</cl-form-item>
</cl-form>
</demo-item>
<demo-item>
<cl-text :pt="{ className: '!text-sm p-2' }">{{
<cl-text pre-wrap :pt="{ className: '!text-sm p-2' }">{{
JSON.stringify(formData, null, 4)
}}</cl-text>
</demo-item>
@@ -58,13 +106,15 @@
<cl-footer>
<view class="flex flex-row">
<cl-button type="info" :pt="{ className: 'flex-1' }" @click="reset">重置</cl-button>
<cl-button type="info" :pt="{ className: 'flex-1' }" @click="reset">{{
t("重置")
}}</cl-button>
<cl-button
type="primary"
:loading="saving"
:pt="{ className: 'flex-1' }"
@click="submit"
>提交</cl-button
>{{ t("提交") }}</cl-button
>
</view>
</cl-footer>
@@ -72,38 +122,78 @@
</template>
<script setup lang="ts">
import { reactive, ref, type Ref } from "vue";
import { ref, type Ref } from "vue";
import DemoItem from "../components/item.uvue";
import { useForm, useUi, type ClFormRule, type ClSelectOption } from "@/uni_modules/cool-ui";
import {
useCascader,
useForm,
useUi,
type ClFormRule,
type ClSelectOption
} from "@/uni_modules/cool-ui";
import pca from "@/data/pca.json";
import { t } from "@/locale";
import { dayUts } from "@/cool";
const ui = useUi();
const { formRef, validate, clearValidate } = useForm();
const options = reactive({
gender: [
// 性别选项
const genderOptions = [
{
label: "未知",
label: t("未知"),
value: 0
},
{
label: "男",
label: t("男"),
value: 1
},
{
label: "女",
label: t("女"),
value: 2
}
] as ClSelectOption[]
});
] as ClSelectOption[];
// 标签选项
const tagsOptions = [
{
label: t("篮球"),
value: 1
},
{
label: t("足球"),
value: 2
},
{
label: t("羽毛球"),
value: 3
},
{
label: t("乒乓球"),
value: 4
},
{
label: t("游泳"),
value: 5
}
] as ClSelectOption[];
// 地区选项
const pcaOptions = useCascader(pca);
// 自定义表单数据类型
type FormData = {
avatarUrl: string;
nickName: string;
email: string;
age: number;
height: number;
weight: number;
gender: number;
description: string;
pics: string[];
pca: string[];
tags: number[];
birthday: string;
isPublic: boolean;
};
// 表单数据
@@ -111,27 +201,68 @@ const formData = ref<FormData>({
avatarUrl: "",
nickName: "神仙都没用",
email: "",
age: 18,
height: 180,
weight: 70,
gender: 0,
description: "",
pics: []
pca: [],
tags: [1, 2],
birthday: "",
isPublic: false
}) as Ref<FormData>;
// 表单验证规则
const rules = new Map<string, ClFormRule[]>([
["avatarUrl", [{ required: true, message: "头像不能为空" }]],
[
"nickName",
[
{ required: true, message: "用户名不能为空" },
{ min: 3, max: 20, message: "用户名长度在3-20个字符之间" }
{ required: true, message: t("用户名不能为空") },
{ min: 3, max: 20, message: t("用户名长度在3-20个字符之间") }
]
],
[
"email",
[
{ required: true, message: "邮箱不能为空" },
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "邮箱格式不正确" }
{ required: true, message: t("邮箱不能为空") },
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: t("邮箱格式不正确") }
]
],
[
"height",
[
{ required: true, message: t("身高不能为空") },
{ min: 160, max: 190, message: t("身高在160-190cm之间") }
]
],
[
"weight",
[
{ required: true, message: t("体重不能为空") },
{ min: 40, max: 100, message: t("体重在40-100kg之间") }
]
],
[
"tags",
[
{ required: true, message: t("标签不能为空") },
{ min: 1, max: 2, message: t("标签最多选择2个") }
]
],
["gender", [{ required: true, message: t("性别不能为空") }]],
["pca", [{ required: true, message: t("所在地区不能为空") }]],
[
"birthday",
[
{ required: true, message: t("出生年月不能为空") },
{
validator(value) {
if (dayUts(value).isAfter(dayUts("2010-01-01"))) {
return t("出生年月不大于2010-01-01");
}
return true;
}
}
]
]
]);
@@ -139,18 +270,24 @@ const rules = new Map<string, ClFormRule[]>([
// 是否保存中
const saving = ref(false);
// 重置表单数据
function reset() {
formData.value.avatarUrl = "";
formData.value.nickName = "";
formData.value.email = "";
formData.value.age = 18;
formData.value.height = 180;
formData.value.weight = 70;
formData.value.gender = 0;
formData.value.description = "";
formData.value.pics = [];
formData.value.pca = [];
formData.value.tags = [];
formData.value.birthday = "";
formData.value.isPublic = false;
clearValidate();
}
// 提交表单
function submit() {
validate((valid, errors) => {
if (valid) {
@@ -158,7 +295,7 @@ function submit() {
setTimeout(() => {
ui.showToast({
message: "提交成功",
message: t("提交成功"),
icon: "check-line"
});

View File

@@ -42,7 +42,7 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, watch, type PropType } from "vue";
import { parseClass, parsePt } from "@/cool";
import { isEqual, parseClass, parsePt } from "@/cool";
import type { ClFormLabelPosition, PassThroughProps } from "../../types";
import { useForm } from "../../hooks";
@@ -163,11 +163,21 @@ watch(
onMounted(() => {
watch(
computed(() => getValue(props.prop)!),
() => {
computed(() => {
const value = getValue(props.prop);
if (value == null) {
return "";
}
return value;
}),
(val: any, val2: any) => {
if (props.required) {
if (!isEqual(val, val2)) {
validateField(props.prop);
}
}
},
{
deep: true

View File

@@ -67,6 +67,7 @@ const props = defineProps({
}
});
// 透传样式类型
type PassThrough = {
className?: string;
};
@@ -98,8 +99,15 @@ const showMessage = computed(() => props.showMessage);
// 是否禁用整个表单
const disabled = computed(() => props.disabled);
// 错误信息锁定
const errorLock = ref(false);
// 设置字段错误信息
function setError(prop: string, error: string) {
if (errorLock.value) {
return;
}
if (prop != "") {
errors.value.set(prop, error);
}
@@ -171,19 +179,31 @@ function validateRule(value: any | null, rule: ClFormRule): null | string {
// 最小长度验证
if (rule.min != null) {
if (typeof value == "number") {
if ((value as number) < rule.min) {
return rule.message ?? $t(`最小值为{min}`, { min: rule.min });
}
} else {
const len = Array.isArray(value) ? value.length : `${value}`.length;
if (len < rule.min) {
return rule.message ?? $t(`最少需要{min}个字符`, { min: rule.min });
}
}
}
// 最大长度验证
if (rule.max != null) {
if (typeof value == "number") {
if (value > rule.max) {
return rule.message ?? $t(`最大值为{max}`, { max: rule.max });
}
} else {
const len = Array.isArray(value) ? value.length : `${value}`.length;
if (len > rule.max) {
return rule.message ?? $t(`最多允许{max}个字符`, { max: rule.max });
}
}
}
// 正则验证
if (rule.pattern != null) {
@@ -205,8 +225,11 @@ function validateRule(value: any | null, rule: ClFormRule): null | string {
// 清除所有验证
function clearValidate() {
errorLock.value = true;
nextTick(() => {
clearErrors();
errorLock.value = false;
});
}