refactor(utils&form): 优化设备信息获取与表单组件样式配置
1. 为设备信息相关方法添加可选链操作符,避免空值报错 2. 重构getDeviceInfo和getWindowInfo的异常处理逻辑,简化代码 3. 重构ns-form表单组件,新增全局样式配置支持,兼容多端样式自定义 4. 优化表单布局样式、必填项和选中态颜色,修复鸿蒙元服务TextArea适配问题 5. 新增标签宽度自动计算功能,优化表单排版对齐效果
This commit is contained in:
@@ -1,36 +1,59 @@
|
||||
<template>
|
||||
<view class="form-wrap form-component">
|
||||
<view class="form-wrap form-component" :style="formWrapStyle">
|
||||
<block v-for="(item, index) in formData">
|
||||
<view class="order-wrap" v-if="item.controller == 'Text'">
|
||||
<view class="order-cell">
|
||||
<view class="name">
|
||||
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Text'">
|
||||
<view class="order-cell" :style="rowCellStyle">
|
||||
<view class="name" :style="nameWidthStyle">
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required">{{ item.value.required ? '*' : '' }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box">
|
||||
<input type="text" :placeholder="item.value.placeholder" placeholder-class="placeholder color-tip" v-model="item.val" />
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="item.value.placeholder"
|
||||
placeholder-class="placeholder color-tip"
|
||||
v-model="item.val"
|
||||
:style="inputStyle"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="order-wrap" v-if="item.controller == 'Textarea'">
|
||||
<view class="order-cell flex-box textarea">
|
||||
<view class="name">
|
||||
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Textarea'">
|
||||
<view class="order-cell flex-box textarea" :style="rowCellStyle">
|
||||
<view class="name" :style="nameWidthStyle">
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required">{{ item.value.required ? '*' : '' }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box">
|
||||
<textarea :placeholder="item.value.placeholder" placeholder-class="placeholder color-tip" v-model="item.val"></textarea>
|
||||
<!-- #ifdef MP-HARMONY -->
|
||||
<!-- 鸿蒙元服务渲染为原生 ArkUI TextArea,必须用 :rows 控制初始显示行数;CSS height 在原生组件上不生效 -->
|
||||
<textarea
|
||||
:rows="item.value.rows || 2"
|
||||
:placeholder="item.value.placeholder"
|
||||
placeholder-class="placeholder color-tip"
|
||||
v-model="item.val"
|
||||
:style="inputStyle"
|
||||
></textarea>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-HARMONY -->
|
||||
<textarea
|
||||
:placeholder="item.value.placeholder"
|
||||
placeholder-class="placeholder color-tip"
|
||||
v-model="item.val"
|
||||
:style="inputStyle"
|
||||
></textarea>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<picker mode="selector" :range="item.value.options" v-if="item.controller == 'Select'" @change="pickerChange($event, index)">
|
||||
<view class="order-wrap">
|
||||
<view class="order-cell">
|
||||
<view class="name">
|
||||
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle">
|
||||
<view class="order-cell" :style="rowCellStyle">
|
||||
<view class="name" :style="nameWidthStyle">
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required">{{ item.value.required ? '*' : '' }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box">
|
||||
<text v-if="item.val != ''">{{ item.val }}</text>
|
||||
@@ -41,18 +64,18 @@
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<view class="order-wrap" v-if="item.controller == 'Checkbox'">
|
||||
<view class="order-cell">
|
||||
<view class="name">
|
||||
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Checkbox'">
|
||||
<view class="order-cell" :style="rowCellStyle">
|
||||
<view class="name" :style="nameWidthStyle">
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required">{{ item.value.required ? '*' : '' }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box check-group-box">
|
||||
<checkbox-group @change="checkboxChange($event, index)">
|
||||
<label v-for="(v, k) in item.option_lists" :key="k">
|
||||
<checkbox :value="v.value" :checked="v.checked" />
|
||||
<view class="checkbox">
|
||||
<text class="iconfont" :class="{ 'icon-fuxuankuang2': !v.checked, 'icon-fuxuankuang1 color-base-text': v.checked }"></text>
|
||||
<text class="iconfont" :class="{ 'icon-fuxuankuang2': !v.checked, 'icon-fuxuankuang1 color-base-text': v.checked }" :style="{ color: v.checked ? checkedColor : '' }"></text>
|
||||
{{ v.value }}
|
||||
</view>
|
||||
</label>
|
||||
@@ -61,18 +84,18 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="order-wrap" v-if="item.controller == 'Radio'">
|
||||
<view class="order-cell">
|
||||
<view class="name">
|
||||
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Radio'">
|
||||
<view class="order-cell" :style="rowCellStyle">
|
||||
<view class="name" :style="nameWidthStyle">
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required">{{ item.value.required ? '*' : '' }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box radio-group-box">
|
||||
<radio-group @change="radioChange($event, index)">
|
||||
<label v-for="(v, k) in item.option_lists" :key="k">
|
||||
<radio :value="v.value" :checked="item.val == v.value" />
|
||||
<view class="radio-box">
|
||||
<text class="iconfont" :class="{ 'icon-yuan_checkbox': item.val != v.value, 'icon-yuan_checked color-base-text': item.val == v.value }"></text>
|
||||
<text class="iconfont" :class="{ 'icon-yuan_checkbox': item.val != v.value, 'icon-yuan_checked color-base-text': item.val == v.value }" :style="{ color: item.val == v.value ? checkedColor : '' }"></text>
|
||||
{{ v.value }}
|
||||
</view>
|
||||
</label>
|
||||
@@ -81,11 +104,11 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="order-wrap" v-if="item.controller == 'Img'">
|
||||
<view class="order-cell flex-box">
|
||||
<view class="name">
|
||||
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Img'">
|
||||
<view class="order-cell flex-box" :style="rowCellStyle">
|
||||
<view class="name" :style="nameWidthStyle">
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required">{{ item.value.required ? '*' : '' }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box img-boxs">
|
||||
<view class="img-box" v-for="(v, k) in item.img_lists" :key="k" @click="uploadImg(index)">
|
||||
@@ -97,11 +120,11 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="order-wrap" v-if="item.controller == 'Date'">
|
||||
<view class="order-cell">
|
||||
<view class="name">
|
||||
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Date'">
|
||||
<view class="order-cell" :style="rowCellStyle">
|
||||
<view class="name" :style="nameWidthStyle">
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required">{{ item.value.required ? '*' : '' }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box box-flex">
|
||||
<picker mode="date" :value="item.val" @change="bindDateChange($event, index)">
|
||||
@@ -112,11 +135,11 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="order-wrap" v-if="item.controller == 'Datelimit'">
|
||||
<view class="order-cell flex-box">
|
||||
<view class="name">
|
||||
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Datelimit'">
|
||||
<view class="order-cell flex-box" :style="rowCellStyle">
|
||||
<view class="name" :style="nameWidthStyle">
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required">{{ item.value.required ? '*' : '' }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box date-boxs">
|
||||
<view class="date-box">
|
||||
@@ -138,11 +161,11 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="order-wrap" v-if="item.controller == 'Time'">
|
||||
<view class="order-cell">
|
||||
<view class="name">
|
||||
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Time'">
|
||||
<view class="order-cell" :style="rowCellStyle">
|
||||
<view class="name" :style="nameWidthStyle">
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required">{{ item.value.required ? '*' : '' }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box box-flex">
|
||||
<picker mode="time" :value="item.val" @change="bindTimeChange($event, index)">
|
||||
@@ -153,11 +176,11 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="order-wrap" v-if="item.controller == 'Timelimit'">
|
||||
<view class="order-cell flex-box">
|
||||
<view class="name">
|
||||
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Timelimit'">
|
||||
<view class="order-cell flex-box" :style="rowCellStyle">
|
||||
<view class="name" :style="nameWidthStyle">
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required">{{ item.value.required ? '*' : '' }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box date-boxs">
|
||||
<view class="date-box">
|
||||
@@ -179,11 +202,11 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="order-wrap" v-if="item.controller == 'City'">
|
||||
<view class="order-cell box-flex">
|
||||
<view class="name">
|
||||
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'City'">
|
||||
<view class="order-cell box-flex" :style="rowCellStyle">
|
||||
<view class="name" :style="nameWidthStyle">
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required">{{ item.value.required ? '*' : '' }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box">
|
||||
<pick-regions :default-regions="item.default_regions" :select-arr="item.select_arr" @getRegions="handleGetRegions($event, index)">
|
||||
@@ -205,32 +228,262 @@
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: {}
|
||||
default: () => []
|
||||
},
|
||||
customAttr: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 表单样式配置(由后端 designer 通过 API 传入)。
|
||||
* 字段名同时支持 camelCase(如 formPaddingX / inputHeight / labelColor)
|
||||
* 与 snake_case(如 form_padding_x / input_height / label_color),camelCase 优先。
|
||||
* 不传则使用组件内置默认(与重构前硬编码值一致,无视觉回归)。
|
||||
*
|
||||
* 支持的键:
|
||||
* formPaddingX, formBgColor, rowPaddingY, rowMinHeight, labelGap,
|
||||
* inputHeight, inputRadius, labelWidthMode ('auto'|'fixed'), labelFixedWidth,
|
||||
* labelFontSize, labelColor, requiredColor, checkedColor,
|
||||
* placeholderColor, showRowDivider, rowDividerColor
|
||||
*
|
||||
* 平台差异化:formConfig.terminal.{h5|weapp|harmony|app|quickapp} 只放差异字段
|
||||
*/
|
||||
formConfig: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: this.data
|
||||
formData: [],
|
||||
maxLabelWidth: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 当前运行平台标识(与后端 formConfig.terminal 键名对齐)
|
||||
platform() {
|
||||
// #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 MP-HARMONY
|
||||
return 'harmony';
|
||||
// #endif
|
||||
// #ifdef QUICKAPP-WEBVIEW
|
||||
return 'quickapp';
|
||||
// #endif
|
||||
return 'h5';
|
||||
},
|
||||
// 后端键名 → 内部 platform 名 兼容层
|
||||
platformAliases() {
|
||||
return {
|
||||
'h5': 'h5',
|
||||
'weapp': 'weapp',
|
||||
'mp-weixin': 'weapp',
|
||||
'wechat': 'weapp',
|
||||
'harmony': 'harmony',
|
||||
'app': 'app',
|
||||
'app-plus': 'app'
|
||||
};
|
||||
},
|
||||
// 当前平台的有效配置键集合
|
||||
platformConfigKeys() {
|
||||
const cur = this.platform;
|
||||
const aliases = this.platformAliases;
|
||||
const keys = [];
|
||||
Object.keys(aliases).forEach(k => {
|
||||
if (aliases[k] === cur) keys.push(k);
|
||||
});
|
||||
if (keys.indexOf(cur) === -1) keys.push(cur);
|
||||
return keys;
|
||||
},
|
||||
// 合并配置:内置默认 < 全局 formConfig < 当前平台 formConfig.terminal[platform]
|
||||
// 同时兼容 snake_case 与 camelCase 字段名
|
||||
config() {
|
||||
// 1) 内置默认值(最低优先级,与重构前的硬编码值一一对应,确保未传 formConfig 时零回归)
|
||||
const defaults = {
|
||||
formPaddingX: 0,
|
||||
formBgColor: '#ffffff',
|
||||
rowPaddingY: 20,
|
||||
rowMinHeight: 60,
|
||||
labelGap: 16,
|
||||
inputHeight: 60,
|
||||
inputRadius: 0,
|
||||
labelWidthMode: 'fixed', // 旧版 width: 80vw 用 fixed 180rpx 等价替代
|
||||
labelFixedWidth: 180,
|
||||
labelFontSize: 28,
|
||||
labelColor: '#1f2937',
|
||||
requiredColor: '#ef4444',
|
||||
checkedColor: '#ff4544',
|
||||
placeholderColor: '#909399',
|
||||
showRowDivider: true,
|
||||
rowDividerColor: '#f0f0f0'
|
||||
};
|
||||
const fc = this.formConfig || {};
|
||||
|
||||
// 同时读 camelCase 与 snake_case,camelCase 优先
|
||||
const pick = (camel, snake) => {
|
||||
const v1 = fc[camel];
|
||||
if (v1 !== undefined && v1 !== null && v1 !== '') return v1;
|
||||
return fc[snake];
|
||||
};
|
||||
const base = {
|
||||
formPaddingX: pick('formPaddingX', 'form_padding_x'),
|
||||
formBgColor: pick('formBgColor', 'form_bg_color'),
|
||||
rowPaddingY: pick('rowPaddingY', 'row_padding_y'),
|
||||
rowMinHeight: pick('rowMinHeight', 'row_min_height'),
|
||||
labelGap: pick('labelGap', 'label_gap'),
|
||||
inputHeight: pick('inputHeight', 'input_height'),
|
||||
inputRadius: pick('inputRadius', 'input_radius'),
|
||||
labelWidthMode: pick('labelWidthMode', 'label_width_mode'),
|
||||
labelFixedWidth: pick('labelFixedWidth', 'label_fixed_width'),
|
||||
labelFontSize: pick('labelFontSize', 'label_font_size'),
|
||||
labelColor: pick('labelColor', 'label_color'),
|
||||
requiredColor: pick('requiredColor', 'required_color'),
|
||||
checkedColor: pick('checkedColor', 'checked_color'),
|
||||
placeholderColor: pick('placeholderColor', 'placeholder_color'),
|
||||
showRowDivider: pick('showRowDivider', 'show_row_divider'),
|
||||
rowDividerColor: pick('rowDividerColor', 'row_divider_color')
|
||||
};
|
||||
// 移除 undefined / 空字符串(保留 0 / false)
|
||||
Object.keys(base).forEach(k => {
|
||||
if (base[k] === undefined || base[k] === null || base[k] === '') delete base[k];
|
||||
});
|
||||
// showRowDivider 在前后端可能传数字 0/1,统一转 boolean
|
||||
if ('showRowDivider' in base) {
|
||||
base.showRowDivider = base.showRowDivider === true || base.showRowDivider === 1 || base.showRowDivider === '1';
|
||||
}
|
||||
|
||||
// 2) 当前平台差异化配置(最高优先级),从 formConfig.terminal 取出当前平台 + 所有别名键,合并
|
||||
const termBlock = fc.terminal || {};
|
||||
const platformKeys = this.platformConfigKeys || [];
|
||||
const platformCfg = Object.assign(
|
||||
{},
|
||||
...platformKeys.map(k => termBlock[k] || fc[k] || {})
|
||||
);
|
||||
// 平台块可能也是 snake_case,统一转 camelCase
|
||||
const platformCfgCamel = {};
|
||||
if (this.$util && typeof this.$util.snakeToCamel === 'function') {
|
||||
Object.keys(platformCfg).forEach(k => {
|
||||
platformCfgCamel[this.$util.snakeToCamel(k)] = platformCfg[k];
|
||||
});
|
||||
} else {
|
||||
Object.assign(platformCfgCamel, platformCfg);
|
||||
}
|
||||
|
||||
// 3) 三层合并:defaults < base < platformCfgCamel
|
||||
return Object.assign({}, defaults, base, platformCfgCamel);
|
||||
},
|
||||
// 容器样式:内边距 + 背景 + 16 个 CSS 自定义属性
|
||||
formWrapStyle() {
|
||||
const cfg = this.config;
|
||||
const num = (v, fb) => (typeof v === 'number' ? v : parseInt(v) || fb);
|
||||
return this.$util.objToStyle({
|
||||
padding: `0 ${cfg.formPaddingX}rpx`,
|
||||
background: cfg.formBgColor,
|
||||
'--form-padding-x': `${cfg.formPaddingX}rpx`,
|
||||
'--form-bg-color': cfg.formBgColor,
|
||||
'--row-padding-y': `${cfg.rowPaddingY}rpx`,
|
||||
'--row-min-height': `${cfg.rowMinHeight}rpx`,
|
||||
'--label-gap': `${cfg.labelGap}rpx`,
|
||||
'--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,
|
||||
'--placeholder-color': cfg.placeholderColor,
|
||||
'--row-divider-color': cfg.rowDividerColor,
|
||||
'--show-row-divider': cfg.showRowDivider === false ? '0' : '1'
|
||||
});
|
||||
},
|
||||
rowWrapStyle() {
|
||||
return this.$util.objToStyle({
|
||||
padding: `${this.config.rowPaddingY}rpx 0`
|
||||
});
|
||||
},
|
||||
rowWrapClass() {
|
||||
return this.config.showRowDivider === false ? 'order-wrap no-divider' : 'order-wrap';
|
||||
},
|
||||
rowCellStyle() {
|
||||
return this.$util.objToStyle({
|
||||
'min-height': `${this.config.rowMinHeight}rpx`
|
||||
});
|
||||
},
|
||||
// 标签宽度:根据 labelWidthMode 决定;fixed 用 --label-fixed-width,auto 用 maxLabelWidth
|
||||
nameWidthStyle() {
|
||||
const cfg = this.config;
|
||||
if (cfg.labelWidthMode === 'fixed') {
|
||||
return { width: `${cfg.labelFixedWidth}rpx` };
|
||||
}
|
||||
return this.maxLabelWidth ? { width: `${this.maxLabelWidth}rpx` } : { width: 'auto' };
|
||||
},
|
||||
// 输入框样式(input / textarea 共用)
|
||||
inputStyle() {
|
||||
return this.$util.objToStyle({
|
||||
'min-height': `${this.config.inputHeight}rpx`,
|
||||
'border-radius': `${this.config.inputRadius}rpx`,
|
||||
'font-size': `${this.config.labelFontSize}rpx`,
|
||||
'color': this.config.labelColor,
|
||||
'line-height': `${this.config.inputHeight}rpx`
|
||||
});
|
||||
},
|
||||
// 必填星号颜色
|
||||
requiredColor() {
|
||||
return this.config.requiredColor;
|
||||
},
|
||||
// 复选 / 单选 选中色
|
||||
checkedColor() {
|
||||
return this.config.checkedColor;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.setFormData();
|
||||
this.initFormData();
|
||||
this.$nextTick(() => this.calculateMaxLabelWidth());
|
||||
},
|
||||
watch: {
|
||||
data: function() {
|
||||
this.setFormData();
|
||||
data: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.initFormData();
|
||||
this.$nextTick(() => this.calculateMaxLabelWidth());
|
||||
}
|
||||
},
|
||||
formConfig: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.$nextTick(() => this.calculateMaxLabelWidth());
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initFormData() {
|
||||
// 深拷贝 prop 数据到本地状态,避免 mutate 上游
|
||||
this.formData = JSON.parse(JSON.stringify(this.data || []));
|
||||
this.processFormData();
|
||||
},
|
||||
setFormData() {
|
||||
this.formData = this.data;
|
||||
|
||||
this.initFormData();
|
||||
this.$nextTick(() => this.calculateMaxLabelWidth());
|
||||
},
|
||||
processFormData() {
|
||||
this.formData.forEach(item => {
|
||||
if (!item.val) item.val = item.value.default ? item.value.default : '';
|
||||
|
||||
@@ -356,6 +609,25 @@
|
||||
}
|
||||
});
|
||||
},
|
||||
// 计算最长 label 宽度(与后端 design.js calculateMaxLabelWidth 算法对齐)
|
||||
// 用于 labelWidthMode === 'auto' 时把 .name 的 width 撑开,避免长短 label 错位
|
||||
calculateMaxLabelWidth() {
|
||||
if (!this.formData || this.formData.length === 0) {
|
||||
this.maxLabelWidth = 0;
|
||||
return;
|
||||
}
|
||||
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);
|
||||
},
|
||||
verify() {
|
||||
let verify = true;
|
||||
|
||||
@@ -643,9 +915,33 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
/* CSS 自定义属性:默认值,与组件内 config.defaults 一一对应,
|
||||
formConfig 未传时作为兜底;运行时会被 formWrapStyle 注入的 inline style 覆盖 */
|
||||
.form-wrap {
|
||||
--form-padding-x: 0rpx;
|
||||
--form-bg-color: #ffffff;
|
||||
--row-padding-y: 20rpx;
|
||||
--row-min-height: 60rpx;
|
||||
--label-gap: 16rpx;
|
||||
--input-height: 60rpx;
|
||||
--input-radius: 0rpx;
|
||||
--label-fixed-width: 180rpx;
|
||||
--label-font-size: 28rpx;
|
||||
--label-color: #1f2937;
|
||||
--required-color: #ef4444;
|
||||
--checked-color: #ff4544;
|
||||
--placeholder-color: #909399;
|
||||
--row-divider-color: #f0f0f0;
|
||||
--show-row-divider: 1;
|
||||
}
|
||||
|
||||
.order-wrap {
|
||||
padding: 20rpx 0;
|
||||
padding: var(--row-padding-y, 20rpx) 0;
|
||||
|
||||
&.no-divider {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
@@ -658,9 +954,10 @@
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
min-height: var(--row-min-height, 60rpx);
|
||||
|
||||
&.textarea {
|
||||
align-items: unset;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&.clear-flex {
|
||||
@@ -674,7 +971,8 @@
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: solid 1px #eee;
|
||||
// calc + 0/1 控制分割线显隐(formConfig.showRowDivider = false 时整条隐藏)
|
||||
border-bottom: calc(var(--show-row-divider, 1) * 2rpx) solid var(--row-divider-color, #f0f0f0);
|
||||
}
|
||||
|
||||
&.align-top {
|
||||
@@ -682,28 +980,30 @@
|
||||
}
|
||||
|
||||
text {
|
||||
font-size: $font-size-base;
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 80vw;
|
||||
width: var(--label-fixed-width, 180rpx);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: var(--label-gap, 16rpx);
|
||||
}
|
||||
|
||||
.tit {
|
||||
text-align: left;
|
||||
font-size: $font-size-base;
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
color: var(--label-color, #1f2937);
|
||||
|
||||
text {
|
||||
font-size: $font-size-base;
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
}
|
||||
}
|
||||
|
||||
.required {
|
||||
color: red;
|
||||
font-size: 28rpx;
|
||||
color: var(--required-color, #ef4444);
|
||||
font-size: var(--label-font-size, 28rpx);
|
||||
margin-left: 4rpx;
|
||||
width: 14rpx;
|
||||
text-align: left;
|
||||
@@ -717,16 +1017,58 @@
|
||||
text-align: left;
|
||||
|
||||
input {
|
||||
font-size: $font-size-base;
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
text-align: left;
|
||||
color: var(--label-color, #1f2937);
|
||||
// 显式 height / line-height / min-height 同步覆盖 WebView 默认与鸿蒙原生 TextInput 的固有高度,
|
||||
// 避免 placeholder / 文字下半部分被裁切
|
||||
line-height: var(--input-height, 60rpx);
|
||||
height: var(--input-height, 60rpx);
|
||||
min-height: var(--input-height, 60rpx);
|
||||
border-radius: var(--input-radius, 0rpx);
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// placeholder-class 钩子:在鸿蒙原生 TextInput / WebView 中都生效,
|
||||
// 限定在 .form-wrap 内避免污染全局 .placeholder / .color-tip
|
||||
.placeholder {
|
||||
color: var(--placeholder-color, #909399);
|
||||
font-size: var(--label-font-size, 28rpx);
|
||||
line-height: var(--input-height, 60rpx);
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-size: $font-size-base;
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
width: 100%;
|
||||
color: var(--label-color, #1f2937);
|
||||
// 鸿蒙元服务渲染为原生 ArkUI TextArea,不识别 CSS height,由 :rows 控制行数;
|
||||
// 这里给所有端都设一个最小高度基线,避免空内容时塌缩
|
||||
min-height: 88rpx;
|
||||
height: 88rpx;
|
||||
line-height: 44rpx;
|
||||
text-align: left;
|
||||
border-radius: var(--input-radius, 0rpx);
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.uni-input {
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
color: var(--label-color, #1f2937);
|
||||
line-height: var(--input-height, 60rpx);
|
||||
min-height: var(--input-height, 60rpx);
|
||||
}
|
||||
|
||||
.select-address {
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
color: var(--label-color, #1f2937);
|
||||
}
|
||||
|
||||
.color-tip {
|
||||
color: var(--placeholder-color, #909399);
|
||||
}
|
||||
|
||||
checkbox-group {
|
||||
@@ -847,7 +1189,7 @@
|
||||
|
||||
.iconfont {
|
||||
color: $color-tip;
|
||||
font-size: $font-size-base;
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
}
|
||||
|
||||
.box-flex picker {
|
||||
|
||||
Reference in New Issue
Block a user