1. 为ns-newform和ns-form的单选/多选组添加内联样式属性`_optionLayoutStyle` 2. 预计算垂直/流式布局的flex样式,解决跨端渲染兼容问题 3. 调整本地配置文件的启用项注释
1481 lines
47 KiB
Vue
1481 lines
47 KiB
Vue
<template>
|
||
<view class="form-wrap form-component" :style="formWrapStyle" >
|
||
<view v-for="(item, index) in formData" :key="index">
|
||
<!-- 文本输入框 -->
|
||
<view v-if="item.controller == 'Text'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]" :style="rowCellStyle">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="tit">{{ item.value.title }}</text>
|
||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'align-left']" :style="item._boxAlignStyle">
|
||
<input
|
||
type="text"
|
||
:placeholder="item.value.placeholder"
|
||
placeholder-class="placeholder color-tip"
|
||
v-model="item.val"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 多行文本输入框 -->
|
||
<view v-if="item.controller == 'Textarea'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
|
||
<view :class="['order-cell', 'flex-box', 'textarea', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="tit">{{ item.value.title }}</text>
|
||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'align-left']" :style="item._boxAlignStyle">
|
||
<textarea
|
||
:placeholder="item.value.placeholder"
|
||
placeholder-class="placeholder color-tip"
|
||
v-model="item.val"
|
||
></textarea>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 下拉选择器 -->
|
||
<view v-if="item.controller == 'Select'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
|
||
<picker
|
||
mode="selector"
|
||
:range="item.value.options"
|
||
@change="pickerChange($event, index)"
|
||
>
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="tit">{{ item.value.title }}</text>
|
||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'box-flex', 'align-right']">
|
||
<text v-if="item.val != ''">{{ item.val }}</text>
|
||
<text v-else class="color-tip">{{ item.value.placeholder || $lang('components.ns-newform.selectPlaceholder') }}</text>
|
||
<text class="iconfont icon-right"></text>
|
||
</view>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<!-- 复选框 -->
|
||
<view v-if="item.controller == 'Checkbox'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="tit">{{ item.value.title }}</text>
|
||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'check-group-box', 'align-left', 'option-layout-' + (item.value.option_layout || 'wrap')]">
|
||
<checkbox-group @change="checkboxChange($event, index)" :style="item._optionLayoutStyle">
|
||
<label v-for="(v, k) in item.option_lists" :key="k">
|
||
<checkbox :value="v.value" :checked="v.checked"></checkbox>
|
||
<view class="checkbox">
|
||
<text
|
||
:class="[
|
||
'iconfont',
|
||
!v.checked ? 'icon-fuxuankuang2' : '',
|
||
v.checked ? 'icon-fuxuankuang1' : ''
|
||
]"
|
||
:style="{ color: v.checked ? checkedColor : '' }"
|
||
></text>
|
||
{{ v.value }}
|
||
</view>
|
||
</label>
|
||
</checkbox-group>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 单选框 -->
|
||
<view v-if="item.controller == 'Radio'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="tit">{{ item.value.title }}</text>
|
||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'radio-group-box', 'align-left', 'option-layout-' + (item.value.option_layout || 'wrap')]">
|
||
<radio-group @change="radioChange($event, index)" :style="item._optionLayoutStyle">
|
||
<label v-for="(v, k) in item.option_lists" :key="k">
|
||
<radio :value="v.value" :checked="item.val == v.value"></radio>
|
||
<view class="radio-box">
|
||
<text
|
||
:class="[
|
||
'iconfont',
|
||
item.val != v.value ? 'icon-yuan_checkbox' : '',
|
||
item.val == v.value ? 'icon-yuan_checked' : ''
|
||
]"
|
||
:style="{ color: item.val == v.value ? checkedColor : '' }"
|
||
></text>
|
||
{{ v.value }}
|
||
</view>
|
||
</label>
|
||
</radio-group>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 图片上传 -->
|
||
<view v-if="item.controller == 'Img'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
|
||
<view :class="['order-cell', 'flex-box', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="tit">{{ item.value.title }}</text>
|
||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'img-boxs', 'align-left']" :style="item._boxAlignStyle">
|
||
<view v-for="(v, k) in item.img_lists" :key="k" class="img-box" @tap="uploadImg(index)">
|
||
<image :src="$util.img(v)" mode="aspectFill" show-menu-by-longpress></image>
|
||
<text class="iconfont icon-guanbi" @tap.stop="delImg(k, index)"></text>
|
||
</view>
|
||
<view class="img-box" @tap="addImg(index)">
|
||
<text class="iconfont icon-add1"></text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 日期选择器 -->
|
||
<view v-if="item.controller == 'Date'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="tit">{{ item.value.title }}</text>
|
||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'box-flex', 'align-right']">
|
||
<picker
|
||
mode="date"
|
||
:value="item.val"
|
||
@change="bindDateChange($event, index)"
|
||
>
|
||
<view :class="['uni-input', !item.val ? 'color-tip' : '']">
|
||
{{ item.val ? item.val : (item.value.placeholder || $lang('components.ns-newform.datePlaceholder')) }}
|
||
</view>
|
||
</picker>
|
||
<text class="iconfont icon-right"></text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 日期范围选择器 -->
|
||
<view v-if="item.controller == 'Datelimit'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
|
||
<view :class="['order-cell', 'flex-box', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="tit">{{ item.value.title }}</text>
|
||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'date-boxs', 'align-center']" :style="item._boxAlignStyle">
|
||
<view class="date-box">
|
||
<picker
|
||
mode="date"
|
||
:value="item.start_date"
|
||
@change="bindStartDateChange($event, index)"
|
||
>
|
||
<view class="picker-box">
|
||
<view :class="['uni-input', !item.start_date ? 'color-tip' : '']">
|
||
{{ item.start_date ? item.start_date : (item.value.placeholder_start || $lang('components.ns-newform.startDate')) }}
|
||
</view>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
<view class="interval iconfont icon-jian"></view>
|
||
<view class="date-box">
|
||
<picker
|
||
mode="date"
|
||
:value="item.end_date"
|
||
@change="bindEndDateChange($event, index)"
|
||
>
|
||
<view class="picker-box">
|
||
<view :class="['uni-input', !item.end_date ? 'color-tip' : '']">
|
||
{{ item.end_date ? item.end_date : (item.value.placeholder_end || $lang('components.ns-newform.endDate')) }}
|
||
</view>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 时间选择器 -->
|
||
<view v-if="item.controller == 'Time'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="tit">{{ item.value.title }}</text>
|
||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'box-flex', 'align-right']">
|
||
<picker
|
||
mode="time"
|
||
:value="item.val"
|
||
@change="bindTimeChange($event, index)"
|
||
>
|
||
<view :class="['uni-input', !item.val ? 'color-tip' : '']">
|
||
{{ item.val ? item.val : (item.value.placeholder || $lang('components.ns-newform.timePlaceholder')) }}
|
||
</view>
|
||
</picker>
|
||
<text class="iconfont icon-right"></text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 时间范围选择器 -->
|
||
<view v-if="item.controller == 'Timelimit'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
|
||
<view :class="['order-cell', 'flex-box', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="tit">{{ item.value.title }}</text>
|
||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'date-boxs', 'align-center']" :style="item._boxAlignStyle">
|
||
<view class="date-box">
|
||
<picker
|
||
mode="time"
|
||
:value="item.start_time"
|
||
@change="bindStartTimeChange($event, index)"
|
||
>
|
||
<view class="picker-box">
|
||
<view :class="['uni-input', !item.start_time ? 'color-tip' : '']">
|
||
{{ item.start_time ? item.start_time : (item.value.placeholder_start || $lang('components.ns-newform.startTime')) }}
|
||
</view>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
<view class="interval iconfont icon-jian"></view>
|
||
<view class="date-box">
|
||
<picker
|
||
mode="time"
|
||
:value="item.end_time"
|
||
@change="bindEndTimeChange($event, index)"
|
||
>
|
||
<view class="picker-box">
|
||
<view :class="['uni-input', !item.end_time ? 'color-tip' : '']">
|
||
{{ item.end_time ? item.end_time : (item.value.placeholder_end || $lang('components.ns-newform.endTime')) }}
|
||
</view>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 城市选择器 -->
|
||
<view v-if="item.controller == 'City'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="tit">{{ item.value.title }}</text>
|
||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'box-flex', 'align-right']">
|
||
<pick-regions
|
||
:defaultRegions="item.default_regions"
|
||
:selectArr="item.select_arr"
|
||
@getRegions="handleGetRegions($event, index)"
|
||
>
|
||
<view :class="['select-address', !item.val ? 'empty' : '', !item.val ? 'color-tip' : '']">
|
||
{{ item.val ? item.val : (item.value.placeholder || (item.value.default_type == 1 ? $lang('components.ns-newform.selectProvinceCity') : $lang('components.ns-newform.selectProvinceCityDistrict'))) }}
|
||
</view>
|
||
</pick-regions>
|
||
<text class="iconfont icon-right"></text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import formPlatformMixin from '@/common/js/form-platform-mixin.js';
|
||
|
||
export default {
|
||
name: 'ns-newform',
|
||
mixins: [formPlatformMixin],
|
||
props: {
|
||
data: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
customAttr: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
mode: {
|
||
type: String,
|
||
default: 'horizontal',
|
||
validator: (value) => !value || ['horizontal', 'vertical'].includes(value)
|
||
},
|
||
// 表单配置(按平台差异化)
|
||
// 格式:{
|
||
// form_padding_x: 14, // 全局默认值
|
||
// mp: { form_padding_x: 10 }, // 微信小程序平台覆盖
|
||
// h5: { row_padding_y: 8 }, // H5 平台覆盖
|
||
// harmony: { ... } // 鸿蒙元服务覆盖
|
||
// }
|
||
formConfig: {
|
||
type: Object,
|
||
default: () => ({})
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
formData: [],
|
||
maxLabelWidth: 0
|
||
}
|
||
},
|
||
created() {
|
||
// 避免直接修改 prop,初始化本地数据
|
||
this.initFormData();
|
||
},
|
||
mounted() {
|
||
this.$nextTick(() => {
|
||
this.calculateMaxLabelWidth();
|
||
});
|
||
},
|
||
watch: {
|
||
data: {
|
||
handler() {
|
||
this.initFormData();
|
||
this.$nextTick(() => {
|
||
this.calculateMaxLabelWidth();
|
||
});
|
||
},
|
||
deep: true
|
||
},
|
||
// 配置变化(如字号 / 模式切换)时,重新计算 maxLabelWidth
|
||
formConfig: {
|
||
handler() {
|
||
this.$nextTick(() => {
|
||
this.calculateMaxLabelWidth();
|
||
});
|
||
},
|
||
deep: true
|
||
},
|
||
mode() {
|
||
this.$nextTick(() => {
|
||
this.calculateMaxLabelWidth();
|
||
});
|
||
}
|
||
},
|
||
computed: {
|
||
// 合并配置:内置默认 < 全局 < 当前平台
|
||
config() {
|
||
// 内置默认值(最低优先级)- 与后端 D:\projects\shop-projects\backend\src\app\component\view\form\js\design.js getDefaultFormData().formConfig 对齐
|
||
const defaults = {
|
||
formPaddingX: 14,
|
||
bannerPaddingX: 0,
|
||
tipPaddingX: 14,
|
||
rowPaddingY: 4,
|
||
rowMinHeight: 60,
|
||
labelGap: 16,
|
||
formBgColor: '#ffffff',
|
||
rowDividerColor: '#f0f0f0',
|
||
showRowDivider: true,
|
||
inputHeight: 60,
|
||
inputRadius: 10,
|
||
labelWidthMode: 'auto',
|
||
labelFixedWidth: 180,
|
||
labelFontSize: 28,
|
||
labelColor: '#1f2937',
|
||
labelFontWeight: 600,
|
||
labelAutoMaxWidth: 200, // auto 模式下 label 宽度上限(rpx),超过后 ellipsis 截断
|
||
requiredColor: '#ef4444',
|
||
checkedColor: '#ff4544',
|
||
placeholderColor: '#ddd',
|
||
placeholderFontSize: 28,
|
||
// 输入框视觉增强(可由后端 form_config 覆盖)
|
||
inputBorderColor: '#e5e7eb',
|
||
inputBgColor: '#f9fafb',
|
||
inputFocusBorderColor: '#ff4544',
|
||
inputFocusBgColor: '#ffffff',
|
||
labelFontWeight: 600,
|
||
// 提交按钮
|
||
submitColor: '#ffffff',
|
||
submitBgColor: '#ff4544',
|
||
submitMarginTop: 0, // 提交按钮顶部外边距(rpx):调整表单与按钮之间的距离
|
||
// 提示语
|
||
tipFontSize: 32,
|
||
tipColor: '#000000',
|
||
tipBgColor: 'transparent'
|
||
};
|
||
// 全局配置(中间优先级)
|
||
const globalCfg = this.formConfig || {};
|
||
// 当前平台配置(最高优先级):从 formConfig.terminal 多个别名中按顺序合并
|
||
const formConfigRoot = this.formConfig || {};
|
||
// 兼容两种结构:
|
||
// 1) 后端格式:formConfig.terminal = { h5: {...}, weapp: {...}, harmony: {...} }
|
||
// 2) 旧式扁平:formConfig[platformKey] = { ... }
|
||
const terminalBlock = formConfigRoot.terminal || {};
|
||
const platformKeys = this.platformConfigKeys || [];
|
||
const platformCfg = Object.assign(
|
||
{},
|
||
...platformKeys.map(k => (terminalBlock[k] || formConfigRoot[k] || {}))
|
||
);
|
||
// 合并:defaults <- globalCfg <- platformCfg
|
||
return Object.assign({}, defaults, globalCfg, platformCfg);
|
||
},
|
||
nameWidthStyle() {
|
||
if (this.mode === 'vertical') {
|
||
return {};
|
||
}
|
||
let width;
|
||
if (this.config.labelWidthMode === 'fixed') {
|
||
width = this.$util.rpx(this.config.labelFixedWidth);
|
||
} else {
|
||
width = this.maxLabelWidth ? this.$util.rpx(this.maxLabelWidth) : 'auto';
|
||
}
|
||
return {
|
||
width: width
|
||
};
|
||
},
|
||
// 容器样式(与后端 form-preview-content 对齐:通过 CSS 变量驱动子元素样式)
|
||
formWrapStyle() {
|
||
const cfg = this.config;
|
||
const num = (v, fb) => (typeof v === 'number' ? v : parseInt(v) || fb);
|
||
|
||
return this.$util.objToStyle({
|
||
// 仅保留左右内边距 = form-padding-x;上下由每行 row-padding-y 承担
|
||
padding: `0 ${this.$util.rpx(cfg.formPaddingX)}`,
|
||
background: cfg.formBgColor,
|
||
// 通过 CSS 变量传递:与后端 design.css 的 :root 变量一一对应
|
||
'--form-padding-x': this.$util.rpx(cfg.formPaddingX),
|
||
'--banner-padding-x': this.$util.rpx(cfg.bannerPaddingX),
|
||
'--tip-padding-x': this.$util.rpx(cfg.tipPaddingX),
|
||
'--tip-font-size': this.$util.rpx(cfg.tipFontSize),
|
||
'--tip-color': cfg.tipColor,
|
||
'--tip-bg-color': cfg.tipBgColor,
|
||
'--row-padding-y': this.$util.rpx(cfg.rowPaddingY),
|
||
'--row-min-height': this.$util.rpx(cfg.rowMinHeight),
|
||
'--label-gap': this.$util.rpx(cfg.labelGap),
|
||
'--form-bg-color': cfg.formBgColor,
|
||
'--row-divider-color': cfg.rowDividerColor,
|
||
'--input-height': this.$util.rpx(cfg.inputHeight),
|
||
'--input-radius': this.$util.rpx(cfg.inputRadius),
|
||
'--label-fixed-width': this.$util.rpx(cfg.labelFixedWidth),
|
||
'--label-font-size': this.$util.rpx(num(cfg.labelFontSize, 28)),
|
||
'--label-color': cfg.labelColor,
|
||
'--required-color': cfg.requiredColor,
|
||
'--checked-color': cfg.checkedColor,
|
||
'--placeholder-color': cfg.placeholderColor,
|
||
'--placeholder-font-size': this.$util.rpx(num(cfg.placeholderFontSize, 28)),
|
||
// 输入框视觉增强(边框 / 背景 / 聚焦 / label 字重)
|
||
'--input-border-color': cfg.inputBorderColor,
|
||
'--input-bg-color': cfg.inputBgColor,
|
||
'--input-focus-border-color': cfg.inputFocusBorderColor,
|
||
'--input-focus-bg-color': cfg.inputFocusBgColor,
|
||
'--label-font-weight': cfg.labelFontWeight,
|
||
'--submit-color': cfg.submitColor,
|
||
'--submit-bg-color': cfg.submitBgColor,
|
||
'--submit-margin-top': this.$util.rpx(cfg.submitMarginTop || 0),
|
||
// 分割线显隐
|
||
'--show-row-divider': cfg.showRowDivider === false ? '0' : '1'
|
||
});
|
||
},
|
||
// 行间距样式:仅上下内边距,水平由外层 form-wrap 控制(与后端 form-gorup 对齐)
|
||
rowWrapStyle() {
|
||
return this.$util.objToStyle({
|
||
padding: `${this.$util.rpx(this.config.rowPaddingY)} 0`
|
||
});
|
||
},
|
||
// 行容器 class:用于控制分割线显隐
|
||
rowWrapClass() {
|
||
return this.config.showRowDivider === false ? 'order-wrap no-divider' : 'order-wrap';
|
||
},
|
||
// 行最小高度
|
||
rowCellStyle() {
|
||
return this.$util.objToStyle({
|
||
'min-height': this.$util.rpx(this.config.rowMinHeight)
|
||
});
|
||
},
|
||
// 标签与输入框间距
|
||
nameStyle() {
|
||
if (this.mode === 'vertical') {
|
||
return {};
|
||
}
|
||
return {
|
||
'margin-right': this.$util.rpx(this.config.labelGap)
|
||
};
|
||
},
|
||
// 输入框样式(全局,未指定 item 时使用)
|
||
inputStyle() {
|
||
return this.$util.objToStyle({
|
||
'min-height': this.$util.rpx(this.config.inputHeight),
|
||
'border-radius': this.$util.rpx(this.config.inputRadius)
|
||
});
|
||
},
|
||
labelStyle() {
|
||
const style = {
|
||
fontSize: this.$util.rpx(this.config.labelFontSize),
|
||
color: this.config.labelColor
|
||
};
|
||
return this.$util.objToStyle(style);
|
||
},
|
||
requiredColor() {
|
||
return this.config.requiredColor;
|
||
},
|
||
checkedColor() {
|
||
return this.config.checkedColor;
|
||
},
|
||
rowDividerStyle() {
|
||
const style = {};
|
||
if (this.config.showRowDivider === false) {
|
||
style.display = 'none';
|
||
} else {
|
||
style.background = this.config.rowDividerColor;
|
||
}
|
||
return this.$util.objToStyle(style);
|
||
}
|
||
},
|
||
methods: {
|
||
// 预计算每项的样式(写到 item._labelStyle / item._boxAlignStyle),
|
||
// 避免在模板里以方法形式调用:微信小程序 WXML 不支持 :style="getXxx(item)" 语法。
|
||
// 触发时机:processFormData 末尾 / calculateMaxLabelWidth 末尾 / config 变化 / mode 变化。
|
||
recomputeItemStyles() {
|
||
if (!this.formData || this.formData.length === 0) return;
|
||
|
||
const widthStyle = this.nameWidthStyle || {};
|
||
const nameMarginStyle = this.nameStyle || {};
|
||
|
||
this.formData.forEach((item) => {
|
||
const itemValue = (item && item.value) || {};
|
||
const style = Object.assign({}, widthStyle);
|
||
|
||
// item 级别 label 字号 / 颜色覆盖(与原 getItemLabelStyle 逻辑一致)
|
||
if (itemValue.label_font_size !== undefined && itemValue.label_font_size !== null && itemValue.label_font_size !== '') {
|
||
style['--item-label-font-size'] = this.$util.rpx(itemValue.label_font_size);
|
||
}
|
||
if (itemValue.label_color) {
|
||
style['--item-label-color'] = itemValue.label_color;
|
||
}
|
||
// 合并全局的 nameStyle(margin-right)
|
||
Object.assign(style, nameMarginStyle);
|
||
|
||
// $set 包裹以确保新属性是响应式的(Vue 2 对动态加的属性需要 $set)
|
||
this.$set(item, '_labelStyle', style);
|
||
|
||
// item 级别 box 对齐方式(与原 getItemBoxAlignStyle 逻辑一致)
|
||
const align = itemValue.input_align;
|
||
this.$set(item, '_boxAlignStyle', align ? { 'text-align': align } : {});
|
||
|
||
// 选项排列样式:vertical 时给 checkbox-group / radio-group 加 flex-direction
|
||
// 走 inline style(不依赖 dev 编译产物的 SCSS 类,跨端兼容更稳)
|
||
// 直接写 flex-direction 等 CSS 属性,inline style 优先级最高
|
||
const optLayout = itemValue.option_layout;
|
||
const optStyle = {};
|
||
if (optLayout === 'vertical') {
|
||
optStyle['flex-direction'] = 'column';
|
||
optStyle['align-items'] = 'flex-start';
|
||
optStyle['gap'] = '8px';
|
||
} else {
|
||
// wrap 模式:用 display: flex 基础属性兜底(不强制,让浏览器按 SCSS 默认渲染)
|
||
optStyle['flex-direction'] = 'row';
|
||
optStyle['align-items'] = 'center';
|
||
optStyle['gap'] = '6px';
|
||
}
|
||
this.$set(item, '_optionLayoutStyle', optStyle);
|
||
});
|
||
},
|
||
// 容器样式(提供给外部 diy-form 使用)
|
||
getFormWrapStyle() {
|
||
return this.formWrapStyle;
|
||
},
|
||
calculateMaxLabelWidth() {
|
||
if (this.mode === 'vertical' || !this.formData || this.formData.length === 0) {
|
||
this.maxLabelWidth = 0;
|
||
return;
|
||
}
|
||
// 与后端 design.js calculateMaxLabelWidth 算法对齐:
|
||
// 取所有 title 中最长字符数(必填 +1),按 fontSize(rpx)近似换算宽度
|
||
// 后端在 H5 中按 1rpx = 0.5px 计算宽度,再加 12 边距
|
||
const fontSize = parseInt(this.config.labelFontSize) || 28;
|
||
let maxChars = 0;
|
||
this.formData.forEach((item) => {
|
||
if (!item.value || !item.value.title) return;
|
||
let len = (item.value.title || '').length;
|
||
if (item.value.required) len += 1;
|
||
if (len > maxChars) maxChars = len;
|
||
});
|
||
// 与后端公式一致:chars * (fontSize/2) * 1.1 + 12,单位 px → 转 rpx(×2)
|
||
let widthPx = Math.max(40, maxChars * (fontSize / 2) * 1.1 + 12);
|
||
let widthRpx = Math.round(widthPx * 2);
|
||
// auto 模式下加宽度上限:超长 title 撑爆布局时,按 labelAutoMaxWidth 截断(rpx)
|
||
// 上限内 ellipsis 截断,保证控件区有足够空间;上限外也不会让 label 占满整行
|
||
const maxWidthRpx = parseInt(this.config.labelAutoMaxWidth) || 200;
|
||
if (widthRpx > maxWidthRpx) widthRpx = maxWidthRpx;
|
||
this.maxLabelWidth = widthRpx;
|
||
// 宽度变了,需要把全局 nameWidthStyle 重新合到每项 _labelStyle 里
|
||
this.recomputeItemStyles();
|
||
},
|
||
|
||
initFormData() {
|
||
// 深拷贝 prop 数据到本地状态
|
||
this.formData = JSON.parse(JSON.stringify(this.data || []));
|
||
this.processFormData();
|
||
},
|
||
|
||
setFormData() {
|
||
// 重置为初始值
|
||
this.initFormData();
|
||
},
|
||
|
||
processFormData() {
|
||
this.formData.forEach((item) => {
|
||
// Text / Textarea:当 placeholder 为空字符串或没有值时,不设置默认值
|
||
const skipDefault = (item.controller == 'Text' || item.controller == 'Textarea') && !item.value.placeholder;
|
||
if (!item.val && !skipDefault) {
|
||
item.val = item.value.default ? item.value.default : '';
|
||
}
|
||
|
||
if (item.value.options) {
|
||
item.option_lists = [];
|
||
item.value.options.forEach((option, index) => {
|
||
let optionItem = {};
|
||
optionItem.value = option;
|
||
optionItem.checked = false;
|
||
|
||
if (item.controller == 'Radio') {
|
||
if ((!item.val && index == 0) || (item.val && item.val == option)) {
|
||
optionItem.checked = true;
|
||
item.val = option;
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'Checkbox' && item.val) {
|
||
let valArray = item.val.split(',');
|
||
optionItem.checked = valArray.indexOf(option) != -1;
|
||
}
|
||
|
||
item.option_lists.push(optionItem);
|
||
});
|
||
}
|
||
|
||
if (item.controller == 'Img') {
|
||
item.img_lists = item.val ? item.val.split(',') : [];
|
||
}
|
||
|
||
if (item.controller == 'Date' && !item.val) {
|
||
if (item.value.is_show_default) {
|
||
if (item.value.is_current) {
|
||
item.val = this.getDate();
|
||
} else {
|
||
item.val = item.value.default;
|
||
}
|
||
} else {
|
||
item.val = '';
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'Datelimit') {
|
||
if (item.val) {
|
||
let dateArray = item.val.split(' - ');
|
||
item.start_date = dateArray[0];
|
||
item.end_date = dateArray[1];
|
||
} else {
|
||
item.val = '';
|
||
|
||
if (item.value.is_show_default_start) {
|
||
if (item.value.is_current_start) {
|
||
item.start_date = this.getDate();
|
||
} else {
|
||
item.start_date = item.value.default_start;
|
||
}
|
||
} else {
|
||
item.start_date = '';
|
||
}
|
||
|
||
if (item.value.is_show_default_end) {
|
||
if (item.value.is_current_end) {
|
||
item.end_date = this.getDate();
|
||
} else {
|
||
item.end_date = item.value.default_end;
|
||
}
|
||
}
|
||
|
||
if (item.start_date && item.end_date) {
|
||
item.val = item.start_date + ' - ' + item.end_date;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'Time' && !item.val) {
|
||
if (item.value.is_show_default) {
|
||
if (item.value.is_current) {
|
||
item.val = this.getTime();
|
||
} else {
|
||
item.val = item.value.default;
|
||
}
|
||
} else {
|
||
item.val = '';
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'Timelimit') {
|
||
if (item.val) {
|
||
let timeArray = item.val.split(' - ');
|
||
item.start_time = timeArray[0];
|
||
item.end_time = timeArray[1];
|
||
} else {
|
||
item.val = '';
|
||
|
||
if (item.value.is_show_default_start) {
|
||
if (item.value.is_current_start) {
|
||
item.start_time = this.getTime();
|
||
} else {
|
||
item.start_time = item.value.default_start;
|
||
}
|
||
} else {
|
||
item.start_time = '';
|
||
}
|
||
|
||
if (item.value.is_show_default_end) {
|
||
if (item.value.is_current_end) {
|
||
item.end_time = this.getTime();
|
||
} else {
|
||
item.end_time = item.value.default_end;
|
||
}
|
||
}
|
||
|
||
if (item.start_time && item.end_time) {
|
||
item.val = item.start_time + ' - ' + item.end_time;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'City') {
|
||
item.full_address = '';
|
||
item.select_arr = item.value.default_type == 1 ? '2' : '3';
|
||
if (item.val) {
|
||
item.default_regions = item.val.split('-');
|
||
} else {
|
||
item.default_regions = [];
|
||
}
|
||
}
|
||
});
|
||
// 数据处理完后再算每项的 _labelStyle / _boxAlignStyle(依赖 nameWidthStyle,而 maxLabelWidth 此时可能还没算)
|
||
// 这里先算一次;calculateMaxLabelWidth 完成后会再算一次(带正确的 maxLabelWidth)
|
||
this.recomputeItemStyles();
|
||
},
|
||
|
||
verify() {
|
||
for (let i = 0; i < this.formData.length; i++) {
|
||
let item = this.formData[i];
|
||
|
||
if (item.controller == 'Text') {
|
||
if (item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseInput', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.name == 'ID_CARD' && !/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(item.val)) {
|
||
if (!item.value.required) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.idCardInvalid') });
|
||
return false;
|
||
}
|
||
if (item.val != '') {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.idCardInvalid') });
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (item.name == 'MOBILE' && !this.$util.verifyMobile(item.val)) {
|
||
if (!item.value.required) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.mobileInvalid') });
|
||
return false;
|
||
}
|
||
if (item.val != '') {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.mobileInvalid') });
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'Textarea' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseInput', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.controller == 'Select' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelect', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.controller == 'Checkbox' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelectAtLeastOne', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.controller == 'Img' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseUploadAtLeastOne', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.controller == 'Date' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelect', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.controller == 'Datelimit') {
|
||
if (item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelect', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (this.$util.timeTurnTimeStamp(item.start_date) > this.$util.timeTurnTimeStamp(item.end_date)) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.endDateBeforeStartDate') });
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'Time' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelect', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.controller == 'Timelimit') {
|
||
if (item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelect', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.start_time >= item.end_time) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.endTimeBeforeStartTime') });
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'City' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelect', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return this.formData;
|
||
},
|
||
|
||
pickerChange(event, index) {
|
||
this.formData[index].val = this.data[index].value.options[event.detail.value];
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
checkboxChange(event, index) {
|
||
this.formData[index].val = event.detail.value.toString();
|
||
this.formData[index].option_lists.forEach((option) => {
|
||
option.checked = event.detail.value.indexOf(option.value) != -1;
|
||
});
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
radioChange(event, index) {
|
||
this.formData[index].val = event.detail.value;
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
uploadImg(index) {
|
||
let self = this;
|
||
this.$util.upload(Number(this.formData[index].value.max_count), { path: 'evaluateimg' }, function(res) {
|
||
if (res.length > 0) {
|
||
res.forEach(function(img) {
|
||
if (self.formData[index].img_lists.length >= Number(self.formData[index].value.max_count)) {
|
||
self.$util.showToast({ title: self.$lang('components.ns-newform.maxUploadImages', { max: self.formData[index].value.max_count }) });
|
||
return false;
|
||
}
|
||
self.formData[index].img_lists.push(img);
|
||
});
|
||
self.formData[index].val = self.formData[index].img_lists.toString();
|
||
self.$forceUpdate();
|
||
}
|
||
});
|
||
},
|
||
|
||
addImg(index) {
|
||
let self = this;
|
||
if (this.formData[index].img_lists.length >= Number(this.formData[index].value.max_count)) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.maxUploadImages', { max: this.formData[index].value.max_count }) });
|
||
return false;
|
||
}
|
||
|
||
this.$util.upload(Number(this.formData[index].value.max_count), { path: 'evaluateimg' }, function(res) {
|
||
if (res.length > 0) {
|
||
res.forEach(function(img) {
|
||
self.formData[index].img_lists.push(img);
|
||
});
|
||
self.formData[index].val = self.formData[index].img_lists.toString();
|
||
self.$forceUpdate();
|
||
}
|
||
});
|
||
},
|
||
|
||
delImg(imgIndex, formIndex) {
|
||
this.formData[formIndex].img_lists.splice(imgIndex, 1);
|
||
this.formData[formIndex].val = this.formData[formIndex].img_lists.toString();
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
getDate() {
|
||
let date = new Date();
|
||
let year = date.getFullYear();
|
||
let month = date.getMonth() + 1;
|
||
let day = date.getDate();
|
||
month = month > 9 ? month : '0' + month;
|
||
day = day > 9 ? day : '0' + day;
|
||
return `${year}-${month}-${day}`;
|
||
},
|
||
|
||
getTime() {
|
||
let date = new Date();
|
||
let hours = date.getHours();
|
||
let minutes = date.getMinutes();
|
||
hours = hours > 9 ? hours : '0' + hours;
|
||
minutes = minutes > 9 ? minutes : '0' + minutes;
|
||
return `${hours}:${minutes}`;
|
||
},
|
||
|
||
bindDateChange(event, index) {
|
||
this.formData[index].val = event.detail.value;
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
bindStartDateChange(event, index) {
|
||
this.$set(this.formData[index], 'start_date', event.detail.value);
|
||
this.$set(this.formData[index], 'val', this.formData[index].start_date + ' - ' + this.formData[index].end_date);
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
bindEndDateChange(event, index) {
|
||
this.$set(this.formData[index], 'end_date', event.detail.value);
|
||
this.$set(this.formData[index], 'val', this.formData[index].start_date + ' - ' + this.formData[index].end_date);
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
bindTimeChange(event, index) {
|
||
this.formData[index].val = event.detail.value;
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
bindStartTimeChange(event, index) {
|
||
this.formData[index].start_time = event.detail.value;
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
bindEndTimeChange(event, index) {
|
||
this.formData[index].end_time = event.detail.value;
|
||
this.formData[index].val = this.formData[index].start_time + ' - ' + this.formData[index].end_time;
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
handleGetRegions(regions, index) {
|
||
this.formData[index].val = '';
|
||
this.formData[index].val += regions[0] != undefined ? regions[0].label : '';
|
||
this.formData[index].val += regions[1] != undefined ? '-' + regions[1].label : '';
|
||
this.formData[index].val += regions[2] != undefined ? '-' + regions[2].label : '';
|
||
this.$forceUpdate();
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 与后端 D:\projects\shop-projects\backend\src\app\component\view\form\css\design.css 对齐:
|
||
在 .form-wrap 上通过内联 style 写入 --form-* / --label-* 等 CSS 变量,
|
||
子元素统一读取。 */
|
||
.form-wrap {
|
||
background: #fff;
|
||
/* 默认值兜底:与 ns-newform.config 默认值一致 */
|
||
--form-padding-x: 14rpx;
|
||
--banner-padding-x: 0rpx;
|
||
--tip-padding-x: 14rpx;
|
||
--tip-font-size: 24rpx;
|
||
--tip-color: #666666;
|
||
--tip-bg-color: #ffffff;
|
||
--row-padding-y: 4rpx;
|
||
--row-min-height: 60rpx;
|
||
--label-gap: 16rpx;
|
||
--form-bg-color: #ffffff;
|
||
--row-divider-color: #f0f0f0;
|
||
--input-height: 60rpx;
|
||
--input-radius: 10rpx;
|
||
--label-fixed-width: 180rpx;
|
||
--label-font-size: 28rpx;
|
||
--label-color: #1f2937;
|
||
--required-color: #ef4444;
|
||
--checked-color: #ff4544;
|
||
--submit-color: #ffffff;
|
||
--submit-bg-color: #ff4544;
|
||
--placeholder-color: #ddd;
|
||
--placeholder-font-size: 28rpx;
|
||
// 输入框视觉增强(边框 / 背景 / 聚焦 / label 字重)
|
||
--input-border-color: #e5e7eb;
|
||
--input-bg-color: #f9fafb;
|
||
--input-focus-border-color: #ff4544;
|
||
--input-focus-bg-color: #ffffff;
|
||
--label-font-weight: 600;
|
||
--show-row-divider: 1;
|
||
|
||
// 与后端 form-preview-wrap.no-divider-all 对齐:整组隐藏所有行分割线
|
||
&.no-divider-all {
|
||
.order-wrap {
|
||
border-bottom: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.order-wrap {
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
padding: 0;
|
||
background: var(--form-bg-color);
|
||
box-sizing: border-box;
|
||
// 行分割线:与后端 design.css .form-preview-wrap .form-gorup 对齐
|
||
// 用 border-bottom 而非 ::after 伪元素(亚像素不可见 + 多端兼容性差)
|
||
// border-bottom: 2rpx solid var(--row-divider-color, #f0f0f0);
|
||
border-bottom-width: 2rpx;
|
||
border-bottom-style: solid;
|
||
border-bottom-color: var(--row-divider-color, #f0f0f0);
|
||
|
||
|
||
// 后端 show_row_divider = 0:隐藏所有行分割线
|
||
&.no-divider {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.order-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
background: transparent;
|
||
position: relative;
|
||
padding: 0;
|
||
min-height: var(--row-min-height, 60rpx);
|
||
flex-wrap: nowrap;
|
||
|
||
// 水平布局(默认)
|
||
&:not(.vertical-layout) {
|
||
flex-direction: row;
|
||
.name {
|
||
flex-shrink: 0;
|
||
margin-bottom: 0;
|
||
margin-right: var(--label-gap, 16rpx);
|
||
}
|
||
}
|
||
|
||
&.vertical-layout {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
|
||
.name {
|
||
width: 100%;
|
||
margin-bottom: var(--label-gap, 16rpx);
|
||
margin-right: 0;
|
||
}
|
||
|
||
.box {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
&.textarea {
|
||
align-items: flex-start;
|
||
|
||
.name {
|
||
padding-top: 8rpx;
|
||
height: auto;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
&.clear-flex {
|
||
display: block;
|
||
|
||
.box {
|
||
margin-top: 12rpx;
|
||
text-align: left;
|
||
}
|
||
}
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
&.align-top {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
text {
|
||
font-size: var(--label-font-size, 28rpx);
|
||
}
|
||
|
||
.name {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: var(--input-height, 60rpx);
|
||
line-height: 1.5;
|
||
|
||
.tit {
|
||
text-align: left;
|
||
font-size: var(--item-label-font-size, var(--label-font-size, 28rpx));
|
||
color: var(--item-label-color, var(--label-color, #1f2937));
|
||
font-weight: var(--item-label-font-weight, var(--label-font-weight, 600));
|
||
letter-spacing: 0.2rpx;
|
||
|
||
text {
|
||
font-size: var(--item-label-font-size, var(--label-font-size, 28rpx));
|
||
}
|
||
}
|
||
|
||
.required {
|
||
color: var(--required-color, #ef4444);
|
||
font-size: var(--item-label-font-size, var(--label-font-size, 28rpx));
|
||
margin-left: 4rpx;
|
||
font-weight: bold;
|
||
display: inline-block;
|
||
vertical-align: middle;
|
||
}
|
||
}
|
||
|
||
.box {
|
||
flex: 1;
|
||
padding: 0;
|
||
line-height: inherit;
|
||
min-width: 0; // 防止 flex 子元素内容溢出
|
||
|
||
// 对齐方式
|
||
&.align-left {
|
||
text-align: left;
|
||
}
|
||
|
||
&.align-right {
|
||
text-align: right;
|
||
}
|
||
|
||
&.align-center {
|
||
text-align: center;
|
||
}
|
||
|
||
// 让 box 在 City / Select / Date / Time 等"picker + 右侧箭头"场景下成为
|
||
// 独立的 flex 行容器:picker 占满剩余宽度,箭头自然贴右
|
||
// (与 .order-cell.vertical-layout 不冲突 —— 在垂直布局下 .order-cell 是
|
||
// column,.box.box-flex 自身仍是 row,箭头与 picker 仍同行)
|
||
&.box-flex {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
picker,
|
||
.pick-regions {
|
||
min-width: 0;
|
||
width: auto; // 覆盖下方 .box picker,.pick-regions 的 width: 100%
|
||
}
|
||
|
||
.iconfont.icon-right {
|
||
flex-shrink: 0;
|
||
margin-left: 8rpx;
|
||
line-height: 1;
|
||
}
|
||
}
|
||
|
||
// 让原生 picker 与自定义 pick-regions 撑满父容器,
|
||
// 使 .box.align-right / align-center 的 text-align 能正确传递到内部
|
||
// .uni-input / .select-address(inline-block),与后端 .more 的右对齐效果一致
|
||
picker,
|
||
.pick-regions {
|
||
display: block;
|
||
width: 100%;
|
||
}
|
||
|
||
// H5 端 <input> 被 uni-app 编译为 <uni-input> 自定义组件,
|
||
// 真实 <input> 是其子节点,需要 ::v-deep 穿透 scoped 才能命中
|
||
::v-deep input {
|
||
font-size: var(--label-font-size, 28rpx);
|
||
text-align: left;
|
||
height: var(--input-height, 60rpx);
|
||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||
border-radius: var(--input-radius, 10rpx);
|
||
line-height: var(--input-height, 60rpx);
|
||
padding: 0 12rpx;
|
||
background: var(--input-bg-color, #f9fafb);
|
||
color: var(--label-color, #1f2937);
|
||
transition: all 0.3s ease;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
|
||
&:focus {
|
||
background: var(--input-focus-bg-color, #ffffff);
|
||
border-color: var(--input-focus-border-color, #ff4544);
|
||
}
|
||
|
||
&::placeholder {
|
||
color: var(--placeholder-color, #ddd);
|
||
font-size: var(--placeholder-font-size, 28rpx);
|
||
}
|
||
}
|
||
|
||
// 非 H5 端(小程序 / 鸿蒙 / App)<input> 是直接原生元素,普通选择器即可
|
||
input {
|
||
font-size: var(--label-font-size, 28rpx);
|
||
text-align: left;
|
||
height: var(--input-height, 60rpx);
|
||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||
border-radius: var(--input-radius, 10rpx);
|
||
line-height: var(--input-height, 60rpx);
|
||
padding: 0 12rpx;
|
||
background: var(--input-bg-color, #f9fafb);
|
||
color: var(--label-color, #1f2937);
|
||
transition: all 0.3s ease;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
|
||
&:focus {
|
||
background: var(--input-focus-bg-color, #ffffff);
|
||
border-color: var(--input-focus-border-color, #ff4544);
|
||
}
|
||
|
||
&::placeholder {
|
||
color: var(--placeholder-color, #ddd);
|
||
font-size: var(--placeholder-font-size, 28rpx);
|
||
}
|
||
}
|
||
|
||
::v-deep textarea {
|
||
font-size: var(--label-font-size, 28rpx);
|
||
width: 100%;
|
||
min-height: 120rpx;
|
||
line-height: 1.5;
|
||
text-align: left;
|
||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||
border-radius: var(--input-radius, 10rpx);
|
||
padding: 12rpx;
|
||
background: var(--input-bg-color, #f9fafb);
|
||
color: var(--label-color, #1f2937);
|
||
transition: all 0.3s ease;
|
||
box-sizing: border-box;
|
||
height: auto;
|
||
|
||
&:focus {
|
||
background: var(--input-focus-bg-color, #ffffff);
|
||
border-color: var(--input-focus-border-color, #ff4544);
|
||
}
|
||
|
||
&::placeholder {
|
||
color: var(--placeholder-color, #ddd);
|
||
font-size: var(--placeholder-font-size, 28rpx);
|
||
}
|
||
}
|
||
|
||
textarea {
|
||
font-size: var(--label-font-size, 28rpx);
|
||
width: 100%;
|
||
min-height: 120rpx;
|
||
line-height: 1.5;
|
||
text-align: left;
|
||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||
border-radius: var(--input-radius, 10rpx);
|
||
padding: 12rpx;
|
||
background: var(--input-bg-color, #f9fafb);
|
||
color: var(--label-color, #1f2937);
|
||
transition: all 0.3s ease;
|
||
box-sizing: border-box;
|
||
height: auto;
|
||
|
||
&:focus {
|
||
background: var(--input-focus-bg-color, #ffffff);
|
||
border-color: var(--input-focus-border-color, #ff4544);
|
||
}
|
||
|
||
&::placeholder {
|
||
color: var(--placeholder-color, #ddd);
|
||
font-size: var(--placeholder-font-size, 28rpx);
|
||
}
|
||
}
|
||
|
||
.uni-input, .select-address {
|
||
display: inline-block;
|
||
padding: 0;
|
||
border-radius: var(--input-radius, 10rpx);
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--label-color, #1f2937);
|
||
font-size: var(--label-font-size, 28rpx);
|
||
transition: all 0.3s ease;
|
||
min-height: var(--input-height, 60rpx);
|
||
box-sizing: border-box;
|
||
line-height: var(--input-height, 60rpx);
|
||
|
||
&.color-tip {
|
||
color: var(--placeholder-color, #ddd);
|
||
}
|
||
|
||
&.empty {
|
||
color: var(--placeholder-color, #ddd);
|
||
}
|
||
}
|
||
|
||
checkbox-group {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
width: 100%;
|
||
|
||
// 隐藏 uniapp 默认的 checkbox 样式
|
||
checkbox {
|
||
position: absolute;
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
radio-group {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
width: 100%;
|
||
|
||
// 隐藏 uniapp 默认的 radio 样式
|
||
radio {
|
||
position: absolute;
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
// 选项排列样式:vertical = 每个选项独立一行;wrap(默认)= 横向自动换行
|
||
.option-layout-vertical {
|
||
checkbox-group,
|
||
radio-group {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 16rpx;
|
||
}
|
||
}
|
||
|
||
label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
line-height: 1;
|
||
margin: 0;
|
||
padding: 6rpx 0;
|
||
border: none;
|
||
background: transparent;
|
||
transition: all 0.3s ease;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
color: var(--item-label-color, var(--label-color, #1f2937));
|
||
font-size: var(--item-label-font-size, var(--label-font-size, 28rpx));
|
||
|
||
&:active {
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
|
||
// 自定义勾选图标
|
||
.checkbox,
|
||
.radio-box {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
&.img-boxs {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
|
||
// 与后端 design.css .form-preview-wrap .img-box 对齐:
|
||
// 100rpx (≈ 50px) + 1rpx solid #E6E6E6 + 4rpx radius
|
||
.img-box {
|
||
margin: 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
border: 1rpx solid #E6E6E6;
|
||
border-radius: 4rpx;
|
||
position: relative;
|
||
background: transparent;
|
||
box-sizing: border-box;
|
||
transition: all 0.3s ease;
|
||
overflow: hidden;
|
||
|
||
&:active {
|
||
border-color: var(--checked-color, #ff4544);
|
||
}
|
||
|
||
// 关闭按钮:与后端 .form-preview-wrap .img-box .close 对齐
|
||
// top/right = -14rpx (≈ -7px),size = 28rpx (≈ 14px),背景 #999
|
||
.icon-guanbi {
|
||
position: absolute;
|
||
top: -14rpx;
|
||
right: -14rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 28rpx;
|
||
height: 28rpx;
|
||
line-height: 1;
|
||
color: #fff;
|
||
background: #999;
|
||
border-radius: 50%;
|
||
font-size: 28rpx;
|
||
z-index: 10;
|
||
}
|
||
|
||
.icon-add1 {
|
||
font-size: 48rpx;
|
||
color: #9ca3af;
|
||
}
|
||
}
|
||
}
|
||
|
||
&.date-boxs {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
|
||
.date-box {
|
||
flex: 1;
|
||
|
||
.picker-box {
|
||
.uni-input {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
}
|
||
}
|
||
|
||
.interval {
|
||
padding: 0 8rpx;
|
||
color: #9ca3af;
|
||
font-size: 28rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.icon-right {
|
||
color: #9ca3af;
|
||
font-size: 32rpx;
|
||
margin-left: 12rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style lang="scss" scoped>
|
||
.placeholder {
|
||
color: #909399;
|
||
font-size: 28rpx;
|
||
|
||
// #ifdef H5
|
||
height: 100% !important;
|
||
// #endif
|
||
}
|
||
|
||
.color-tip {
|
||
color: #909399;
|
||
}
|
||
</style> |