mrr.sj.front/components/dynamic-form/dynamic-form.vue

325 lines
9.2 KiB
Vue
Raw Normal View History

2026-06-09 17:49:15 +08:00
<template>
2026-06-10 10:15:10 +08:00
<view class="dynamic-form">
<view v-for="(field, index) in fields" :key="field.key" class="form-section">
<!-- 单行文本 -->
<view v-if="field.type === 'text'" class="form-item">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<input type="text" :value="formData[field.key]" :placeholder="field.placeholder" placeholder-class="placeholder"
class="form-input" @input="onInput(field.key, $event)" />
</view>
<!-- 多行文本 -->
<view v-else-if="field.type === 'textarea'" class="detail-card">
<view class="detail-row">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<view class="detail-textarea-wrap">
<textarea class="detail-textarea" :value="formData[field.key]" :placeholder="field.placeholder"
placeholder-class="placeholder" maxlength="200" @input="onInput(field.key, $event)" />
<text class="detail-count">{{ (formData[field.key] || '').length }}/200</text>
</view>
</view>
</view>
<!-- 数字输入 -->
<view v-else-if="field.type === 'number'" class="form-item" :style="field.tip ? 'flex-wrap: wrap;' : ''">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<input type="digit" :value="formData[field.key]" :placeholder="field.placeholder"
placeholder-class="placeholder" class="form-input" @input="onInput(field.key, $event)" />
<text v-if="field.tips && !field.tip" class="form-tips">{{ field.tips }}</text>
<view v-if="field.tip" class="price-tip">
<text class="price-tip-card">{{ field.tip }}</text>
</view>
</view>
<!-- 多图上传 -->
<view v-else-if="field.type === 'image_multi'" class="form-item-up upload-item">
<view>
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<text class="upload-tip">(仅可上传{{ field.maxCount || 9 }}个图片)</text>
</view>
<view class="upload-content">
<form-item-image :value="formData[field.key]" :maxCount="field.maxCount || 9"
@input="onInput(field.key, $event)" />
</view>
</view>
<!-- 视频上传 -->
<view v-else-if="field.type === 'video'" class="form-item-up upload-item">
<view>
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<text class="upload-tip">(仅可上传1个视频)</text>
</view>
<view class="upload-content">
<form-item-video :value="formData[field.key]" @input="onInput(field.key, $event)" />
</view>
</view>
<!-- 下拉选择 -->
<view v-else-if="field.type === 'select'" class="form-item">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<form-item-select :value="formData[field.key]" :options="field.options || []" :placeholder="field.placeholder"
@input="onInput(field.key, $event)" />
</view>
<!-- 单选 -->
<view v-else-if="field.type === 'single_select'" class="form-item" style="flex-wrap: wrap;">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<view class="select-options">
<form-item-radio :value="formData[field.key]" :options="field.options || []"
@input="onInput(field.key, $event)" />
</view>
</view>
<!-- 多选 -->
<view v-else-if="field.type === 'multi_select'" class="form-item" style="flex-wrap: wrap;">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<view class="select-options">
<form-item-checkbox :value="formData[field.key]" :options="field.options || []"
:maxCount="field.maxCount || 0" @input="onInput(field.key, $event)" />
</view>
</view>
<!-- 图文编辑器 -->
<view v-else-if="field.type === 'richtext_editor'" class="form-item">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<form-item-richtext :value="formData[field.key]" @input="onInput(field.key, $event)" />
</view>
</view>
</view>
2026-06-09 17:49:15 +08:00
</template>
<script>
import FormItemText from './FormItemText.vue';
import FormItemTextarea from './FormItemTextarea.vue';
import FormItemNumber from './FormItemNumber.vue';
import FormItemImage from './FormItemImage.vue';
import FormItemVideo from './FormItemVideo.vue';
import FormItemSelect from './FormItemSelect.vue';
import FormItemRadio from './FormItemRadio.vue';
import FormItemCheckbox from './FormItemCheckbox.vue';
import FormItemRichtext from './FormItemRichtext.vue';
export default {
2026-06-10 10:15:10 +08:00
name: 'DynamicForm',
components: {
'form-item-text': FormItemText,
'form-item-textarea': FormItemTextarea,
'form-item-number': FormItemNumber,
'form-item-image': FormItemImage,
'form-item-video': FormItemVideo,
'form-item-select': FormItemSelect,
'form-item-radio': FormItemRadio,
'form-item-checkbox': FormItemCheckbox,
'form-item-richtext': FormItemRichtext
},
props: {
fields: {
type: Array,
default: function () { return []; }
},
value: {
type: Object,
default: function () { return {}; }
}
},
data: function () {
return {
formData: {}
};
},
watch: {
value: {
handler: function (val) {
this.formData = Object.assign({}, val);
},
immediate: true
},
fields: {
handler: function () {
var self = this;
var data = {};
this.fields.forEach(function (field) {
if (self.formData[field.key] !== undefined) {
data[field.key] = self.formData[field.key];
} else if (field.default_value !== undefined) {
data[field.key] = field.default_value;
} else if (field.type === 'multi_select' || field.type === 'image_multi') {
data[field.key] = [];
} else {
data[field.key] = '';
}
});
this.formData = Object.assign({}, this.formData, data);
},
immediate: true
}
},
methods: {
onInput: function (key, event) {
var value = event;
if (event && event.target && event.target.value !== undefined) {
value = event.target.value;
}
this.$set(this.formData, key, value);
this.$emit('input', this.formData);
},
getData: function () {
return Object.assign({}, this.formData);
},
validate: function () {
var self = this;
for (var i = 0; i < this.fields.length; i++) {
var field = this.fields[i];
if (field.is_required === true) {
var value = self.formData[field.key];
if (!value || (Array.isArray(value) && value.length === 0)) {
uni.showToast({
title: field.label + '不能为空',
icon: 'none'
});
return false;
}
}
}
return true;
}
}
2026-06-09 17:49:15 +08:00
};
</script>
<style scoped>
.dynamic-form {
2026-06-10 10:15:10 +08:00
width: 100%;
}
.form-section {
width: 100%;
2026-06-09 17:49:15 +08:00
}
.form-item {
2026-06-10 10:15:10 +08:00
background-color: #fff;
padding: 24rpx;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
border-radius: 10rpx;
margin-bottom: 24rpx;
}
.form-label {
font-size: 28rpx;
color: #333333;
}
.required::before {
content: '*';
color: #ff0000;
margin-right: 4rpx;
}
.form-tips {
font-size: 24rpx;
color: #999999;
margin-left: 10rpx;
}
.price-tip {
width: 100%;
margin-top: 40rpx;
}
.price-tip-card {
padding: 8rpx 12rpx;
background: #FDF0F0;
border-radius: 8rpx;
font-weight: 400;
font-size: 24rpx;
color: #E8101E;
line-height: 32rpx;
2026-06-09 17:49:15 +08:00
}
.form-item-up {
2026-06-10 10:15:10 +08:00
background-color: #fff;
padding: 24rpx;
display: flex;
flex-flow: column nowrap;
justify-content: space-between;
align-items: flex-start;
box-sizing: border-box;
border-radius: 10rpx;
margin-bottom: 24rpx;
2026-06-09 17:49:15 +08:00
}
2026-06-10 10:15:10 +08:00
.upload-item {
padding-bottom: 30rpx;
2026-06-09 17:49:15 +08:00
}
2026-06-10 10:15:10 +08:00
.upload-tip {
font-size: 24rpx;
color: #999999;
margin-left: 10rpx;
2026-06-09 17:49:15 +08:00
}
2026-06-10 10:15:10 +08:00
.upload-content {
margin-top: 20rpx;
width: 100%;
2026-06-09 17:49:15 +08:00
}
2026-06-10 10:15:10 +08:00
.select-options {
width: 100%;
margin-top: 16rpx;
}
.detail-card {
background: #ffffff;
border-radius: 20rpx;
padding: 0 24rpx 8rpx;
margin-bottom: 24rpx;
box-sizing: border-box;
overflow: hidden;
}
.detail-row {
padding: 24rpx 0 36rpx;
}
.detail-row .form-label {
display: block;
margin-bottom: 20rpx;
line-height: 40rpx;
}
.detail-textarea-wrap {
position: relative;
background: #f7f7f7;
border-radius: 16rpx;
padding: 24rpx 24rpx 52rpx;
box-sizing: border-box;
}
.detail-textarea {
width: 100%;
height: 108rpx;
min-height: 108rpx;
margin: 0;
padding: 0;
background: transparent;
border: 0;
font-size: 28rpx;
line-height: 40rpx;
color: #333333;
box-sizing: border-box;
2026-06-09 17:49:15 +08:00
}
2026-06-10 10:15:10 +08:00
.detail-count {
position: absolute;
right: 24rpx;
bottom: 16rpx;
font-size: 22rpx;
color: #999999;
line-height: 32rpx;
2026-06-09 17:49:15 +08:00
}
</style>