1719 lines
61 KiB
Vue
1719 lines
61 KiB
Vue
<template>
|
||
<view class="form-wrap form-component" :style="formWrapStyle" >
|
||
<view v-for="(item, index) in formData" :key="index">
|
||
<!-- 文本输入框 -->
|
||
<view v-if="item.controller == 'Text'" class="order-wrap" :class="rowWrapClass" :style="item._fieldWrapStyle">
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]" :style="rowCellStyle">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ 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="item._fieldWrapStyle">
|
||
<view :class="['order-cell', 'flex-box', 'textarea', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'align-left']" :style="item._boxAlignStyle">
|
||
<!-- 所有平台统一通过 formConfig.textareaRows / item.value.rows 控制显示行数:
|
||
- H5 / 微信:原生 textarea 识别 :rows 属性;
|
||
- 鸿蒙:渲染为 ArkUI 原生 TextArea,CSS height 不生效,必须靠 :rows 控制。
|
||
rows 全部由 formConfig 动态下发,与 ns-form 行为保持一致。 -->
|
||
<textarea
|
||
:rows="item.value.rows || textareaRowsValue"
|
||
:placeholder="item.value.placeholder"
|
||
placeholder-class="placeholder color-tip"
|
||
v-model="item.val"
|
||
:style="textareaStyle"
|
||
></textarea>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 下拉选择器 -->
|
||
<view v-if="item.controller == 'Select'" class="order-wrap" :class="rowWrapClass" :style="item._fieldWrapStyle">
|
||
<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="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'box-flex', 'align-right']">
|
||
<text v-if="item.val != ''">{{ item.val }}</text>
|
||
<text v-else class="color-tip">{{ item.value.placeholder || $lang('components.ns-newform.selectPlaceholder') }}</text>
|
||
<text class="iconfont icon-right"></text>
|
||
</view>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<!-- 复选框 -->
|
||
<view v-if="item.controller == 'Checkbox'" class="order-wrap" :class="rowWrapClass" :style="item._fieldWrapStyle">
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'check-group-box', 'align-left', 'option-layout-' + (item.value.option_layout || 'vertical')]">
|
||
<checkbox-group @change="checkboxChange($event, index)" :style="item._optionLayoutStyle">
|
||
<label v-for="(v, k) in item.option_lists" :key="k">
|
||
<checkbox :value="v.value" :checked="v.checked"></checkbox>
|
||
<view class="checkbox">
|
||
<text
|
||
:class="[
|
||
'iconfont',
|
||
'check-icon',
|
||
!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="item._fieldWrapStyle">
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'radio-group-box', 'align-left', 'option-layout-' + (item.value.option_layout || 'vertical')]">
|
||
<radio-group @change="radioChange($event, index)" :style="item._optionLayoutStyle">
|
||
<label v-for="(v, k) in item.option_lists" :key="k">
|
||
<radio :value="v.value" :checked="item.val == v.value"></radio>
|
||
<view class="radio-box">
|
||
<text
|
||
:class="[
|
||
'iconfont',
|
||
'radio-icon',
|
||
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="item._fieldWrapStyle">
|
||
<view :class="['order-cell', 'flex-box', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ 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="item._fieldWrapStyle">
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'box-flex', 'align-right']">
|
||
<picker
|
||
mode="date"
|
||
:value="item.val"
|
||
@change="bindDateChange($event, index)"
|
||
>
|
||
<view :class="['uni-input', !item.val ? 'color-tip' : '']">
|
||
{{ item.val ? item.val : (item.value.placeholder || $lang('components.ns-newform.datePlaceholder')) }}
|
||
</view>
|
||
</picker>
|
||
<text class="iconfont icon-right"></text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 日期范围选择器 -->
|
||
<view v-if="item.controller == 'Datelimit'" class="order-wrap" :class="rowWrapClass" :style="item._fieldWrapStyle">
|
||
<view :class="['order-cell', 'flex-box', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'date-boxs', 'align-center']" :style="item._boxAlignStyle">
|
||
<view class="date-box">
|
||
<picker
|
||
mode="date"
|
||
:value="item.start_date"
|
||
@change="bindStartDateChange($event, index)"
|
||
>
|
||
<view class="picker-box">
|
||
<view :class="['uni-input', !item.start_date ? 'color-tip' : '']">
|
||
{{ item.start_date ? item.start_date : (item.value.placeholder_start || $lang('components.ns-newform.startDate')) }}
|
||
</view>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
<view class="interval iconfont icon-jian"></view>
|
||
<view class="date-box">
|
||
<picker
|
||
mode="date"
|
||
:value="item.end_date"
|
||
@change="bindEndDateChange($event, index)"
|
||
>
|
||
<view class="picker-box">
|
||
<view :class="['uni-input', !item.end_date ? 'color-tip' : '']">
|
||
{{ item.end_date ? item.end_date : (item.value.placeholder_end || $lang('components.ns-newform.endDate')) }}
|
||
</view>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 时间选择器 -->
|
||
<view v-if="item.controller == 'Time'" class="order-wrap" :class="rowWrapClass" :style="item._fieldWrapStyle">
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'box-flex', 'align-right']">
|
||
<picker
|
||
mode="time"
|
||
:value="item.val"
|
||
@change="bindTimeChange($event, index)"
|
||
>
|
||
<view :class="['uni-input', !item.val ? 'color-tip' : '']">
|
||
{{ item.val ? item.val : (item.value.placeholder || $lang('components.ns-newform.timePlaceholder')) }}
|
||
</view>
|
||
</picker>
|
||
<text class="iconfont icon-right"></text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 时间范围选择器 -->
|
||
<view v-if="item.controller == 'Timelimit'" class="order-wrap" :class="rowWrapClass" :style="item._fieldWrapStyle">
|
||
<view :class="['order-cell', 'flex-box', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'date-boxs', 'align-center']" :style="item._boxAlignStyle">
|
||
<view class="date-box">
|
||
<picker
|
||
mode="time"
|
||
:value="item.start_time"
|
||
@change="bindStartTimeChange($event, index)"
|
||
>
|
||
<view class="picker-box">
|
||
<view :class="['uni-input', !item.start_time ? 'color-tip' : '']">
|
||
{{ item.start_time ? item.start_time : (item.value.placeholder_start || $lang('components.ns-newform.startTime')) }}
|
||
</view>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
<view class="interval iconfont icon-jian"></view>
|
||
<view class="date-box">
|
||
<picker
|
||
mode="time"
|
||
:value="item.end_time"
|
||
@change="bindEndTimeChange($event, index)"
|
||
>
|
||
<view class="picker-box">
|
||
<view :class="['uni-input', !item.end_time ? 'color-tip' : '']">
|
||
{{ item.end_time ? item.end_time : (item.value.placeholder_end || $lang('components.ns-newform.endTime')) }}
|
||
</view>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 城市选择器 -->
|
||
<view v-if="item.controller == 'City'" class="order-wrap" :class="rowWrapClass" :style="item._fieldWrapStyle">
|
||
<view :class="['order-cell', { 'vertical-layout': mode === 'vertical' }]">
|
||
<view class="name" :style="item._labelStyle">
|
||
<text class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
<text class="tit">{{ item.value.title }}</text><text v-if="requiredPosition === 'after'" class="required" :style="item._starStyle">{{ item.value.required ? '*' : '' }}</text>
|
||
</view>
|
||
<view :class="['box', 'box-flex', 'align-right']">
|
||
<pick-regions
|
||
:defaultRegions="item.default_regions"
|
||
:selectArr="item.select_arr"
|
||
@getRegions="handleGetRegions($event, index)"
|
||
>
|
||
<view :class="['select-address', !item.val ? 'empty' : '', !item.val ? 'color-tip' : '']">
|
||
{{ item.val ? item.val : (item.value.placeholder || (item.value.default_type == 1 ? $lang('components.ns-newform.selectProvinceCity') : $lang('components.ns-newform.selectProvinceCityDistrict'))) }}
|
||
</view>
|
||
</pick-regions>
|
||
<text class="iconfont icon-right"></text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 信息卡片(展示型组件,与 addon/form 新版 INFO_CARD 对齐)
|
||
- 无标签/必填/校验/存储/导出,verify() 跳过
|
||
- 内容存 value.content(富文本 HTML),rich-text 跨端渲染
|
||
- 容器底色 value.bg_color;底部分割线 show_divider + divider_* 五参数
|
||
- 老数据兼容:content 为空但存在 title/items 时按旧结构渲染 -->
|
||
<view v-if="item.controller == 'InfoCard'" class="info-card-wrap" :style="item._infoCardWrapStyle">
|
||
<rich-text v-if="item._infoCardHasContent" class="info-card-content" :nodes="item.value.content"></rich-text>
|
||
<template v-else-if="item.value.title || (item.value.items && item.value.items.length)">
|
||
<view v-if="item.value.title" class="info-card-title" :style="{ fontSize: ((item.value.title_font_size || 36) / 2) + 'px', color: item.value.title_color || '#1f2937', fontWeight: 700 }">
|
||
{{ item.value.title }}
|
||
</view>
|
||
<view v-if="item.value.items && item.value.items.length" class="info-card-items">
|
||
<view
|
||
v-for="(it, iidx) in item.value.items"
|
||
:key="iidx"
|
||
class="info-card-item"
|
||
:style="{ fontSize: ((item.value.item_font_size || 26) / 2) + 'px', color: item.value.item_color || '#6b7280' }"
|
||
>{{ it }}</view>
|
||
</view>
|
||
</template>
|
||
<view v-if="item.value.show_divider == 1" class="info-card-divider" :style="item._infoCardDividerStyle"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import formPlatformMixin from '@/common/js/form-platform-mixin.js';
|
||
|
||
export default {
|
||
name: 'ns-newform',
|
||
mixins: [formPlatformMixin],
|
||
props: {
|
||
data: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
customAttr: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
mode: {
|
||
type: String,
|
||
default: 'vertical',
|
||
validator: (value) => !value || ['horizontal', 'vertical'].includes(value)
|
||
},
|
||
// 表单配置(按平台差异化)
|
||
// 格式:{
|
||
// form_padding_x: 14, // 全局默认值
|
||
// mp: { form_padding_x: 10 }, // 微信小程序平台覆盖
|
||
// h5: { row_padding_y: 8 }, // H5 平台覆盖
|
||
// harmony: { ... } // 鸿蒙元服务覆盖
|
||
// }
|
||
formConfig: {
|
||
type: Object,
|
||
default: () => ({})
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
formData: [],
|
||
maxLabelWidth: 0
|
||
}
|
||
},
|
||
created() {
|
||
// 避免直接修改 prop,初始化本地数据
|
||
this.initFormData();
|
||
},
|
||
mounted() {
|
||
this.$nextTick(() => {
|
||
this.calculateMaxLabelWidth();
|
||
});
|
||
},
|
||
watch: {
|
||
data: {
|
||
handler() {
|
||
this.initFormData();
|
||
this.$nextTick(() => {
|
||
this.calculateMaxLabelWidth();
|
||
});
|
||
},
|
||
deep: true
|
||
},
|
||
// 配置变化(如字号 / 模式切换)时,重新计算 maxLabelWidth
|
||
formConfig: {
|
||
handler() {
|
||
this.$nextTick(() => {
|
||
this.calculateMaxLabelWidth();
|
||
});
|
||
},
|
||
deep: true
|
||
},
|
||
mode() {
|
||
this.$nextTick(() => {
|
||
this.calculateMaxLabelWidth();
|
||
});
|
||
}
|
||
},
|
||
computed: {
|
||
// 合并配置:内置默认 < 全局 < 当前平台
|
||
config() {
|
||
// 内置默认值(最低优先级)- 与后端 D:\projects\shop-projects\backend\src\app\component\view\form\js\design.js getDefaultFormData().formConfig 对齐
|
||
const defaults = {
|
||
formPaddingX: 28,
|
||
bannerPaddingX: 0,
|
||
tipPaddingX: 28,
|
||
rowPaddingY: 24,
|
||
rowMinHeight: 60,
|
||
labelGap: 20,
|
||
formBgColor: '#ffffff',
|
||
rowDividerColor: '#f0f0f0',
|
||
showRowDivider: false,
|
||
inputHeight: 84,
|
||
inputRadius: 16,
|
||
labelWidthMode: 'auto',
|
||
labelFixedWidth: 180,
|
||
labelFontSize: 32,
|
||
labelColor: '#1f2937',
|
||
labelFontWeight: 700,
|
||
labelAutoMaxWidth: 200, // auto 模式下 label 宽度上限(rpx),超过后 ellipsis 截断
|
||
requiredColor: '#ef4444',
|
||
checkedColor: '#3b82f6',
|
||
placeholderColor: '#9ca3af',
|
||
placeholderFontSize: 28,
|
||
// textarea 显示行数 / 行高(与 ns-form 保持一致,由 formConfig 覆盖)
|
||
// 改用 :rows + line-height 驱动高度,rows 给原生 <textarea>,line-height 在 textareaStyle 算 px 高度
|
||
textareaRows: 4,
|
||
textareaLineHeight: 48,
|
||
// 输入框视觉增强(可由后端 form_config 覆盖)
|
||
inputBorderColor: '#e5e7eb',
|
||
inputBgColor: '#ffffff',
|
||
inputFocusBorderColor: '#3b82f6',
|
||
inputFocusBgColor: '#ffffff',
|
||
labelFontWeight: 700,
|
||
// 提交按钮
|
||
submitColor: '#ffffff',
|
||
submitBgColor: '#3b82f6',
|
||
submitMarginTop: 0, // 提交按钮顶部外边距(rpx):调整表单与按钮之间的距离
|
||
// 提示语
|
||
tipFontSize: 32,
|
||
tipColor: '#000000',
|
||
tipBgColor: 'transparent'
|
||
};
|
||
// 全局配置(中间优先级)
|
||
const globalCfg = this.formConfig || {};
|
||
// 当前平台配置(最高优先级):从 formConfig.terminal 多个别名中按顺序合并
|
||
const formConfigRoot = this.formConfig || {};
|
||
// 兼容两种结构:
|
||
// 1) 后端格式:formConfig.terminal = { h5: {...}, weapp: {...}, harmony: {...} }
|
||
// 2) 旧式扁平:formConfig[platformKey] = { ... }
|
||
const terminalBlock = formConfigRoot.terminal || {};
|
||
const platformKeys = this.platformConfigKeys || [];
|
||
const platformCfg = Object.assign(
|
||
{},
|
||
...platformKeys.map(k => (terminalBlock[k] || formConfigRoot[k] || {}))
|
||
);
|
||
// 合并:defaults <- globalCfg <- platformCfg
|
||
return Object.assign({}, defaults, globalCfg, platformCfg);
|
||
},
|
||
nameWidthStyle() {
|
||
if (this.mode === 'vertical') {
|
||
return {};
|
||
}
|
||
let width;
|
||
if (this.config.labelWidthMode === 'fixed') {
|
||
width = this.$util.rpx(this.config.labelFixedWidth);
|
||
} else {
|
||
width = this.maxLabelWidth ? this.$util.rpx(this.maxLabelWidth) : 'auto';
|
||
}
|
||
return {
|
||
width: width
|
||
};
|
||
},
|
||
// 容器样式(与后端 form-preview-content 对齐:通过 CSS 变量驱动子元素样式)
|
||
formWrapStyle() {
|
||
const cfg = this.config;
|
||
const num = (v, fb) => (typeof v === 'number' ? v : parseInt(v) || fb);
|
||
|
||
return this.$util.objToStyle({
|
||
// 仅保留左右内边距 = form-padding-x;上下由每行 row-padding-y 承担
|
||
padding: `0 ${this.$util.rpx(cfg.formPaddingX)}`,
|
||
background: cfg.formBgColor,
|
||
// 通过 CSS 变量传递:与后端 design.css 的 :root 变量一一对应
|
||
'--form-padding-x': this.$util.rpx(cfg.formPaddingX),
|
||
'--banner-padding-x': this.$util.rpx(cfg.bannerPaddingX),
|
||
'--tip-padding-x': this.$util.rpx(cfg.tipPaddingX),
|
||
'--tip-font-size': this.$util.rpx(cfg.tipFontSize),
|
||
'--tip-color': cfg.tipColor,
|
||
'--tip-bg-color': cfg.tipBgColor,
|
||
'--row-padding-y': this.$util.rpx(cfg.rowPaddingY),
|
||
'--row-min-height': this.$util.rpx(cfg.rowMinHeight),
|
||
'--label-gap': this.$util.rpx(cfg.labelGap),
|
||
'--form-bg-color': cfg.formBgColor,
|
||
'--row-divider-color': cfg.rowDividerColor,
|
||
'--input-height': this.$util.rpx(cfg.inputHeight),
|
||
'--input-radius': this.$util.rpx(cfg.inputRadius),
|
||
'--label-fixed-width': this.$util.rpx(cfg.labelFixedWidth),
|
||
'--label-font-size': this.$util.rpx(num(cfg.labelFontSize, 28)),
|
||
'--label-color': cfg.labelColor,
|
||
'--required-color': cfg.requiredColor,
|
||
'--checked-color': cfg.checkedColor,
|
||
'--placeholder-color': cfg.placeholderColor,
|
||
'--placeholder-font-size': this.$util.rpx(num(cfg.placeholderFontSize, 28)),
|
||
// textarea 行数 / 行高(CSS 兜底 + JS 算高度都用得到)
|
||
'--textarea-rows': num(cfg.textareaRows, 4),
|
||
'--textarea-line-height': this.$util.rpx(num(cfg.textareaLineHeight, 44)),
|
||
// 输入框视觉增强(边框 / 背景 / 聚焦 / label 字重)
|
||
'--input-border-color': cfg.inputBorderColor,
|
||
'--input-bg-color': cfg.inputBgColor,
|
||
'--input-focus-border-color': cfg.inputFocusBorderColor,
|
||
'--input-focus-bg-color': cfg.inputFocusBgColor,
|
||
'--label-font-weight': cfg.labelFontWeight,
|
||
'--submit-color': cfg.submitColor,
|
||
'--submit-bg-color': cfg.submitBgColor,
|
||
'--submit-margin-top': this.$util.rpx(cfg.submitMarginTop || 0),
|
||
// 分割线显隐
|
||
'--show-row-divider': cfg.showRowDivider === false ? '0' : '1'
|
||
});
|
||
},
|
||
// 行容器 class:用于控制分割线显隐
|
||
rowWrapClass() {
|
||
return this.config.showRowDivider === false ? 'order-wrap no-divider' : 'order-wrap';
|
||
},
|
||
// 行最小高度
|
||
rowCellStyle() {
|
||
return this.$util.objToStyle({
|
||
'min-height': this.$util.rpx(this.config.rowMinHeight)
|
||
});
|
||
},
|
||
// 标签与输入框间距
|
||
nameStyle() {
|
||
if (this.mode === 'vertical') {
|
||
return {};
|
||
}
|
||
return {
|
||
'margin-right': this.$util.rpx(this.config.labelGap)
|
||
};
|
||
},
|
||
// 输入框样式(全局,未指定 item 时使用)
|
||
inputStyle() {
|
||
return this.$util.objToStyle({
|
||
'min-height': this.$util.rpx(this.config.inputHeight),
|
||
'border-radius': this.$util.rpx(this.config.inputRadius)
|
||
});
|
||
},
|
||
// textarea 显示行数(来自 formConfig.textareaRows,未配置时回退到默认 4)
|
||
textareaRowsValue() {
|
||
const n = parseInt(this.config.textareaRows, 10);
|
||
return Number.isFinite(n) && n > 0 ? n : 4;
|
||
},
|
||
// textarea 行高(来自 formConfig.textareaLineHeight,用于 :rows × line-height 算出可视高度)
|
||
textareaLineHeightValue() {
|
||
const n = parseInt(this.config.textareaLineHeight, 10);
|
||
return Number.isFinite(n) && n > 0 ? n : 44;
|
||
},
|
||
// textarea 专用 inline style(所有平台统一使用,见 template 中 textarea 节点)
|
||
// 高度 = rows × lineHeight + padding + border,由 formConfig 动态控制,与 ns-form 同公式
|
||
textareaStyle() {
|
||
const lh = this.$util.rpx(this.textareaLineHeightValue);
|
||
const rows = this.textareaRowsValue;
|
||
const paddings = this.$util.rpx(24) + this.$util.rpx(2); // padding 12rpx 上下 + border 1rpx 上下
|
||
const h = (this.$util.rpx(this.textareaLineHeightValue * (rows - 1)) + lh) + paddings;
|
||
return this.$util.objToStyle({
|
||
'height': h,
|
||
'min-height': h,
|
||
'line-height': lh,
|
||
'border-radius': this.$util.rpx(this.config.inputRadius)
|
||
});
|
||
},
|
||
labelStyle() {
|
||
const style = {
|
||
fontSize: this.$util.rpx(this.config.labelFontSize),
|
||
color: this.config.labelColor
|
||
};
|
||
return this.$util.objToStyle(style);
|
||
},
|
||
// 必填星号位置(before=标签前(默认)/ after=标签后;与后端 form_config.required_position 对齐)
|
||
// 星号的颜色/字号/间距动态值由 recomputeItemStyles 预计算到 item._starStyle(inline,跨端安全)
|
||
requiredPosition() {
|
||
return this.config.requiredPosition === 'after' ? 'after' : 'before';
|
||
},
|
||
checkedColor() {
|
||
return this.config.checkedColor;
|
||
},
|
||
rowDividerStyle() {
|
||
const style = {};
|
||
if (this.config.showRowDivider === false) {
|
||
style.display = 'none';
|
||
} else {
|
||
style.background = this.config.rowDividerColor;
|
||
}
|
||
return this.$util.objToStyle(style);
|
||
}
|
||
},
|
||
methods: {
|
||
// 预计算每项的样式(写到 item._labelStyle / item._boxAlignStyle),
|
||
// 避免在模板里以方法形式调用:微信小程序 WXML 不支持 :style="getXxx(item)" 语法。
|
||
// 触发时机:processFormData 末尾 / calculateMaxLabelWidth 末尾 / config 变化 / mode 变化。
|
||
recomputeItemStyles() {
|
||
if (!this.formData || this.formData.length === 0) return;
|
||
|
||
const widthStyle = this.nameWidthStyle || {};
|
||
const nameMarginStyle = this.nameStyle || {};
|
||
// 字段级间距(每个组件独立,互不影响):数值解析(''/null/undefined/非数字 → null = 未设置,跟随全局)
|
||
const spNum = (x) => {
|
||
if (x === undefined || x === null || x === '') return null;
|
||
const n = parseFloat(x);
|
||
return isNaN(n) ? null : n;
|
||
};
|
||
// 行容器基础上下内边距(原 rowWrapStyle 语义:全局行间距 rowPaddingY;左右恒 0)
|
||
const rowPadY = this.$util.rpx(this.config.rowPaddingY);
|
||
const clampRpx = (n, min, max) => this.$util.rpx(Math.max(min, Math.min(max, n)));
|
||
|
||
this.formData.forEach((item) => {
|
||
const itemValue = (item && item.value) || {};
|
||
|
||
// ---- 字段级间距(每个组件独立):外边距直出(允许负值上移贴近相邻组件)----
|
||
// clamp 与后端 fieldSpacingStyle 一致(margin -100..400 / padding 0..400 rpx)
|
||
const mTop = spNum(itemValue.margin_top), mBottom = spNum(itemValue.margin_bottom),
|
||
mLeft = spNum(itemValue.margin_left), mRight = spNum(itemValue.margin_right);
|
||
const fieldMargin = {};
|
||
if (mTop !== null) fieldMargin['margin-top'] = clampRpx(mTop, -100, 400);
|
||
if (mBottom !== null) fieldMargin['margin-bottom'] = clampRpx(mBottom, -100, 400);
|
||
if (mLeft !== null) fieldMargin['margin-left'] = clampRpx(mLeft, -100, 400);
|
||
if (mRight !== null) fieldMargin['margin-right'] = clampRpx(mRight, -100, 400);
|
||
const pTop = spNum(itemValue.padding_top), pBottom = spNum(itemValue.padding_bottom),
|
||
pLeft = spNum(itemValue.padding_left), pRight = spNum(itemValue.padding_right);
|
||
// 11 种输入控件行容器(.order-wrap,原 rowWrapStyle 承载层):
|
||
// margin 直出;padding 覆盖对应边(未设置的边跟随全局:上下 rowPaddingY / 左右 0)
|
||
if (item.controller != 'InfoCard') {
|
||
const fieldWrapStyle = Object.assign({}, fieldMargin);
|
||
fieldWrapStyle['padding-top'] = pTop !== null ? clampRpx(pTop, 0, 400) : rowPadY;
|
||
fieldWrapStyle['padding-bottom'] = pBottom !== null ? clampRpx(pBottom, 0, 400) : rowPadY;
|
||
fieldWrapStyle['padding-left'] = pLeft !== null ? clampRpx(pLeft, 0, 400) : '0';
|
||
fieldWrapStyle['padding-right'] = pRight !== null ? clampRpx(pRight, 0, 400) : '0';
|
||
this.$set(item, '_fieldWrapStyle', fieldWrapStyle);
|
||
}
|
||
|
||
const style = Object.assign({}, widthStyle);
|
||
|
||
// item 级别 label 字号 / 颜色覆盖(与原 getItemLabelStyle 逻辑一致)
|
||
if (itemValue.label_font_size !== undefined && itemValue.label_font_size !== null && itemValue.label_font_size !== '') {
|
||
style['--item-label-font-size'] = this.$util.rpx(itemValue.label_font_size);
|
||
}
|
||
if (itemValue.label_color) {
|
||
style['--item-label-color'] = itemValue.label_color;
|
||
}
|
||
// 合并全局的 nameStyle(margin-right)
|
||
Object.assign(style, nameMarginStyle);
|
||
|
||
// $set 包裹以确保新属性是响应式的(Vue 2 对动态加的属性需要 $set)
|
||
this.$set(item, '_labelStyle', style);
|
||
|
||
// item 级别 box 对齐方式(与原 getItemBoxAlignStyle 逻辑一致)
|
||
const align = itemValue.input_align;
|
||
this.$set(item, '_boxAlignStyle', align ? { 'text-align': align } : {});
|
||
|
||
// 必填星号样式:颜色/字号/间距全部预计算 inline(微信小程序/鸿蒙元服务对
|
||
// 嵌套 var() fallback 与 .required-after 类 margin 翻转支持不全,动态值不走 CSS 变量链)。
|
||
// 字号优先 item 级 label_font_size,回退全局 labelFontSize(默认 28);
|
||
// after 位置 margin-left,before 位置 margin-right(跨端用 $util.rpx,H5 转 px)
|
||
const starFontSize = this.$util.rpx(itemValue.label_font_size || this.config.labelFontSize || 28);
|
||
const starStyle = {
|
||
color: this.config.requiredColor || '#ef4444',
|
||
'font-size': starFontSize,
|
||
'font-weight': 'bold'
|
||
};
|
||
if (this.requiredPosition === 'after') {
|
||
starStyle['margin-left'] = this.$util.rpx(6);
|
||
} else {
|
||
starStyle['margin-right'] = this.$util.rpx(6);
|
||
}
|
||
this.$set(item, '_starStyle', starStyle);
|
||
|
||
// 选项排列样式:默认 vertical(每项一行),仅当后端显式配置 wrap 时横向换行
|
||
// 走 inline style(不依赖 dev 编译产物的 SCSS 类,跨端兼容更稳)
|
||
// 直接写 flex-direction 等 CSS 属性,inline style 优先级最高
|
||
const optLayout = itemValue.option_layout;
|
||
const optStyle = {};
|
||
if (optLayout === 'wrap') {
|
||
// wrap 模式:横向自动换行
|
||
optStyle['flex-direction'] = 'row';
|
||
optStyle['align-items'] = 'center';
|
||
optStyle['gap'] = '12rpx';
|
||
} else {
|
||
// vertical 模式(默认):每项独立一行
|
||
optStyle['flex-direction'] = 'column';
|
||
optStyle['align-items'] = 'flex-start';
|
||
optStyle['gap'] = '20rpx';
|
||
}
|
||
this.$set(item, '_optionLayoutStyle', optStyle);
|
||
|
||
// 信息卡片:容器底色 + 空白判定 + 分割线样式(与后台 design.js 的
|
||
// getInfoCardContainerStyle / getInfoCardDividerStyle 对齐;数值单位为 px 直出,
|
||
// 与编辑器输入单位一致,375 宽屏上与后台 PC 预览视觉一致)
|
||
if (item.controller == 'InfoCard') {
|
||
// 容器:bg_color 配置且非 transparent 时输出底色 + 圆角 + 内边距
|
||
const wrapStyle = {};
|
||
const bg = itemValue.bg_color;
|
||
if (bg && bg !== 'transparent') {
|
||
wrapStyle['background-color'] = bg;
|
||
wrapStyle['border-radius'] = '6px';
|
||
wrapStyle['padding'] = '8px 10px';
|
||
}
|
||
// 字段级间距(InfoCard 行容器即卡片本身,无全局行内边距):margin 直出;
|
||
// padding 覆盖背景卡默认内边距的对应边(无 bg 时直接生效)
|
||
Object.keys(fieldMargin).forEach((k) => { wrapStyle[k] = fieldMargin[k]; });
|
||
if (pTop !== null) wrapStyle['padding-top'] = clampRpx(pTop, 0, 400);
|
||
if (pBottom !== null) wrapStyle['padding-bottom'] = clampRpx(pBottom, 0, 400);
|
||
if (pLeft !== null) wrapStyle['padding-left'] = clampRpx(pLeft, 0, 400);
|
||
if (pRight !== null) wrapStyle['padding-right'] = clampRpx(pRight, 0, 400);
|
||
this.$set(item, '_infoCardWrapStyle', wrapStyle);
|
||
// 空白判定:去标签 + + 去空白后为空 → 不渲染富文本(与后台一致,避免空白块)
|
||
const raw = itemValue.content == null ? '' : String(itemValue.content);
|
||
const textOnly = raw.replace(/<[^>]+>/g, '').replace(/ /gi, ' ').replace(/\s/g, '');
|
||
this.$set(item, '_infoCardHasContent', !!textOnly);
|
||
// 分割线:border-top 渲染支持 solid/dashed/dotted/double;
|
||
// 宽度 calc(w% - 2*px),w<100 且 px=0 时 margin auto 居中(与后台同规则)
|
||
if (itemValue.show_divider == 1) {
|
||
const h = parseInt(itemValue.divider_height);
|
||
const mt = parseInt(itemValue.divider_margin_top);
|
||
let w = parseInt(itemValue.divider_width);
|
||
let px = parseInt(itemValue.divider_padding_x);
|
||
if (!Number.isFinite(w) || w <= 0 || w > 100) w = 100;
|
||
if (!Number.isFinite(px) || px < 0) px = 0;
|
||
const dStyle = {
|
||
height: '0',
|
||
'border-top': (Number.isFinite(h) && h > 0 ? h : 1) + 'px ' + (itemValue.divider_style || 'solid') + ' ' + (itemValue.divider_color || '#f0f0f0'),
|
||
'margin-top': (Number.isFinite(mt) && mt >= 0 ? mt : 8) + 'px',
|
||
'margin-left': px + 'px',
|
||
'margin-right': px + 'px',
|
||
width: 'calc(' + w + '% - ' + (px * 2) + 'px)'
|
||
};
|
||
if (w < 100 && px === 0) {
|
||
dStyle['margin-left'] = 'auto';
|
||
dStyle['margin-right'] = 'auto';
|
||
}
|
||
this.$set(item, '_infoCardDividerStyle', dStyle);
|
||
} else {
|
||
this.$set(item, '_infoCardDividerStyle', {});
|
||
}
|
||
}
|
||
});
|
||
},
|
||
// 容器样式(提供给外部 diy-form 使用)
|
||
getFormWrapStyle() {
|
||
return this.formWrapStyle;
|
||
},
|
||
calculateMaxLabelWidth() {
|
||
if (this.mode === 'vertical' || !this.formData || this.formData.length === 0) {
|
||
this.maxLabelWidth = 0;
|
||
return;
|
||
}
|
||
// 与后端 design.js calculateMaxLabelWidth 算法对齐:
|
||
// 取所有 title 中最长字符数(必填 +1),按 fontSize(rpx)近似换算宽度
|
||
// 后端在 H5 中按 1rpx = 0.5px 计算宽度,再加 12 边距
|
||
const fontSize = parseInt(this.config.labelFontSize) || 28;
|
||
let maxChars = 0;
|
||
this.formData.forEach((item) => {
|
||
if (!item.value || !item.value.title) return;
|
||
let len = (item.value.title || '').length;
|
||
if (item.value.required) len += 1;
|
||
if (len > maxChars) maxChars = len;
|
||
});
|
||
// 与后端公式一致:chars * (fontSize/2) * 1.1 + 12,单位 px → 转 rpx(×2)
|
||
let widthPx = Math.max(40, maxChars * (fontSize / 2) * 1.1 + 12);
|
||
let widthRpx = Math.round(widthPx * 2);
|
||
// auto 模式下加宽度上限:超长 title 撑爆布局时,按 labelAutoMaxWidth 截断(rpx)
|
||
// 上限内 ellipsis 截断,保证控件区有足够空间;上限外也不会让 label 占满整行
|
||
const maxWidthRpx = parseInt(this.config.labelAutoMaxWidth) || 200;
|
||
if (widthRpx > maxWidthRpx) widthRpx = maxWidthRpx;
|
||
this.maxLabelWidth = widthRpx;
|
||
// 宽度变了,需要把全局 nameWidthStyle 重新合到每项 _labelStyle 里
|
||
this.recomputeItemStyles();
|
||
},
|
||
|
||
initFormData() {
|
||
// 深拷贝 prop 数据到本地状态
|
||
this.formData = JSON.parse(JSON.stringify(this.data || []));
|
||
this.processFormData();
|
||
},
|
||
|
||
setFormData() {
|
||
// 重置为初始值
|
||
this.initFormData();
|
||
},
|
||
|
||
processFormData() {
|
||
this.formData.forEach((item) => {
|
||
// Text / Textarea:当 placeholder 为空字符串或没有值时,不设置默认值
|
||
const skipDefault = (item.controller == 'Text' || item.controller == 'Textarea') && !item.value.placeholder;
|
||
if (!item.val && !skipDefault) {
|
||
item.val = item.value.default ? item.value.default : '';
|
||
}
|
||
|
||
if (item.value.options) {
|
||
item.option_lists = [];
|
||
item.value.options.forEach((option, index) => {
|
||
let optionItem = {};
|
||
optionItem.value = option;
|
||
optionItem.checked = false;
|
||
|
||
if (item.controller == 'Radio') {
|
||
if ((!item.val && index == 0) || (item.val && item.val == option)) {
|
||
optionItem.checked = true;
|
||
item.val = option;
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'Checkbox' && item.val) {
|
||
let valArray = item.val.split(',');
|
||
optionItem.checked = valArray.indexOf(option) != -1;
|
||
}
|
||
|
||
item.option_lists.push(optionItem);
|
||
});
|
||
}
|
||
|
||
if (item.controller == 'Img') {
|
||
item.img_lists = item.val ? item.val.split(',') : [];
|
||
}
|
||
|
||
if (item.controller == 'Date' && !item.val) {
|
||
if (item.value.is_show_default) {
|
||
if (item.value.is_current) {
|
||
item.val = this.getDate();
|
||
} else {
|
||
item.val = item.value.default;
|
||
}
|
||
} else {
|
||
item.val = '';
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'Datelimit') {
|
||
if (item.val) {
|
||
let dateArray = item.val.split(' - ');
|
||
item.start_date = dateArray[0];
|
||
item.end_date = dateArray[1];
|
||
} else {
|
||
item.val = '';
|
||
|
||
if (item.value.is_show_default_start) {
|
||
if (item.value.is_current_start) {
|
||
item.start_date = this.getDate();
|
||
} else {
|
||
item.start_date = item.value.default_start;
|
||
}
|
||
} else {
|
||
item.start_date = '';
|
||
}
|
||
|
||
if (item.value.is_show_default_end) {
|
||
if (item.value.is_current_end) {
|
||
item.end_date = this.getDate();
|
||
} else {
|
||
item.end_date = item.value.default_end;
|
||
}
|
||
}
|
||
|
||
if (item.start_date && item.end_date) {
|
||
item.val = item.start_date + ' - ' + item.end_date;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'Time' && !item.val) {
|
||
if (item.value.is_show_default) {
|
||
if (item.value.is_current) {
|
||
item.val = this.getTime();
|
||
} else {
|
||
item.val = item.value.default;
|
||
}
|
||
} else {
|
||
item.val = '';
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'Timelimit') {
|
||
if (item.val) {
|
||
let timeArray = item.val.split(' - ');
|
||
item.start_time = timeArray[0];
|
||
item.end_time = timeArray[1];
|
||
} else {
|
||
item.val = '';
|
||
|
||
if (item.value.is_show_default_start) {
|
||
if (item.value.is_current_start) {
|
||
item.start_time = this.getTime();
|
||
} else {
|
||
item.start_time = item.value.default_start;
|
||
}
|
||
} else {
|
||
item.start_time = '';
|
||
}
|
||
|
||
if (item.value.is_show_default_end) {
|
||
if (item.value.is_current_end) {
|
||
item.end_time = this.getTime();
|
||
} else {
|
||
item.end_time = item.value.default_end;
|
||
}
|
||
}
|
||
|
||
if (item.start_time && item.end_time) {
|
||
item.val = item.start_time + ' - ' + item.end_time;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'City') {
|
||
item.full_address = '';
|
||
item.select_arr = item.value.default_type == 1 ? '2' : '3';
|
||
if (item.val) {
|
||
item.default_regions = item.val.split('-');
|
||
} else {
|
||
item.default_regions = [];
|
||
}
|
||
}
|
||
});
|
||
// 数据处理完后再算每项的 _labelStyle / _boxAlignStyle(依赖 nameWidthStyle,而 maxLabelWidth 此时可能还没算)
|
||
// 这里先算一次;calculateMaxLabelWidth 完成后会再算一次(带正确的 maxLabelWidth)
|
||
this.recomputeItemStyles();
|
||
},
|
||
|
||
verify() {
|
||
for (let i = 0; i < this.formData.length; i++) {
|
||
let item = this.formData[i];
|
||
|
||
// 信息卡片:不参与必填/校验(设计如此:仅展示活动/事件描述)
|
||
if (item.controller == 'InfoCard') {
|
||
continue;
|
||
}
|
||
|
||
if (item.controller == 'Text') {
|
||
if (item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseInput', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.name == 'ID_CARD' && !/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(item.val)) {
|
||
if (!item.value.required) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.idCardInvalid') });
|
||
return false;
|
||
}
|
||
if (item.val != '') {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.idCardInvalid') });
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (item.name == 'MOBILE' && !this.$util.verifyMobile(item.val)) {
|
||
if (!item.value.required) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.mobileInvalid') });
|
||
return false;
|
||
}
|
||
if (item.val != '') {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.mobileInvalid') });
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'Textarea' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseInput', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.controller == 'Select' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelect', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.controller == 'Checkbox' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelectAtLeastOne', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.controller == 'Img' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseUploadAtLeastOne', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.controller == 'Date' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelect', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.controller == 'Datelimit') {
|
||
if (item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelect', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (this.$util.timeTurnTimeStamp(item.start_date) > this.$util.timeTurnTimeStamp(item.end_date)) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.endDateBeforeStartDate') });
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'Time' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelect', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.controller == 'Timelimit') {
|
||
if (item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelect', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
|
||
if (item.start_time >= item.end_time) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.endTimeBeforeStartTime') });
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (item.controller == 'City' && item.value.required && !item.val) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.pleaseSelect', { title: item.value.title }) });
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return this.formData;
|
||
},
|
||
|
||
pickerChange(event, index) {
|
||
this.formData[index].val = this.data[index].value.options[event.detail.value];
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
checkboxChange(event, index) {
|
||
this.formData[index].val = event.detail.value.toString();
|
||
this.formData[index].option_lists.forEach((option) => {
|
||
option.checked = event.detail.value.indexOf(option.value) != -1;
|
||
});
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
radioChange(event, index) {
|
||
this.formData[index].val = event.detail.value;
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
uploadImg(index) {
|
||
let self = this;
|
||
this.$util.upload(Number(this.formData[index].value.max_count), { path: 'evaluateimg' }, function(res) {
|
||
if (res.length > 0) {
|
||
res.forEach(function(img) {
|
||
if (self.formData[index].img_lists.length >= Number(self.formData[index].value.max_count)) {
|
||
self.$util.showToast({ title: self.$lang('components.ns-newform.maxUploadImages', { max: self.formData[index].value.max_count }) });
|
||
return false;
|
||
}
|
||
self.formData[index].img_lists.push(img);
|
||
});
|
||
self.formData[index].val = self.formData[index].img_lists.toString();
|
||
self.$forceUpdate();
|
||
}
|
||
});
|
||
},
|
||
|
||
addImg(index) {
|
||
let self = this;
|
||
if (this.formData[index].img_lists.length >= Number(this.formData[index].value.max_count)) {
|
||
this.$util.showToast({ title: this.$lang('components.ns-newform.maxUploadImages', { max: this.formData[index].value.max_count }) });
|
||
return false;
|
||
}
|
||
|
||
this.$util.upload(Number(this.formData[index].value.max_count), { path: 'evaluateimg' }, function(res) {
|
||
if (res.length > 0) {
|
||
res.forEach(function(img) {
|
||
self.formData[index].img_lists.push(img);
|
||
});
|
||
self.formData[index].val = self.formData[index].img_lists.toString();
|
||
self.$forceUpdate();
|
||
}
|
||
});
|
||
},
|
||
|
||
delImg(imgIndex, formIndex) {
|
||
this.formData[formIndex].img_lists.splice(imgIndex, 1);
|
||
this.formData[formIndex].val = this.formData[formIndex].img_lists.toString();
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
getDate() {
|
||
let date = new Date();
|
||
let year = date.getFullYear();
|
||
let month = date.getMonth() + 1;
|
||
let day = date.getDate();
|
||
month = month > 9 ? month : '0' + month;
|
||
day = day > 9 ? day : '0' + day;
|
||
return `${year}-${month}-${day}`;
|
||
},
|
||
|
||
getTime() {
|
||
let date = new Date();
|
||
let hours = date.getHours();
|
||
let minutes = date.getMinutes();
|
||
hours = hours > 9 ? hours : '0' + hours;
|
||
minutes = minutes > 9 ? minutes : '0' + minutes;
|
||
return `${hours}:${minutes}`;
|
||
},
|
||
|
||
bindDateChange(event, index) {
|
||
this.formData[index].val = event.detail.value;
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
bindStartDateChange(event, index) {
|
||
this.$set(this.formData[index], 'start_date', event.detail.value);
|
||
this.$set(this.formData[index], 'val', this.formData[index].start_date + ' - ' + this.formData[index].end_date);
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
bindEndDateChange(event, index) {
|
||
this.$set(this.formData[index], 'end_date', event.detail.value);
|
||
this.$set(this.formData[index], 'val', this.formData[index].start_date + ' - ' + this.formData[index].end_date);
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
bindTimeChange(event, index) {
|
||
this.formData[index].val = event.detail.value;
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
bindStartTimeChange(event, index) {
|
||
this.formData[index].start_time = event.detail.value;
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
bindEndTimeChange(event, index) {
|
||
this.formData[index].end_time = event.detail.value;
|
||
this.formData[index].val = this.formData[index].start_time + ' - ' + this.formData[index].end_time;
|
||
this.$forceUpdate();
|
||
},
|
||
|
||
handleGetRegions(regions, index) {
|
||
this.formData[index].val = '';
|
||
this.formData[index].val += regions[0] != undefined ? regions[0].label : '';
|
||
this.formData[index].val += regions[1] != undefined ? '-' + regions[1].label : '';
|
||
this.formData[index].val += regions[2] != undefined ? '-' + regions[2].label : '';
|
||
this.$forceUpdate();
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 与后端 D:\projects\shop-projects\backend\src\app\component\view\form\css\design.css 对齐:
|
||
在 .form-wrap 上通过内联 style 写入 --form-* / --label-* 等 CSS 变量,
|
||
子元素统一读取。 */
|
||
.form-wrap {
|
||
background: #fff;
|
||
/* 默认值兜底:与 ns-newform.config 默认值一致 */
|
||
--form-padding-x: 28rpx;
|
||
--banner-padding-x: 0rpx;
|
||
--tip-padding-x: 28rpx;
|
||
--tip-font-size: 32rpx;
|
||
--tip-color: #666666;
|
||
--tip-bg-color: #ffffff;
|
||
--row-padding-y: 24rpx;
|
||
--row-min-height: 60rpx;
|
||
--label-gap: 20rpx;
|
||
--form-bg-color: #ffffff;
|
||
--row-divider-color: #f0f0f0;
|
||
--input-height: 84rpx;
|
||
--input-radius: 16rpx;
|
||
--label-fixed-width: 180rpx;
|
||
--label-font-size: 32rpx;
|
||
--label-color: #1f2937;
|
||
--required-color: #ef4444;
|
||
--checked-color: #3b82f6;
|
||
--submit-color: #ffffff;
|
||
--submit-bg-color: #3b82f6;
|
||
--placeholder-color: #9ca3af;
|
||
--placeholder-font-size: 28rpx;
|
||
// textarea 行数 / 行高(运行时被 formWrapStyle 注入的 inline 覆盖;这里是兜底)
|
||
--textarea-rows: 4;
|
||
--textarea-line-height: 48rpx;
|
||
// 输入框视觉增强(边框 / 背景 / 聚焦 / label 字重)
|
||
--input-border-color: #e5e7eb;
|
||
--input-bg-color: #ffffff;
|
||
--input-focus-border-color: #3b82f6;
|
||
--input-focus-bg-color: #ffffff;
|
||
--label-font-weight: 700;
|
||
--show-row-divider: 0;
|
||
|
||
// 与后端 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%;
|
||
min-height: auto;
|
||
margin-bottom: var(--label-gap, 20rpx);
|
||
margin-right: 0;
|
||
}
|
||
|
||
.box {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
&.textarea {
|
||
align-items: flex-start;
|
||
|
||
.name {
|
||
padding-top: 8rpx;
|
||
height: auto;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
&.clear-flex {
|
||
display: block;
|
||
|
||
.box {
|
||
margin-top: 12rpx;
|
||
text-align: left;
|
||
}
|
||
}
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
&.align-top {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
text {
|
||
font-size: var(--label-font-size, 28rpx);
|
||
}
|
||
|
||
.name {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: var(--input-height, 60rpx);
|
||
line-height: 1.5;
|
||
|
||
.tit {
|
||
text-align: left;
|
||
font-size: var(--item-label-font-size, var(--label-font-size, 28rpx));
|
||
color: var(--item-label-color, var(--label-color, #1f2937));
|
||
font-weight: var(--item-label-font-weight, var(--label-font-weight, 600));
|
||
letter-spacing: 0.2rpx;
|
||
|
||
text {
|
||
font-size: var(--item-label-font-size, var(--label-font-size, 28rpx));
|
||
}
|
||
}
|
||
|
||
/* 必填星号:动态值(颜色/字号/间距)由 item._starStyle 内联提供;
|
||
类内只留静态兜底(recompute 前的首次渲染),不依赖 var() 嵌套 fallback(MP/鸿蒙支持不全) */
|
||
.required {
|
||
color: #ef4444;
|
||
font-size: 28rpx;
|
||
margin-right: 6rpx;
|
||
font-weight: bold;
|
||
display: inline-block;
|
||
vertical-align: middle;
|
||
}
|
||
}
|
||
|
||
.box {
|
||
flex: 1;
|
||
padding: 0;
|
||
line-height: inherit;
|
||
min-width: 0; // 防止 flex 子元素内容溢出
|
||
|
||
// 对齐方式
|
||
&.align-left {
|
||
text-align: left;
|
||
}
|
||
|
||
&.align-right {
|
||
text-align: right;
|
||
}
|
||
|
||
&.align-center {
|
||
text-align: center;
|
||
}
|
||
|
||
// 让 box 在 City / Select / Date / Time 等"picker + 右侧箭头"场景下成为
|
||
// 独立的 flex 行容器:picker 占满剩余宽度,箭头自然贴右
|
||
// (与 .order-cell.vertical-layout 不冲突 —— 在垂直布局下 .order-cell 是
|
||
// column,.box.box-flex 自身仍是 row,箭头与 picker 仍同行)
|
||
&.box-flex {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
picker,
|
||
.pick-regions {
|
||
min-width: 0;
|
||
width: auto; // 覆盖下方 .box picker,.pick-regions 的 width: 100%
|
||
}
|
||
|
||
.iconfont.icon-right {
|
||
flex-shrink: 0;
|
||
margin-left: 8rpx;
|
||
line-height: 1;
|
||
}
|
||
}
|
||
|
||
// 让原生 picker 与自定义 pick-regions 撑满父容器,
|
||
// 使 .box.align-right / align-center 的 text-align 能正确传递到内部
|
||
// .uni-input / .select-address(inline-block),与后端 .more 的右对齐效果一致
|
||
picker,
|
||
.pick-regions {
|
||
display: block;
|
||
width: 100%;
|
||
}
|
||
|
||
// H5 端 <input> 被 uni-app 编译为 <uni-input> 自定义组件,
|
||
// 真实 <input> 是其子节点,需要 ::v-deep 穿透 scoped 才能命中
|
||
::v-deep input {
|
||
font-size: var(--label-font-size, 28rpx);
|
||
text-align: left;
|
||
height: var(--input-height, 84rpx);
|
||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||
border-radius: var(--input-radius, 16rpx);
|
||
line-height: calc(var(--input-height, 84rpx) - 2rpx);
|
||
padding: 0 24rpx;
|
||
background: var(--input-bg-color, #ffffff);
|
||
color: var(--label-color, #1f2937);
|
||
transition: all 0.3s ease;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
|
||
&:focus {
|
||
background: var(--input-focus-bg-color, #ffffff);
|
||
border-color: var(--input-focus-border-color, #3b82f6);
|
||
}
|
||
|
||
&::placeholder {
|
||
color: var(--placeholder-color, #9ca3af);
|
||
font-size: var(--placeholder-font-size, 28rpx);
|
||
}
|
||
}
|
||
|
||
// 非 H5 端(小程序 / 鸿蒙 / App)<input> 是直接原生元素,普通选择器即可
|
||
input {
|
||
font-size: var(--label-font-size, 28rpx);
|
||
text-align: left;
|
||
height: var(--input-height, 84rpx);
|
||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||
border-radius: var(--input-radius, 16rpx);
|
||
line-height: calc(var(--input-height, 84rpx) - 2rpx);
|
||
padding: 0 24rpx;
|
||
background: var(--input-bg-color, #ffffff);
|
||
color: var(--label-color, #1f2937);
|
||
transition: all 0.3s ease;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
|
||
&:focus {
|
||
background: var(--input-focus-bg-color, #ffffff);
|
||
border-color: var(--input-focus-border-color, #3b82f6);
|
||
}
|
||
|
||
&::placeholder {
|
||
color: var(--placeholder-color, #9ca3af);
|
||
font-size: var(--placeholder-font-size, 28rpx);
|
||
}
|
||
}
|
||
|
||
// H5 端 <textarea> 被 uni-app 编译为 <uni-textarea> 自定义组件,
|
||
// 真实 <textarea> 是其子节点,需要 ::v-deep 穿透 scoped 才能命中
|
||
::v-deep textarea {
|
||
font-size: var(--label-font-size, 28rpx);
|
||
width: 100%;
|
||
// 行高 / 高度全部由 formConfig 动态决定(CSS 兜底公式与 JS textareaStyle 一致)
|
||
line-height: var(--textarea-line-height, 48rpx);
|
||
min-height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 48rpx) + 26rpx);
|
||
height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 48rpx) + 26rpx);
|
||
text-align: left;
|
||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||
border-radius: var(--input-radius, 16rpx);
|
||
padding: 12rpx 24rpx;
|
||
background: var(--input-bg-color, #ffffff);
|
||
color: var(--label-color, #1f2937);
|
||
transition: all 0.3s ease;
|
||
box-sizing: border-box;
|
||
|
||
&:focus {
|
||
background: var(--input-focus-bg-color, #ffffff);
|
||
border-color: var(--input-focus-border-color, #3b82f6);
|
||
}
|
||
|
||
&::placeholder {
|
||
color: var(--placeholder-color, #9ca3af);
|
||
font-size: var(--placeholder-font-size, 28rpx);
|
||
}
|
||
}
|
||
|
||
// 非 H5 端(小程序 / 鸿蒙 / App)<textarea> 是直接原生元素,普通选择器即可
|
||
textarea {
|
||
font-size: var(--label-font-size, 28rpx);
|
||
width: 100%;
|
||
// 行高 / 高度全部由 formConfig 动态决定(与 ::v-deep textarea 同步)
|
||
line-height: var(--textarea-line-height, 48rpx);
|
||
min-height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 48rpx) + 26rpx);
|
||
height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 48rpx) + 26rpx);
|
||
text-align: left;
|
||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||
border-radius: var(--input-radius, 16rpx);
|
||
padding: 12rpx 24rpx;
|
||
background: var(--input-bg-color, #ffffff);
|
||
color: var(--label-color, #1f2937);
|
||
transition: all 0.3s ease;
|
||
box-sizing: border-box;
|
||
|
||
&:focus {
|
||
background: var(--input-focus-bg-color, #ffffff);
|
||
border-color: var(--input-focus-border-color, #3b82f6);
|
||
}
|
||
|
||
&::placeholder {
|
||
color: var(--placeholder-color, #9ca3af);
|
||
font-size: var(--placeholder-font-size, 28rpx);
|
||
}
|
||
}
|
||
|
||
.uni-input, .select-address {
|
||
display: inline-block;
|
||
padding: 0;
|
||
border-radius: var(--input-radius, 16rpx);
|
||
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, 84rpx);
|
||
box-sizing: border-box;
|
||
line-height: var(--input-height, 84rpx);
|
||
|
||
&.color-tip {
|
||
color: var(--placeholder-color, #9ca3af);
|
||
}
|
||
|
||
&.empty {
|
||
color: var(--placeholder-color, #9ca3af);
|
||
}
|
||
}
|
||
|
||
checkbox-group {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
width: 100%;
|
||
|
||
// 隐藏 uniapp 默认的 checkbox 样式
|
||
checkbox {
|
||
position: absolute;
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
radio-group {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
width: 100%;
|
||
|
||
// 隐藏 uniapp 默认的 radio 样式
|
||
radio {
|
||
position: absolute;
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
// 选项排列样式:vertical = 每个选项独立一行;wrap = 横向自动换行
|
||
// option-layout-* 是 .box 自身的类,必须用 & 嵌套,否则编译成 .box .option-layout-* 选不中实际 DOM
|
||
&.option-layout-vertical {
|
||
checkbox-group,
|
||
radio-group {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
width: 100%;
|
||
|
||
label {
|
||
width: 100%;
|
||
justify-content: flex-start;
|
||
padding: 12rpx 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
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: 12rpx;
|
||
}
|
||
|
||
.check-icon,
|
||
.radio-icon {
|
||
font-size: 40rpx;
|
||
line-height: 1;
|
||
color: #d1d5db;
|
||
}
|
||
|
||
&.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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 信息卡片(与 addon/form/shop/view/public/css/form.css 的 .form-info-card* 对齐)
|
||
// 展示型组件:富文本内容 + 可选底部分割线;不参与数据校验/存储
|
||
.info-card-wrap {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 4rpx 0 2rpx;
|
||
|
||
// 富文本内容(编辑器产出的 p/strong/em/u/span/a/img/video/hr/iframe)
|
||
.info-card-content {
|
||
display: block;
|
||
font-size: 28rpx;
|
||
color: #303133;
|
||
line-height: 1.7;
|
||
word-break: break-all;
|
||
|
||
::v-deep p { margin: 0; }
|
||
::v-deep p + p { margin-top: 6rpx; }
|
||
::v-deep img { max-width: 100%; }
|
||
::v-deep video { max-width: 100%; display: block; }
|
||
::v-deep hr { border: none; border-top: 1px solid #e0e0e0; margin: 16rpx 0; }
|
||
::v-deep a { color: #1278FE; }
|
||
}
|
||
|
||
// 底部分割线:线样式/颜色/粗细/上边距/宽度/左右边距由内联 _infoCardDividerStyle 控制
|
||
.info-card-divider {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
// 老数据结构(title + items)兜底渲染
|
||
.info-card-title {
|
||
line-height: 1.4;
|
||
// 字号 / 颜色由模板内联 style 控制
|
||
}
|
||
|
||
.info-card-items {
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.info-card-item {
|
||
line-height: 1.6;
|
||
word-break: break-all;
|
||
// 字号 / 颜色由模板内联 style 控制
|
||
|
||
& + .info-card-item {
|
||
margin-top: 4rpx;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style lang="scss" scoped>
|
||
.placeholder {
|
||
color: #9ca3af;
|
||
font-size: 28rpx;
|
||
|
||
// #ifdef H5
|
||
height: 100% !important;
|
||
// #endif
|
||
}
|
||
|
||
.color-tip {
|
||
color: #9ca3af;
|
||
}
|
||
</style> |