UniApp快速表单组件

环境:vue3 + uni-app
依赖库:uview-plus、dayjs

通过配置项快速构建 form 表单

使用

<script setup>import CustomCard from '@/components/custom-card.vue';import { ref } from 'vue';import CustomFormItem from '@/components/form/custom-form-item.vue';import { formatDate } from '@/sheep/util';import { useForm } from '@/components/form/useForm';const formData = ref({useName: undefined,areaValue: undefined,address: undefined,});const formItems = [[{label: '企业名称',prop: 'useName',required: true,}, {label: '所属地区',type: 'area',prop: 'areaValue',required: true,}],[{label: '企业规模',prop: 'scale',}, {label: '成立日期',type: 'date',prop: 'startDate',elAttrs: {formatter(value) {return formatDate(value, 'YYYY-MM-DD');},},}, {label: '说明',type: 'custom',prop: 'tip',}],];const formConfig = {elAttrs: { border: 'none', inputAlign: 'right' },itemAttrs: { borderBottom: true },labelSuffix: ':',};const form = useForm(formItems, formConfig);function handleAreaChane(item, info) {formData.value[item.prop] = [info.province_name, info.city_name, info.district_name].join(' / ');}function handleDatetimeChange(item, data) {formData.value[item.prop] = formatDate(data.value);}
</script><template><u--formref="formRef":model="formData":rules="form.formRules"labelWidth="auto":labelStyle="{marginLeft: '4px'}"><custom-card padding="0 14px"><custom-form-itemv-model="formData":items="form.itemList[0]"@area-change="handleAreaChane"></custom-form-item></custom-card><custom-card padding="0 14px"><custom-form-itemv-model="formData":items="form.itemList[1]"@datetime-change="handleDatetimeChange"><template #tip><view style="width: 100%; text-align: right">这里是自定义插槽内容</view></template></custom-form-item></custom-card></u--form>
</template><style scoped lang="scss"></style>

关于 formatter 方法,是用于格式化显示内容的方法,不会影响绑定值的变化,目前仅 choose、date、datetime、area类型支持

源码

useForm.js 文件

核心form工具类,用于解析、转换配置

