feat(移动端订单): 优化商品自定义表单控件样式与交互

This commit is contained in:
From-wh 2026-10-21 11:45:12 +08:00 committed by 吴昊天
parent 7405042247
commit a8d86e1301

View File

@ -10,48 +10,68 @@
<view class="custom-form-section" v-if="visible && fields.length > 0">
<view class="section-title">{{ $t('必填信息') }}</view>
<view class="form-list">
<view class="form-item" v-for="(field, index) in fields" :key="index">
<view class="form-item" v-for="(field, index) in fields" :key="index" :class="{ 'is-bordered': index > 0 }">
<view class="form-label">
{{ field.label }}
<text class="required" v-if="field.required">*</text>
<text class="required-holder" v-else></text>
{{ field.label }}
</view>
<view class="form-input-wrap">
<!-- 文本类:文本框/数字/邮件/身份证/手机号,按类型切换键盘 -->
<input
v-if="isTextInput(field)"
class="form-input"
:class="{ 'is-focus': focusField === field.field }"
:type="textInputKeyboard(field)"
:placeholder="$t('请输入') + field.label"
:value="formData[field.field]"
@focus="focusField = field.field"
@blur="focusField = ''"
@input="onInput(field.field, $event)"
/>
<!-- 下拉选择(自定义 options) -->
<!-- 下拉选择(自定义 options,历史值) -->
<picker
v-else-if="field.type === 'picker'"
class="form-picker-wrap"
:range="field.options"
range-key="label"
@change="onPickerChange(field, $event)"
>
<view class="form-picker">
{{ formData[field.field] || $t('请选择') }}
<view class="form-picker" :class="{ 'is-set': formData[field.field] }">
<view class="pick-main">
<text class="iconfont pick-icon icon-fenlei1"></text>
<text class="pick-value">{{ pickLabel(field) || $t('请选择') }}</text>
</view>
<text class="picker-arrow">›</text>
</view>
</picker>
<!-- 日期(管理端类型值 data,兼容 date) -->
<picker
v-else-if="field.type === 'data' || field.type === 'date'"
class="form-picker-wrap"
mode="date"
@change="onValueChange(field.field, $event)"
>
<view class="form-picker">
{{ formData[field.field] || $t('请选择') + field.label }}
<view class="form-picker" :class="{ 'is-set': formData[field.field] }">
<view class="pick-main">
<text class="iconfont pick-icon icon-rilitubiao"></text>
<text class="pick-value">{{ formData[field.field] || $t('请选择') + field.label }}</text>
</view>
<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 }}
<picker
v-else-if="field.type === 'time'"
class="form-picker-wrap"
mode="time"
@change="onValueChange(field.field, $event)"
>
<view class="form-picker" :class="{ 'is-set': formData[field.field] }">
<view class="pick-main">
<text class="iconfont pick-icon icon-shijian"></text>
<text class="pick-value">{{ formData[field.field] || $t('请选择') + field.label }}</text>
</view>
<text class="picker-arrow">›</text>
</view>
</picker>
@ -71,8 +91,11 @@
<textarea
v-else-if="field.type === 'textarea'"
class="form-textarea"
:class="{ 'is-focus': focusField === field.field }"
:placeholder="$t('请输入') + field.label"
:value="formData[field.field]"
@focus="focusField = field.field"
@blur="focusField = ''"
@input="onInput(field.field, $event)"
></textarea>
</view>
@ -82,7 +105,7 @@
</template>
<script setup>
import { computed } from 'vue';
import { computed, ref } from 'vue';
import i18n from '@/utils/lang.js';
import util from '@/utils/util.js';
@ -102,6 +125,9 @@ const emit = defineEmits(['update:modelValue', 'change']);
/** 当前表单数据(基于 value 拷贝避免直接修改 prop) */
const formData = computed(() => props.modelValue || {});
/** 当前聚焦的输入字段(用于高亮样式) */
const focusField = ref('');
/** 文本类输入类型(input 为历史值,其余为管理端类型) */
var TEXT_INPUT_TYPES = ['input', 'text', 'number', 'email', 'id', 'phone'];
/** 各类型格式校验规则,返回错误提示文案,通过返回空串 */
@ -121,6 +147,14 @@ function textInputKeyboard(field) {
return field.type === 'number' || field.type === 'phone' ? 'number' : 'text';
}
/** 下拉字段按已选 value 反查选项文案(无匹配时回退显示原值) */
function pickLabel(field) {
var val = formData.value[field.field];
if (val === undefined || val === null || val === '') return '';
var opt = (field.options || []).find(function (o) { return o.value === val; });
return opt ? opt.label : String(val);
}
/** 更新字段值并通知父组件 */
function setFieldValue(field, val) {
var next = { ...formData.value, [field]: val };
@ -186,64 +220,135 @@ defineExpose({ validate });
margin: 20rpx;
background: #fff;
border-radius: 12rpx;
padding: 24rpx 30rpx;
padding: 24rpx 30rpx 12rpx;
}
.section-title {
display: flex;
align-items: center;
font-size: 30rpx;
font-weight: bold;
color: #282828;
margin-bottom: 20rpx;
margin-bottom: 8rpx;
}
/* 标题左侧主题色装饰条 */
.section-title::before {
content: '';
width: 6rpx;
height: 28rpx;
border-radius: 3rpx;
background: var(--view-theme, #e93323);
margin-right: 12rpx;
}
.form-list {
border-top: 1px dashed #eee;
padding-top: 16rpx;
padding-top: 4rpx;
}
.form-item {
padding: 16rpx 0;
padding: 20rpx 0;
}
/* 相邻选择器小程序 WXSS 支持不稳,用 class 显式加分隔线 */
.form-item.is-bordered {
border-top: 1px solid #f5f5f5;
}
.form-label {
display: flex;
align-items: center;
font-size: 26rpx;
color: #333;
margin-bottom: 12rpx;
margin-bottom: 14rpx;
line-height: 1;
}
/* 必填星号前置,与必填占位符等宽,保证所有标题左对齐 */
.form-label .required {
display: inline-block;
width: 24rpx;
color: var(--view-theme, #e93323);
margin-left: 4rpx;
font-size: 28rpx;
line-height: 1;
}
.required-holder {
display: inline-block;
width: 24rpx;
}
.form-input-wrap {
display: flex;
align-items: center;
}
.form-input {
flex: 1;
height: 80rpx;
background: #f8f8f8;
border-radius: 8rpx;
padding: 0 24rpx;
font-size: 28rpx;
}
.form-input,
.form-textarea {
flex: 1;
height: 160rpx;
width: 100%;
background: #f8f8f8;
border-radius: 8rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
}
.form-picker {
flex: 1;
height: 80rpx;
background: #f8f8f8;
border-radius: 8rpx;
border: 1px solid transparent;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #282828;
box-sizing: border-box;
transition: background-color 0.2s, border-color 0.2s;
}
.form-input {
height: 80rpx;
}
.form-textarea {
height: 160rpx;
padding: 20rpx 24rpx;
}
/* 聚焦态:白底 + 主题色描边 */
.form-input.is-focus,
.form-textarea.is-focus {
background: #fff;
border-color: var(--view-theme, #e93323);
}
/* 下拉/日期/时间选择器:占满整行(picker 包裹层不伸展会导致内容宽度塌缩) */
.form-picker-wrap {
flex: 1;
width: 100%;
display: block;
}
.form-picker {
width: 100%;
height: 80rpx;
background: #f8f8f8;
border: 1px solid transparent;
border-radius: 12rpx;
padding: 0 24rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
color: #999;
transition: background-color 0.2s, border-color 0.2s;
}
.form-picker:active {
background: #f2f2f2;
border-color: var(--view-theme, #e93323);
}
.pick-main {
display: flex;
align-items: center;
min-width: 0;
}
/* 日期/时间/下拉的类型图标 */
.pick-icon {
font-size: 30rpx;
color: var(--view-theme, #e93323);
margin-right: 12rpx;
line-height: 1;
}
.pick-value {
font-size: 28rpx;
color: #bbb;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 已选择值用深色 */
.form-picker.is-set .pick-value {
color: #282828;
}
.picker-arrow {
font-size: 36rpx;
font-size: 32rpx;
color: #ccc;
margin-left: 12rpx;
}
.form-upload {
position: relative;
@ -254,7 +359,7 @@ defineExpose({ validate });
width: 100%;
height: 100%;
border: 1px dashed #ddd;
border-radius: 8rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
@ -264,7 +369,7 @@ defineExpose({ validate });
.upload-preview {
width: 100%;
height: 100%;
border-radius: 8rpx;
border-radius: 12rpx;
}
.upload-del {
position: absolute;