弹窗样式修改,组件预定义

This commit is contained in:
丁杰 2026-06-10 10:15:10 +08:00
parent a7f9544bc5
commit 6aa66ce311
13 changed files with 2233 additions and 750 deletions

View File

@ -1,97 +1,86 @@
<template> <template>
<view class="form-checkbox"> <view class="form-checkbox">
<view v-for="(item, index) in options" :key="index" class="checkbox-item" @click="toggle(item)"> <view v-for="(item, index) in options" :key="index" class="checkbox-item" @click="onToggle(item)">
<view :class="['checkbox-dot', { checked: isSelected(item.value) }]"> <view class="checkbox-dot" :class="{ active: isChecked(item) }"></view>
<text v-if="isSelected(item.value)" class="check-icon">&#10003;</text> <text class="checkbox-label">{{ item }}</text>
</view> </view>
<text class="checkbox-label">{{ item.label }}</text> </view>
</view>
</view>
</template> </template>
<script> <script>
export default { export default {
name: 'FormItemCheckbox', name: 'FormItemCheckbox',
props: { props: {
value: { value: { type: Array, default: function () { return []; } },
type: Array, options: { type: Array, default: function () { return []; } },
default: function () { maxCount: { type: Number, default: 0 }
return []; },
} methods: {
}, isChecked: function (val) {
options: { return this.value && this.value.indexOf(val) !== -1;
type: Array, },
default: function () { onToggle: function (val) {
return []; var newValue = (this.value || []).slice();
} var index = newValue.indexOf(val);
} if (index === -1) {
}, if (this.maxCount > 0 && newValue.length >= this.maxCount) {
data: function () { uni.showToast({
return { title: '最多只能选择' + this.maxCount + '项',
selectedValues: [] icon: 'none'
}; });
}, return;
watch: { }
value: { newValue.push(val);
handler: function (val) { } else {
this.selectedValues = Array.isArray(val) ? val.slice() : []; newValue.splice(index, 1);
}, }
immediate: true this.$emit('input', newValue);
} }
}, }
methods: {
isSelected: function (val) {
return this.selectedValues.indexOf(val) > -1;
},
toggle: function (item) {
var index = this.selectedValues.indexOf(item.value);
if (index > -1) {
this.selectedValues.splice(index, 1);
} else {
this.selectedValues.push(item.value);
}
this.$emit('input', this.selectedValues.slice());
}
}
}; };
</script> </script>
<style scoped> <style scoped>
.form-checkbox { .form-checkbox {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 24rpx;
} }
.checkbox-item { .checkbox-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 16rpx 0; margin-right: 40rpx;
margin-bottom: 16rpx;
} }
.checkbox-dot { .checkbox-dot {
width: 36rpx; width: 36rpx;
height: 36rpx; height: 36rpx;
border-radius: 6rpx; border-radius: 8rpx;
border: 2rpx solid #dddddd; border: 2rpx solid #cccccc;
display: flex; margin-right: 12rpx;
align-items: center; position: relative;
justify-content: center;
margin-right: 12rpx;
} }
.checkbox-dot.checked { .checkbox-dot.active {
border-color: #e8101e; border-color: #e8101e;
background-color: #e8101e; background-color: #e8101e;
} }
.check-icon { .checkbox-dot.active::after {
color: #ffffff; content: '';
font-size: 24rpx; position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -60%) rotate(-45deg);
width: 20rpx;
height: 12rpx;
border-left: 4rpx solid #ffffff;
border-bottom: 4rpx solid #ffffff;
} }
.checkbox-label { .checkbox-label {
font-size: 28rpx; font-size: 28rpx;
color: #333333; color: #333333;
} }
</style> </style>

View File