/*** 是否数组*/
function isArray(value) {if (typeof Array.isArray === 'function') {return Array.isArray(value);}return Object.prototype.toString.call(value) === '[object Array]';
}/*** @description 深度克隆* @param {object} obj 需要深度克隆的对象* @returns {*} 克隆后的对象或者原值(不是对象)*/
function deepClone(obj) {// 对常见的“非”值,直接返回原来值if ([null, undefined, NaN, false].includes(obj)) return obj;if (typeof obj !== 'object' && typeof obj !== 'function') {// 原始类型直接返回return obj;}const o = isArray(obj) ? [] : {};for (const i in obj) {if (obj.hasOwnProperty(i)) {o[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i];}}return o;
}/*** @description JS对象深度合并* @param {object} target 需要拷贝的对象* @param {object} source 拷贝的来源对象* @returns {object|boolean} 深度合并后的对象或者false(入参有不是对象)*/
export function deepMerge(target = {}, source = {}) {target = deepClone(target);if (typeof target !== 'object' || typeof source !== 'object') return false;for (const prop in source) {if (!source.hasOwnProperty(prop)) continue;if (prop in target) {if (typeof target[prop] !== 'object') {target[prop] = source[prop];} else if (typeof source[prop] !== 'object') {target[prop] = source[prop];} else if (target[prop].concat && source[prop].concat) {target[prop] = target[prop].concat(source[prop]);} else {target[prop] = deepMerge(target[prop], source[prop]);}} else {target[prop] = source[prop];}}return target;
}export const typeMap = {INPUT: 'input',INPUT_NUMBER: 'inputNumber',TEXTAREA: 'textarea',NUMBER: 'number',CHOOSE: 'choose',DATE: 'date',DATETIME: 'datetime',TIME: 'time',AREA: 'area',
};
export const perMap = {[typeMap.INPUT]: '请输入',[typeMap.INPUT_NUMBER]: '请输入',[typeMap.TEXTAREA]: '请输入',[typeMap.NUMBER]: '请填写',[typeMap.CHOOSE]: '请选择',[typeMap.DATE]: '请选择',[typeMap.DATETIME]: '请选择',[typeMap.TIME]: '请选择',[typeMap.AREA]: '请选择',
};const defaultConfig = {// formItem 属性itemAttrs: {},// formItem 中内容属性elAttrs: {},// 校验规则rule: {},// 标签追加字符串labelSuffix: '',// 是否将 rule 放到 formItem 上isItemRule: true,
};/*** form 快速生成* ---* <p>* elAttrs 具有的额外属性* - textareaAlign 文本域对齐方式* - formatter 格式化显示内容* - resourceIdField 上传文件的资源主键属性* </p>* ---* <p>* itemAttrs 具有的额外属性* - labelPosition 自定义 label 时 label 的位置,同时也是每个 item 的单独的属性* </p>* ---* @param {{*   label?: string,*   type?: typeMap[keyof typeMap],*   prop: string,*   required?: boolean,*   customLabel?: boolean,*   elAttrs?: {*     textareaAlign?: 'left' | 'right',*     numberAlign?: 'left' | 'right',*     formatter?: Function,*     resourceIdField?: string*   } | Object,*   itemAttrs?: {*     labelPosition: 'top' | 'left'*   } | Object,*   rule?: Object | Object[],*   showRender?: Function,*   itemAttrsRender?: Function,*   elAttrsRender?: Function,*   valueRender?: Function* }[]|{*   label?: string,*   type?: typeMap[keyof typeMap],*   prop: string,*   required?: boolean,*   customLabel?: boolean,*   elAttrs?: {*     textareaAlign?: 'left' | 'right',*     numberAlign?: 'left' | 'right',*     formatter?: Function,*     resourceIdField?: string*   } | Object,*   itemAttrs?: {*     labelPosition: 'top' | 'left'*   } | Object,*   rule?: Object | Object[],*   showRender?: Function,*   itemAttrsRender?: Function,*   elAttrsRender?: Function,*   valueRender?: Function* }[][]} items form 项* @param {{*   itemAttrs?: Object,*   elAttrs?: Object,*   rule?: Object,*   labelSuffix?: string,*   isItemRule?: boolean,* }} [config] 配置* @return {{formRules: *, itemList: *}}*/
export function useForm(items, config) {const startTime = Date.now();const props = Object.assign({}, defaultConfig, config || {});const itemList = (items || []).map(item => relItem(item));const formRules = getFormRules();function relItem(item) {if (item instanceof Array) {return item.map(item => relItem(item));}const itemNew = deepClone(item);itemNew.originLabel = itemNew.label;itemNew.label = itemNew.originLabel + props.labelSuffix;if (!itemNew.type) itemNew.type = typeMap.INPUT;const itemAttrs = deepClone(props.itemAttrs || {});itemNew.itemAttrs = deepMerge(itemAttrs, itemNew.itemAttrs);itemNew.itemAttrs.required = itemNew.itemAttrs.required || (itemNew.required || false);const elAttrs = deepClone(props.elAttrs);itemNew.elAttrs = deepMerge(elAttrs, itemNew.elAttrs);itemNew.elAttrs.placeholder = relPlaceholder(itemNew);return itemNew;}function getFormRules() {const rules = {};itemList.forEach((item) => {doGetFormRules(item, rules);});return rules;}function doGetFormRules(item, rules) {if (item instanceof Array) {item.forEach(item => {doGetFormRules(item, rules);});}let rule = {};if (item.itemAttrs && item.itemAttrs.required) {let type = 'string';if ([typeMap.INPUT_NUMBER, typeMap.NUMBER].includes(item.type)) {// 数字类型type = 'number';}rule = {type,required: true,message: relPlaceholder(item),trigger: ['blur', 'change'],};}if (item.rule) {if (item.rule instanceof Array) {rule = item.rule;} else if (typeof item.rule === 'object') {let propsRule = {};if (props.rule && Object.keys(item.rule).length > 0) {propsRule = props.rule;}rule = deepMerge(rule, propsRule);rule = deepMerge(rule, item.rule);}}rules[item.prop] = rule;}function relPlaceholder(item) {const elAttrs = item.elAttrs;if (elAttrs.placeholder || elAttrs.placeholder === '') return elAttrs.placeholder;const perStr = perMap[item.type];if (perStr) {return perStr + item.originLabel;}return '';}// 将 rule 添加到 itemAttrs 中function setRuleToItem(items, ruleKeys) {if (items instanceof Array) {for (let item of items) {setRuleToItem(item, ruleKeys);}} else if (ruleKeys.includes(items.prop)) {const rule = formRules[items.prop];if (!items.itemAttrs) {items.itemAttrs = {};}if (rule instanceof Array) {items.itemAttrs.rules = rule;} else if (typeof rule === 'object') {items.itemAttrs.rules = [rule];}}}// 开启 formItem rule 时,将 rule 放到 item 中if (props.isItemRule) {const ruleKeys = Object.keys(formRules);for (let item of itemList) {setRuleToItem(item, ruleKeys);}}// // 将最后一个 formItem 下边框设置为 false// function setLastItemBottomBorderHide(items) {//   if (!items || items.length === 0) return;//   for (let i = 1; i <= items.length; i++) {//     if (i === items.length) {//       const item = items[i - 1];//       if (item) {//         if (!item.itemAttrs) {//           item.itemAttrs = {};//         }//         item.itemAttrs.borderBottom = false;//       }//     }//   }// }//// // 隐藏最后一个 formItem 下边框处理// if (props.hideLastItemBottomBorder) {//   if (itemList instanceof Array) {//     for (let items of itemList) {//       setLastItemBottomBorderHide(items);//     }//   } else {//     setLastItemBottomBorderHide(itemList);//   }// }console.log('useForm 处理完毕,耗时:', ((Date.now() - startTime) / 1000).toFixed(4) + '秒');return {itemList,formRules,};
}export const commonFormStyleConfig = {elAttrs: {border: 'none',inputAlign: 'right',clearable: true,textareaAlign: 'right',numberAlign: 'right',},labelSuffix: ':',itemAttrs: { borderBottom: true, labelWidth: 'auto' },
};/*** 传入表单 ref 校验表单* @param form 表单 ref* @return {Promise<void>}*/
export async function validateForm(form) {await form.validate();
}/*** 传入表单 ref 和字段名,校验指定字段* @param form 表单 ref* @param {...string} fields 字段名,可多个*/
export function validateFields(form, ...fields) {for (let field of fields) {form.validateField(field);}
}/*** 传入配置合并默认的配置* @param {{*   itemAttrs?: Object,*   elAttrs?: Object,*   rule?: Object,*   labelSuffix?: string,*   isItemRule?: boolean,* }} [config] 配置* @return {{elAttrs: {border: string, clearable: boolean, inputAlign: string, numberAlign: string, textareaAlign: string}, itemAttrs: {labelWidth: string, borderBottom: boolean}, labelSuffix: string}|Object}*/
export function commonFormStyleConfigMerge(config) {let commonConfig = deepMerge({}, commonFormStyleConfig);if (commonConfig) {commonConfig = deepMerge(commonConfig, config);}if (commonConfig) {return commonConfig;}return commonFormStyleConfig;
}

custom-form-item.vue 文件

核心 form-item 组件,用于自动生成表单项

为什么没有 custom-form 组件?

其实本来有,但是在实际使用过程中发现,custom-form 局限性过大,以及无法做到深层插槽能力,所以去掉了,而使用原生的 form 以提高可用性。

<script setup>import CustomDatetimePicker from '@/components/custom-datetime-picker.vue';import { typeMap } from '@/components/form/useForm';import { computed, getCurrentInstance, ref, watch } from 'vue';import CustomIconInput from '@/components/form/custom-icon-input.vue';import CustomRegionPicker from '@/components/custom-region-picker.vue';const instance = getCurrentInstance();defineOptions({options: {virtualHost: true,},});const props = defineProps({modelValue: Object,items: Array,});const formData = ref({});const emits = defineEmits(['update:modelValue', 'choose', 'datetimeChange', 'areaChange', 'enterpriseChange']);const currentItem = ref(undefined);watch(() => props.modelValue, () => {formData.value = props.modelValue;}, {immediate: true,deep: true,});watch(() => formData.value, () => {emits('update:modelValue', formData.value);}, {deep: true,});const itemList = computed(() => {if (!props.items) return [];return props.items.map(item => {item.show = relItemShow(item);item.classList = relItemClass(item);item.elWrapperClassList = relElWrapperClass(item);// item.elAttrs = relElAttrs(item);item.attrs = relItemAttrs(item);return item;});});// 可显示的 formItem 的长度,用于判断是否显示底部边框const showLen = computed(() => {const showList = itemList.value.filter(item => item.show);if (!showList) return 0;return showList.length;});function handleDatetimeChange(item, data) {emits('datetimeChange', item, data);// 关闭日期选择instance.refs[`datetimePicker${item.prop}Ref`][0].close();}function handleChoose(item) {emits('choose', item);}function handleChooseArea(item) {instance.refs[`regionPicker${item.prop}Ref`][0].open();}function handleAreaChange(item, info) {emits('areaChange', item, info);instance.refs[`regionPicker${item.prop}Ref`][0].close();}// 解析是否显示formItem中的元素function relShow(item, type) {if (type instanceof Array) {if (!type.includes(item.type)) return false;} else if (item.type !== type) return false;return true;}// 解析是否显示formItemfunction relItemShow(item) {if (!item) return false;if (item.showRender && typeof item.showRender === 'function') {return item.showRender(formData.value, item);}return true;}// 解析formItem的classfunction relItemClass(item) {const classArr = [];if (item.customLabel) {classArr.push('custom-label');if (item.itemAttrs && item.itemAttrs.labelPosition) {classArr.push('custom-label-position-' + item.itemAttrs.labelPosition);}}const elAttrs = item.elAttrs;if (elAttrs) {if (elAttrs.textareaAlign) {classArr.push(`textarea-align-${elAttrs.textareaAlign}`);}}return classArr.join(' ');}// 解析formItem中组件容器的classfunction relElWrapperClass(item) {const classArr = [];const elAttrs = item.elAttrs;if (elAttrs) {if (elAttrs.textareaAlign) {classArr.push(`textarea-align-${elAttrs.textareaAlign}`);}}return classArr.join(' ');}// 解析组件属性function relElAttrs(item) {const attrs = {};const elAttrs = item.elAttrs;if (elAttrs && typeof elAttrs === 'object') {Object.assign(attrs, elAttrs);}const elAttrsRender = item.elAttrsRender;if (elAttrsRender && typeof elAttrsRender === 'function') {const attrsRes = elAttrsRender();Object.assign(attrs, attrsRes);}return attrs;}// 解析formItem属性function relItemAttrs(item) {const attrs = {};const itemAttrs = item.itemAttrs;if (itemAttrs && typeof itemAttrs === 'object') {Object.assign(attrs, itemAttrs);}const itemAttrsRender = item.itemAttrsRender;if (itemAttrsRender && typeof itemAttrsRender === 'function') {const attrsRes = itemAttrsRender();Object.assign(attrs, attrsRes);}return attrs;}// 解析数字组件对齐方式function relNumberStyle(item) {const style = {justifyContent: 'flex-start',};if (item.elAttrs) {if (item.elAttrs.numberAlign && item.elAttrs.numberAlign === 'right') {style.justifyContent = 'flex-end';}}return style;}
</script><template><view class="custom-form-item-list"><viewclass="custom-form-item"v-for="(item, index) in itemList":key="item.prop":class="[item.classList, {'last-item': index + 1 >= showLen}]"><slot :name="`${item.prop}Top`" :item="item"></slot><view class="custom-form-item-container"><view class="custom-label" v-if="item.customLabel"><slot :name="`${item.prop}Label`" :item="item"></slot></view><up-form-item:label="item.label":prop="item.prop"v-bind="item.attrs"v-if="item.show":border-bottom="index + 1 < showLen"><viewv-if="item.type !== 'custom'"class="form-item-el-wrapper":class="[item.elWrapperClassList]"><up-inputv-if="relShow(item, typeMap.INPUT)"v-model="formData[item.prop]"v-bind="relElAttrs(item)"></up-input><up-inputv-if="relShow(item, typeMap.INPUT_NUMBER)"v-model.number="formData[item.prop]"v-bind="relElAttrs(item)"></up-input><up-textareav-if="relShow(item, typeMap.TEXTAREA)"v-model="formData[item.prop]"v-bind="relElAttrs(item)"></up-textarea><viewclass="form-item-flex":style="relNumberStyle(item)"v-if="relShow(item, typeMap.NUMBER)"><up-number-box:name="item.prop"v-model="formData[item.prop]"v-bind="relElAttrs(item)"inputWidth="84rpx"bgColor="transparent"iconStyle="font-size: 20rpx;"></up-number-box></view><custom-icon-inputv-else-if="relShow(item, typeMap.CHOOSE)"@click="handleChoose(item)"v-model="formData[item.prop]"v-bind="relElAttrs(item)"><template #suffix><slot :name="`${item.prop}ChooseSuffix`"></slot></template></custom-icon-input><template v-else-if="relShow(item, typeMap.AREA)"><custom-icon-input@click="handleChooseArea(item)"v-model="formData[item.prop]"v-bind="relElAttrs(item)"></custom-icon-input><custom-region-picker:ref="`regionPicker${item.prop}Ref`":model-value="item.valueRender ? item.valueRender(formData[item.prop], item) : []"@confirm="(info) => handleAreaChange(item, info)"v-bind="relElAttrs(item)" /></template><custom-datetime-pickerv-else-if="relShow(item, [typeMap.DATETIME, typeMap.DATE, typeMap.TIME])"v-model="formData[item.prop]":default-date="formData[item.prop]":mode="item.type":ref="`datetimePicker${item.prop}Ref`"@confirm="(data) => handleDatetimeChange(item, data)"v-bind="relElAttrs(item)"></custom-datetime-picker></view><template v-if="item.type === 'custom'"><slot :name="item.prop" :item="item"></slot></template></up-form-item></view><up-line v-if="item.customLabel && item.itemAttrs && item.itemAttrs.borderBottom"></up-line><slot :name="`${item.prop}Bottom`" :item="item"></slot></view></view>
</template><style lang="scss" scoped>.form-item-el-wrapper {width: 100%;}.form-item-flex {width: 100%;display: flex;flex-direction: row;justify-content: flex-start;}// 自定义表单样式,下面的样式如果无效,放入全局样式中.custom-form-item {// 自定义表单隐藏 label,用于自定义 label&.custom-label {& > .custom-form-item-container {// 自定义表单 label 时,flex 布局display: flex;flex-direction: row;box-sizing: border-box;& > .u-form-item {flex: 1;padding: 0;& > .u-form-item__body {& > .u-form-item__body__left {display: none;}}// form校验提示& > .u-form-item__body__right__message {margin-bottom: 5px;}& > .u-line {display: none;}}}// label 位置为 top 时&.custom-label-position-top {& > .custom-form-item-container {flex-direction: column;}}}// 最后的 formItem&.last-item {& > .custom-form-item-container {& > .u-form-item {// form校验提示& > .u-form-item__body__right__message {margin-bottom: 5px;}}}}.form-item-el-wrapper {// 文本域居右&.textarea-align-right {.u-textarea textarea {text-align: right;}}}}
</style>

custom-form-label.vue 文件

一般用于自定义 label 使用

<script setup>import { computed } from 'vue';const props = defineProps({required: Boolean,labelStyle: Object,});const labelStyleComputed = computed(() => {const style = {};style.justifyContent = 'flex-start';return Object.assign(style, props.labelStyle || {});});
</script><template><view class="custom-form-label"><text v-if="required" class="required u-form-item__body__left__content__required">*</text><text class="label u-form-item__body__left__content__label" :style="labelStyleComputed"><slot></slot></text></view>
</template><style scoped lang="scss">.custom-form-label {position: relative;display: flex;flex-direction: row;align-items: center;padding-right: 10rpx;.required {font-size: 20px;line-height: 20px;top: 3px;}.label {display: flex;flex-direction: row;align-items: center;flex: 1;font-size: 15px;}}
</style>

custom-icon-input.vue 文件

一般表单箭头输入框使用

<script setup>import { ref, watch } from 'vue';const props = defineProps({// 绑定值modelValue: {type: [String, Number, Boolean],default: '',},// 后置图标suffixIcon: {type: String,default: 'arrow-right',},// 后置图片suffixImg: {type: String,},// 隐藏后置插槽hideSuffixSlot: Boolean,// 输入框为空时的占位符placeholder: String,// 格式化显示内容formatter: Function,// 只读readonly: {type: Boolean,default: true,},// 自定义样式customStyle: Object,// 禁用disabled: Boolean,});const inputValue = ref(undefined);const emits = defineEmits(['update:modelValue', 'click']);watch(() => props.modelValue, () => {if (props.formatter) {inputValue.value = props.formatter(props.modelValue);return;}inputValue.value = props.modelValue;}, {immediate: true,});watch(() => inputValue.value, (value) => {if (props.formatter || props.readonly) return;emits('update:modelValue', value);});function handleClick() {if (props.disabled) return;emits('click');}
</script><template><viewclass="custom-icon-input":class="{disabled: disabled}"@click="handleClick"><up-inputv-model="inputValue":readonly="readonly":placeholder="placeholder":customStyle="customStyle":disabled="disabled"disabled-color="transparent"v-bind="$attrs"><template #suffix><slot v-if="!hideSuffixSlot" name="suffix"></slot><up-icon v-if="suffixIcon" :name="suffixIcon" size="24rpx" color="#999"></up-icon><view v-if="suffixImg" class="flex-center u-flex-center"><imagestyle="width: 44.0rpx; height: 44.0rpx":src="suffixImg"></image></view></template></up-input></view>
</template><style scoped lang="scss">.custom-icon-input {width: 100%;}
</style>

custom-region-picker.vue 文件

自定义区域选择器,仅提供示范,获取区域、弹窗自行完善修改

<script setup>import CustomPopup from '@/components/custom-popup.vue';import { computed, ref, watch } from 'vue';import { useAreaStore } from '@/sheep/store/area';import CustomCommonFixedBottom from '@/components/custom-common-fixed-bottom.vue';const { getArea } = useAreaStore();const props = defineProps({modelValue: Array,title: {type: String,default: '选择区域',},});const popupRef = ref(null);const areaData = ref([]);const currentIndex = ref([0, 0, 0]);// 列是否还在滑动中,微信小程序如果在滑动中就点确定,结果可能不准确const moving = ref(false);const show = ref(false);const emits = defineEmits(['confirm', 'cancel', 'change']);const provinceList = computed(() => {if (!areaData.value) return [];return areaData.value;});const cityList = computed(() => {if (provinceList.value.length === 0) return [];const item = provinceList.value[currentIndex.value[0]];if (!item) return [];return item.children || [];});const districtList = computed(() => {if (cityList.value.length === 0) return [];const item = cityList.value[currentIndex.value[1]];return item.children || [];});watch(() => props.modelValue, () => {getCurrentIndexByValue();}, {immediate: true,deep: true,});watch(() => areaData.value, () => {getCurrentIndexByValue();});function getCurrentIndexByValue() {if (props.modelValue) {if (props.modelValue[0]) {const index = provinceList.value.findIndex(item => item.name === props.modelValue[0]);currentIndex.value[0] = index === -1 ? 0 : index;}if (props.modelValue[1]) {const index = cityList.value.findIndex(item => item.name === props.modelValue[1]);currentIndex.value[1] = index === -1 ? 0 : index;}if (props.modelValue[2]) {const index = districtList.value.findIndex(item => item.name === props.modelValue[2]);currentIndex.value[2] = index === -1 ? 0 : index;}}}getArea().then((res) => {areaData.value = res;});// 标识滑动开始,只有微信小程序才有这样的事件const pickstart = () => {// #ifdef MP-WEIXINmoving.value = true;// #endif};// 标识滑动结束const pickend = () => {// #ifdef MP-WEIXINmoving.value = false;// #endif};const onCancel = () => {emits('cancel');show.value = false;};// 用户更改picker的列选项const change = (e) => {if (currentIndex.value[0] === e.detail.value[0] &&currentIndex.value[1] === e.detail.value[1]) {// 不更改省市区列表currentIndex.value[2] = e.detail.value[2];return;} else {// 更改省市区列表if (currentIndex.value[0] !== e.detail.value[0]) {e.detail.value[1] = 0;}e.detail.value[2] = 0;currentIndex.value = e.detail.value;}emits('change', currentIndex.value);};// 用户点击确定按钮const onConfirm = (event = null) => {// #ifdef MP-WEIXINif (moving.value) return;// #endiflet index = currentIndex.value;let province = provinceList.value[index[0]];let city = cityList.value[index[1]];let district = districtList.value[index[2]];let result = {province_name: province.name,province_id: province.id,city_name: city.name,city_id: city.id,district_name: district.name,district_id: district.id,};if (event) emits(event, result);};const getSizeByNameLength = (name) => {let length = name.length;if (length <= 7) return '';if (length < 9) {return 'font-size:28rpx';} else {return 'font-size: 24rpx';}};function open() {popupRef.value.open();show.value = true;}function close() {popupRef.value.close();show.value = false;}defineExpose({open,close,});
</script><template><custom-popupref="popupRef":title="title"bg-color="#fff"@close="onCancel"height="auto":scroll-view="false"><view class="region-container"><view class="picker-container"><picker-viewv-if="provinceList.length && cityList.length && districtList.length":value="currentIndex"@change="change"class="ui-picker-view"@pickstart="pickstart"@pickend="pickend"indicator-style="height: 68rpx"><picker-view-column><view class="ui-column-item" v-for="province in provinceList" :key="province.id"><view :style="getSizeByNameLength(province.name)">{{ province.name }}</view></view></picker-view-column><picker-view-column><view class="ui-column-item" v-for="city in cityList" :key="city.id"><view :style="getSizeByNameLength(city.name)">{{ city.name }}</view></view></picker-view-column><picker-view-column><view class="ui-column-item" v-for="district in districtList" :key="district.id"><view :style="getSizeByNameLength(district.name)">{{ district.name }}</view></view></picker-view-column></picker-view></view></view><template #bottom><custom-common-fixed-bottom :fixed="false" :safe-height="false"><u-button type="primary" @click="onConfirm('confirm')">确认</u-button></custom-common-fixed-bottom></template></custom-popup>
</template><style scoped lang="scss">.region-container {height: 500rpx;display: flex;flex-direction: column;.picker-container {flex: 1;}.ui-picker-view {height: 100%;box-sizing: border-box;}.ui-column-item {display: flex;align-items: center;justify-content: center;font-size: 32rpx;color: #333;padding: 0 8rpx;}}
</style>

custom-datetime-picker.vue 文件

自定义日期选择器,更适合form表单

<script setup>import { ref, watch } from 'vue';import { onReady } from '@dcloudio/uni-app';import dayjs from 'dayjs';const props = defineProps({modelValue: [String, Number],placeholder: {type: String,default: '请选择日期',},mode: {type: String,default: 'date',},inputAlign: {type: String,default: 'left',},formatter: Function,defaultDate: [String, Number],minDate: {type: Number,default: 0,},maxDate: Number,dateFormatter: {type: Function,default: function(type, value) {if (type === 'year') {return `${value}`;}if (type === 'month') {return `${value}`;}if (type === 'day') {return `${value}`;}if (type === 'hour') {return `${value}`;}if (type === 'minute') {return `${value}`;}return value;},},});const emits = defineEmits(['update:modelValue', 'confirm', 'cancel']);const datetimePickerRef = ref(null);const datetime = ref(undefined);const show = ref(false);const inputValue = ref('');watch(() => props.defaultDate, () => {if (props.defaultDate) {datetime.value = dayjs(props.defaultDate).valueOf();}}, {immediate: true,});watch(() => props.modelValue, () => {if (props.formatter) {inputValue.value = props.formatter(props.modelValue);return;}inputValue.value = props.modelValue;}, {immediate: true,});function handleConfirm(data) {emits('update:modelValue', data.value);emits('confirm', data);}function handleCancel() {show.value = false;emits('cancel');}function setFormatter(fun) {datetimePickerRef.value.setFormatter(fun);}function close() {show.value = false;}onReady(() => {// 为了兼容微信小程序datetimePickerRef.value.setFormatter((type, value) => {if (type === 'year') {return `${value}`;}if (type === 'month') {return `${value}`;}if (type === 'day') {return `${value}`;}if (type === 'hour') {return `${value}`;}if (type === 'minute') {return `${value}`;}return value;});});defineExpose({// 兼容微信小程序抛出设置格式化方法setFormatter,close,});
</script><template><view class="custom-datetime-picker" @click="show = true"><u--inputborder="none"v-model="inputValue":placeholder="placeholder":inputAlign="inputAlign"readonly><template #suffix><view class="datetime-icon"><imagestyle="width: 44.0rpx; height: 44.0rpx":src="'/static/images/icon-' + (mode === 'time' ? 'time' : 'calendar') + '.png'"></image></view></template></u--input><u-datetime-pickerref="datetimePickerRef"v-model="datetime":show="show":maxDate="maxDate":minDate="minDate":mode="mode"@cancel="handleCancel"@confirm="handleConfirm":formatter="dateFormatter"v-bind="$attrs"></u-datetime-picker></view>
</template><style scoped lang="scss">.custom-datetime-picker {width: 100%;}.datetime-icon {height: 100%;display: flex;align-items: center;}
</style>

属性

CustomFormItem属性
参数说明类型默认值可选值
modelValue-Object--
items-Array--
useForm-items属性
参数说明类型默认值可选值
label表单标签,通过该属性值自动拼装 placeholder 与校验提示语String--
prop表单属性,用于双向绑定及表单验证String--
required必填,会自动添加红星,自动拼装提示语Booleanfalsetrue
type表单项类型,内置默认的一些类型,不满足的可通过设置 custom 类型自定义内容,插槽名称为 prop 属性值Stringinputcustom:自定义(通过prop插槽自定义内容)、choose:选择(只显示选择样式)、date:日期、datetime:日期时间、time:时间、area:区域选择
customLabel自定义 label 内容,可通过 prop + Label 插槽自定义内容,如果只是想添加额外元素而保留原 label 样式可使用 custom-form-label 组件Booleanfalse-
itemAttrsformItem 组件的属性,具体见 uview 的 u-form-item 属性Object--
elAttrs表单项内组件的属性,具体依不同类型组件而异Object--
rule表单项的验证,当内容为对象时,会根据 config 的 rule 属性即 item 的 required 属性生成的验证自动合并,优先级:required < config < item,内容为数组时则直接以 item 的 rule 为验证规则Object|Object[]--
showRender是否显示的渲染函数,参数:formData表单数据、item信息,返回 true 或 false 来控制当前项是否显示Function--
useForm-config属性
参数说明类型默认值可选值
itemAttrs全局的 formItem 属性Object--
elAttrs全局的表单项内组件的属性Object--
rule全局的表单验证规则,优先级小于 item 的 rule 大于 required 生成的规则Object--
labelSuffixlabel 文本追加的内容---

回调时间可通过 elAttrs 中定义on事件函数实现,如onClick,或者使用全局回调事件

CustomFormItem插槽

其中 {prop} 为 useForm 中 items 的 prop 值

名称说明
{prop}Top当前项上方插槽
{prop}Bottom当前项下方插槽
{prop}Label自定义 label 插槽,需通过 customLabel 属性开启
{prop}ChooseSuffixchoose 组件的后置内容,type=choose 时有效
{prop}表单项内容,type=custom 时有效

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/899758.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android: Handler 的用法详解

Android 中 Handler 的用法详解 Handler 是 Android 中用于线程间通信的重要机制&#xff0c;主要用于在不同线程之间发送和处理消息。以下是 Handler 的全面用法指南&#xff1a; 一、Handler 的基本原理 Handler 基于消息队列(MessageQueue)和循环器(Looper)工作&#xff…

UE5学习笔记 FPS游戏制作33 游戏保存

文章目录 核心思想创建数据对象创建UIUI参数和方法打开UI存档文件的位置可以保存的数据类型 核心思想 UE自己有保存游戏的功能&#xff0c;核心节点&#xff0c;类似于json操作&#xff0c;需要一个数据类的对象来进行保存和读取 创建存档 加载存档 保存存档 创建数据对象…

【蓝桥杯】 枚举和模拟练习题

系列文章目录 蓝桥杯例题 枚举和模拟 文章目录 系列文章目录前言一、好数&#xff1a; 题目参考&#xff1a;核心思想&#xff1a;代码实现&#xff1a; 二、艺术与篮球&#xff1a; 题目参考&#xff1a;核心思想&#xff1a;代码实现: 总结 前言 今天距离蓝桥杯还有13天&…

大数据技术之Scala:特性、应用与生态系统

摘要 Scala 作为一门融合面向对象编程与函数式编程范式的编程语言&#xff0c;在大数据领域展现出独特优势。本文深入探讨 Scala 的核心特性&#xff0c;如函数式编程特性、类型系统以及与 Java 的兼容性等。同时&#xff0c;阐述其在大数据处理框架&#xff08;如 Apache Spa…

Linux信号——信号的产生(1)

注&#xff1a;信号vs信号量&#xff1a;两者没有任何关系&#xff01; 信号是什么&#xff1f; Linux系统提供的&#xff0c;让用户&#xff08;进程&#xff09;给其他进程发送异步信息的一种方式。 进程看待信号的方式&#xff1a; 1.信号在没有发生的时候&#xff0c;进…

数据结构和算法——汉诺塔问题

前言 先讲个故事&#xff0c;传说古代印度有三根黄金柱&#xff0c;64个石盘&#xff0c;需要将石盘从第一根移动到第三根上&#xff0c;规定每次只能移动一片&#xff0c;并且小盘在放置时必须在大盘上。 当石盘移动完毕时&#xff0c;世界就会毁灭。 汉诺塔——递归 接下来…

2023年3月全国计算机等级考试真题(二级C语言)

&#x1f600; 第1题 下列叙述中错误的是 A. 向量是线性结构 B. 非空线性结构中只有一个结点没有前件 C. 非空线性结构中只有一个结点没有后件 D. 只有一个根结点和一个叶子结点的结构必定是线性结构 概念澄清 首先&#xff0c;我们需要明确几个关键概念&#xf…

Kafka简单的性能调优

Kafka 的性能调优是一个系统性工程&#xff0c;需要从生产者、消费者、Broker 配置以及集群架构等多个层面进行综合调整。以下是一些关键的性能调优策略&#xff1a; 一、生产者性能优化 批量发送 batch.size&#xff1a;控制消息批量的最大字节数&#xff0c;默认值为 16KB。…

微前端 - 以无界为例

一、微前端核心概念 微前端是一种将单体前端应用拆分为多个独立子应用的架构模式&#xff0c;每个子应用可独立开发、部署和运行&#xff0c;具备以下特点&#xff1a; 技术栈无关性&#xff1a;允许主应用和子应用使用不同框架&#xff08;如 React Vue&#xff09;。独立部…

企业级日志分析平台: ELK 集群搭建指南

前言&#xff1a;在当今数字化时代&#xff0c;数据已经成为企业决策的核心驱动力。无论是日志分析、用户行为追踪&#xff0c;还是实时监控和异常检测&#xff0c;高效的数据处理和可视化能力都至关重要。ELK&#xff08;Elasticsearch、Logstash、Kibana&#xff09;作为全球…

1.2-WAF\CDN\OSS\反向代理\负载均衡

WAF&#xff1a;就是网站应用防火墙&#xff0c;有硬件类、软件类、云WAF&#xff1b; 还有网站内置的WAF&#xff0c;内置的WAF就是直接嵌在代码中的安全防护代码 硬件类&#xff1a;Imperva、天清WAG 软件&#xff1a;安全狗、D盾、云锁 云&#xff1a;阿里云盾、腾讯云WA…

MybatisPlus(SpringBoot版)学习第四讲:常用注解

目录 1.TableName 1.1 问题 1.2 通过TableName解决问题 1.3 通过全局配置解决问题 2.TableId 2.1 问题 2.2 通过TableId解决问题 2.3 TableId的value属性 2.4 TableId的type属性 2.5 雪花算法 1.背景 2.数据库分表 ①垂直分表 ②水平分表 1>主键自增 2>取…

第二届计算机网络和云计算国际会议(CNCC 2025)

重要信息 官网&#xff1a;www.iccncc.org 时间&#xff1a;2025年4月11-13日 地点&#xff1a;中国南昌 简介 第二届计算机网络和云计算国际会议&#xff08;CNCC 2025&#xff09;将于2025年4月11-13日在中国南昌召开。围绕“计算机网络”与“云计算”展开研讨&#xff…

【大模型基础_毛玉仁】5.4 定位编辑法:ROME

目录 5.4 定位编辑法&#xff1a;ROME5.4.1 知识存储位置1&#xff09;因果跟踪实验2&#xff09;阻断实验 5.4.2 知识存储机制5.4.3 精准知识编辑1&#xff09;确定键向量2&#xff09;优化值向量3&#xff09;插入知识 5.4 定位编辑法&#xff1a;ROME 定位编辑&#xff1a;…

横扫SQL面试——连续性登录问题

横扫SQL面试 &#x1f4cc; 连续性登录问题 在互联网公司的SQL面试中&#xff0c;连续性问题堪称“必考之王”。&#x1f4bb;&#x1f50d; 用户连续登录7天送优惠券&#x1f31f;&#xff0c;服务器连续报警3次触发熔断⚠️&#xff0c;图书馆连续3天人流破百开启限流⚡” …

Spring AI Alibaba 对话记忆使用

一、对话记忆 (ChatMemory)简介 1、对话记忆介绍 ”大模型的对话记忆”这一概念&#xff0c;根植于人工智能与自然语言处理领域&#xff0c;特别是针对具有深度学习能力的大型语言模型而言&#xff0c;它指的是模型在与用户进行交互式对话过程中&#xff0c;能够追踪、理解并利…

vdi模式是什么

‌VDI模式&#xff08;Virtual Desktop Infrastructure&#xff09;是一种基于服务器的计算模型&#xff0c;其核心思想是将所有计算和存储资源集中在服务器上&#xff0c;用户通过前端设备&#xff08;如瘦客户机&#xff09;访问服务器上的虚拟桌面‌‌ VDI模式的工作原理 在…

【分布式】深入剖析 Sentinel 限流:原理、实现

在当今分布式系统盛行的时代&#xff0c;流量的剧增给系统稳定性带来了巨大挑战。Sentinel 作为一款强大的流量控制组件&#xff0c;在保障系统平稳运行方面发挥着关键作用。本文将深入探讨 Sentinel 限流的原理、实现方案以及其优缺点&#xff0c;助力开发者更好地运用这一工具…

c#winform,倒鸭子字幕效果,typemonkey字幕效果,抖音瀑布流字幕效果

不废话 直接上效果图 C# winform 开发抖音的瀑布流字幕。 也是typemonkey插件字幕效果 或者咱再网上常说的倒鸭子字幕效果 主要功能 1&#xff0c;软件可以自定义添加字幕内容 2&#xff0c;软件可以添加字幕显示的时间区间 3&#xff0c;可以自定义字幕颜色&#xff0c;可以随…

Pycharm(八):字符串切片

一、字符串分片介绍 对操作的对象截取其中一部分的操作&#xff0c;比如想要获取字符串“888666qq.com前面的qq号的时候就可以用切片。 字符串、列表、元组都支持切片操作。 语法&#xff1a;字符串变量名 [起始:结束:步长] 口诀&#xff1a;切片其实很简单&#xff0c;只顾头来…