Files
lucky_shop/components/ns-newform/ns-newform.vue
ZF sun e3c69e7e32 style&feat: 优化表单提示语与提交按钮样式
1.  调整默认提示语字号从24rpx改为32rpx,颜色改为纯黑,背景设为透明
2.  新增提交按钮顶部外边距配置项,支持自定义表单与按钮间距
3.  同步更新diy-form组件的提示语样式与提交按钮外层布局
2026-06-12 14:46:12 +08:00

1409 lines
42 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" >
<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', 'align-right']">
<text v-if="item.val != ''">{{ item.val }}</text>
<text v-else class="color-tip">{{ item.value.placeholder || '请选择' }}</text>
</view>
<text class="iconfont icon-right"></text>
</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']">
<checkbox-group @change="checkboxChange($event, index)">
<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']">
<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"></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 || '请选择日期') }}
</view>
</picker>
</view>
<text class="iconfont icon-right"></text>
</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 || '开始日期') }}
</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 || '结束日期') }}
</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 || '请选择时间') }}
</view>
</picker>
</view>
<text class="iconfont icon-right"></text>
</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 || '开始时间') }}
</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 || '结束时间') }}
</view>
</view>
</picker>
</view>
</view>
</view>
</view>
<!-- 城市选择器 -->
<view v-if="item.controller == 'City'" class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
<view :class="['order-cell', 'box-flex', { '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-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 || '请选择城市') }}
</view>
</pick-regions>
</view>
<text class="iconfont icon-right"></text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'ns-newform',
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: {
// 当前运行平台标识(与后端 formConfig.terminal 键名对齐:h5 / wechat / weapp / harmony)
platform() {
// 优先使用后端在 formConfig.terminal 中使用的键名
// #ifdef H5
return 'h5';
// #endif
// #ifdef MP-WEIXIN
return 'weapp';
// #endif
// #ifdef MP-ALIPAY
return 'mp-alipay';
// #endif
// #ifdef MP-BAIDU
return 'mp-baidu';
// #endif
// #ifdef MP-TOUTIAO
return 'mp-toutiao';
// #endif
// #ifdef APP-PLUS
return 'app';
// #endif
// #ifdef APP-HARMONY
return 'harmony';
// #endif
// #ifdef QUICKAPP-WEBVIEW
return 'quickapp';
// #endif
return 'h5';
},
// 平台配置兼容层:后端键名(h5/wechat/weapp/harmony)→ 内部 platform 名
platformAliases() {
return {
'h5': 'h5',
'weapp': 'weapp',
'mp-weixin': 'weapp',
'wechat': 'wechat',
'harmony': 'harmony',
'app': 'app',
'app-plus': 'app'
};
},
// 当前平台的有效配置键集合(用于 config() 中查找 platformCfg)
platformConfigKeys() {
const cur = this.platform;
const aliases = this.platformAliases;
// 找出所有映射到当前平台的后端键名
const keys = [];
Object.keys(aliases).forEach(k => {
if (aliases[k] === cur) keys.push(k);
});
// 加入当前平台名(兼容 formConfig[platform] 这种写法)
if (keys.indexOf(cur) === -1) keys.push(cur);
return keys;
},
// 合并配置:内置默认 < 全局 < 当前平台
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',
requiredColor: '#ef4444',
checkedColor: '#ff4544',
// 提交按钮
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.config.labelFixedWidth}rpx`;
} else {
width = this.maxLabelWidth ? `${this.maxLabelWidth}rpx` : '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 ${cfg.formPaddingX}rpx`,
background: cfg.formBgColor,
// 通过 CSS 变量传递:与后端 design.css 的 :root 变量一一对应
'--form-padding-x': `${cfg.formPaddingX}rpx`,
'--banner-padding-x': `${cfg.bannerPaddingX}rpx`,
'--tip-padding-x': `${cfg.tipPaddingX}rpx`,
'--tip-font-size': `${cfg.tipFontSize}rpx`,
'--tip-color': cfg.tipColor,
'--tip-bg-color': cfg.tipBgColor,
'--row-padding-y': `${cfg.rowPaddingY}rpx`,
'--row-min-height': `${cfg.rowMinHeight}rpx`,
'--label-gap': `${cfg.labelGap}rpx`,
'--form-bg-color': cfg.formBgColor,
'--row-divider-color': cfg.rowDividerColor,
'--input-height': `${cfg.inputHeight}rpx`,
'--input-radius': `${cfg.inputRadius}rpx`,
'--label-fixed-width': `${cfg.labelFixedWidth}rpx`,
'--label-font-size': `${num(cfg.labelFontSize, 28)}rpx`,
'--label-color': cfg.labelColor,
'--required-color': cfg.requiredColor,
'--checked-color': cfg.checkedColor,
'--submit-color': cfg.submitColor,
'--submit-bg-color': cfg.submitBgColor,
'--submit-margin-top': `${cfg.submitMarginTop || 0}rpx`,
// 分割线显隐
'--show-row-divider': cfg.showRowDivider === false ? '0' : '1'
});
},
// 行间距样式:仅上下内边距,水平由外层 form-wrap 控制(与后端 form-gorup 对齐)
rowWrapStyle() {
return this.$util.objToStyle({
padding: `${this.config.rowPaddingY}rpx 0`
});
},
// 行容器 class:用于控制分割线显隐
rowWrapClass() {
return this.config.showRowDivider === false ? 'order-wrap no-divider' : 'order-wrap';
},
// 行最小高度
rowCellStyle() {
return this.$util.objToStyle({
'min-height': `${this.config.rowMinHeight}rpx`
});
},
// 标签与输入框间距
nameStyle() {
if (this.mode === 'vertical') {
return this.$util.objToStyle({});
}
return this.$util.objToStyle({
'margin-right': `${this.config.labelGap}rpx`
});
},
// 输入框样式(全局,未指定 item 时使用)
inputStyle() {
return this.$util.objToStyle({
'min-height': `${this.config.inputHeight}rpx`,
'border-radius': `${this.config.inputRadius}rpx`
});
},
labelStyle() {
let style = {};
let fontSize = this.config.labelFontSize;
if (typeof fontSize === 'number') {
fontSize = `${fontSize}rpx`;
} else if (typeof fontSize === 'string' && !fontSize.endsWith('rpx') && !fontSize.endsWith('px')) {
fontSize = `${fontSize}rpx`;
}
style.fontSize = fontSize;
style.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: {
// 处理 rpx 字号:与后端 label_font_size 字段语义对齐
_resolveFontSize(value, fallback) {
let v = value;
if (v === '' || v === null || v === undefined) {
v = fallback;
}
if (typeof v === 'number') {
return `${v}rpx`;
}
if (typeof v === 'string') {
if (v.endsWith('rpx') || v.endsWith('px')) return v;
return `${v}rpx`;
}
return v;
},
// 预计算每项的样式(写到 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 !== '') {
let fs = itemValue.label_font_size;
if (typeof fs === 'number') fs = `${fs}rpx`;
else if (typeof fs === 'string' && !fs.endsWith('rpx') && !fs.endsWith('px')) fs = `${fs}rpx`;
style['--item-label-font-size'] = fs;
}
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 } : {});
});
},
// 容器样式(提供给外部 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)
const widthPx = Math.max(40, maxChars * (fontSize / 2) * 1.1 + 12);
this.maxLabelWidth = Math.round(widthPx * 2);
// 宽度变了,需要把全局 nameWidthStyle 重新合到每项 _labelStyle 里
this.recomputeItemStyles();
},
initFormData() {
// 深拷贝 prop 数据到本地状态
this.formData = JSON.parse(JSON.stringify(this.data || []));
this.processFormData();
},
setFormData() {
// 重置为初始值
this.initFormData();
},
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((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: '请输入' + 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: '身份证输入不合法' });
return false;
}
if (item.val != '') {
this.$util.showToast({ title: '身份证输入不合法' });
return false;
}
}
if (item.name == 'MOBILE' && !this.$util.verifyMobile(item.val)) {
if (!item.value.required) {
this.$util.showToast({ title: '手机号输入不合法' });
return false;
}
if (item.val != '') {
this.$util.showToast({ title: '手机号输入不合法' });
return false;
}
}
}
if (item.controller == 'Textarea' && item.value.required && !item.val) {
this.$util.showToast({ title: '请输入' + item.value.title });
return false;
}
if (item.controller == 'Select' && item.value.required && !item.val) {
this.$util.showToast({ title: '请选择' + item.value.title });
return false;
}
if (item.controller == 'Checkbox' && item.value.required && !item.val) {
this.$util.showToast({ title: '请至少选择一个' + item.value.title });
return false;
}
if (item.controller == 'Img' && item.value.required && !item.val) {
this.$util.showToast({ title: '请至少上传一张' + item.value.title });
return false;
}
if (item.controller == 'Date' && item.value.required && !item.val) {
this.$util.showToast({ title: '请选择' + item.value.title });
return false;
}
if (item.controller == 'Datelimit') {
if (item.value.required && !item.val) {
this.$util.showToast({ title: '请选择' + item.value.title });
return false;
}
if (this.$util.timeTurnTimeStamp(item.start_date) > this.$util.timeTurnTimeStamp(item.end_date)) {
this.$util.showToast({ title: '结束日期不能小于开始日期' });
return false;
}
}
if (item.controller == 'Time' && item.value.required && !item.val) {
this.$util.showToast({ title: '请选择' + item.value.title });
return false;
}
if (item.controller == 'Timelimit') {
if (item.value.required && !item.val) {
this.$util.showToast({ title: '请选择' + item.value.title });
return false;
}
if (item.start_time >= item.end_time) {
this.$util.showToast({ title: '结束时间必须大于开始时间' });
return false;
}
}
if (item.controller == 'City' && item.value.required && !item.val) {
this.$util.showToast({ 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.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.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;
--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: 8rpx;
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: 500;
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;
}
input {
font-size: var(--label-font-size, 28rpx);
text-align: left;
height: var(--input-height, 60rpx);
border: none;
border-radius: var(--input-radius, 10rpx);
line-height: var(--input-height, 60rpx);
padding: 0;
background: transparent;
color: var(--label-color, #1f2937);
transition: all 0.3s ease;
width: 100%;
box-sizing: border-box;
&:focus {
background: transparent;
}
&::placeholder {
color: var(--placeholder-color, #ddd);
font-size: var(--label-font-size, 28rpx);
}
}
textarea {
font-size: var(--label-font-size, 28rpx);
width: 100%;
min-height: 120rpx;
line-height: 1.5;
text-align: left;
border: none;
border-radius: var(--input-radius, 10rpx);
padding: 0;
background: transparent;
color: var(--label-color, #1f2937);
transition: all 0.3s ease;
box-sizing: border-box;
height: auto;
&:focus {
background: transparent;
}
&::placeholder {
color: var(--placeholder-color, #ddd);
font-size: var(--label-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;
}
}
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>