@ -16,44 +16,44 @@
<script> <script>
export default { export default {
name: "FormItemImage", name: 'FormItemImage',
props: { props: {
value: { type: Array, default: function() { return []; } }, value: { type: Array, default: function () { return []; } },
maxCount: { type: Number, default: 9 } maxCount: { type: Number, default: 9 }
}, },
data: function() { data: function () {
return { return {
imageList: [] imageList: []
}; };
}, },
watch: { watch: {
value: { value: {
handler: function(val) { handler: function (val) {
this.imageList = Array.isArray(val) ? val.slice() : []; this.imageList = Array.isArray(val) ? val.slice() : [];
}, },
immediate: true immediate: true
} }
}, },
methods: { methods: {
chooseImage: function() { chooseImage: function () {
var remaining = this.maxCount - this.imageList.length; var remaining = this.maxCount - this.imageList.length;
if (remaining <= 0) return; if (remaining <= 0) return;
var self = this; var self = this;
uni.chooseImage({ uni.chooseImage({
count: remaining, count: remaining,
sizeType: ["compressed"], sizeType: ['compressed'],
sourceType: ["album", "camera"], sourceType: ['album', 'camera'],
success: function(res) { success: function (res) {
self.imageList = self.imageList.concat(res.tempFilePaths); self.imageList = self.imageList.concat(res.tempFilePaths);
self.$emit("input", self.imageList); self.$emit('input', self.imageList);
} }
}); });
}, },
deleteImage: function(index) { deleteImage: function (index) {
this.imageList.splice(index, 1); this.imageList.splice(index, 1);
this.$emit("input", this.imageList); this.$emit('input', this.imageList);
}, },
previewImage: function(index) { previewImage: function (index) {
uni.previewImage({ uni.previewImage({
urls: this.imageList, urls: this.imageList,
current: index current: index
@ -67,44 +67,54 @@ export default {
.form-image { .form-image {
width: 100%; width: 100%;
} }
.image-list { .image-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16rpx;
} }
.image-item { .image-item {
width: 200rpx; width: 200rpx;
height: 200rpx; height: 200rpx;
margin-right: 10rpx;
margin-bottom: 20rpx;
border-radius: 8rpx; border-radius: 8rpx;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
.preview-image { .preview-image {
width: 100%; width: 200rpx;
height: 100%; height: 200rpx;
border-radius: 8rpx;
} }
.delete-btn { .delete-btn {
position: absolute; position: absolute;
top: 0; top: -20rpx;
right: 0; right: -20rpx;
width: 40rpx; width: 40rpx;
height: 40rpx; height: 40rpx;
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.delete-icon { .delete-icon {
color: #ffffff; color: #ffffff;
font-size: 28rpx; font-size: 32rpx;
} }
.add-btn { .add-btn {
background-color: #f5f5f5; background-color: #ffffff;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 2rpx dashed #cccccc; border: 2rpx dashed #dddddd;
} }
.add-icon { .add-icon {
font-size: 60rpx; font-size: 60rpx;
color: #999999; color: #999999;

View File

@ -1,39 +1,39 @@
<template> <template>
<input type="number" class="form-input" :value="value" :placeholder="placeholder" placeholder-class="placeholder" <view class="form-number-wrap">
@input="$emit('input', $event.detail.value)" /> <input type="digit" :value="value" :placeholder="placeholder" placeholder-class="placeholder" class="form-input" @input="onInput" />
</view>
</template> </template>
<script> <script>
export default { export default {
name: 'FormItemNumber', name: 'FormItemNumber',
props: { props: {
value: { value: { type: [String, Number], default: '' },
type: [String, Number], placeholder: { type: String, default: '请输入' }
default: '' },
}, methods: {
placeholder: { onInput: function(e) {
type: String, this.$emit('input', e.target.value);
default: '请输入数字' }
} }
}
}; };
</script> </script>
<style scoped> <style scoped>
.form-input { .form-number-wrap {
flex: 1; flex: 1;
height: 80rpx; display: flex;
font-size: 28rpx; align-items: center;
color: #333333; }
text-align: center; .form-input {
background-color: #f5f5f5; flex: 1;
border-radius: 8rpx; font-size: 28rpx;
padding: 0 24rpx; color: #333333;
text-align: center;
} }
.placeholder { .placeholder {
font-weight: 400; font-weight: 400;
font-size: 28rpx; font-size: 28rpx;
color: #999999; color: #999999;
} }
</style> </style>

View File

@ -1,30 +1,22 @@
<template> <template>
<view class="form-radio"> <view class="form-radio">
<view v-for="(item, index) in options" :key="index" class="radio-item" @click="onSelect(item.value)"> <view v-for="(item, index) in options" :key="index" class="radio-item" @click="onSelect(item)">
<view class="radio-dot" :class="{ active: value == item.value }"></view> <view class="radio-dot" :class="{ active: value === item }"></view>
<text class="radio-label">{{ item.label }}</text> <text class="radio-label">{{ item }}</text>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
name: "FormItemRadio", name: 'FormItemRadio',
props: { props: {
value: { value: { type: [String, Number], default: '' },
type: [String, Number], options: { type: Array, default: function () { return []; } }
default: ""
},
options: {
type: Array,
default: function() {
return [];
}
}
}, },
methods: { methods: {
onSelect: function(val) { onSelect: function (val) {
this.$emit("input", val); this.$emit('input', val);
} }
} }
}; };
@ -35,12 +27,14 @@ export default {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.radio-item { .radio-item {
display: flex; display: flex;
align-items: center; align-items: center;
margin-right: 40rpx; margin-right: 40rpx;
margin-bottom: 16rpx; margin-bottom: 16rpx;
} }
.radio-dot { .radio-dot {
width: 36rpx; width: 36rpx;
height: 36rpx; height: 36rpx;
@ -49,12 +43,14 @@ export default {
margin-right: 12rpx; margin-right: 12rpx;
position: relative; position: relative;
} }
.radio-dot.active { .radio-dot.active {
border-color: #e8101e; border-color: #e8101e;
background-color: #e8101e; background-color: #e8101e;
} }
.radio-dot.active::after { .radio-dot.active::after {
content: ""; content: '';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
@ -64,6 +60,7 @@ export default {
border-radius: 50%; border-radius: 50%;
background-color: #ffffff; background-color: #ffffff;
} }
.radio-label { .radio-label {
font-size: 28rpx; font-size: 28rpx;
color: #333333; color: #333333;

View File

@ -1,83 +1,43 @@
<template> <template>
<view class="form-richtext"> <view class="form-richtext">
<view class="richtext-toolbar"> <view class="richtext-trigger" @click="goEdit">
<text class="toolbar-btn" @click="insertTag('b')">加粗</text> <text class="richtext-value">{{ value ? '已设置' : '去设置' }}</text>
<text class="toolbar-btn" @click="insertTag('i')">斜体</text>
<text class="toolbar-btn" @click="insertImage">图片</text>
</view>
<textarea class="richtext-editor" :value="value" placeholder="请输入图文内容" placeholder-class="placeholder"
@input="$emit('input', $event.detail.value)" />
<view v-if="value" class="richtext-preview">
<rich-text :nodes="value" />
</view> </view>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
name: "FormItemRichtext", name: 'FormItemRichtext',
props: { props: {
value: { type: String, default: "" }, value: { type: String, default: '' }
}, },
methods: { methods: {
insertTag(tag) { goEdit: function () {
const text = `<${tag}></${tag}>`; uni.navigateTo({
this.$emit("input", (this.value || "") + text); url: '/pages/shop/add-img-text?list=' + this.value
},
insertImage() {
uni.chooseImage({
count: 1,
success: (res) => {
const imgTag = `<img src="${res.tempFilePaths[0]}" />`;
this.$emit("input", (this.value || "") + imgTag);
},
}); });
}, }
}, }
}; };
</script> </script>
<style scoped> <style scoped>
.form-richtext { .form-richtext {
width: 100%; flex: 1;
}
.richtext-toolbar {
display: flex; display: flex;
gap: 16rpx; align-items: center;
padding: 16rpx; justify-content: flex-end;
background-color: #f0f0f0;
border-radius: 8rpx 8rpx 0 0;
} }
.toolbar-btn { .richtext-trigger {
padding: 8rpx 16rpx; display: flex;
background-color: #ffffff; align-items: center;
border-radius: 4rpx;
font-size: 24rpx;
color: #333333;
} }
.richtext-editor { .richtext-value {
width: 100%;
height: 300rpx;
background-color: #ffffff;
border: 2rpx solid #e0e0e0;
border-top: none;
border-radius: 0 0 8rpx 8rpx;
padding: 20rpx;
font-size: 28rpx; font-size: 28rpx;
color: #333333; color: #333333;
} text-align: right;
.placeholder {
color: #999999;
}
.richtext-preview {
margin-top: 16rpx;
padding: 16rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
} }
</style> </style>

View File

@ -1,53 +1,29 @@
<template> <template>
<picker :range="optionLabels" @change="onChange"> <picker :range="options" @change="onChange">
<view class="form-select"> <view class="form-select">
<text :class="['select-text', selectedLabel ? '' : 'placeholder']"> <text :class="['select-text', value ? '' : 'placeholder']">
{{ selectedLabel || placeholder }} {{ value || placeholder }}
</text> </text>
<image src="https://mrrplus.oss-cn-beijing.aliyuncs.com/wxstaic/images/arrow_right.png" class="arrow-right" mode="aspectFit" /> <image src="https://mrrplus.oss-cn-beijing.aliyuncs.com/wxstaic/images/arrow_right.png" class="arrow-right"
mode="aspectFit" />
</view> </view>
</picker> </picker>
</template> </template>
<script> <script>
export default { export default {
name: "FormItemSelect", name: 'FormItemSelect',
props: { props: {
value: { value: { type: [String, Number], default: '' },
type: [String, Number], options: { type: Array, default: function () { return []; } },
default: "" placeholder: { type: String, default: '请选择' }
},
options: {
type: Array,
default: function() {
return [];
}
},
placeholder: {
type: String,
default: "请选择"
}
},
computed: {
optionLabels: function() {
return this.options.map(function(item) {
return item.label;
});
},
selectedLabel: function() {
var self = this;
var found = this.options.find(function(item) {
return item.value == self.value;
});
return found ? found.label : "";
}
}, },
methods: { methods: {
onChange: function(e) { onChange: function (e) {
var index = e.detail.value; var index = e.detail.value;
var selected = this.options[index]; var selected = this.options[index];
if (selected) { if (selected !== undefined) {
this.$emit("input", selected.value); this.$emit('input', selected);
} }
} }
} }
@ -59,21 +35,22 @@ export default {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%;
height: 80rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 0 24rpx;
} }
.select-text { .select-text {
font-size: 28rpx; font-size: 28rpx;
color: #333333; color: #333333;
margin-right: 16rpx;
} }
.placeholder { .placeholder {
font-weight: 400;
font-size: 28rpx;
color: #999999; color: #999999;
} }
.arrow-right { .arrow-right {
width: 24rpx; width: 32rpx;
height: 24rpx; height: 32rpx;
} }
</style> </style>

View File

@ -1,39 +1,39 @@
<template> <template>
<input type="text" class="form-input" :value="value" :placeholder="placeholder" placeholder-class="placeholder" <view class="form-text-wrap">
@input="$emit('input', $event.detail.value)" /> <input type="text" :value="value" :placeholder="placeholder" placeholder-class="placeholder" class="form-input" @input="onInput" />
</view>
</template> </template>
<script> <script>
export default { export default {
name: 'FormItemText', name: 'FormItemText',
props: { props: {
value: { value: { type: [String, Number], default: '' },
type: String, placeholder: { type: String, default: '请输入' }
default: '' },
}, methods: {
placeholder: { onInput: function(e) {
type: String, this.$emit('input', e.target.value);
default: '请输入' }
} }
}
}; };
</script> </script>
<style scoped> <style scoped>
.form-input { .form-text-wrap {
flex: 1; flex: 1;
height: 80rpx; display: flex;
font-size: 28rpx; align-items: center;
color: #333333; }
text-align: center; .form-input {
background-color: #f5f5f5; flex: 1;
border-radius: 8rpx; font-size: 28rpx;
padding: 0 24rpx; color: #333333;
text-align: center;
} }
.placeholder { .placeholder {
font-weight: 400; font-weight: 400;
font-size: 28rpx; font-size: 28rpx;
color: #999999; color: #999999;
} }
</style> </style>

View File

@ -1,56 +1,52 @@
<template> <template>
<view class="textarea-wrap"> <view class="detail-textarea-wrap">
<textarea class="form-textarea" :value="value" :placeholder="placeholder" placeholder-class="placeholder" <textarea class="detail-textarea" :value="value" :placeholder="placeholder" placeholder-class="placeholder" maxlength="200" @input="onInput" />
maxlength="200" @input="$emit('input', $event.detail.value)" /> <text class="detail-count">{{ (value || '').length }}/200</text>
<text class="char-count">{{ (value || '').length }}/200</text> </view>
</view>
</template> </template>
<script> <script>
export default { export default {
name: 'FormItemTextarea', name: 'FormItemTextarea',
props: { props: {
value: { value: { type: String, default: '' },
type: String, placeholder: { type: String, default: '请输入' }
default: '' },
}, methods: {
placeholder: { onInput: function(e) {
type: String, this.$emit('input', e.target.value);
default: '请输入' }
} }
}
}; };
</script> </script>
<style scoped> <style scoped>
.textarea-wrap { .detail-textarea-wrap {
width: 100%; position: relative;
position: relative; background: #f7f7f7;
border-radius: 16rpx;
padding: 24rpx 24rpx 52rpx;
box-sizing: border-box;
} }
.detail-textarea {
.form-textarea { width: 100%;
width: 100%; height: 108rpx;
height: 150rpx; min-height: 108rpx;
font-size: 28rpx; margin: 0;
color: #333333; padding: 0;
line-height: 1.5; background: transparent;
background-color: #f5f5f5; border: 0;
border-radius: 8rpx; font-size: 28rpx;
padding: 20rpx; line-height: 40rpx;
box-sizing: border-box; color: #333333;
box-sizing: border-box;
} }
.detail-count {
.placeholder { position: absolute;
font-weight: 400; right: 24rpx;
font-size: 28rpx; bottom: 16rpx;
color: #999999; font-size: 22rpx;
} color: #999999;
line-height: 32rpx;
.char-count {
position: absolute;
right: 16rpx;
bottom: 16rpx;
font-size: 24rpx;
color: #999999;
} }
</style> </style>

View File

@ -1,52 +1,51 @@
<template> <template>
<view class="form-video"> <view class="form-video">
<view v-if="videoUrl" class="video-preview"> <view v-if="videoUrl" class="video-preview">
<video :src="videoUrl" class="video-player" controls /> <video :src="videoUrl" class="preview-video" controls />
<view class="delete-btn" @click="deleteVideo"> <view class="delete-btn" @click="deleteVideo">
<text class="delete-icon">&times;</text> <text class="delete-icon">&times;</text>
</view> </view>
</view> </view>
<view v-else class="video-add" @click="chooseVideo"> <view v-else class="upload-btn" @click="chooseVideo">
<text class="add-icon">+</text> <text class="plus-icon">+</text>
<text class="add-text">选择视频</text>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
export default { export default {
name: "FormItemVideo", name: 'FormItemVideo',
props: { props: {
value: { type: String, default: "" } value: { type: String, default: '' }
}, },
data: function() { data: function () {
return { return {
videoUrl: "" videoUrl: ''
}; };
}, },
watch: { watch: {
value: { value: {
handler: function(val) { handler: function (val) {
this.videoUrl = val || ""; this.videoUrl = val || '';
}, },
immediate: true immediate: true
} }
}, },
methods: { methods: {
chooseVideo: function() { chooseVideo: function () {
var self = this; var self = this;
uni.chooseVideo({ uni.chooseVideo({
sourceType: ["album", "camera"], sourceType: ['album', 'camera'],
maxDuration: 60, maxDuration: 60,
success: function(res) { success: function (res) {
self.videoUrl = res.tempFilePath; self.videoUrl = res.tempFilePath;
self.$emit("input", self.videoUrl); self.$emit('input', self.videoUrl);
} }
}); });
}, },
deleteVideo: function() { deleteVideo: function () {
this.videoUrl = ""; this.videoUrl = '';
this.$emit("input", ""); this.$emit('input', '');
} }
} }
}; };
@ -56,49 +55,50 @@ export default {
.form-video { .form-video {
width: 100%; width: 100%;
} }
.video-preview { .video-preview {
width: 100%; width: 200rpx;
height: 200rpx;
position: relative; position: relative;
} }
.video-player {
width: 100%; .preview-video {
height: 400rpx; width: 200rpx;
height: 200rpx;
border-radius: 8rpx; border-radius: 8rpx;
} }
.delete-btn { .delete-btn {
position: absolute; position: absolute;
top: 16rpx; top: -20rpx;
right: 16rpx; right: -20rpx;
width: 48rpx; width: 40rpx;
height: 48rpx; height: 40rpx;
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.delete-icon { .delete-icon {
color: #ffffff; color: #ffffff;
font-size: 32rpx; font-size: 32rpx;
} }
.video-add {
width: 300rpx; .upload-btn {
height: 300rpx; width: 200rpx;
background-color: #f5f5f5; height: 200rpx;
border: 2rpx dashed #cccccc; background-color: #ffffff;
border: 2rpx dashed #dddddd;
border-radius: 8rpx; border-radius: 8rpx;
display: flex; display: flex;
flex-direction: column;
align-items: center;
justify-content: center; justify-content: center;
align-items: center;
} }
.add-icon {
.plus-icon {
font-size: 60rpx; font-size: 60rpx;
color: #999999; color: #999999;
} }
.add-text {
font-size: 24rpx;
color: #999999;
margin-top: 8rpx;
}
</style> </style>

View File

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

View File

@ -99,13 +99,12 @@
</view> </view>
</view> </view>
<!-- <view class="add-btn"></view> -->
<!-- 服务分类选择弹窗 --> <!-- 服务分类选择弹窗 -->
<view class="category-popup-mask" v-if="showCategoryPopup" @click="closeCategoryPopup"> <view class="category-popup-mask" v-if="showCategoryPopup" @click="closeCategoryPopup">
<view class="category-popup" @click.stop> <view class="category-popup" @click.stop>
<view class="category-popup-header"> <view class="category-popup-header">
<text class="category-popup-title">选择服务</text> <text class="category-popup-title">选择服务</text>
<view class="category-popup-close" @click="closeCategoryPopup"> <view class="category-popup-close" @click="closeCategoryPopup">
<text class="category-popup-close-icon">&times;</text> <text class="category-popup-close-icon">&times;</text>
</view> </view>
@ -124,10 +123,16 @@
<view v-if="secondClassList.length === 0" class="category-empty"> <view v-if="secondClassList.length === 0" class="category-empty">
<text class="category-empty-text">暂无子分类</text> <text class="category-empty-text">暂无子分类</text>
</view> </view>
<view v-for="item in secondClassList" :key="item.id" class="category-right-item" <view v-for="item in secondClassList" :key="item.id"
:class="['category-right-item', { selected: selectedSecondId === item.id }]"
@click="selectSecondClass(item)"> @click="selectSecondClass(item)">
<text class="category-right-text">{{ item.title }}</text> <text class="category-right-text">{{ item.title }}</text>
<view :class="['category-dot', { selected: selectedSecondId === item.id }]"></view> <image class="category-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/b6fa6b5b-b5c1-45e3-9372-74ade548f76b.png"
mode="widthFix" v-if="!selectedSecondId || selectedSecondId !== item.id"></image>
<image class="category-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/f0f97cad-03f6-4363-abb5-58d3176cfa81.png"
mode="widthFix" v-else></image>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@ -187,31 +192,6 @@ export default {
selectedSecondId: null, selectedSecondId: null,
}; };
}, },
// computed: {
// filteredServices() {
// return this.services.filter(service => {
// //
// if (this.title && !service.title.includes(this.title)) {
// return false
// }
// //
// switch (this.currentTab) {
// case 0: // 稿
// return service.state === 2
// case 1: //
// return service.state === 1
// case 2: //
// return service.state === 2
// case 3: //
// return service.state === 3
// case 4: //
// return service.state === 4
// default:
// return true
// }
// })
// }
// },
onLoad() { onLoad() {
this.getServicesList(); this.getServicesList();
}, },
@ -256,7 +236,7 @@ export default {
title: "下架成功", title: "下架成功",
icon: "none", icon: "none",
}); });
this.getServicesList(0); // this.getServicesList(0);
} else { } else {
uni.showToast({ uni.showToast({
title: res.msg, title: res.msg,
@ -280,7 +260,7 @@ export default {
title: "上架成功", title: "上架成功",
icon: "none", icon: "none",
}); });
this.getServicesList(0); // this.getServicesList(0);
} else { } else {
uni.showToast({ uni.showToast({
title: res.msg, title: res.msg,
@ -296,7 +276,6 @@ export default {
}, },
addService() { addService() {
//
request.post("/sj/firstclass", {}).then((res) => { request.post("/sj/firstclass", {}).then((res) => {
if (res.data && res.data.length > 0) { if (res.data && res.data.length > 0) {
this.firstClassList = res.data; this.firstClassList = res.data;
@ -304,7 +283,6 @@ export default {
this.selectedSecondId = null; this.selectedSecondId = null;
this.secondClassList = []; this.secondClassList = [];
this.showCategoryPopup = true; this.showCategoryPopup = true;
//
this.loadSecondClass(res.data[0].id); this.loadSecondClass(res.data[0].id);
} else { } else {
uni.showToast({ uni.showToast({
@ -314,7 +292,6 @@ export default {
} }
}); });
}, },
//
loadSecondClass(firstId) { loadSecondClass(firstId) {
request.post("/user/secondclass", { request.post("/user/secondclass", {
id: firstId, id: firstId,
@ -322,18 +299,15 @@ export default {
this.secondClassList = res.data || []; this.secondClassList = res.data || [];
}); });
}, },
//
selectFirstClass(index, item) { selectFirstClass(index, item) {
if (this.selectedFirstIndex === index) return; if (this.selectedFirstIndex === index) return;
this.selectedFirstIndex = index; this.selectedFirstIndex = index;
this.selectedSecondId = null; this.selectedSecondId = null;
this.loadSecondClass(item.id); this.loadSecondClass(item.id);
}, },
//
selectSecondClass(item) { selectSecondClass(item) {
this.selectedSecondId = item.id; this.selectedSecondId = item.id;
}, },
//
confirmCategory() { confirmCategory() {
if (this.selectedFirstIndex < 0) { if (this.selectedFirstIndex < 0) {
uni.showToast({ uni.showToast({
@ -342,7 +316,6 @@ export default {
}); });
return; return;
} }
//
if (this.secondClassList.length > 0 && !this.selectedSecondId) { if (this.secondClassList.length > 0 && !this.selectedSecondId) {
uni.showToast({ uni.showToast({
title: "请选择服务子类", title: "请选择服务子类",
@ -355,7 +328,6 @@ export default {
uni.navigateTo({ url }); uni.navigateTo({ url });
this.closeCategoryPopup(); this.closeCategoryPopup();
}, },
//
closeCategoryPopup() { closeCategoryPopup() {
this.showCategoryPopup = false; this.showCategoryPopup = false;
this.firstClassList = []; this.firstClassList = [];
@ -377,7 +349,7 @@ export default {
title: "删除成功", title: "删除成功",
icon: "none", icon: "none",
}); });
this.getServicesList(0); // this.getServicesList(0);
} else { } else {
uni.showToast({ uni.showToast({
title: res.msg, title: res.msg,
@ -391,11 +363,8 @@ export default {
}, },
touchStart(e, index) { touchStart(e, index) {
if (this.currentTab != 3) return; if (this.currentTab != 3) return;
//
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
//
if (e.touches && e.touches[0]) { if (e.touches && e.touches[0]) {
this.startX = e.touches[0].clientX; this.startX = e.touches[0].clientX;
this.currentIndex = index; this.currentIndex = index;
@ -404,87 +373,60 @@ export default {
touchMove(e) { touchMove(e) {
if (this.currentTab != 3) return; if (this.currentTab != 3) return;
if (this.currentIndex === -1) return; if (this.currentIndex === -1) return;
//
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
//
if (e.touches && e.touches[0]) { if (e.touches && e.touches[0]) {
this.moveX = e.touches[0].clientX - this.startX; this.moveX = e.touches[0].clientX - this.startX;
//
if (this.moveX > 0) { if (this.moveX > 0) {
this.moveX = 0; this.moveX = 0;
} }
//
if (this.moveX < -160) { if (this.moveX < -160) {
this.moveX = -160; this.moveX = -160;
} }
//
this.$set(this.services[this.currentIndex], "slideX", this.moveX); this.$set(this.services[this.currentIndex], "slideX", this.moveX);
} }
}, },
touchEnd(e) { touchEnd(e) {
if (this.currentTab != 3) return; if (this.currentTab != 3) return;
if (this.currentIndex === -1) return; if (this.currentIndex === -1) return;
//
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
//
if (this.moveX < -80) { if (this.moveX < -80) {
this.$set(this.services[this.currentIndex], "slideX", -160); this.$set(this.services[this.currentIndex], "slideX", -160);
} else { } else {
//
this.$set(this.services[this.currentIndex], "slideX", 0); this.$set(this.services[this.currentIndex], "slideX", 0);
} }
this.currentIndex = -1; this.currentIndex = -1;
}, },
mouseDown(e, index) { mouseDown(e, index) {
if (this.currentTab != 3) return; if (this.currentTab != 3) return;
//
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.startX = e.clientX; this.startX = e.clientX;
this.currentIndex = index; this.currentIndex = index;
}, },
mouseMove(e) { mouseMove(e) {
if (this.currentTab != 3) return; if (this.currentTab != 3) return;
if (this.currentIndex === -1) return; if (this.currentIndex === -1) return;
//
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.moveX = e.clientX - this.startX; this.moveX = e.clientX - this.startX;
//
if (this.moveX > 0) { if (this.moveX > 0) {
this.moveX = 0; this.moveX = 0;
} }
//
if (this.moveX < -160) { if (this.moveX < -160) {
this.moveX = -160; this.moveX = -160;
} }
//
this.$set(this.services[this.currentIndex], "slideX", this.moveX); this.$set(this.services[this.currentIndex], "slideX", this.moveX);
}, },
mouseUp(e) { mouseUp(e) {
if (this.currentTab != 3) return; if (this.currentTab != 3) return;
if (this.currentIndex === -1) return; if (this.currentIndex === -1) return;
//
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
//
if (this.moveX < -80) { if (this.moveX < -80) {
this.$set(this.services[this.currentIndex], "slideX", -160); this.$set(this.services[this.currentIndex], "slideX", -160);
} else { } else {
//
this.$set(this.services[this.currentIndex], "slideX", 0); this.$set(this.services[this.currentIndex], "slideX", 0);
} }
this.currentIndex = -1; this.currentIndex = -1;
@ -578,7 +520,7 @@ export default {
transform: translateX(-50%); transform: translateX(-50%);
width: 32rpx; width: 32rpx;
height: 6rpx; height: 6rpx;
background-color: #E8101E; background-color: #FF4767;
border-radius: 3rpx; border-radius: 3rpx;
} }
@ -623,7 +565,7 @@ export default {
top: 0; top: 0;
width: 160rpx; width: 160rpx;
height: 100%; height: 100%;
background-color: #E8101E; background-color: #FF4767;
color: #ffffff; color: #ffffff;
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -669,20 +611,20 @@ export default {
.price-symbol { .price-symbol {
font-size: 24rpx; font-size: 24rpx;
color: #E8101E; color: #FF4767;
font-weight: 500; font-weight: 500;
} }
.price-value { .price-value {
font-size: 28rpx; font-size: 28rpx;
color: #E8101E; color: #FF4767;
font-weight: 500; font-weight: 500;
margin: 0 4rpx; margin: 0 4rpx;
} }
.price-unit { .price-unit {
font-size: 24rpx; font-size: 24rpx;
color: #E8101E; color: #FF4767;
} }
.status-tag { .status-tag {
@ -700,11 +642,11 @@ export default {
/* 已上架 */ /* 已上架 */
.status-tag.state2 { .status-tag.state2 {
background-color: rgba(232, 16, 30, 0.2); background-color: rgba(255, 71, 103, 0.2);
} }
.status-tag.state2 .status-text { .status-tag.state2 .status-text {
color: #E8101E; color: #FF4767;
} }
/* 审核中 */ /* 审核中 */
@ -720,13 +662,13 @@ export default {
.status-tag.state0, .status-tag.state0,
.status-tag.state3, .status-tag.state3,
.status-tag.state4 { .status-tag.state4 {
background-color: rgba(209, 15, 19, 0.2); background-color: rgba(255, 71, 103, 0.2);
} }
.status-tag.state0 .status-text, .status-tag.state0 .status-text,
.status-tag.state3 .status-text, .status-tag.state3 .status-text,
.status-tag.state4 .status-text { .status-tag.state4 .status-text {
color: #d10f13; color: #FF4767;
} }
.btns { .btns {
@ -816,7 +758,6 @@ export default {
/* #ifdef MP-WEIXIN */ /* #ifdef MP-WEIXIN */
.search-box input { .search-box input {
/* 小程序搜索框样式调整 */
box-sizing: border-box; box-sizing: border-box;
} }
@ -825,7 +766,6 @@ export default {
} }
.service-item { .service-item {
/* 小程序阴影效果 */
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
} }
@ -846,17 +786,15 @@ export default {
height: 88rpx; height: 88rpx;
text-align: center; text-align: center;
line-height: 88rpx; line-height: 88rpx;
background-color: #E8101E; background-color: #FF4767;
border-radius: 16px; border-radius: 16px;
.serviceBtn-text { .serviceBtn-text {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #ffffff; color: #ffffff;
} }
} }
} }
/* 分类选择弹窗样式 */ /* 分类选择弹窗样式 */
@ -881,175 +819,160 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
}
.category-popup-header { .category-popup-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: space-between;
padding: 32rpx; padding: 32rpx;
position: relative; position: relative;
border-bottom: 1rpx solid #f0f0f0; border-bottom: 1rpx solid #f0f0f0;
} }
.category-popup-title { .category-popup-title {
font-size: 34rpx; font-size: 34rpx;
font-weight: 600; font-weight: 600;
color: #333333; color: #333333;
} }
.category-popup-close { .category-popup-close {
position: absolute; position: absolute;
right: 32rpx; right: 32rpx;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
width: 48rpx; width: 48rpx;
height: 48rpx; height: 48rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.category-popup-close-icon { .category-popup-close-icon {
font-size: 48rpx; font-size: 48rpx;
color: #999999; color: #999999;
line-height: 1; line-height: 1;
} }
.category-popup-body { .category-popup-body {
flex: 1; flex: 1;
display: flex; display: flex;
overflow: hidden; overflow: hidden;
} }
.category-left { .category-left {
width: 220rpx; width: 220rpx;
background-color: #f5f5f5; background-color: #f5f5f5;
height: 100%; height: 100%;
} }
.category-left-item { .category-left-item {
padding: 30rpx 24rpx; padding: 30rpx 24rpx;
text-align: center; text-align: center;
position: relative; position: relative;
} }
.category-left-item.active { .category-left-item.active {
background-color: #ffffff; background-color: #ffffff;
} }
.category-left-item.active::before { .category-left-item.active::before {
content: ""; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
width: 6rpx; width: 6rpx;
height: 40rpx; height: 40rpx;
background-color: #E8101E; background-color: #FF4767;
border-radius: 0 3rpx 3rpx 0; border-radius: 0 3rpx 3rpx 0;
} }
.category-left-text { .category-left-text {
font-size: 28rpx; font-size: 28rpx;
color: #333333; color: #333333;
} }
.category-left-item.active .category-left-text { .category-left-item.active .category-left-text {
color: #E8101E; color: #FF4767;
font-weight: 500; font-weight: 500;
} }
.category-right { .category-right {
flex: 1; flex: 1;
background-color: #ffffff; background-color: #ffffff;
height: 100%; height: 100%;
} }
.category-right-item { .category-right-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 30rpx 32rpx; padding: 30rpx 32rpx;
border-bottom: 1rpx solid #f5f5f5; /* 移除 border-bottom: 1rpx solid #f5f5f5; */
} }
.category-right-text { .category-right-item.selected .category-right-text {
font-size: 28rpx; color: #FF4767;
color: #333333; font-weight: 500;
flex: 1; }
}
.category-dot { .category-right-text {
width: 36rpx; font-size: 28rpx;
height: 36rpx; color: #333333;
border-radius: 50%; flex: 1;
border: 2rpx solid #dddddd; }
margin-left: 16rpx;
position: relative;
}
.category-dot.selected { .category-icon {
border-color: #E8101E; width: 36rpx;
background-color: #E8101E; height: 36rpx;
} margin-left: 16rpx;
}
.category-dot.selected::after { .category-empty {
content: ""; display: flex;
position: absolute; align-items: center;
top: 50%; justify-content: center;
left: 50%; padding: 100rpx 0;
transform: translate(-50%, -50%); }
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background-color: #ffffff;
}
.category-empty { .category-empty-text {
display: flex; font-size: 28rpx;
align-items: center; color: #999999;
justify-content: center; }
padding: 100rpx 0;
}
.category-empty-text { .category-popup-footer {
font-size: 28rpx; display: flex;
color: #999999; padding: 24rpx 32rpx;
} padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0;
background-color: #ffffff;
}
.category-popup-footer { .category-popup-btn {
display: flex; flex: 1;
padding: 24rpx 32rpx; height: 88rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); display: flex;
border-top: 1rpx solid #f0f0f0; align-items: center;
background-color: #ffffff; justify-content: center;
} border-radius: 44rpx;
font-size: 30rpx;
}
.category-popup-btn { .category-popup-btn-cancel {
flex: 1; background-color: #f5f5f5;
height: 88rpx; margin-right: 24rpx;
display: flex; }
align-items: center;
justify-content: center;
border-radius: 44rpx;
font-size: 30rpx;
}
.category-popup-btn-cancel { .category-popup-btn-cancel text {
background-color: #f5f5f5; color: #666666;
margin-right: 24rpx; }
}
.category-popup-btn-cancel text { .category-popup-btn-confirm {
color: #666666; background-color: #FF4767;
} }
.category-popup-btn-confirm { .category-popup-btn-confirm text {
background-color: #E8101E; color: #ffffff;
} }
.category-popup-btn-confirm text {
color: #ffffff;
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@ -151,20 +151,20 @@ export default {
async getTemplate() { async getTemplate() {
// //
const mockFields = [ const mockFields = [
{ key: 'title', type: 'text', label: '服务名称', is_required: 1, placeholder: '请输入服务名称' }, { key: 'title', type: 'text', label: '服务名称', is_required: true, placeholder: '请输入服务名称' },
{ key: 'service_price', type: 'number', label: '服务定价', is_required: 1, placeholder: '请输入定价,最多两位小数', tips: '(元)' }, { key: 'service_price', type: 'number', label: '服务定价', is_required: true, placeholder: '请输入定价,最多两位小数', tips: '(元)', tip: '服务的基础定价,未参与任何优惠的价格' },
{ key: 'sale_price', type: 'number', label: '服务售价', is_required: 1, placeholder: '请输入售价,最多两位小数', tips: '(元)' }, { key: 'sale_price', type: 'number', label: '服务售价', is_required: true, placeholder: '请输入售价,最多两位小数', tips: '(元)', tip: '服务参与单品优惠后的价格' },
{ key: 'service_time', type: 'number', label: '服务时长', is_required: 1, placeholder: '请输入服务时间', tips: '(分钟)' }, { key: 'service_time', type: 'number', label: '服务时长', is_required: true, placeholder: '请输入服务时间', tips: '(分钟)' },
{ key: 'effect', type: 'textarea', label: '服务功效', is_required: 1, placeholder: '请输入服务功效' }, { key: 'effect', type: 'textarea', label: '服务功效', is_required: true, placeholder: '请输入服务功效' },
{ key: 'crowd', type: 'textarea', label: '适用人群', is_required: 1, placeholder: '请输入不适用人群,例如:未成年人/孕妇不适用' }, { key: 'crowd', type: 'textarea', label: '适用人群', is_required: true, placeholder: '请输入不适用人群,例如:未成年人/孕妇不适用' },
{ key: 'product', type: 'textarea', label: '产品清单', is_required: 0, placeholder: '请输入产品清单' }, { key: 'product', type: 'textarea', label: '产品清单', is_required: false, placeholder: '请输入产品清单' },
{ key: 'scope', type: 'textarea', label: '适用范围', is_required: 0, placeholder: '请输入使用时间/适用人数' }, { key: 'scope', type: 'textarea', label: '适用范围', is_required: false, placeholder: '请输入使用时间/适用人数' },
{ key: 'tips_text', type: 'textarea', label: '温馨提示', is_required: 0, placeholder: '请输入其他注意事项' }, { key: 'tips_text', type: 'textarea', label: '温馨提示', is_required: false, placeholder: '请输入其他注意事项' },
{ key: 'images', type: 'image_multi', label: '服务图片', is_required: 1, maxCount: 9 }, { key: 'images', type: 'image_multi', label: '服务图片', is_required: true, maxCount: 9 },
{ key: 'video', type: 'video', label: '上传短视频', is_required: 0 }, { key: 'video', type: 'video', label: '上传短视频', is_required: false },
{ key: 'category', type: 'select', label: '服务类', is_required: 1, placeholder: '请选择服务分类', options: [{ label: '美发', value: '1' }, { label: '美甲', value: '2' }, { label: '美容', value: '3' }] }, { key: 'service_type', type: 'single_select', label: '服务', is_required: true, options: ['到店服务', '上门服务'] },
{ key: 'service_type', type: 'single_select', label: '服务类型', is_required: 1, options: [{ label: '到店服务', value: '1' }, { label: '上门服务', value: '2' }] }, { key: 'features', type: 'multi_select', label: '服务特色', is_required: false, options: ['一对一服务', '免费咨询', '售后保障'], maxCount: 2 },
{ key: 'features', type: 'multi_select', label: '服务特色', is_required: 0, options: [{ label: '一对一服务', value: '1' }, { label: '免费咨询', value: '2' }, { label: '售后保障', value: '3' }] }, { key: 'graphic_details', type: 'richtext_editor', label: '图文详情', is_required: false },
]; ];
try { try {
const res = await request.post("/sj/servers/getTemplate", { const res = await request.post("/sj/servers/getTemplate", {