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.pswp.cn/news/899758.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/899758.shtml
英文地址,請注明出處:http://en.pswp.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;只顧頭來…