1. 修复cart页面unload时调用不存在方法的报错 2. 适配鸿蒙themeColor绑定和$nextTick兼容问题 3. 给多处逻辑添加try/catch防止页面空白 4. 补全v-for循环的key属性
1161 lines
51 KiB
Vue
1161 lines
51 KiB
Vue
<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">
|
||
<checkbox-group @change="checkboxChange($event, index)">
|
||
<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">
|
||
<radio-group @change="radioChange($event, index)">
|
||
<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',
|
||
requiredColor: '#ef4444',
|
||
checkedColor: '#ff4544',
|
||
placeholderColor: '#909399',
|
||
placeholderFontSize: 28,
|
||
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'),
|
||
requiredColor: pick('requiredColor', 'required_color'),
|
||
checkedColor: pick('checkedColor', 'checked_color'),
|
||
placeholderColor: pick('placeholderColor', 'placeholder_color'),
|
||
placeholderFontSize: pick('placeholderFontSize', 'placeholder_font_size'),
|
||
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,
|
||
'--required-color': cfg.requiredColor,
|
||
'--checked-color': cfg.checkedColor,
|
||
'--placeholder-color': cfg.placeholderColor,
|
||
'--placeholder-font-size': this.$util.rpx(num(cfg.placeholderFontSize, 28)),
|
||
'--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: {
|
||
initFormData() {
|
||
// 深拷贝 prop 数据到本地状态,避免 mutate 上游
|
||
this.formData = JSON.parse(JSON.stringify(this.data || []));
|
||
this.processFormData();
|
||
},
|
||
setFormData() {
|
||
this.initFormData();
|
||
this.$nextTick(() => this.calculateMaxLabelWidth());
|
||
},
|
||
processFormData() {
|
||
this.formData.forEach(item => {
|
||
if (!item.val) 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;
|
||
--required-color: #ef4444;
|
||
--placeholder-font-size: 28rpx;
|
||
--checked-color: #ff4544;
|
||
--placeholder-color: #909399;
|
||
--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);
|
||
|
||
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;
|
||
|
||
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;
|
||
background: transparent;
|
||
|
||
// 原生 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);
|
||
}
|
||
|
||
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: 0;
|
||
background: transparent;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
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> |