Files
lucky_shop/components/ns-form/ns-form.vue
ZF sun beaa330651 fix(forms): 修复单选多选组布局样式兼容问题
1. 为ns-newform和ns-form的单选/多选组添加内联样式属性`_optionLayoutStyle`
2. 预计算垂直/流式布局的flex样式,解决跨端渲染兼容问题
3. 调整本地配置文件的启用项注释
2026-07-31 13:55:49 +08:00

1244 lines
56 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="form-wrap form-component" :style="formWrapStyle">
<block v-for="(item, index) in formData" :key="index">
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Text'">
<view class="order-cell" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view class="box">
<input
type="text"
:placeholder="item.value.placeholder"
placeholder-class="placeholder color-tip"
v-model="item.val"
:style="inputStyle"
/>
</view>
</view>
</view>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Textarea'">
<view class="order-cell flex-box textarea" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view class="box">
<!-- #ifdef MP-HARMONY -->
<!-- 鸿蒙元服务渲染为原生 ArkUI TextArea,必须用 :rows 控制初始显示行数;CSS height 在原生组件上不生效 -->
<textarea
:rows="item.value.rows || 2"
:placeholder="item.value.placeholder"
placeholder-class="placeholder color-tip"
v-model="item.val"
:style="inputStyle"
></textarea>
<!-- #endif -->
<!-- #ifndef MP-HARMONY -->
<textarea
:placeholder="item.value.placeholder"
placeholder-class="placeholder color-tip"
v-model="item.val"
:style="inputStyle"
></textarea>
<!-- #endif -->
</view>
</view>
</view>
<picker mode="selector" :range="item.value.options" v-if="item.controller == 'Select'" @change="pickerChange($event, index)">
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
<view class="order-cell" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view class="box">
<text v-if="item.val != ''">{{ item.val }}</text>
<text v-else class="color-tip">{{ $lang('components.nsForm.selectPlaceholder') }}</text>
</view>
<text class="iconfont icon-right"></text>
</view>
</view>
</picker>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Checkbox'">
<view class="order-cell" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'check-group-box', '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" />
<view class="checkbox">
<text class="iconfont" :class="{ 'icon-fuxuankuang2': !v.checked, 'icon-fuxuankuang1 color-base-text': v.checked }" :style="{ color: v.checked ? checkedColor : '' }"></text>
{{ v.value }}
</view>
</label>
</checkbox-group>
</view>
</view>
</view>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Radio'">
<view class="order-cell" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view :class="['box', 'radio-group-box', '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" />
<view class="radio-box">
<text class="iconfont" :class="{ 'icon-yuan_checkbox': item.val != v.value, 'icon-yuan_checked color-base-text': item.val == v.value }" :style="{ color: item.val == v.value ? checkedColor : '' }"></text>
{{ v.value }}
</view>
</label>
</radio-group>
</view>
</view>
</view>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Img'">
<view class="order-cell flex-box" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view class="box img-boxs">
<view class="img-box" v-for="(v, k) in item.img_lists" :key="k" @click="uploadImg(index)">
<image :src="$util.img(v)" mode="aspectFill" show-menu-by-longpress></image>
<text class="iconfont icon-guanbi" @click.stop="delImg(k, index)"></text>
</view>
<view class="img-box" @click="addImg(index)"><text class="iconfont icon-add1"></text></view>
</view>
</view>
</view>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Date'">
<view class="order-cell" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view class="box box-flex">
<picker mode="date" :value="item.val" @change="bindDateChange($event, index)">
<view class="uni-input" :class="{ 'color-tip': !item.val }">{{ item.val ? item.val : item.value.placeholder }}</view>
</picker>
</view>
<text class="iconfont icon-right"></text>
</view>
</view>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Datelimit'">
<view class="order-cell flex-box" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view class="box date-boxs">
<view class="date-box">
<picker mode="date" :value="item.start_date" @change="bindStartDateChange($event, index)">
<view class="picker-box">
<view class="uni-input" :class="{ 'color-tip': !item.start_date }">{{ item.start_date ? item.start_date : item.value.placeholder_start }}</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" :class="{ 'color-tip': !item.end_date }">{{ item.end_date ? item.end_date : item.value.placeholder_end }}</view>
</view>
</picker>
</view>
</view>
</view>
</view>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Time'">
<view class="order-cell" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view class="box box-flex">
<picker mode="time" :value="item.val" @change="bindTimeChange($event, index)">
<view class="uni-input" :class="{ 'color-tip': !item.val }">{{ item.val ? item.val : item.value.placeholder }}</view>
</picker>
</view>
<text class="iconfont icon-right"></text>
</view>
</view>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Timelimit'">
<view class="order-cell flex-box" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view class="box date-boxs">
<view class="date-box">
<picker mode="time" :value="item.start_time" @change="bindStartTimeChange($event, index)">
<view class="picker-box">
<view class="uni-input" :class="{ 'color-tip': !item.start_time }">{{ item.start_time ? item.start_time : item.value.placeholder_start }}</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" :class="{ 'color-tip': !item.end_time }">{{ item.end_time ? item.end_time : item.value.placeholder_end }}</view>
</view>
</picker>
</view>
</view>
</view>
</view>
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'City'">
<view class="order-cell box-flex" :style="rowCellStyle">
<view class="name" :style="nameWidthStyle">
<text class="tit">{{ item.value.title }}</text>
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view>
<view class="box">
<pick-regions :default-regions="item.default_regions" :select-arr="item.select_arr" @getRegions="handleGetRegions($event, index)">
<view class="select-address " :class="{ empty: !item.val, 'color-tip': !item.val }">
{{ item.val ? item.val : (item.select_arr == '2' ? $lang('components.nsForm.selectProvinceCity') : $lang('components.nsForm.selectProvinceCityDistrict')) }}
</view>
</pick-regions>
</view>
<text class="iconfont icon-right"></text>
</view>
</view>
</block>
</view>
</template>
<script>
import formPlatformMixin from '@/common/js/form-platform-mixin.js';
export default {
name: 'ns-form',
mixins: [formPlatformMixin],
props: {
data: {
type: Array,
default: () => []
},
customAttr: {
type: Object,
default: function() {
return {};
}
},
/**
* 表单样式配置(由后端 designer 通过 API 传入)。
* 字段名同时支持 camelCase(如 formPaddingX / inputHeight / labelColor)
* 与 snake_case(如 form_padding_x / input_height / label_color),camelCase 优先。
* 不传则使用组件内置默认(与重构前硬编码值一致,无视觉回归)。
*
* 支持的键:
* formPaddingX, formBgColor, rowPaddingY, rowMinHeight, labelGap,
* inputHeight, inputRadius, labelWidthMode ('auto'|'fixed'), labelFixedWidth,
* labelFontSize, labelColor, requiredColor, checkedColor,
* placeholderColor, showRowDivider, rowDividerColor
*
* 平台差异化:formConfig.terminal.{h5|weapp|harmony|app|quickapp} 只放差异字段
*/
formConfig: {
type: Object,
default: () => ({})
}
},
data() {
return {
formData: [],
maxLabelWidth: 0
};
},
computed: {
// 合并配置:内置默认 < 全局 formConfig < 当前平台 formConfig.terminal[platform]
// 同时兼容 snake_case 与 camelCase 字段名
config() {
// 1) 内置默认值(最低优先级,与重构前的硬编码值一一对应,确保未传 formConfig 时零回归)
const defaults = {
formPaddingX: 0,
formBgColor: '#ffffff',
rowPaddingY: 20,
rowMinHeight: 60,
labelGap: 16,
inputHeight: 60,
inputRadius: 0,
labelWidthMode: 'fixed', // 旧版 width: 80vw 用 fixed 180rpx 等价替代
labelFixedWidth: 180,
labelFontSize: 28,
labelColor: '#1f2937',
labelFontWeight: 600,
labelAutoMaxWidth: 200,
requiredColor: '#ef4444',
checkedColor: '#ff4544',
placeholderColor: '#909399',
placeholderFontSize: 28,
// 输入框视觉增强(与 ns-newform 对齐)
inputBorderColor: '#e5e7eb',
inputBgColor: '#f9fafb',
inputFocusBorderColor: '#ff4544',
inputFocusBgColor: '#ffffff',
showRowDivider: true,
rowDividerColor: '#f0f0f0'
};
const fc = this.formConfig || {};
// 同时读 camelCase 与 snake_case,camelCase 优先
const pick = (camel, snake) => {
const v1 = fc[camel];
if (v1 !== undefined && v1 !== null && v1 !== '') return v1;
return fc[snake];
};
const base = {
formPaddingX: pick('formPaddingX', 'form_padding_x'),
formBgColor: pick('formBgColor', 'form_bg_color'),
rowPaddingY: pick('rowPaddingY', 'row_padding_y'),
rowMinHeight: pick('rowMinHeight', 'row_min_height'),
labelGap: pick('labelGap', 'label_gap'),
inputHeight: pick('inputHeight', 'input_height'),
inputRadius: pick('inputRadius', 'input_radius'),
labelWidthMode: pick('labelWidthMode', 'label_width_mode'),
labelFixedWidth: pick('labelFixedWidth', 'label_fixed_width'),
labelFontSize: pick('labelFontSize', 'label_font_size'),
labelColor: pick('labelColor', 'label_color'),
labelFontWeight: pick('labelFontWeight', 'label_font_weight'),
requiredColor: pick('requiredColor', 'required_color'),
checkedColor: pick('checkedColor', 'checked_color'),
placeholderColor: pick('placeholderColor', 'placeholder_color'),
placeholderFontSize: pick('placeholderFontSize', 'placeholder_font_size'),
inputBorderColor: pick('inputBorderColor', 'input_border_color'),
inputBgColor: pick('inputBgColor', 'input_bg_color'),
inputFocusBorderColor: pick('inputFocusBorderColor', 'input_focus_border_color'),
inputFocusBgColor: pick('inputFocusBgColor', 'input_focus_bg_color'),
showRowDivider: pick('showRowDivider', 'show_row_divider'),
rowDividerColor: pick('rowDividerColor', 'row_divider_color')
};
// 移除 undefined / 空字符串(保留 0 / false)
Object.keys(base).forEach(k => {
if (base[k] === undefined || base[k] === null || base[k] === '') delete base[k];
});
// showRowDivider 在前后端可能传数字 0/1,统一转 boolean
if ('showRowDivider' in base) {
base.showRowDivider = base.showRowDivider === true || base.showRowDivider === 1 || base.showRowDivider === '1';
}
// 2) 当前平台差异化配置(最高优先级),从 formConfig.terminal 取出当前平台 + 所有别名键,合并
const termBlock = fc.terminal || {};
const platformKeys = this.platformConfigKeys || [];
const platformCfg = Object.assign(
{},
...platformKeys.map(k => termBlock[k] || fc[k] || {})
);
// 平台块可能也是 snake_case,统一转 camelCase
const platformCfgCamel = {};
if (this.$util && typeof this.$util.snakeToCamel === 'function') {
Object.keys(platformCfg).forEach(k => {
platformCfgCamel[this.$util.snakeToCamel(k)] = platformCfg[k];
});
} else {
Object.assign(platformCfgCamel, platformCfg);
}
// 3) 三层合并:defaults < base < platformCfgCamel
return Object.assign({}, defaults, base, platformCfgCamel);
},
// 容器样式:内边距 + 背景 + 16 个 CSS 自定义属性
formWrapStyle() {
const cfg = this.config;
const num = (v, fb) => (typeof v === 'number' ? v : parseInt(v) || fb);
// 后端 formConfig 一律以 rpx 传递;
// _rpx() 按端分支:H5 输出 'Npx'(uni.upx2px),其它端输出 'Nrpx'
return this.$util.objToStyle({
padding: `0 ${this.$util.rpx(cfg.formPaddingX)}`,
background: cfg.formBgColor,
'--form-padding-x': this.$util.rpx(cfg.formPaddingX),
'--form-bg-color': cfg.formBgColor,
'--row-padding-y': this.$util.rpx(cfg.rowPaddingY),
'--row-min-height': this.$util.rpx(cfg.rowMinHeight),
'--label-gap': this.$util.rpx(cfg.labelGap),
'--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,
'--label-font-weight': cfg.labelFontWeight,
'--required-color': cfg.requiredColor,
'--checked-color': cfg.checkedColor,
'--placeholder-color': cfg.placeholderColor,
'--placeholder-font-size': this.$util.rpx(num(cfg.placeholderFontSize, 28)),
// 输入框视觉增强(与 ns-newform 对齐)
'--input-border-color': cfg.inputBorderColor,
'--input-bg-color': cfg.inputBgColor,
'--input-focus-border-color': cfg.inputFocusBorderColor,
'--input-focus-bg-color': cfg.inputFocusBgColor,
'--row-divider-color': cfg.rowDividerColor,
'--show-row-divider': cfg.showRowDivider === false ? '0' : '1'
});
},
rowWrapStyle() {
return this.$util.objToStyle({
padding: `${this.$util.rpx(this.config.rowPaddingY)} 0`
});
},
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)
});
},
// 标签宽度:根据 labelWidthMode 决定;fixed 用 --label-fixed-width,auto 用 maxLabelWidth
nameWidthStyle() {
const cfg = this.config;
if (cfg.labelWidthMode === 'fixed') {
return { width: this.$util.rpx(cfg.labelFixedWidth) };
}
return this.maxLabelWidth ? { width: this.$util.rpx(this.maxLabelWidth) } : { width: 'auto' };
},
// 输入框样式(input / textarea 共用)
inputStyle() {
return this.$util.objToStyle({
'min-height': this.$util.rpx(this.config.inputHeight),
'border-radius': this.$util.rpx(this.config.inputRadius),
'font-size': this.$util.rpx(this.config.labelFontSize),
'color': this.config.labelColor,
'line-height': this.$util.rpx(this.config.inputHeight)
});
},
// 必填星号颜色
requiredColor() {
return this.config.requiredColor;
},
// 复选 / 单选 选中色
checkedColor() {
return this.config.checkedColor;
}
},
created() {
this.initFormData();
this.$nextTick(() => this.calculateMaxLabelWidth());
},
watch: {
data: {
deep: true,
handler() {
this.initFormData();
this.$nextTick(() => this.calculateMaxLabelWidth());
}
},
formConfig: {
deep: true,
handler() {
this.$nextTick(() => this.calculateMaxLabelWidth());
}
}
},
methods: {
// 预计算每项的 option_layout inline style(与 ns-newform 对齐)
// 跨端兼容:小程序 / 鸿蒙元服务 WXML 不支持模板里的方法调用,必须预计算到 item 上
recomputeItemStyles() {
if (!this.formData || this.formData.length === 0) return;
this.formData.forEach((item) => {
const itemValue = (item && item.value) || {};
const optLayout = itemValue.option_layout;
const optStyle = {};
if (optLayout === 'vertical') {
optStyle['flex-direction'] = 'column';
optStyle['align-items'] = 'flex-start';
optStyle['gap'] = '8px';
} else {
// wrap 模式
optStyle['flex-direction'] = 'row';
optStyle['align-items'] = 'center';
optStyle['gap'] = '6px';
}
this.$set(item, '_optionLayoutStyle', optStyle);
});
},
initFormData() {
// 深拷贝 prop 数据到本地状态,避免 mutate 上游
this.formData = JSON.parse(JSON.stringify(this.data || []));
this.processFormData();
// 预计算每项 inline style(与 ns-newform 对齐,跨端兼容)
this.recomputeItemStyles();
},
setFormData() {
this.initFormData();
this.$nextTick(() => this.calculateMaxLabelWidth());
},
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((v, k) => {
var obj = {};
obj.value = v;
obj.checked = false;
if (item.controller == 'Radio' && ((!item.val && k == 0) || (item.val &&
item.val == v))) {
obj.checked = true;
item.val = v;
}
if (item.controller == 'Checkbox' && item.val) {
let val = item.val.split(',');
obj.checked = val.indexOf(v) != -1;
}
item.option_lists.push(obj);
});
}
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 date = item.val.split(' - ');
item.start_date = date[0];
item.end_date = date[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;
}
} else {
item.end_date = '';
}
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 time = item.val.split(' - ');
item.start_time = time[0];
item.end_time = time[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;
}
} else {
item.end_time = '';
}
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 = [];
}
});
},
// 计算最长 label 宽度(封装自 util.calculateMaxLabelWidth)
// 用于 labelWidthMode === 'auto' 时把 .name 的 width 撑开,避免长短 label 错位
calculateMaxLabelWidth() {
this.maxLabelWidth = this.$util.calculateMaxLabelWidth(
this.formData,
this.config.labelFontSize
);
},
verify() {
let verify = true;
for (let i = 0; i < this.formData.length; i++) {
let item = this.formData[i];
if (item.controller == 'Text') {
if (item.value.required && !item.val) {
verify = false;
this.$util.showToast({
title: this.$lang('components.nsForm.pleaseInput', { title: item.value.title })
});
break;
}
var reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
if (item.name == 'ID_CARD' && reg.test(item.val) === false) {
verify = !item.value.required;
if (verify == false) {
this.$util.showToast({
title: this.$lang('components.nsForm.idCardInvalid')
});
return false;
}
if (verify == true && item.val != '') {
this.$util.showToast({
title: this.$lang('components.nsForm.idCardInvalid')
});
return false;
}
}
// 验证手机号
if (item.name == 'MOBILE' && this.$util.verifyMobile(item.val) === false) {
verify = !item.value.required;
if (verify == false) {
this.$util.showToast({
title: this.$lang('components.nsForm.mobileInvalid')
});
return false;
}
if (verify == true && item.val != '') {
this.$util.showToast({
title: this.$lang('components.nsForm.mobileInvalid')
});
return false;
}
}
}
if (item.controller == 'Textarea') {
if (item.value.required && !item.val) {
verify = false;
this.$util.showToast({
title: this.$lang('components.nsForm.pleaseInput', { title: item.value.title })
});
break;
}
}
if (item.controller == 'Select') {
if (item.value.required && !item.val) {
verify = false;
this.$util.showToast({
title: this.$lang('components.nsForm.pleaseSelect', { title: item.value.title })
});
break;
}
}
if (item.controller == 'Checkbox') {
if (item.value.required && !item.val) {
verify = false;
this.$util.showToast({
title: this.$lang('components.nsForm.pleaseSelectAtLeastOne', { title: item.value.title })
});
break;
}
}
if (item.controller == 'Img') {
if (item.value.required && !item.val) {
verify = false;
this.$util.showToast({
title: this.$lang('components.nsForm.pleaseUploadAtLeastOne', { title: item.value.title })
});
break;
}
}
if (item.controller == 'Date') {
if (item.value.required && !item.val) {
verify = false;
this.$util.showToast({
title: this.$lang('components.nsForm.pleaseSelect', { title: item.value.title })
});
break;
}
}
if (item.controller == 'Datelimit') {
if (item.value.required && !item.val) {
verify = false;
this.$util.showToast({
title: this.$lang('components.nsForm.pleaseSelect', { title: item.value.title })
});
break;
}
if (this.$util.timeTurnTimeStamp(item.start_date) > this.$util.timeTurnTimeStamp(item.end_date)) {
verify = false;
this.$util.showToast({
title: this.$lang('components.nsForm.endDateBeforeStartDate')
});
break;
}
}
if (item.controller == 'Time') {
if (item.value.required && !item.val) {
verify = false;
this.$util.showToast({
title: this.$lang('components.nsForm.pleaseSelect', { title: item.value.title })
});
break;
}
}
if (item.controller == 'Timelimit') {
if (item.value.required && !item.val) {
verify = false;
this.$util.showToast({
title: this.$lang('components.nsForm.pleaseSelect', { title: item.value.title })
});
break;
}
if (item.start_time >= item.end_time) {
verify = false;
this.$util.showToast({
title: this.$lang('components.nsForm.endTimeBeforeStartTime')
});
break;
}
}
if (item.controller == 'City') {
if (item.value.required && !item.val) {
verify = false;
this.$util.showToast({
title: this.$lang('components.nsForm.pleaseSelect', { title: item.value.title })
});
break;
}
}
}
if (verify) {
return this.formData;
} else return verify;
},
pickerChange(e, index) {
this.formData[index].val = this.data[index].value.options[e.detail.value];
this.$forceUpdate();
},
checkboxChange: function(e, index) {
this.formData[index].val = e.detail.value.toString();
this.formData[index].option_lists.forEach(item => {
item.checked = e.detail.value.indexOf(item.value) != -1;
});
this.$forceUpdate();
},
radioChange: function(e, index) {
this.formData[index].val = e.detail.value;
this.$forceUpdate();
},
// 修改图片
uploadImg(index) {
this.$util.upload(
Number(this.formData[index].value.max_count), {
path: 'evaluateimg'
},
res => {
if (res.length > 0) {
res.forEach(v => {
if (this.formData[index].img_lists.length >= Number(this.formData[index].value.max_count)) {
this.$util.showToast({
title: this.$lang('components.nsForm.maxUploadImages', { max: this.formData[index].value.max_count })
});
return false;
} else {
this.formData[index].img_lists.push(v);
}
});
}
this.formData[index].val = this.formData[index].img_lists.toString();
this.$forceUpdate();
}
);
},
// 添加图片
addImg(index) {
if (this.formData[index].img_lists.length >= Number(this.formData[index].value.max_count)) {
this.$util.showToast({
title: this.$lang('components.nsForm.maxUploadImages', { max: this.formData[index].value.max_count })
});
return false;
}
this.$util.upload(
Number(this.formData[index].value.max_count), {
path: 'evaluateimg'
},
res => {
if (res.length > 0) {
res.forEach(v => {
this.formData[index].img_lists.push(v);
});
}
this.formData[index].val = this.formData[index].img_lists.toString();
this.$forceUpdate();
}
);
},
// index 当前数据的下标 k 当前图片的下标
delImg(k, index) {
this.formData[index].img_lists.splice(k, 1);
this.formData[index].val = this.formData[index].img_lists.toString();
this.$forceUpdate();
},
getDate() {
const 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() {
const date = new Date();
let hour = date.getHours(); //得到小时
let min = date.getMinutes(); //得到分钟
hour = hour > 9 ? hour : '0' + hour;
min = min > 9 ? min : '0' + min;
return `${hour}:${min}`;
},
bindDateChange(e, index) {
this.formData[index].val = e.detail.value;
this.$forceUpdate();
},
bindStartDateChange(e, index) {
this.$set(this.formData[index], 'start_date', e.detail.value);
this.$set(this.formData[index], 'val', this.formData[index].start_date + ' - ' + this.formData[index].end_date);
this.$forceUpdate();
},
bindEndDateChange(e, index) {
this.$set(this.formData[index], 'end_date', e.detail.value);
this.$set(this.formData[index], 'val', this.formData[index].start_date + ' - ' + this.formData[index].end_date);
this.$forceUpdate();
},
bindTimeChange(e, index) {
this.formData[index].val = e.detail.value;
this.$forceUpdate();
},
bindStartTimeChange(e, index) {
this.formData[index].start_time = e.detail.value;
this.$forceUpdate();
},
bindEndTimeChange(e, index) {
this.formData[index].end_time = e.detail.value;
this.formData[index].val = this.formData[index].start_time + ' - ' + this.formData[index].end_time;
this.$forceUpdate();
},
// 获取选择的地区
handleGetRegions(e, index) {
this.formData[index].val = '';
this.formData[index].val += e[0] != undefined ? e[0].label : '';
this.formData[index].val += e[1] != undefined ? '-' + e[1].label : '';
this.formData[index].val += e[2] != undefined ? '-' + e[2].label : '';
this.$forceUpdate();
}
}
};
</script>
<style lang="scss" scoped>
/* CSS 自定义属性:默认值,与组件内 config.defaults 一一对应,
formConfig 未传时作为兜底;运行时会被 formWrapStyle 注入的 inline style 覆盖。
后端 formConfig 一律以 rpx 传递,前端按 rpx 写入;
仅 H5 端需要在 JS computed(formWrapStyle 等)里通过 _rpx() 转 px。 */
.form-wrap {
--form-padding-x: 0rpx;
--form-bg-color: #ffffff;
--row-padding-y: 20rpx;
--row-min-height: 60rpx;
--label-gap: 16rpx;
--input-height: 60rpx;
--input-radius: 0rpx;
--label-fixed-width: 180rpx;
--label-font-size: 28rpx;
--label-color: #1f2937;
--label-font-weight: 600;
--required-color: #ef4444;
--placeholder-font-size: 28rpx;
--checked-color: #ff4544;
--placeholder-color: #909399;
// 输入框视觉增强(与 ns-newform 对齐)
--input-border-color: #e5e7eb;
--input-bg-color: #f9fafb;
--input-focus-border-color: #ff4544;
--input-focus-bg-color: #ffffff;
--row-divider-color: #f0f0f0;
--show-row-divider: 1;
}
.order-wrap {
padding: var(--row-padding-y, 20rpx) 0;
&.no-divider {
border-bottom: 0;
}
&:last-child {
margin-bottom: 0;
border-bottom: 0;
}
}
.order-cell {
display: flex;
align-items: center;
background: #fff;
position: relative;
min-height: var(--row-min-height, 60rpx);
&.textarea {
align-items: flex-start;
}
&.clear-flex {
display: block;
.box {
margin-top: 16rpx;
text-align: left;
}
}
&:last-child {
margin-bottom: 0;
// calc + 0/1 控制分割线显隐(formConfig.showRowDivider = false 时整条隐藏)
border-bottom: calc(var(--show-row-divider, 1) * 2rpx) solid var(--row-divider-color, #f0f0f0);
}
&.align-top {
align-items: flex-start;
}
text {
font-size: var(--label-font-size, $font-size-base);
}
.name {
width: var(--label-fixed-width, 180rpx);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: var(--label-gap, 16rpx);
}
.tit {
text-align: left;
font-size: var(--label-font-size, $font-size-base);
color: var(--label-color, #1f2937);
font-weight: var(--label-font-weight, 600);
text {
font-size: var(--label-font-size, $font-size-base);
}
}
.required {
color: var(--required-color, #ef4444);
font-size: var(--label-font-size, 28rpx);
margin-left: 4rpx;
width: 14rpx;
text-align: left;
display: inline-block;
}
.box {
flex: 1;
padding: 0 10rpx;
line-height: inherit;
text-align: left;
// H5 端 <input> 可能被 uni-app 编译为 <uni-input> 包装组件,::v-deep 穿透 scoped 命中真实原生 input
::v-deep input,
input {
font-size: var(--label-font-size, $font-size-base);
text-align: left;
color: var(--label-color, #1f2937);
// 显式 height / line-height / min-height 同步覆盖 WebView 默认与鸿蒙原生 TextInput 的固有高度,
// 避免 placeholder / 文字下半部分被裁切
line-height: var(--input-height, 60rpx);
height: var(--input-height, 60rpx);
min-height: var(--input-height, 60rpx);
border-radius: var(--input-radius, 0rpx);
box-sizing: border-box;
padding: 0 12rpx;
// 输入框视觉增强(与 ns-newform 对齐)
border: 1rpx solid var(--input-border-color, #e5e7eb);
background: var(--input-bg-color, #f9fafb);
transition: all 0.3s ease;
&:focus {
background: var(--input-focus-bg-color, #ffffff);
border-color: var(--input-focus-border-color, #ff4544);
}
// 原生 placeholder 样式(WebView / H5 生效;小程序/鸿蒙的 placeholder
// 通过 placeholder-class="placeholder" 钩子走 .placeholder 规则)
&::placeholder {
color: var(--placeholder-color, #909399);
font-size: var(--placeholder-font-size, 28rpx);
}
}
// placeholder-class 钩子:在鸿蒙原生 TextInput / WebView 中都生效,
// 限定在 .form-wrap 内避免污染全局 .placeholder / .color-tip
.placeholder {
color: var(--placeholder-color, #909399);
font-size: var(--placeholder-font-size, 28rpx);
line-height: var(--input-height, 60rpx);
}
::v-deep textarea,
textarea {
font-size: var(--label-font-size, $font-size-base);
width: 100%;
color: var(--label-color, #1f2937);
// 鸿蒙元服务渲染为原生 ArkUI TextArea,不识别 CSS height,由 :rows 控制行数;
// 这里给所有端都设一个最小高度基线,避免空内容时塌缩
min-height: 88rpx;
height: 88rpx;
line-height: 44rpx;
text-align: left;
border-radius: var(--input-radius, 0rpx);
box-sizing: border-box;
padding: 12rpx;
// 输入框视觉增强(与 ns-newform 对齐)
border: 1rpx solid var(--input-border-color, #e5e7eb);
background: var(--input-bg-color, #f9fafb);
transition: all 0.3s ease;
&:focus {
background: var(--input-focus-bg-color, #ffffff);
border-color: var(--input-focus-border-color, #ff4544);
}
}
.uni-input {
font-size: var(--label-font-size, $font-size-base);
color: var(--label-color, #1f2937);
line-height: var(--input-height, 60rpx);
min-height: var(--input-height, 60rpx);
// 输入框视觉增强(与 ns-newform 对齐)
border: 1rpx solid var(--input-border-color, #e5e7eb);
background: var(--input-bg-color, #f9fafb);
border-radius: var(--input-radius, 0rpx);
transition: all 0.3s ease;
}
.select-address {
font-size: var(--label-font-size, $font-size-base);
color: var(--label-color, #1f2937);
}
.color-tip {
color: var(--placeholder-color, #909399);
}
checkbox-group {
display: flex;
flex-wrap: wrap;
}
radio-group {
display: flex;
flex-wrap: wrap;
}
// 选项排列样式:vertical = 每个选项独立一行;wrap(默认)= 横向自动换行
.option-layout-vertical {
checkbox-group,
radio-group {
flex-direction: column;
align-items: flex-start;
}
}
label {
display: flex;
align-items: center;
line-height: 1;
margin-right: 30rpx;
}
&.img-boxs {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.img-box {
margin: 10rpx 20rpx 10rpx 0;
display: flex;
justify-content: center;
align-items: center;
width: 100rpx;
height: 100rpx;
border: 1rpx solid $color-line;
border-radius: 4rpx;
position: relative;
.icon-guanbi {
position: absolute;
top: -14rpx;
right: -14rpx;
display: inline-block;
width: 28rpx;
height: 28rpx;
line-height: 28rpx;
color: $color-tip;
}
.icon-add1 {
font-size: 40rpx;
}
image {
width: 100%;
height: 100%;
}
}
&.box-flex {
display: flex;
align-items: center;
justify-content: space-between;
}
&.date-boxs {
padding: 0 10rpx;
display: flex;
align-items: center;
}
.interval {
margin: 0 12rpx;
color: #000;
font-weight: bold;
}
.date-box {
.picker-box {
display: flex;
align-items: center;
justify-content: flex-end;
}
}
}
.radio-group-box {
radio {
display: none;
}
.radio-box {
display: flex;
align-items: center;
line-height: 1;
.iconfont {
font-size: 32rpx;
margin-right: 10rpx;
}
}
}
.check-group-box {
checkbox {
display: none;
}
.checkbox {
display: flex;
align-items: center;
line-height: 1;
.iconfont {
font-size: 32rpx;
margin-right: 10rpx;
}
}
}
.iconfont {
color: $color-tip;
font-size: var(--label-font-size, $font-size-base);
}
.box-flex picker {
display: block;
width: 100%;
}
.icon-right {
line-height: 1;
position: unset;
}
}
</style>