Procházet zdrojové kódy

添加 cl-form 组件

icssoa před 7 měsíci
rodič
revize
ae566bf919

+ 16 - 0
cool/utils/comm.ts

@@ -596,6 +596,22 @@ export function base64ToBlob(data: string, type: string = "image/jpeg"): Blob {
 }
 
 /**
+ * 检查两个值是否相等
+ * @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 是否为小程序环境
  */

+ 2 - 2
cool/utils/parse.ts

@@ -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
 }

+ 189 - 52
pages/demo/form/form.uvue

@@ -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: [
-		{
-			label: "未知",
-			value: 0
-		},
-		{
-			label: "男",
-			value: 1
-		},
-		{
-			label: "女",
-			value: 2
-		}
-	] as ClSelectOption[]
-});
+// 性别选项
+const genderOptions = [
+	{
+		label: t("未知"),
+		value: 0
+	},
+	{
+		label: t("男"),
+		value: 1
+	},
+	{
+		label: t("女"),
+		value: 2
+	}
+] 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"
 				});
 

+ 14 - 4
uni_modules/cool-ui/components/cl-form-item/cl-form-item.uvue

@@ -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,10 +163,20 @@ 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) {
-				validateField(props.prop);
+				if (!isEqual(val, val2)) {
+					validateField(props.prop);
+				}
 			}
 		},
 		{

+ 29 - 6
uni_modules/cool-ui/components/cl-form/cl-form.uvue

@@ -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,17 +179,29 @@ function validateRule(value: any | null, rule: ClFormRule): null | string {
 
 	// 最小长度验证
 	if (rule.min != null) {
-		const len = Array.isArray(value) ? value.length : `${value}`.length;
-		if (len < rule.min) {
-			return rule.message ?? $t(`最少需要{min}个字符`, { min: rule.min });
+		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) {
-		const len = Array.isArray(value) ? value.length : `${value}`.length;
-		if (len > rule.max) {
-			return rule.message ?? $t(`最多允许{max}个字符`, { max: rule.max });
+		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 });
+			}
 		}
 	}
 
@@ -205,8 +225,11 @@ function validateRule(value: any | null, rule: ClFormRule): null | string {
 
 // 清除所有验证
 function clearValidate() {
+	errorLock.value = true;
+
 	nextTick(() => {
 		clearErrors();
+		errorLock.value = false;
 	});
 }