feat(移动端): 订单自定义表单支持全部管理端类型与格式校验

支持 text/number/email/data/time/id/phone/img 八类型(键盘按类型切换、日期时间选择器、图片上传预览),兼容历史 input/textarea/picker;数字/邮箱/手机号/身份证格式校验
This commit is contained in:
吴昊天 2026-10-16 15:15:40 +08:00
parent 6b6be74959
commit cf78510816

View File

@ -2,6 +2,9 @@
订单自定义表单
根据后端 custom_form 动态渲染必填信息字段
字段结构:{ label, field, type, options, required }
type 为管理端「自定义表单」类型:text 文本框 / number 数字 / email 邮件 /
data 日期 / time 时间 / id 身份证 / phone 手机号 / img 图片,
兼容历史值 input / textarea / picker
-->
<template>
<view class="custom-form-section" v-if="visible && fields.length > 0">
@ -13,15 +16,16 @@
<text class="required" v-if="field.required">*</text>
</view>
<view class="form-input-wrap">
<!-- 单行文本 -->
<!-- 文本类:文本框/数字/邮件/身份证/手机号,按类型切换键盘 -->
<input
v-if="field.type === 'input'"
v-if="isTextInput(field)"
class="form-input"
:type="textInputKeyboard(field)"
:placeholder="$t('请输入') + field.label"
:value="formData[field.field]"
@input="onInput(field.field, $event)"
/>
<!-- 下拉选择 -->
<!-- 下拉选择(自定义 options) -->
<picker
v-else-if="field.type === 'picker'"
:range="field.options"
@ -33,6 +37,36 @@
<text class="picker-arrow">›</text>
</view>
</picker>
<!-- 日期(管理端类型值 data,兼容 date) -->
<picker
v-else-if="field.type === 'data' || field.type === 'date'"
mode="date"
@change="onValueChange(field.field, $event)"
>
<view class="form-picker">
{{ formData[field.field] || $t('请选择') + field.label }}
<text class="picker-arrow">›</text>
</view>
</picker>
<!-- 时间 -->
<picker v-else-if="field.type === 'time'" mode="time" @change="onValueChange(field.field, $event)">
<view class="form-picker">
{{ formData[field.field] || $t('请选择') + field.label }}
<text class="picker-arrow">›</text>
</view>
</picker>
<!-- 图片上传 -->
<view v-else-if="field.type === 'img'" class="form-upload">
<image
v-if="formData[field.field]"
class="upload-preview"
:src="formData[field.field]"
mode="aspectFill"
@click="previewUpload(formData[field.field])"
/>
<view v-else class="upload-add" @click="uploadImage(field.field)">+</view>
<view v-if="formData[field.field]" class="upload-del" @click.stop="removeImage(field.field)">×</view>
</view>
<!-- 多行文本 -->
<textarea
v-else-if="field.type === 'textarea'"
@ -50,6 +84,7 @@
<script setup>
import { computed } from 'vue';
import i18n from '@/utils/lang.js';
import util from '@/utils/util.js';
defineOptions({ name: 'OrderCustomForm' });
@ -67,30 +102,75 @@ const emit = defineEmits(['update:modelValue', 'change']);
/** 当前表单数据(基于 value 拷贝避免直接修改 prop) */
const formData = computed(() => props.modelValue || {});
/** 文本类输入事件,更新对应字段并通知父组件 */
function onInput(field, e) {
var val = e.detail.value;
/** 文本类输入类型(input 为历史值,其余为管理端类型) */
var TEXT_INPUT_TYPES = ['input', 'text', 'number', 'email', 'id', 'phone'];
/** 各类型格式校验规则,返回错误提示文案,通过返回空串 */
var FORMAT_RULES = {
number: /^[+-]?\d+(\.\d+)?$/,
email: /^[\w.+-]+@[\w-]+(\.[\w-]+)+$/,
phone: /^1[3-9]\d{9}$/,
id: /(^\d{15}$)|(^\d{17}[\dXx]$)/,
};
/** 是否文本类输入框 */
function isTextInput(field) {
return TEXT_INPUT_TYPES.indexOf(field.type) !== -1;
}
/** 文本类输入键盘:数字/手机号用数字键盘,其余默认键盘 */
function textInputKeyboard(field) {
return field.type === 'number' || field.type === 'phone' ? 'number' : 'text';
}
/** 更新字段值并通知父组件 */
function setFieldValue(field, val) {
var next = { ...formData.value, [field]: val };
emit('update:modelValue', next);
emit('change', next);
}
/** 文本类输入事件 */
function onInput(field, e) {
setFieldValue(field, e.detail.value);
}
/** picker 选择事件,根据 options 取出 value */
function onPickerChange(field, e) {
var option = field.options[e.detail.value] || {};
var next = { ...formData.value, [field.field]: option.value };
emit('update:modelValue', next);
emit('change', next);
setFieldValue(field.field, option.value);
}
/** 校验必填字段是否已填写 */
/** 日期/时间选择事件,直接取选中值 */
function onValueChange(field, e) {
setFieldValue(field, e.detail.value);
}
/** 上传图片字段:选择并上传后存图片 URL */
function uploadImage(field) {
util.uploadImageOne('upload/image', function (res) {
setFieldValue(field, res.data.url);
});
}
/** 删除已上传图片 */
function removeImage(field) {
setFieldValue(field, '');
}
/** 预览已上传图片 */
function previewUpload(url) {
uni.previewImage({ urls: [url] });
}
/** 校验必填与格式,通过返回 true */
function validate() {
for (var i = 0; i < props.fields.length; i++) {
var field = props.fields[i];
if (field.required) {
var val = formData.value[field.field];
if (!val || (typeof val === 'string' && val.trim() === '')) {
uni.showToast({ title: i18n.t('请填写') + field.label, icon: 'none' });
return false;
}
var val = formData.value[field.field];
var isEmpty = !val || (typeof val === 'string' && val.trim() === '');
if (field.required && isEmpty) {
uni.showToast({ title: i18n.t('请填写') + field.label, icon: 'none' });
return false;
}
if (isEmpty) continue;
var rule = FORMAT_RULES[field.type];
if (rule && !rule.test(String(val).trim())) {
uni.showToast({ title: i18n.t('请输入正确的') + field.label, icon: 'none' });
return false;
}
}
return true;
@ -165,4 +245,38 @@ defineExpose({ validate });
.picker-arrow {
font-size: 36rpx;
}
.form-upload {
position: relative;
width: 140rpx;
height: 140rpx;
}
.upload-add {
width: 100%;
height: 100%;
border: 1px dashed #ddd;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 60rpx;
color: #c0c4cc;
}
.upload-preview {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.upload-del {
position: absolute;
top: -14rpx;
right: -14rpx;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: #666;
color: #fff;
font-size: 28rpx;
line-height: 36rpx;
text-align: center;
}
</style>