添加 cl-form 组件
This commit is contained in:
@@ -1,22 +1,22 @@
|
|||||||
import { getCurrentInstance } from "vue";
|
import { getCurrentInstance } from "vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取父组件实例
|
* 获取父组件
|
||||||
*
|
* @param name 组件名称
|
||||||
* 用于在子组件中获取父组件实例,以便访问父组件的属性和方法
|
* @example useParent<ClFormComponentPublicInstance>("cl-form")
|
||||||
*
|
* @returns 父组件
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* // 在子组件中使用
|
|
||||||
* const parent = useParent<ParentType>();
|
|
||||||
* // 访问父组件属性
|
|
||||||
* console.log(parent.someProperty);
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @template T 父组件实例的类型
|
|
||||||
* @returns {T} 返回父组件实例
|
|
||||||
*/
|
*/
|
||||||
export function useParent<T>(): T {
|
export function useParent<T>(name: string): T | null {
|
||||||
const { proxy } = getCurrentInstance()!;
|
const { proxy } = getCurrentInstance()!;
|
||||||
return proxy?.$parent as T;
|
|
||||||
|
let p = proxy?.$parent;
|
||||||
|
|
||||||
|
while (p != null) {
|
||||||
|
if (p.$options.name == name) {
|
||||||
|
return p as T | null;
|
||||||
|
}
|
||||||
|
p = p.$parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return p as T | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export function get(object: any, path: string, defaultValue: any | null = null):
|
|||||||
* 设置对象的属性值
|
* 设置对象的属性值
|
||||||
* @example set({a: 1}, 'b', 2) // {a: 1, b: 2}
|
* @example set({a: 1}, 'b', 2) // {a: 1, b: 2}
|
||||||
*/
|
*/
|
||||||
export function set(object: any, key: string, value: any): void {
|
export function set(object: any, key: string, value: any | null): void {
|
||||||
(object as UTSJSONObject)[key] = value;
|
(object as UTSJSONObject)[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ref, type Ref } from "vue";
|
||||||
import { forEach, forInObject, isArray, isObject, isString } from "./comm";
|
import { forEach, forInObject, isArray, isObject, isString } from "./comm";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,6 +105,21 @@ export const parseClass = (data: any): string => {
|
|||||||
return names.join(" ");
|
return names.join(" ");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将自定义类型数据转换为UTSJSONObject对象
|
||||||
|
* @param data 要转换的数据
|
||||||
|
* @returns 转换后的UTSJSONObject对象
|
||||||
|
*/
|
||||||
|
export function parseToObject<T>(data: T): UTSJSONObject {
|
||||||
|
// #ifdef APP
|
||||||
|
return JSON.parseObject(JSON.stringify(data)!)!;
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef APP
|
||||||
|
return JSON.parse(JSON.stringify(data)) as UTSJSONObject;
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将数值或字符串转换为rpx单位的字符串
|
* 将数值或字符串转换为rpx单位的字符串
|
||||||
* @param val 要转换的值,可以是数字或字符串
|
* @param val 要转换的值,可以是数字或字符串
|
||||||
|
|||||||
@@ -112,6 +112,12 @@
|
|||||||
"navigationBarTitleText": "Tag 标签"
|
"navigationBarTitleText": "Tag 标签"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "form/form",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "Form 表单验证"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "form/input",
|
"path": "form/input",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
175
pages/demo/form/form.uvue
Normal file
175
pages/demo/form/form.uvue
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<template>
|
||||||
|
<cl-page>
|
||||||
|
<view class="p-3">
|
||||||
|
<demo-item>
|
||||||
|
<cl-form
|
||||||
|
:pt="{
|
||||||
|
className: 'p-2 pb-0'
|
||||||
|
}"
|
||||||
|
v-model="formData"
|
||||||
|
ref="formRef"
|
||||||
|
:rules="rules"
|
||||||
|
:disabled="saving"
|
||||||
|
label-position="top"
|
||||||
|
>
|
||||||
|
<cl-form-item label="头像" prop="avatarUrl" required>
|
||||||
|
<cl-upload v-model="formData.avatarUrl"></cl-upload>
|
||||||
|
</cl-form-item>
|
||||||
|
|
||||||
|
<cl-form-item label="用户名" prop="nickName" required>
|
||||||
|
<cl-input v-model="formData.nickName" placeholder="请输入用户名"></cl-input>
|
||||||
|
</cl-form-item>
|
||||||
|
|
||||||
|
<cl-form-item label="邮箱" prop="email">
|
||||||
|
<cl-input v-model="formData.email" placeholder="请输入邮箱地址"></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>
|
||||||
|
|
||||||
|
<cl-form-item label="性别" prop="gender">
|
||||||
|
<cl-select
|
||||||
|
v-model="formData.gender"
|
||||||
|
:options="options['gender']"
|
||||||
|
></cl-select>
|
||||||
|
</cl-form-item>
|
||||||
|
|
||||||
|
<cl-form-item label="个人简介" prop="description">
|
||||||
|
<cl-textarea
|
||||||
|
v-model="formData.description"
|
||||||
|
placeholder="请输入个人简介"
|
||||||
|
:maxlength="200"
|
||||||
|
></cl-textarea>
|
||||||
|
</cl-form-item>
|
||||||
|
</cl-form>
|
||||||
|
</demo-item>
|
||||||
|
|
||||||
|
<demo-item>
|
||||||
|
<cl-text :pt="{ className: '!text-sm p-2' }">{{
|
||||||
|
JSON.stringify(formData, null, 4)
|
||||||
|
}}</cl-text>
|
||||||
|
</demo-item>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<cl-footer>
|
||||||
|
<view class="flex flex-row">
|
||||||
|
<cl-button type="info" :pt="{ className: 'flex-1' }" @click="reset">重置</cl-button>
|
||||||
|
<cl-button
|
||||||
|
type="primary"
|
||||||
|
:loading="saving"
|
||||||
|
:pt="{ className: 'flex-1' }"
|
||||||
|
@click="submit"
|
||||||
|
>提交</cl-button
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</cl-footer>
|
||||||
|
</cl-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, type Ref } from "vue";
|
||||||
|
import DemoItem from "../components/item.uvue";
|
||||||
|
import { useForm, useUi, type ClFormRule, type ClSelectOption } from "@/uni_modules/cool-ui";
|
||||||
|
|
||||||
|
const ui = useUi();
|
||||||
|
const { formRef, validate, clearValidate } = useForm();
|
||||||
|
|
||||||
|
const options = reactive({
|
||||||
|
gender: [
|
||||||
|
{
|
||||||
|
label: "未知",
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "男",
|
||||||
|
value: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "女",
|
||||||
|
value: 2
|
||||||
|
}
|
||||||
|
] as ClSelectOption[]
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = {
|
||||||
|
avatarUrl: string;
|
||||||
|
nickName: string;
|
||||||
|
email: string;
|
||||||
|
age: number;
|
||||||
|
gender: number;
|
||||||
|
description: string;
|
||||||
|
pics: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = ref<FormData>({
|
||||||
|
avatarUrl: "",
|
||||||
|
nickName: "神仙都没用",
|
||||||
|
email: "",
|
||||||
|
age: 18,
|
||||||
|
gender: 0,
|
||||||
|
description: "",
|
||||||
|
pics: []
|
||||||
|
}) as Ref<FormData>;
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const rules = new Map<string, ClFormRule[]>([
|
||||||
|
["avatarUrl", [{ required: true, message: "头像不能为空" }]],
|
||||||
|
[
|
||||||
|
"nickName",
|
||||||
|
[
|
||||||
|
{ required: true, message: "用户名不能为空" },
|
||||||
|
{ min: 3, max: 20, message: "用户名长度在3-20个字符之间" }
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"email",
|
||||||
|
[
|
||||||
|
{ required: true, message: "邮箱不能为空" },
|
||||||
|
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "邮箱格式不正确" }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 是否保存中
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
formData.value.avatarUrl = "";
|
||||||
|
formData.value.nickName = "";
|
||||||
|
formData.value.email = "";
|
||||||
|
formData.value.age = 18;
|
||||||
|
formData.value.gender = 0;
|
||||||
|
formData.value.description = "";
|
||||||
|
formData.value.pics = [];
|
||||||
|
|
||||||
|
clearValidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
validate((valid, errors) => {
|
||||||
|
if (valid) {
|
||||||
|
saving.value = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
ui.showToast({
|
||||||
|
message: "提交成功",
|
||||||
|
icon: "check-line"
|
||||||
|
});
|
||||||
|
|
||||||
|
saving.value = false;
|
||||||
|
reset();
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
ui.showToast({
|
||||||
|
message: errors[0].message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -118,6 +118,11 @@ const data = computed<Item[]>(() => {
|
|||||||
{
|
{
|
||||||
label: t("表单组件"),
|
label: t("表单组件"),
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
label: t("表单验证"),
|
||||||
|
icon: "draft-line",
|
||||||
|
path: "/pages/demo/form/form"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t("输入框"),
|
label: t("输入框"),
|
||||||
icon: "input-field",
|
icon: "input-field",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:focus="popupRef?.isOpen"
|
:focus="popupRef?.isOpen"
|
||||||
:text="text"
|
:text="text"
|
||||||
@tap="open"
|
@open="open"
|
||||||
@clear="clear"
|
@clear="clear"
|
||||||
></cl-select-trigger>
|
></cl-select-trigger>
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 获取父组件实例
|
// 获取父组件实例
|
||||||
const parent = useParent<ClRowComponentPublicInstance>();
|
const parent = useParent<ClRowComponentPublicInstance>("cl-row");
|
||||||
|
|
||||||
// 透传类型定义
|
// 透传类型定义
|
||||||
type PassThrough = {
|
type PassThrough = {
|
||||||
@@ -67,7 +67,7 @@ type PassThrough = {
|
|||||||
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
||||||
|
|
||||||
// 计算列的padding,用于实现栅格间隔
|
// 计算列的padding,用于实现栅格间隔
|
||||||
const padding = computed(() => parseRpx(parent.gutter / 2));
|
const padding = computed(() => (parent == null ? "0" : parseRpx(parent.gutter / 2)));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
223
uni_modules/cool-ui/components/cl-form-item/cl-form-item.uvue
Normal file
223
uni_modules/cool-ui/components/cl-form-item/cl-form-item.uvue
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<template>
|
||||||
|
<view class="cl-form-item" :class="[pt.className]">
|
||||||
|
<view class="cl-form-item__inner" :class="[`is-${labelPosition}`, pt.inner?.className]">
|
||||||
|
<view
|
||||||
|
class="cl-form-item__label"
|
||||||
|
:class="[`is-${labelPosition}`, pt.label?.className]"
|
||||||
|
:style="{
|
||||||
|
width: labelPosition != 'top' ? labelWidth : 'auto'
|
||||||
|
}"
|
||||||
|
v-if="label != ''"
|
||||||
|
>
|
||||||
|
<cl-text>{{ label }}</cl-text>
|
||||||
|
|
||||||
|
<cl-text
|
||||||
|
color="error"
|
||||||
|
:pt="{
|
||||||
|
className: 'ml-1'
|
||||||
|
}"
|
||||||
|
v-if="showAsterisk"
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</cl-text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="cl-form-item__content" :class="[pt.content?.className]">
|
||||||
|
<slot></slot>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<slot name="error" :error="errorText" v-if="hasError && showMessage">
|
||||||
|
<cl-text
|
||||||
|
color="error"
|
||||||
|
:pt="{
|
||||||
|
className: parseClass(['mt-2 !text-sm', pt.error?.className])
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ errorText }}
|
||||||
|
</cl-text>
|
||||||
|
</slot>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, onUnmounted, watch, type PropType } from "vue";
|
||||||
|
import { parseClass, parsePt } from "@/cool";
|
||||||
|
import type { ClFormLabelPosition, PassThroughProps } from "../../types";
|
||||||
|
import { useForm } from "../../hooks";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "cl-form-item"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件属性定义
|
||||||
|
const props = defineProps({
|
||||||
|
// 透传样式
|
||||||
|
pt: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
// 字段标签
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
// 字段名称
|
||||||
|
prop: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
// 标签位置
|
||||||
|
labelPosition: {
|
||||||
|
type: String as PropType<ClFormLabelPosition>,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
// 标签宽度
|
||||||
|
labelWidth: {
|
||||||
|
type: String as PropType<string | null>,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
// 是否显示必填星号
|
||||||
|
showAsterisk: {
|
||||||
|
type: Boolean as PropType<boolean | null>,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
// 是否显示错误信息
|
||||||
|
showMessage: {
|
||||||
|
type: Boolean as PropType<boolean | null>,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
// 是否必填
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// cl-form 上下文
|
||||||
|
const { formRef, getError, getValue, validateField, addField, removeField } = useForm();
|
||||||
|
|
||||||
|
// 透传样式类型
|
||||||
|
type PassThrough = {
|
||||||
|
className?: string;
|
||||||
|
inner?: PassThroughProps;
|
||||||
|
label?: PassThroughProps;
|
||||||
|
content?: PassThroughProps;
|
||||||
|
error?: PassThroughProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析透传样式
|
||||||
|
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
||||||
|
|
||||||
|
// 当前错误信息
|
||||||
|
const errorText = computed<string>(() => {
|
||||||
|
return getError(props.prop);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否有错误
|
||||||
|
const hasError = computed<boolean>(() => {
|
||||||
|
return errorText.value != "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当前标签位置
|
||||||
|
const labelPosition = computed<ClFormLabelPosition>(() => {
|
||||||
|
return props.labelPosition ?? formRef.value?.labelPosition ?? "left";
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标签宽度
|
||||||
|
const labelWidth = computed<string>(() => {
|
||||||
|
return props.labelWidth ?? formRef.value?.labelWidth ?? "120rpx";
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否显示必填星号
|
||||||
|
const showAsterisk = computed<boolean>(() => {
|
||||||
|
if (!props.required) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.showAsterisk ?? formRef.value?.showAsterisk ?? true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否显示错误信息
|
||||||
|
const showMessage = computed<boolean>(() => {
|
||||||
|
if (!props.required) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.showMessage ?? formRef.value?.showMessage ?? true;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
computed(() => props.required),
|
||||||
|
(val: boolean) => {
|
||||||
|
if (val) {
|
||||||
|
addField(props.prop);
|
||||||
|
} else {
|
||||||
|
removeField(props.prop);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
watch(
|
||||||
|
computed(() => getValue(props.prop)!),
|
||||||
|
() => {
|
||||||
|
if (props.required) {
|
||||||
|
validateField(props.prop);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
removeField(props.prop);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cl-form-item {
|
||||||
|
@apply w-full mb-6;
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
@apply w-full;
|
||||||
|
|
||||||
|
&.is-top {
|
||||||
|
@apply flex flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-left {
|
||||||
|
@apply flex flex-row;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-right {
|
||||||
|
@apply flex flex-row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
@apply flex flex-row items-center;
|
||||||
|
|
||||||
|
&.is-top {
|
||||||
|
@apply w-full mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-left {
|
||||||
|
@apply mr-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-right {
|
||||||
|
@apply mr-3 justify-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
@apply relative flex-1 w-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
16
uni_modules/cool-ui/components/cl-form-item/props.ts
Normal file
16
uni_modules/cool-ui/components/cl-form-item/props.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { ClFormItemPassThrough } from "./props";
|
||||||
|
import type { ClFormRule } from "../cl-form/props";
|
||||||
|
|
||||||
|
export type ClFormItemProps = {
|
||||||
|
className?: string;
|
||||||
|
pt?: ClFormItemPassThrough;
|
||||||
|
label?: string;
|
||||||
|
prop?: string;
|
||||||
|
required?: boolean;
|
||||||
|
labelPosition?: "left" | "top" | "right";
|
||||||
|
labelWidth?: string;
|
||||||
|
rules?: ClFormRule | ClFormRule[];
|
||||||
|
showRequiredAsterisk?: boolean;
|
||||||
|
error?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
302
uni_modules/cool-ui/components/cl-form/cl-form.uvue
Normal file
302
uni_modules/cool-ui/components/cl-form/cl-form.uvue
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="cl-form"
|
||||||
|
:class="[
|
||||||
|
`cl-form--label-${labelPosition}`,
|
||||||
|
{
|
||||||
|
'cl-form--disabled': disabled
|
||||||
|
},
|
||||||
|
pt.className
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, ref, watch, type PropType } from "vue";
|
||||||
|
import { isEmpty, parsePt, parseToObject } from "@/cool";
|
||||||
|
import type { ClFormLabelPosition, ClFormRule, ClFormValidateError } from "../../types";
|
||||||
|
import { $t, t } from "@/locale";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "cl-form"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件属性定义
|
||||||
|
const props = defineProps({
|
||||||
|
// 透传样式
|
||||||
|
pt: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
// 表单数据模型
|
||||||
|
modelValue: {
|
||||||
|
type: Object as PropType<any>,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
// 表单规则
|
||||||
|
rules: {
|
||||||
|
type: Object as PropType<Map<string, ClFormRule[]>>,
|
||||||
|
default: () => new Map<string, ClFormRule[]>()
|
||||||
|
},
|
||||||
|
// 标签位置
|
||||||
|
labelPosition: {
|
||||||
|
type: String as PropType<ClFormLabelPosition>,
|
||||||
|
default: "top"
|
||||||
|
},
|
||||||
|
// 标签宽度
|
||||||
|
labelWidth: {
|
||||||
|
type: String,
|
||||||
|
default: "140rpx"
|
||||||
|
},
|
||||||
|
// 是否显示必填星号
|
||||||
|
showAsterisk: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否显示错误信息
|
||||||
|
showMessage: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否禁用整个表单
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type PassThrough = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析透传样式
|
||||||
|
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const data = ref({} as UTSJSONObject);
|
||||||
|
|
||||||
|
// 表单字段错误信息
|
||||||
|
const errors = ref(new Map<string, string>());
|
||||||
|
|
||||||
|
// 表单字段集合
|
||||||
|
const fields = ref(new Set<string>([]));
|
||||||
|
|
||||||
|
// 标签位置
|
||||||
|
const labelPosition = computed(() => props.labelPosition);
|
||||||
|
|
||||||
|
// 标签宽度
|
||||||
|
const labelWidth = computed(() => props.labelWidth);
|
||||||
|
|
||||||
|
// 是否显示必填星号
|
||||||
|
const showAsterisk = computed(() => props.showAsterisk);
|
||||||
|
|
||||||
|
// 是否显示错误信息
|
||||||
|
const showMessage = computed(() => props.showMessage);
|
||||||
|
|
||||||
|
// 是否禁用整个表单
|
||||||
|
const disabled = computed(() => props.disabled);
|
||||||
|
|
||||||
|
// 设置字段错误信息
|
||||||
|
function setError(prop: string, error: string) {
|
||||||
|
if (prop != "") {
|
||||||
|
errors.value.set(prop, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除字段错误信息
|
||||||
|
function removeError(prop: string) {
|
||||||
|
if (prop != "") {
|
||||||
|
errors.value.delete(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字段错误信息
|
||||||
|
function getError(prop: string): string {
|
||||||
|
if (prop != "") {
|
||||||
|
return errors.value.get(prop) ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除所有错误信息
|
||||||
|
function clearErrors() {
|
||||||
|
errors.value.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字段值
|
||||||
|
function getValue(prop: string): any | null {
|
||||||
|
if (prop != "") {
|
||||||
|
return data.value[prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册表单字段
|
||||||
|
function addField(prop: string) {
|
||||||
|
if (prop != "") {
|
||||||
|
fields.value.add(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注销表单字段
|
||||||
|
function removeField(prop: string) {
|
||||||
|
if (prop != "") {
|
||||||
|
fields.value.delete(prop);
|
||||||
|
removeError(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字段规则
|
||||||
|
function getRule(prop: string): ClFormRule[] {
|
||||||
|
return props.rules.get(prop) ?? ([] as ClFormRule[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证单个规则
|
||||||
|
function validateRule(value: any | null, rule: ClFormRule): null | string {
|
||||||
|
// 必填验证
|
||||||
|
if (rule.required == true) {
|
||||||
|
if (value == null || value == "" || (Array.isArray(value) && value.length == 0)) {
|
||||||
|
return rule.message ?? t("此字段为必填项");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果值为空且不是必填,直接通过
|
||||||
|
if ((value == null || value == "") && rule.required != true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最小长度验证
|
||||||
|
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 (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 (rule.pattern != null) {
|
||||||
|
if (!rule.pattern.test(`${value}`)) {
|
||||||
|
return rule.message ?? t("格式不正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义验证
|
||||||
|
if (rule.validator != null) {
|
||||||
|
const result = rule.validator(value);
|
||||||
|
if (result != true) {
|
||||||
|
return typeof result == "string" ? result : (rule.message ?? t("验证失败"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除所有验证
|
||||||
|
function clearValidate() {
|
||||||
|
nextTick(() => {
|
||||||
|
clearErrors();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证单个字段
|
||||||
|
function validateField(prop: string): string | null {
|
||||||
|
let error = null as string | null;
|
||||||
|
|
||||||
|
if (prop != "") {
|
||||||
|
const value = getValue(prop);
|
||||||
|
const rules = getRule(prop);
|
||||||
|
|
||||||
|
if (!isEmpty(rules)) {
|
||||||
|
// 逐个验证规则
|
||||||
|
rules.find((rule) => {
|
||||||
|
const msg = validateRule(value, rule);
|
||||||
|
|
||||||
|
if (msg != null) {
|
||||||
|
error = msg;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeError(prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error != null) {
|
||||||
|
setError(prop, error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证整个表单
|
||||||
|
function validate(callback: (valid: boolean, errors: ClFormValidateError[]) => void) {
|
||||||
|
const errs = [] as ClFormValidateError[];
|
||||||
|
|
||||||
|
fields.value.forEach((prop) => {
|
||||||
|
const result = validateField(prop);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
errs.push({
|
||||||
|
field: prop,
|
||||||
|
message: result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
callback(errs.length == 0, errs);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
computed(() => parseToObject(props.modelValue)),
|
||||||
|
(val: UTSJSONObject) => {
|
||||||
|
data.value = val;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
labelPosition,
|
||||||
|
labelWidth,
|
||||||
|
showAsterisk,
|
||||||
|
showMessage,
|
||||||
|
disabled,
|
||||||
|
data,
|
||||||
|
errors,
|
||||||
|
fields,
|
||||||
|
addField,
|
||||||
|
removeField,
|
||||||
|
getValue,
|
||||||
|
setError,
|
||||||
|
getError,
|
||||||
|
removeError,
|
||||||
|
clearErrors,
|
||||||
|
getRule,
|
||||||
|
validateRule,
|
||||||
|
clearValidate,
|
||||||
|
validateField,
|
||||||
|
validate
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cl-form {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
12
uni_modules/cool-ui/components/cl-form/props.ts
Normal file
12
uni_modules/cool-ui/components/cl-form/props.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { ClFormData, ClFormRule, ClFormValidateResult, ClFormPassThrough } from "./props";
|
||||||
|
|
||||||
|
export type ClFormProps = {
|
||||||
|
className?: string;
|
||||||
|
pt?: ClFormPassThrough;
|
||||||
|
modelValue?: ClFormData;
|
||||||
|
rules?: Record<string, ClFormRule | ClFormRule[]>;
|
||||||
|
labelPosition?: "left" | "top" | "right";
|
||||||
|
labelWidth?: string;
|
||||||
|
showRequiredAsterisk?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
class="cl-input-number"
|
class="cl-input-number"
|
||||||
:class="[
|
:class="[
|
||||||
{
|
{
|
||||||
'cl-input-number--disabled': disabled
|
'cl-input-number--disabled': isDisabled
|
||||||
},
|
},
|
||||||
pt.className
|
pt.className
|
||||||
]"
|
]"
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<cl-input
|
<cl-input
|
||||||
:model-value="`${value}`"
|
:model-value="`${value}`"
|
||||||
:type="inputType"
|
:type="inputType"
|
||||||
:disabled="disabled"
|
:disabled="isDisabled"
|
||||||
:clearable="false"
|
:clearable="false"
|
||||||
:readonly="inputable == false"
|
:readonly="inputable == false"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@@ -92,6 +92,7 @@ import { computed, nextTick, ref, watch, type PropType } from "vue";
|
|||||||
import type { PassThroughProps } from "../../types";
|
import type { PassThroughProps } from "../../types";
|
||||||
import type { ClIconProps } from "../cl-icon/props";
|
import type { ClIconProps } from "../cl-icon/props";
|
||||||
import { useLongPress, parsePt, parseRpx } from "@/cool";
|
import { useLongPress, parsePt, parseRpx } from "@/cool";
|
||||||
|
import { useForm } from "../../hooks";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "cl-input-number"
|
name: "cl-input-number"
|
||||||
@@ -156,6 +157,14 @@ const emit = defineEmits(["update:modelValue", "change"]);
|
|||||||
// 长按操作
|
// 长按操作
|
||||||
const longPress = useLongPress();
|
const longPress = useLongPress();
|
||||||
|
|
||||||
|
// cl-form 上下文
|
||||||
|
const { disabled } = useForm();
|
||||||
|
|
||||||
|
// 是否禁用
|
||||||
|
const isDisabled = computed(() => {
|
||||||
|
return disabled.value || props.disabled;
|
||||||
|
});
|
||||||
|
|
||||||
// 数值样式
|
// 数值样式
|
||||||
type ValuePassThrough = {
|
type ValuePassThrough = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -184,10 +193,10 @@ const pt = computed(() => parsePt<PassThrough>(props.pt));
|
|||||||
const value = ref(props.modelValue);
|
const value = ref(props.modelValue);
|
||||||
|
|
||||||
// 是否可以继续增加数值
|
// 是否可以继续增加数值
|
||||||
const isPlus = computed(() => !props.disabled && value.value < props.max);
|
const isPlus = computed(() => !isDisabled.value && value.value < props.max);
|
||||||
|
|
||||||
// 是否可以继续减少数值
|
// 是否可以继续减少数值
|
||||||
const isMinus = computed(() => !props.disabled && value.value > props.min);
|
const isMinus = computed(() => !isDisabled.value && value.value > props.min);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新数值并触发事件
|
* 更新数值并触发事件
|
||||||
@@ -232,7 +241,7 @@ function update() {
|
|||||||
* 在非禁用状态下增加step值
|
* 在非禁用状态下增加step值
|
||||||
*/
|
*/
|
||||||
function onPlus() {
|
function onPlus() {
|
||||||
if (props.disabled || !isPlus.value) return;
|
if (isDisabled.value || !isPlus.value) return;
|
||||||
|
|
||||||
longPress.start(() => {
|
longPress.start(() => {
|
||||||
if (isPlus.value) {
|
if (isPlus.value) {
|
||||||
@@ -248,7 +257,7 @@ function onPlus() {
|
|||||||
* 在非禁用状态下减少step值
|
* 在非禁用状态下减少step值
|
||||||
*/
|
*/
|
||||||
function onMinus() {
|
function onMinus() {
|
||||||
if (props.disabled || !isMinus.value) return;
|
if (isDisabled.value || !isMinus.value) return;
|
||||||
|
|
||||||
longPress.start(() => {
|
longPress.start(() => {
|
||||||
if (isMinus.value) {
|
if (isMinus.value) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
'is-dark': isDark,
|
'is-dark': isDark,
|
||||||
'cl-input--border': border,
|
'cl-input--border': border,
|
||||||
'cl-input--focus': isFocus,
|
'cl-input--focus': isFocus,
|
||||||
'cl-input--disabled': disabled
|
'cl-input--disabled': isDisabled
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
@tap="onTap"
|
@tap="onTap"
|
||||||
@@ -26,13 +26,13 @@
|
|||||||
class="cl-input__inner"
|
class="cl-input__inner"
|
||||||
:class="[
|
:class="[
|
||||||
{
|
{
|
||||||
'is-disabled': disabled,
|
'is-disabled': isDisabled,
|
||||||
'is-dark': isDark
|
'is-dark': isDark
|
||||||
},
|
},
|
||||||
pt.inner?.className
|
pt.inner?.className
|
||||||
]"
|
]"
|
||||||
:value="value"
|
:value="value"
|
||||||
:disabled="readonly ?? disabled"
|
:disabled="readonly ?? isDisabled"
|
||||||
:type="type"
|
:type="type"
|
||||||
:password="isPassword"
|
:password="isPassword"
|
||||||
:focus="isFocus"
|
:focus="isFocus"
|
||||||
@@ -80,11 +80,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, watch, type PropType } from "vue";
|
import { computed, nextTick, onMounted, ref, watch, type PropType } from "vue";
|
||||||
import type { ClInputType, PassThroughProps } from "../../types";
|
import type { ClInputType, PassThroughProps } from "../../types";
|
||||||
import { isDark, parseClass, parsePt } from "@/cool";
|
import { isDark, parseClass, parsePt } from "@/cool";
|
||||||
import type { ClIconProps } from "../cl-icon/props";
|
import type { ClIconProps } from "../cl-icon/props";
|
||||||
import { t } from "@/locale";
|
import { t } from "@/locale";
|
||||||
|
import { useForm } from "../../hooks";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "cl-input"
|
name: "cl-input"
|
||||||
@@ -201,6 +202,14 @@ const emit = defineEmits([
|
|||||||
"keyboardheightchange"
|
"keyboardheightchange"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// cl-form 上下文
|
||||||
|
const { disabled } = useForm();
|
||||||
|
|
||||||
|
// 是否禁用
|
||||||
|
const isDisabled = computed(() => {
|
||||||
|
return disabled.value || props.disabled;
|
||||||
|
});
|
||||||
|
|
||||||
// 透传样式类型定义
|
// 透传样式类型定义
|
||||||
type PassThrough = {
|
type PassThrough = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -271,7 +280,7 @@ function onKeyboardheightchange(e: UniInputKeyboardHeightChangeEvent) {
|
|||||||
|
|
||||||
// 点击事件
|
// 点击事件
|
||||||
function onTap() {
|
function onTap() {
|
||||||
if (props.disabled) {
|
if (isDisabled.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
:focus="popupRef?.isOpen"
|
:focus="popupRef?.isOpen"
|
||||||
:text="text"
|
:text="text"
|
||||||
arrow-icon="calendar-line"
|
arrow-icon="calendar-line"
|
||||||
@tap="open()"
|
@open="open()"
|
||||||
@clear="clear"
|
@clear="clear"
|
||||||
></cl-select-trigger>
|
></cl-select-trigger>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
:focus="popupRef?.isOpen"
|
:focus="popupRef?.isOpen"
|
||||||
:text="text"
|
:text="text"
|
||||||
arrow-icon="time-line"
|
arrow-icon="time-line"
|
||||||
@tap="open()"
|
@open="open()"
|
||||||
@clear="clear"
|
@clear="clear"
|
||||||
></cl-select-trigger>
|
></cl-select-trigger>
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
:class="[
|
:class="[
|
||||||
{
|
{
|
||||||
'is-dark': isDark,
|
'is-dark': isDark,
|
||||||
'cl-select-trigger--disabled': disabled,
|
'cl-select-trigger--disabled': isDisabled,
|
||||||
'cl-select-trigger--focus': focus
|
'cl-select-trigger--focus': focus
|
||||||
},
|
},
|
||||||
pt.className
|
pt.className
|
||||||
]"
|
]"
|
||||||
|
@tap="open"
|
||||||
>
|
>
|
||||||
<view class="cl-select-trigger__content">
|
<view class="cl-select-trigger__content">
|
||||||
<cl-text
|
<cl-text
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
:pt="{
|
:pt="{
|
||||||
className: parseClass([
|
className: parseClass([
|
||||||
{
|
{
|
||||||
'!text-surface-400': disabled
|
'!text-surface-400': isDisabled
|
||||||
},
|
},
|
||||||
pt.text?.className
|
pt.text?.className
|
||||||
])
|
])
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="showText && !disabled" class="cl-select-trigger__icon" @tap.stop="clear">
|
<view v-if="showText && !isDisabled" class="cl-select-trigger__icon" @tap.stop="clear">
|
||||||
<cl-icon
|
<cl-icon
|
||||||
name="close-circle-fill"
|
name="close-circle-fill"
|
||||||
:size="32"
|
:size="32"
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
></cl-icon>
|
></cl-icon>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="!disabled && !showText" class="cl-select-trigger__icon">
|
<view v-if="!isDisabled && !showText" class="cl-select-trigger__icon">
|
||||||
<cl-icon
|
<cl-icon
|
||||||
:name="pt.icon?.name ?? arrowIcon"
|
:name="pt.icon?.name ?? arrowIcon"
|
||||||
:size="pt.icon?.size ?? 32"
|
:size="pt.icon?.size ?? 32"
|
||||||
@@ -61,6 +62,7 @@ import type { ClIconProps } from "../cl-icon/props";
|
|||||||
import { isDark, parseClass, parsePt } from "@/cool";
|
import { isDark, parseClass, parsePt } from "@/cool";
|
||||||
import { t } from "@/locale";
|
import { t } from "@/locale";
|
||||||
import type { PassThroughProps } from "../../types";
|
import type { PassThroughProps } from "../../types";
|
||||||
|
import { useForm } from "../../hooks";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "cl-select-trigger"
|
name: "cl-select-trigger"
|
||||||
@@ -100,7 +102,14 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["clear"]);
|
const emit = defineEmits(["open", "clear"]);
|
||||||
|
|
||||||
|
const { disabled } = useForm();
|
||||||
|
|
||||||
|
// 是否禁用
|
||||||
|
const isDisabled = computed(() => {
|
||||||
|
return disabled.value || props.disabled;
|
||||||
|
});
|
||||||
|
|
||||||
// 透传样式类型定义
|
// 透传样式类型定义
|
||||||
type PassThrough = {
|
type PassThrough = {
|
||||||
@@ -120,6 +129,15 @@ const showText = computed(() => props.text != "");
|
|||||||
function clear() {
|
function clear() {
|
||||||
emit("clear");
|
emit("clear");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开选择器
|
||||||
|
function open() {
|
||||||
|
if (isDisabled.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("open");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:focus="popupRef?.isOpen"
|
:focus="popupRef?.isOpen"
|
||||||
:text="text"
|
:text="text"
|
||||||
@tap="open()"
|
@open="open()"
|
||||||
@clear="clear"
|
@clear="clear"
|
||||||
></cl-select-trigger>
|
></cl-select-trigger>
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@ import { isEmpty, parsePt } from "@/cool";
|
|||||||
import type { ClSelectTriggerPassThrough } from "../cl-select-trigger/props";
|
import type { ClSelectTriggerPassThrough } from "../cl-select-trigger/props";
|
||||||
import type { ClPopupPassThrough } from "../cl-popup/props";
|
import type { ClPopupPassThrough } from "../cl-popup/props";
|
||||||
import { t } from "@/locale";
|
import { t } from "@/locale";
|
||||||
|
import { useForm } from "../../hooks";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "cl-select"
|
name: "cl-select"
|
||||||
@@ -268,10 +269,6 @@ let callback: ((value: Value) => void) | null = null;
|
|||||||
|
|
||||||
// 打开选择器
|
// 打开选择器
|
||||||
function open(cb: ((value: Value) => void) | null = null) {
|
function open(cb: ((value: Value) => void) | null = null) {
|
||||||
if (props.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
callback = cb;
|
callback = cb;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
'is-dark': isDark,
|
'is-dark': isDark,
|
||||||
'cl-textarea--border': border,
|
'cl-textarea--border': border,
|
||||||
'cl-textarea--focus': isFocus,
|
'cl-textarea--focus': isFocus,
|
||||||
'cl-textarea--disabled': disabled
|
'cl-textarea--disabled': isDisabled
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
@tap="onTap"
|
@tap="onTap"
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
class="cl-textarea__inner"
|
class="cl-textarea__inner"
|
||||||
:class="[
|
:class="[
|
||||||
{
|
{
|
||||||
'is-disabled': disabled,
|
'is-disabled': isDisabled,
|
||||||
'is-dark': isDark
|
'is-dark': isDark
|
||||||
},
|
},
|
||||||
pt.inner?.className
|
pt.inner?.className
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
}"
|
}"
|
||||||
:value="value"
|
:value="value"
|
||||||
:name="name"
|
:name="name"
|
||||||
:disabled="readonly ?? disabled"
|
:disabled="readonly ?? isDisabled"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:placeholder-class="`text-surface-400 ${placeholderClass}`"
|
:placeholder-class="`text-surface-400 ${placeholderClass}`"
|
||||||
:maxlength="maxlength"
|
:maxlength="maxlength"
|
||||||
@@ -63,6 +63,7 @@ import { parsePt, parseRpx } from "@/cool";
|
|||||||
import type { PassThroughProps } from "../../types";
|
import type { PassThroughProps } from "../../types";
|
||||||
import { isDark } from "@/cool";
|
import { isDark } from "@/cool";
|
||||||
import { t } from "@/locale";
|
import { t } from "@/locale";
|
||||||
|
import { useForm } from "../../hooks";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "cl-textarea"
|
name: "cl-textarea"
|
||||||
@@ -221,6 +222,14 @@ const emit = defineEmits([
|
|||||||
"keyboardheightchange"
|
"keyboardheightchange"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// cl-form 上下文
|
||||||
|
const { disabled } = useForm();
|
||||||
|
|
||||||
|
// 是否禁用
|
||||||
|
const isDisabled = computed(() => {
|
||||||
|
return disabled.value || props.disabled;
|
||||||
|
});
|
||||||
|
|
||||||
// 透传样式类型定义
|
// 透传样式类型定义
|
||||||
type PassThrough = {
|
type PassThrough = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -277,7 +286,7 @@ function onLineChange(e: UniTextareaLineChangeEvent) {
|
|||||||
|
|
||||||
// 点击事件
|
// 点击事件
|
||||||
function onTap() {
|
function onTap() {
|
||||||
if (props.disabled) {
|
if (isDisabled.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
:class="[
|
:class="[
|
||||||
{
|
{
|
||||||
'is-dark': isDark,
|
'is-dark': isDark,
|
||||||
'is-disabled': disabled
|
'is-disabled': isDisabled
|
||||||
},
|
},
|
||||||
pt.item?.className
|
pt.item?.className
|
||||||
]"
|
]"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
className: 'cl-upload__close'
|
className: 'cl-upload__close'
|
||||||
}"
|
}"
|
||||||
@tap.stop="remove(item.uid)"
|
@tap.stop="remove(item.uid)"
|
||||||
v-if="!disabled"
|
v-if="!isDisabled"
|
||||||
></cl-icon>
|
></cl-icon>
|
||||||
|
|
||||||
<view class="cl-upload__progress" v-if="item.progress < 100">
|
<view class="cl-upload__progress" v-if="item.progress < 100">
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
:class="[
|
:class="[
|
||||||
{
|
{
|
||||||
'is-dark': isDark,
|
'is-dark': isDark,
|
||||||
'is-disabled': disabled
|
'is-disabled': isDisabled
|
||||||
},
|
},
|
||||||
pt.add?.className
|
pt.add?.className
|
||||||
]"
|
]"
|
||||||
@@ -84,6 +84,7 @@ import { forInObject, isDark, parseClass, parsePt, parseRpx, uploadFile, uuid }
|
|||||||
import { t } from "@/locale";
|
import { t } from "@/locale";
|
||||||
import { computed, reactive, ref, watch, type PropType } from "vue";
|
import { computed, reactive, ref, watch, type PropType } from "vue";
|
||||||
import type { ClUploadItem, PassThroughProps } from "../../types";
|
import type { ClUploadItem, PassThroughProps } from "../../types";
|
||||||
|
import { useForm } from "../../hooks";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "cl-upload"
|
name: "cl-upload"
|
||||||
@@ -156,6 +157,14 @@ const emit = defineEmits([
|
|||||||
"progress" // 上传进度更新时触发
|
"progress" // 上传进度更新时触发
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// cl-form 上下文
|
||||||
|
const { disabled } = useForm();
|
||||||
|
|
||||||
|
// 是否禁用
|
||||||
|
const isDisabled = computed(() => {
|
||||||
|
return disabled.value || props.disabled;
|
||||||
|
});
|
||||||
|
|
||||||
// 透传属性的类型定义
|
// 透传属性的类型定义
|
||||||
type PassThrough = {
|
type PassThrough = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -179,7 +188,7 @@ const activeIndex = ref(0);
|
|||||||
const isAdd = computed(() => {
|
const isAdd = computed(() => {
|
||||||
const n = list.value.length;
|
const n = list.value.length;
|
||||||
|
|
||||||
if (props.disabled) {
|
if (isDisabled.value) {
|
||||||
// 禁用状态下,只有没有文件时才显示添加按钮
|
// 禁用状态下,只有没有文件时才显示添加按钮
|
||||||
return n == 0;
|
return n == 0;
|
||||||
} else {
|
} else {
|
||||||
@@ -294,7 +303,7 @@ function remove(uid: string) {
|
|||||||
* @param {number} index - 操作的索引,-1表示新增,其他表示替换
|
* @param {number} index - 操作的索引,-1表示新增,其他表示替换
|
||||||
*/
|
*/
|
||||||
function choose(index: number) {
|
function choose(index: number) {
|
||||||
if (props.disabled) {
|
if (isDisabled.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
86
uni_modules/cool-ui/hooks/form.ts
Normal file
86
uni_modules/cool-ui/hooks/form.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { computed, ref, type ComputedRef } from "vue";
|
||||||
|
import type { ClFormRule, ClFormValidateError } from "../types";
|
||||||
|
import { useParent } from "@/cool";
|
||||||
|
|
||||||
|
class UseForm {
|
||||||
|
public formRef = ref<ClFormComponentPublicInstance | null>(null);
|
||||||
|
public disabled: ComputedRef<boolean>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// 获取 cl-form 实例
|
||||||
|
if (this.formRef.value == null) {
|
||||||
|
const ClForm = useParent<ClFormComponentPublicInstance>("cl-form");
|
||||||
|
|
||||||
|
if (ClForm != null) {
|
||||||
|
this.formRef.value = ClForm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听表单是否禁用
|
||||||
|
this.disabled = computed<boolean>(() => this.formRef.value?.disabled ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册表单字段
|
||||||
|
addField = (prop: string): void => {
|
||||||
|
this.formRef.value!.addField(prop);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 注销表单字段
|
||||||
|
removeField = (prop: string): void => {
|
||||||
|
this.formRef.value!.removeField(prop);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取字段值
|
||||||
|
getValue = (prop: string): any | null => {
|
||||||
|
return this.formRef.value!.getValue(prop);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置字段错误信息
|
||||||
|
setError = (prop: string, error: string): void => {
|
||||||
|
this.formRef.value!.setError(prop, error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取字段错误信息
|
||||||
|
getError = (prop: string): string => {
|
||||||
|
return this.formRef.value!.getError(prop);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移除字段错误信息
|
||||||
|
removeError = (prop: string): void => {
|
||||||
|
this.formRef.value!.removeError(prop);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除所有错误信息
|
||||||
|
clearErrors = (): void => {
|
||||||
|
this.formRef.value!.clearErrors();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取字段规则
|
||||||
|
getRule = (prop: string): ClFormRule[] => {
|
||||||
|
return this.formRef.value!.getRule(prop);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证单个规则
|
||||||
|
validateRule = (value: any | null, rule: ClFormRule): string | null => {
|
||||||
|
return this.formRef.value!.validateRule(value, rule);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除所有验证
|
||||||
|
clearValidate = (): void => {
|
||||||
|
this.formRef.value!.clearValidate();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证单个字段
|
||||||
|
validateField = (prop: string): string | null => {
|
||||||
|
return this.formRef.value!.validateField(prop);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证整个表单
|
||||||
|
validate = (callback: (valid: boolean, errors: ClFormValidateError[]) => void): void => {
|
||||||
|
this.formRef.value!.validate(callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useForm() {
|
||||||
|
return new UseForm();
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./ui";
|
export * from "./ui";
|
||||||
export * from "./component";
|
export * from "./component";
|
||||||
|
export * from "./form";
|
||||||
|
|||||||
4
uni_modules/cool-ui/index.d.ts
vendored
4
uni_modules/cool-ui/index.d.ts
vendored
@@ -16,6 +16,8 @@ import type { ClCropperProps, ClCropperPassThrough } from "./components/cl-cropp
|
|||||||
import type { ClDraggableProps, ClDraggablePassThrough } from "./components/cl-draggable/props";
|
import type { ClDraggableProps, ClDraggablePassThrough } from "./components/cl-draggable/props";
|
||||||
import type { ClFloatViewProps } from "./components/cl-float-view/props";
|
import type { ClFloatViewProps } from "./components/cl-float-view/props";
|
||||||
import type { ClFooterProps, ClFooterPassThrough } from "./components/cl-footer/props";
|
import type { ClFooterProps, ClFooterPassThrough } from "./components/cl-footer/props";
|
||||||
|
import type { ClFormProps } from "./components/cl-form/props";
|
||||||
|
import type { ClFormItemProps } from "./components/cl-form-item/props";
|
||||||
import type { ClIconProps, ClIconPassThrough } from "./components/cl-icon/props";
|
import type { ClIconProps, ClIconPassThrough } from "./components/cl-icon/props";
|
||||||
import type { ClImageProps, ClImagePassThrough } from "./components/cl-image/props";
|
import type { ClImageProps, ClImagePassThrough } from "./components/cl-image/props";
|
||||||
import type { ClIndexBarProps, ClIndexBarPassThrough } from "./components/cl-index-bar/props";
|
import type { ClIndexBarProps, ClIndexBarPassThrough } from "./components/cl-index-bar/props";
|
||||||
@@ -84,6 +86,8 @@ declare module "vue" {
|
|||||||
"cl-draggable": (typeof import('./components/cl-draggable/cl-draggable.uvue')['default']) & import('vue').DefineComponent<ClDraggableProps>;
|
"cl-draggable": (typeof import('./components/cl-draggable/cl-draggable.uvue')['default']) & import('vue').DefineComponent<ClDraggableProps>;
|
||||||
"cl-float-view": (typeof import('./components/cl-float-view/cl-float-view.uvue')['default']) & import('vue').DefineComponent<ClFloatViewProps>;
|
"cl-float-view": (typeof import('./components/cl-float-view/cl-float-view.uvue')['default']) & import('vue').DefineComponent<ClFloatViewProps>;
|
||||||
"cl-footer": (typeof import('./components/cl-footer/cl-footer.uvue')['default']) & import('vue').DefineComponent<ClFooterProps>;
|
"cl-footer": (typeof import('./components/cl-footer/cl-footer.uvue')['default']) & import('vue').DefineComponent<ClFooterProps>;
|
||||||
|
"cl-form": (typeof import('./components/cl-form/cl-form.uvue')['default']) & import('vue').DefineComponent<ClFormProps>;
|
||||||
|
"cl-form-item": (typeof import('./components/cl-form-item/cl-form-item.uvue')['default']) & import('vue').DefineComponent<ClFormItemProps>;
|
||||||
"cl-icon": (typeof import('./components/cl-icon/cl-icon.uvue')['default']) & import('vue').DefineComponent<ClIconProps>;
|
"cl-icon": (typeof import('./components/cl-icon/cl-icon.uvue')['default']) & import('vue').DefineComponent<ClIconProps>;
|
||||||
"cl-image": (typeof import('./components/cl-image/cl-image.uvue')['default']) & import('vue').DefineComponent<ClImageProps>;
|
"cl-image": (typeof import('./components/cl-image/cl-image.uvue')['default']) & import('vue').DefineComponent<ClImageProps>;
|
||||||
"cl-index-bar": (typeof import('./components/cl-index-bar/cl-index-bar.uvue')['default']) & import('vue').DefineComponent<ClIndexBarProps>;
|
"cl-index-bar": (typeof import('./components/cl-index-bar/cl-index-bar.uvue')['default']) & import('vue').DefineComponent<ClIndexBarProps>;
|
||||||
|
|||||||
31
uni_modules/cool-ui/types/component.d.ts
vendored
31
uni_modules/cool-ui/types/component.d.ts
vendored
@@ -159,3 +159,34 @@ declare type ClCropperComponentPublicInstance = {
|
|||||||
chooseImage: () => void;
|
chooseImage: () => void;
|
||||||
toPng: () => Promise<string>;
|
toPng: () => Promise<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declare type ClFormComponentPublicInstance = {
|
||||||
|
labelPosition: "left" | "top" | "right";
|
||||||
|
labelWidth: string;
|
||||||
|
showAsterisk: boolean;
|
||||||
|
showMessage: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
data: UTSJSONObject;
|
||||||
|
errors: Map<string, string>;
|
||||||
|
fields: Set<string>;
|
||||||
|
addField: (prop: string) => void;
|
||||||
|
removeField: (prop: string) => void;
|
||||||
|
getValue: (prop: string) => any | null;
|
||||||
|
setData: (data: UTSJSONObject) => void;
|
||||||
|
setError: (prop: string, error: string) => void;
|
||||||
|
getError: (prop: string) => string;
|
||||||
|
removeError: (prop: string) => void;
|
||||||
|
clearErrors: () => void;
|
||||||
|
getRule: (prop: string) => ClFormRule[];
|
||||||
|
validateRule: (value: any | null, rule: ClFormRule) => string | null;
|
||||||
|
clearValidate: () => void;
|
||||||
|
validateField: (prop: string) => string | null;
|
||||||
|
validate: (callback: (valid: boolean, errors: ClFormValidateError[]) => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type ClFormItemComponentPublicInstance = {
|
||||||
|
validate: () => Promise<boolean>;
|
||||||
|
clearValidate: () => void;
|
||||||
|
hasError: boolean;
|
||||||
|
currentError: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -128,3 +128,31 @@ export type ClSelectDateShortcut = {
|
|||||||
label: string;
|
label: string;
|
||||||
value: string[];
|
value: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 表单规则类型
|
||||||
|
export type ClFormRule = {
|
||||||
|
// 是否必填
|
||||||
|
required?: boolean;
|
||||||
|
// 错误信息
|
||||||
|
message?: string;
|
||||||
|
// 最小长度
|
||||||
|
min?: number;
|
||||||
|
// 最大长度
|
||||||
|
max?: number;
|
||||||
|
// 正则验证
|
||||||
|
pattern?: RegExp;
|
||||||
|
// 自定义验证函数
|
||||||
|
validator?: (value: any | null) => boolean | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClFormValidateError = {
|
||||||
|
field: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClFormValidateResult = {
|
||||||
|
valid: boolean;
|
||||||
|
errors: ClFormValidateError[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClFormLabelPosition = "left" | "top" | "right";
|
||||||
|
|||||||
Reference in New Issue
Block a user