修复bug,优化流程

This commit is contained in:
丁杰 2026-06-11 20:48:30 +08:00
parent 6b659a6878
commit 0e9bca8d97
9 changed files with 1210 additions and 513 deletions

View File

@ -0,0 +1,81 @@
# 添加服务页改动记录
## 2026-06-11
### 1. 接口返回视频字段未正常显示
涉及文件:
- `pages/shop/add-service.vue`
- `components/dynamic-form/dynamic-form.vue`
- `components/dynamic-form/FormItemVideo.vue`
改动内容:
- `pages/shop/add-service.vue`
- 动态模板转换时不再过滤 `fieldType === 'video'` 的字段。
- 新增 `hasDynamicVideoField`,当接口模板已经返回视频字段时,使用动态表单内的视频上传组件。
- 原页面固定视频上传块保留为兜底,仅在模板没有视频字段时显示,避免重复显示两个视频上传组件。
- 给 `dynamic-form` 传入 `upload-user-id`,让动态视频上传能拿到当前发布用户 ID。
- 初始化动态字段时支持分组结构,视频多选字段按数组初始化。
- `components/dynamic-form/dynamic-form.vue`
- 视频字段渲染时传入 `maxCount`、`maxSize/maxFileSize`。
- 新增 `uploadUserId` prop并传给 `form-item-video`
- 多视频字段初始化为空数组,单视频字段初始化为空字符串。
- `components/dynamic-form/FormItemVideo.vue`
- 支持单视频和多视频值类型。
- 使用 `ali-oss-uploader` 上传视频时传入 `kind=2`
- 支持视频上传成功后回写、删除后清空、已有视频回显。
- 上传用户 ID 优先使用父组件传入值,兜底读取 Vuex 中的商家信息。
### 2. 视频上传后传给后端的地址打不开
涉及文件:
- `components/ali-oss-uploader/ali-oss-uploader.vue`
改动内容:
- 上传 OSS 后不再无条件拼接 URL 并回传。
- H5 `fetch` 上传和 APP/小程序 `uni.uploadFile` 上传都必须返回 `2xx` 才认为成功。
- 上传失败时 reject并触发 `error`,避免把不存在的 OSS 地址提交给后端。
- 统一生成 OSS object key上传使用的 key 和回传给后端的 URL 使用同一套路径规则。
- 文件扩展名提取增加兜底逻辑:
- 视频默认 `.mp4`
- 图片默认 `.jpg`
- 清理临时路径中的 query/hash避免生成异常对象名。
### 3. 添加服务页多图上传支持拖动排序
涉及文件:
- `components/ali-oss-uploader/ali-oss-uploader.vue`
- `components/dynamic-form/FormItemImage.vue`
- `pages/shop/add-service.vue`
改动内容:
- `components/ali-oss-uploader/ali-oss-uploader.vue`
- 新增 `sortable` prop默认关闭。
- 图片上传列表支持长按开始拖动。
- 拖动到其他图片位置时实时交换数组顺序。
- 每次排序后立即触发 `input``change`,向父组件回写排序后的 `fileList`
- 拖动中增加视觉状态,降低透明度并缩放。
- 拖动结束后短暂抑制图片预览,避免排序后误触发预览。
- 图片预览使用当前 `fileList` 字符串数组,保持预览顺序和提交顺序一致。
- `components/dynamic-form/FormItemImage.vue`
- 动态表单多图上传开启 `sortable`
- `pages/shop/add-service.vue`
- 提交逻辑沿用当前数组顺序:
- `image_multi` 字段按动态表单当前数组顺序传参。
- `photo` 字段通过 `Object.values(this.formData.photo || {})` 取当前顺序传参。
### 4. 校验情况
- 已执行 `git diff --check`,当前相关改动无 whitespace 错误。
- 项目 `package.json` 未配置 build/lint 脚本,未执行完整构建校验。
- 本地缺少 `vue-template-compiler`,未执行 Vue 模板编译校验。

View File

@ -5,8 +5,13 @@
<view v-if="accept == 'image/*'" class="preview-container" :style="'width:'+width"> <view v-if="accept == 'image/*'" class="preview-container" :style="'width:'+width">
<view <view
v-for="(file, index) in fileList" v-for="(file, index) in fileList"
:key="index" :key="file || index"
class="preview-item" class="preview-item image-preview-item"
:class="{ dragging: dragIndex === index }"
@longpress="startDrag(index)"
@touchmove="handleDragMove"
@touchend="endDrag"
@touchcancel="endDrag"
> >
<image <image
:src="file" :src="file"
@ -145,6 +150,10 @@ export default {
kind: { kind: {
type: Number, type: Number,
default: 1 default: 1
},
sortable: {
type: Boolean,
default: false
} }
}, },
@ -152,7 +161,10 @@ export default {
return { return {
fileList: [], fileList: [],
oneFile: '', oneFile: '',
loading: false loading: false,
dragIndex: -1,
isDragging: false,
suppressPreview: false
} }
}, },
@ -294,29 +306,17 @@ export default {
return Promise.reject(new Error('文件大小超出限制')) return Promise.reject(new Error('文件大小超出限制'))
} }
let date = new Date().getTime() let date = new Date().getTime()
let imageType; const fileExt = this.getFileExt(file)
if(this.accept == 'image/*') {
imageType = file.path.split('.')
}else if(this.accept == 'video/*') {
// #ifdef H5
imageType = file.name.split('.')
// #endif
// #ifdef APP-PLUS || MP-WEIXIN
imageType = file.tempFilePath.split('.')
// #endif
}
console.log('imageType',imageType)
let artisan = this.type==1 ? 'yh' : this.type==2 ? 'syr' : this.type==3 ? 'sj' : ''; let artisan = this.type==1 ? 'yh' : this.type==2 ? 'syr' : this.type==3 ? 'sj' : '';
let name = `${this.userId}_${artisan}_${date}.${imageType[imageType.length - 1]}` let name = `${this.userId}_${artisan}_${date}.${fileExt}`
console.log('name',name) console.log('name',name)
try { try {
// 1. OSS // 1. OSS
const ossConfig = await this.getOssConfig() const ossConfig = await this.getOssConfig()
// 2. // 2.
const uploadRes = await this.uploadToOss(file, ossConfig, name) const fileUrl = await this.uploadToOss(file, ossConfig, name)
// 3. // 3.
const fileUrl = `${ossConfig.host}/${ossConfig.dir}${name}`
if(this.accept == 'image/*') { if(this.accept == 'image/*') {
const newFile = [`${fileUrl}`] const newFile = [`${fileUrl}`]
this.fileList = this.fileList.concat(newFile) this.fileList = this.fileList.concat(newFile)
@ -351,9 +351,29 @@ export default {
} }
}, },
getFileExt(file) {
const defaultExt = this.accept === 'video/*' ? 'mp4' : 'jpg'
const source = file && (file.name || file.tempFilePath || file.path || file.url || '')
const cleanPath = String(source).split('?')[0].split('#')[0]
const ext = cleanPath.includes('.') ? cleanPath.split('.').pop().toLowerCase() : ''
return /^[a-z0-9]+$/.test(ext) ? ext : defaultExt
},
buildFileUrl(ossConfig, name) {
const host = String(ossConfig.host || '').replace(/\/+$/, '')
return `${host}/${this.buildObjectKey(ossConfig, name)}`
},
buildObjectKey(ossConfig, name) {
const dir = String(ossConfig.dir || '').replace(/^\/+/, '').replace(/\/+$/, '')
return dir ? `${dir}/${name}` : name
},
// OSS // OSS
uploadToOss(file, ossConfig, name) { uploadToOss(file, ossConfig, name) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const objectKey = this.buildObjectKey(ossConfig, name)
const fileUrl = this.buildFileUrl(ossConfig, name)
// #ifdef H5 // #ifdef H5
const formData = new FormData() const formData = new FormData()
formData.append('name',name); formData.append('name',name);
@ -361,13 +381,17 @@ export default {
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId); formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
formData.append('success_action_status', '200'); formData.append('success_action_status', '200');
formData.append('signature', ossConfig.signature); formData.append('signature', ossConfig.signature);
formData.append('key', ossConfig.dir + name); formData.append('key', objectKey);
// filefile // filefile
formData.append('file', file); formData.append('file', file.file || file.tempFile || file);
fetch(ossConfig.host, { method: 'POST', body: formData},).then((res) => { fetch(ossConfig.host, { method: 'POST', body: formData},).then((res) => {
resolve(res) if (res.ok) {
}).catch(()=>{ resolve(fileUrl)
reject(new Error(`上传失败: ${res.statusCode}`)) } else {
reject(new Error(`上传失败: ${res.status}`))
}
}).catch((error)=>{
reject(error)
}) })
// #endif // #endif
@ -378,7 +402,7 @@ export default {
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId; formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;
formData.success_action_status = '200'; formData.success_action_status = '200';
formData.signature = ossConfig.signature; formData.signature = ossConfig.signature;
formData.key = ossConfig.dir + name; formData.key = objectKey;
formData.file = file; formData.file = file;
let path; let path;
if(this.accept == 'image/*') { if(this.accept == 'image/*') {
@ -397,14 +421,22 @@ export default {
filePath: path, // filePath: path, //
name: 'file', name: 'file',
formData: { formData: {
key: ossConfig.dir + name, // key: objectKey, //
policy: ossConfig.policy, // policy: ossConfig.policy, //
OSSAccessKeyId: ossConfig.ossAccessKeyId, // ID OSSAccessKeyId: ossConfig.ossAccessKeyId, // ID
success_action_status: 200, // 200,204 success_action_status: 200, // 200,204
signature: ossConfig.signature // signature: ossConfig.signature //
}, },
success(res) { success: (res) => {
resolve(`${ossConfig.host}/${ossConfig.dir}${name}`) const statusCode = Number(res.statusCode)
if (statusCode >= 200 && statusCode < 300) {
resolve(fileUrl)
} else {
reject(new Error(`上传失败: ${statusCode} ${res.data || ''}`))
}
},
fail(error) {
reject(error)
} }
}); });
// #endif // #endif
@ -414,12 +446,58 @@ export default {
// //
handlePreview(index) { handlePreview(index) {
if (this.isDragging || this.suppressPreview) return
uni.previewImage({ uni.previewImage({
current: index, current: index,
urls: this.fileList.map(item => item.url) urls: this.fileList
}) })
}, },
startDrag(index) {
if (!this.sortable || this.accept !== 'image/*' || this.fileList.length < 2) return
this.dragIndex = index
this.isDragging = true
},
handleDragMove(event) {
if (!this.isDragging || this.dragIndex < 0) return
if (event.preventDefault) event.preventDefault()
if (event.stopPropagation) event.stopPropagation()
const touch = event.touches && event.touches[0]
if (!touch) return
uni.createSelectorQuery()
.in(this)
.selectAll('.image-preview-item')
.boundingClientRect((rects) => {
if (!Array.isArray(rects) || !rects.length) return
const targetIndex = rects.findIndex((rect) => {
return touch.clientX >= rect.left &&
touch.clientX <= rect.right &&
touch.clientY >= rect.top &&
touch.clientY <= rect.bottom
})
if (targetIndex < 0 || targetIndex === this.dragIndex || targetIndex >= this.fileList.length) return
const nextList = this.fileList.slice()
const moved = nextList.splice(this.dragIndex, 1)[0]
nextList.splice(targetIndex, 0, moved)
this.fileList = nextList
this.dragIndex = targetIndex
this.$emit('input', this.fileList)
this.$emit('change', this.fileList)
})
.exec()
},
endDrag() {
if (!this.isDragging) return
this.dragIndex = -1
this.suppressPreview = true
setTimeout(() => {
this.isDragging = false
this.suppressPreview = false
}, 200)
},
// //
handleDelete(index) { handleDelete(index) {
// uni.showModal({ // uni.showModal({
@ -504,6 +582,11 @@ export default {
margin-top: 20rpx; margin-top: 20rpx;
} }
.image-preview-item.dragging {
opacity: 0.65;
transform: scale(0.96);
}
.preview-image { .preview-image {
width: 200rpx; width: 200rpx;
height: 200rpx; height: 200rpx;

View File

@ -1,6 +1,6 @@
<template> <template>
<ali-oss-uploader v-model="currentValue" :max-count="maxCount" :max-size="maxSize" accept="image/*" :userId="userId" <ali-oss-uploader v-model="currentValue" :max-count="maxCount" :max-size="maxSize" accept="image/*" :userId="userId"
:type="ossType" width="100%" @input="onInput" @change="onChange" /> :type="ossType" width="100%" :sortable="true" @input="onInput" @change="onChange" />
</template> </template>
<script> <script>

View File

@ -0,0 +1,143 @@
<template>
<view class="form-step">
<view class="form-item">
<text :class="['form-label', required ? 'required' : '']">{{ label }}</text>
<text class="add-btn" @click="addStep">添加</text>
</view>
<view class="step" v-for="(item, i) in steps" :key="i">
<text class="step-label">{{ i + 1 }}</text>
<input type="text" class="step-inp" :value="item.text" placeholder="请输入步骤内容"
placeholder-class="placeholder" @input="onStepInput(i, $event)" />
<image src="https://mrrplus.oss-cn-beijing.aliyuncs.com/wxstaic/images/delete_icon.png"
class="delete-icon" mode="aspectFit" @click="deleteStep(i)"></image>
</view>
</view>
</template>
<script>
export default {
name: 'FormItemServiceStep',
props: {
value: {
type: [Array, String],
default: function () { return []; }
},
label: {
type: String,
default: '服务流程'
},
required: {
type: Boolean,
default: false
}
},
data: function () {
return {
steps: []
};
},
watch: {
value: {
handler: function (val) {
if (typeof val === 'string') {
try {
this.steps = JSON.parse(val) || [];
} catch (e) {
this.steps = [];
}
} else if (Array.isArray(val)) {
this.steps = val.slice();
} else {
this.steps = [];
}
},
immediate: true
}
},
methods: {
addStep: function () {
var i = this.steps.length + 1;
this.steps.push({
title: '第' + i + '步',
text: '',
photo: ''
});
this._emit();
},
deleteStep: function (i) {
this.steps.splice(i, 1);
this._emit();
},
onStepInput: function (i, e) {
var value = e && e.target ? e.target.value : e;
this.steps[i].text = value;
this._emit();
},
_emit: function () {
this.$emit('input', this.steps);
}
}
};
</script>
<style scoped>
.form-step {
width: 100%;
}
.form-item {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
padding: 0;
}
.form-label {
font-size: 28rpx;
color: #333333;
}
.required::before {
content: '*';
color: #ff0000;
margin-right: 4rpx;
}
.add-btn {
font-size: 28rpx;
color: #ff4d6a;
}
.step {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
padding: 20rpx 0 0;
}
.step-label {
font-size: 28rpx;
color: #333333;
white-space: nowrap;
}
.step-inp {
flex: 1;
font-size: 28rpx;
color: #333333;
margin-left: 20rpx;
padding: 0 24rpx;
background-color: rgba(230, 230, 230, 0.5);
height: 72rpx;
border-radius: 8rpx;
}
.delete-icon {
width: 40rpx;
height: 40rpx;
margin-left: 16rpx;
flex-shrink: 0;
}
</style>

View File

@ -1,14 +1,29 @@
<template> <template>
<view> <view>
<ali-oss-uploader v-if="!currentValue" :max-count="1" :max-size="maxSize" accept="video/*" :userId="userId" <view v-if="maxCount > 1" class="video-list">
:type="ossType" width="100%" @input="onInput" @change="onChange" /> <view v-for="(item, index) in currentList" :key="index" class="video-wrapper">
<video :src="item" class="preview-video" controls />
<view class="delete-btn" @click="deleteVideo(index)">
<text class="delete-icon">&times;</text>
</view>
</view>
<ali-oss-uploader v-if="currentList.length < maxCount" :key="currentList.length"
:max-count="maxCount - currentList.length"
:max-size="maxSize" accept="video/*" :userId="uploadUserId" :type="ossType" :kind="2" width="200rpx"
@input="onMultiInput" @change="onChange" />
</view>
<view v-else>
<ali-oss-uploader v-if="!currentValue" :max-count="1" :max-size="maxSize" accept="video/*"
:userId="uploadUserId" :type="ossType" :kind="2" width="100%" :file-string="currentValue" @input="onInput"
@change="onChange" @success="onInput" @delete="deleteSingleVideo" />
<view v-else class="video-wrapper"> <view v-else class="video-wrapper">
<video :src="currentValue" class="preview-video" controls /> <video :src="currentValue" class="preview-video" controls />
<view class="delete-btn" @click="deleteVideo"> <view class="delete-btn" @click="deleteSingleVideo">
<text class="delete-icon">&times;</text> <text class="delete-icon">&times;</text>
</view> </view>
</view> </view>
</view> </view>
</view>
</template> </template>
<script> <script>
@ -18,16 +33,20 @@ export default {
name: 'FormItemVideo', name: 'FormItemVideo',
components: { AliOssUploader }, components: { AliOssUploader },
props: { props: {
value: { type: String, default: '' }, value: { type: [String, Array], default: '' },
maxSize: { type: Number, default: 100 } maxSize: { type: Number, default: 100 },
maxCount: { type: Number, default: 1 },
userId: { type: [Number, String], default: null }
}, },
data: function () { data: function () {
return { return {
currentValue: '' currentValue: '',
currentList: []
}; };
}, },
computed: { computed: {
userId: function () { uploadUserId: function () {
if (this.userId) return this.userId;
return this.$store && this.$store.state && this.$store.state.sjInfo return this.$store && this.$store.state && this.$store.state.sjInfo
? this.$store.state.sjInfo.id : null; ? this.$store.state.sjInfo.id : null;
}, },
@ -39,7 +58,17 @@ export default {
watch: { watch: {
value: { value: {
handler: function (val) { handler: function (val) {
this.currentValue = val || ''; if (this.maxCount > 1) {
if (Array.isArray(val)) {
this.currentList = val.filter(function (v) { return !!v; });
} else if (val) {
this.currentList = [val];
} else {
this.currentList = [];
}
} else {
this.currentValue = Array.isArray(val) ? (val[0] || '') : (val || '');
}
}, },
immediate: true immediate: true
} }
@ -49,18 +78,34 @@ export default {
this.currentValue = val; this.currentValue = val;
this.$emit('input', val); this.$emit('input', val);
}, },
onMultiInput: function (val) {
var arr = Array.isArray(val) ? val : [val];
var list = this.currentList.concat(arr).slice(0, this.maxCount);
this.currentList = list;
this.$emit('input', list);
},
onChange: function (val) { onChange: function (val) {
this.$emit('change', val); this.$emit('change', val);
}, },
deleteVideo: function () { deleteSingleVideo: function () {
this.currentValue = ''; this.currentValue = '';
this.$emit('input', ''); this.$emit('input', '');
},
deleteVideo: function (index) {
this.currentList.splice(index, 1);
this.$emit('input', this.currentList);
} }
} }
}; };
</script> </script>
<style scoped> <style scoped>
.video-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.video-wrapper { .video-wrapper {
position: relative; position: relative;
width: 200rpx; width: 200rpx;

View File

@ -4,12 +4,15 @@
<!-- single --> <!-- single -->
<block v-if="group.type === 'single'"> <block v-if="group.type === 'single'">
<view v-if="group.field.type === 'text'" class="form-item"> <view v-if="group.field.type === 'text'" class="form-item">
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label }}</text> <text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
}}</text>
<input type="text" :value="formData[group.field.key]" :placeholder="group.field.placeholder" <input type="text" :value="formData[group.field.key]" :placeholder="group.field.placeholder"
placeholder-class="placeholder" class="form-input" @input="onInput(group.field.key, $event)" /> placeholder-class="placeholder" class="form-input" @input="onInput(group.field.key, $event)" />
</view> </view>
<view v-else-if="group.field.type === 'number'" class="form-item" :style="group.field.tip ? 'flex-wrap: wrap;' : ''"> <view v-else-if="group.field.type === 'number'" class="form-item"
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label }}</text> :style="group.field.tip ? 'flex-wrap: wrap;' : ''">
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
}}</text>
<input type="digit" :value="formData[group.field.key]" :placeholder="group.field.placeholder" <input type="digit" :value="formData[group.field.key]" :placeholder="group.field.placeholder"
placeholder-class="placeholder" class="form-input" @input="onInput(group.field.key, $event)" /> placeholder-class="placeholder" class="form-input" @input="onInput(group.field.key, $event)" />
<text v-if="group.field.tips && !group.field.tip" class="form-tips">{{ group.field.tips }}</text> <text v-if="group.field.tips && !group.field.tip" class="form-tips">{{ group.field.tips }}</text>
@ -19,7 +22,8 @@
</view> </view>
<view v-else-if="group.field.type === 'image_multi'" class="form-item-up upload-item"> <view v-else-if="group.field.type === 'image_multi'" class="form-item-up upload-item">
<view> <view>
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label }}</text> <text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
}}</text>
<text class="upload-tip">(仅可上传{{ group.field.maxCount || 9 }}个图片)</text> <text class="upload-tip">(仅可上传{{ group.field.maxCount || 9 }}个图片)</text>
</view> </view>
<view class="upload-content"> <view class="upload-content">
@ -29,17 +33,26 @@
</view> </view>
<view v-else-if="group.field.type === 'video'" class="form-item-up upload-item"> <view v-else-if="group.field.type === 'video'" class="form-item-up upload-item">
<view> <view>
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label }}</text> <text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
<text class="upload-tip">(仅可上传1个视频)</text> }}</text>
<text class="upload-tip">(仅可上传{{ group.field.maxCount || 1 }}个视频)</text>
</view> </view>
<view class="upload-content"> <view class="upload-content">
<form-item-video :value="formData[group.field.key]" @input="onInput(group.field.key, $event)" /> <form-item-video :value="formData[group.field.key]" :maxCount="group.field.maxCount || 1"
:maxSize="group.field.maxSize || group.field.maxFileSize || 100"
:userId="uploadUserId"
@input="onInput(group.field.key, $event)" />
</view> </view>
</view> </view>
<view v-else-if="group.field.type === 'richtext_editor'" class="form-item"> <view v-else-if="group.field.type === 'richtext_editor'" class="form-item">
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label }}</text> <text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
}}</text>
<form-item-richtext :value="formData[group.field.key]" @input="onInput(group.field.key, $event)" /> <form-item-richtext :value="formData[group.field.key]" @input="onInput(group.field.key, $event)" />
</view> </view>
<view v-else-if="group.field.type === 'service_step'" class="form-item-up upload-item">
<form-item-service-step :value="formData[group.field.key]" :label="group.field.label"
:required="group.field.is_required === true" @input="onInput(group.field.key, $event)" />
</view>
</block> </block>
<!-- section --> <!-- section -->
<view v-if="group.type === 'section'" class="section-card"> <view v-if="group.type === 'section'" class="section-card">
@ -60,7 +73,8 @@
<text class="detail-count">{{ (formData[field.key] || "").length }}/200</text> <text class="detail-count">{{ (formData[field.key] || "").length }}/200</text>
</view> </view>
</view> </view>
<view v-else-if="field.type === 'number'" class="form-item no-bg" :style="field.tip ? 'flex-wrap: wrap;' : ''"> <view v-else-if="field.type === 'number'" class="form-item no-bg"
:style="field.tip ? 'flex-wrap: wrap;' : ''">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text> <text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<input type="digit" :value="formData[field.key]" :placeholder="field.placeholder" <input type="digit" :value="formData[field.key]" :placeholder="field.placeholder"
placeholder-class="placeholder" class="form-input" @input="onInput(field.key, $event)" /> placeholder-class="placeholder" class="form-input" @input="onInput(field.key, $event)" />
@ -71,8 +85,8 @@
</view> </view>
<view v-else-if="field.type === 'select'" class="form-item no-bg"> <view v-else-if="field.type === 'select'" class="form-item no-bg">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text> <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" <form-item-select :value="formData[field.key]" :options="field.options || []"
@input="onInput(field.key, $event)" /> :placeholder="field.placeholder" @input="onInput(field.key, $event)" />
</view> </view>
<view v-else-if="field.type === 'single_select'" class="form-item no-bg" style="flex-wrap: wrap;"> <view v-else-if="field.type === 'single_select'" class="form-item no-bg" style="flex-wrap: wrap;">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text> <text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
@ -101,16 +115,22 @@
<view v-else-if="field.type === 'video'" class="form-item-up upload-item no-bg"> <view v-else-if="field.type === 'video'" class="form-item-up upload-item no-bg">
<view> <view>
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text> <text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<text class="upload-tip">(仅可上传1个视频)</text> <text class="upload-tip">(仅可上传{{ field.maxCount || 1 }}个视频)</text>
</view> </view>
<view class="upload-content"> <view class="upload-content">
<form-item-video :value="formData[field.key]" @input="onInput(field.key, $event)" /> <form-item-video :value="formData[field.key]" :maxCount="field.maxCount || 1"
:maxSize="field.maxSize || field.maxFileSize || 100" :userId="uploadUserId"
@input="onInput(field.key, $event)" />
</view> </view>
</view> </view>
<view v-else-if="field.type === 'richtext_editor'" class="form-item no-bg"> <view v-else-if="field.type === 'richtext_editor'" class="form-item no-bg">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text> <text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<form-item-richtext :value="formData[field.key]" @input="onInput(field.key, $event)" /> <form-item-richtext :value="formData[field.key]" @input="onInput(field.key, $event)" />
</view> </view>
<view v-else-if="field.type === 'service_step'" class="form-item-up upload-item no-bg">
<form-item-service-step :value="formData[field.key]" :label="field.label"
:required="field.is_required === true" @input="onInput(field.key, $event)" />
</view>
</view> </view>
</view> </view>
</view> </view>
@ -127,6 +147,7 @@ import FormItemSelect from './FormItemSelect.vue';
import FormItemRadio from './FormItemRadio.vue'; import FormItemRadio from './FormItemRadio.vue';
import FormItemCheckbox from './FormItemCheckbox.vue'; import FormItemCheckbox from './FormItemCheckbox.vue';
import FormItemRichtext from './FormItemRichtext.vue'; import FormItemRichtext from './FormItemRichtext.vue';
import FormItemServiceStep from './FormItemServiceStep.vue';
export default { export default {
name: 'DynamicForm', name: 'DynamicForm',
@ -139,7 +160,8 @@ export default {
'form-item-select': FormItemSelect, 'form-item-select': FormItemSelect,
'form-item-radio': FormItemRadio, 'form-item-radio': FormItemRadio,
'form-item-checkbox': FormItemCheckbox, 'form-item-checkbox': FormItemCheckbox,
'form-item-richtext': FormItemRichtext 'form-item-richtext': FormItemRichtext,
'form-item-service-step': FormItemServiceStep
}, },
props: { props: {
fields: { fields: {
@ -149,6 +171,10 @@ export default {
value: { value: {
type: Object, type: Object,
default: function () { return {}; } default: function () { return {}; }
},
uploadUserId: {
type: [Number, String],
default: null
} }
}, },
data: function () { data: function () {
@ -187,7 +213,12 @@ export default {
data[field.key] = this.formData[field.key]; data[field.key] = this.formData[field.key];
} else if (field.default_value !== undefined) { } else if (field.default_value !== undefined) {
data[field.key] = field.default_value; data[field.key] = field.default_value;
} else if (field.type === 'multi_select' || field.type === 'image_multi') { } else if (
field.type === 'multi_select' ||
field.type === 'image_multi' ||
field.type === 'service_step' ||
(field.type === 'video' && field.maxCount > 1)
) {
data[field.key] = []; data[field.key] = [];
} else { } else {
data[field.key] = ''; data[field.key] = '';
@ -329,7 +360,7 @@ export default {
} }
/* section内字段间距20rpx通过section-field包装器控制 */ /* section内字段间距20rpx通过section-field包装器控制 */
.section-field + .section-field { .section-field+.section-field {
padding-top: 12rpx; padding-top: 12rpx;
} }

View File

@ -271,12 +271,13 @@ export default {
}, },
viewEdit(item) { viewEdit(item) {
uni.navigateTo({ uni.navigateTo({
url: `/pages/shop/add-service?id=${item.id}&state=${item.state}`, url: `/pages/shop/add-service?id=${item.id}&state=${item.state}&first_class_id=${item.first_class_id}`,
}); });
}, },
addService() { addService() {
request.post("/sj/firstclass", {}).then((res) => { const userId = uni.getStorageSync('userId') || '';
request.post("/sj/firstclass", { id: userId }).then((res) => {
if (res.data && res.data.length > 0) { if (res.data && res.data.length > 0) {
this.firstClassList = res.data; this.firstClassList = res.data;
this.selectedFirstIndex = 0; this.selectedFirstIndex = 0;
@ -324,7 +325,10 @@ export default {
return; return;
} }
const firstItem = this.firstClassList[this.selectedFirstIndex]; const firstItem = this.firstClassList[this.selectedFirstIndex];
const url = `/pages/shop/add-service?first_class_id=${firstItem.id}`; let url = `/pages/shop/add-service?first_class_id=${firstItem.id}`;
if (this.selectedSecondId) {
url += `&second_class_id=${this.selectedSecondId}`;
}
uni.navigateTo({ url }); uni.navigateTo({ url });
this.closeCategoryPopup(); this.closeCategoryPopup();
}, },

View File

@ -10,9 +10,10 @@
<view v-else class="addTextAndImg-content"> <view v-else class="addTextAndImg-content">
<view v-for="(item, index) in list" :key="index" class="addTextAndImg-content-card"> <view v-for="(item, index) in list" :key="index" class="addTextAndImg-content-card">
<!-- <rich-text :nodes="item.value" v-if="item.type==1"></rich-text> --> <!-- <rich-text :nodes="item.value" v-if="item.type==1"></rich-text> -->
<uv-divider dashed v-if="item.type == 1" ></uv-divider> <uv-divider dashed v-if="item.type == 1"></uv-divider>
<view class="addTextAndImg-content-card-text" v-if="item.type==2">{{ item.value }}</view> <view class="addTextAndImg-content-card-text" v-if="item.type == 2">{{ item.value }}</view>
<image style="width: 100%;" mode="widthFix" :src="item.value" v-else-if="item.type==3|| item.type==4"></image> <image style="width: 100%;" mode="widthFix" :src="item.value" v-else-if="item.type == 3 || item.type == 4">
</image>
<view class="addTextAndImg-content-card-btns"> <view class="addTextAndImg-content-card-btns">
<view class="addTextAndImg-content-card-btns-btn" v-for="item2 in btns" :key="item2.id" <view class="addTextAndImg-content-card-btns-btn" v-for="item2 in btns" :key="item2.id"
@click.stop="handleCard(item2.id, index)" v-if=" @click.stop="handleCard(item2.id, index)" v-if="
@ -60,13 +61,13 @@
</template> </template>
<script> <script>
import uploadImage from '../../utils/uploadImage' import uploadImage from '../../utils/uploadImage'
import permissionUtils from '../../utils/per' import permissionUtils from '../../utils/per'
import locationService from '../../utils/locationService'; import locationService from '../../utils/locationService';
export default { export default {
data() { data() {
return { return {
isReplace:false, isReplace: false,
crIndex: null, // crIndex: null, //
nowQer: 'xc', nowQer: 'xc',
isShowPer: false, isShowPer: false,
@ -120,13 +121,17 @@
}, },
onLoad(options) { onLoad(options) {
console.log(options) console.log(options)
if(options.list){ if (options.list && options.list !== 'null' && options.list !== 'undefined') {
this.list = JSON.parse(options.list) try {
this.list = JSON.parse(options.list) || [];
} catch (e) {
this.list = [];
}
console.log(this.list) console.log(this.list)
} }
}, },
methods: { methods: {
submitForm(){ submitForm() {
uni.$emit('updateGraphicDetails', this.list); uni.$emit('updateGraphicDetails', this.list);
uni.navigateBack() uni.navigateBack()
}, },
@ -172,10 +177,10 @@
this.crIndex = null this.crIndex = null
return return
} }
if (this.crIndex!=null) { if (this.crIndex != null) {
if(this.isReplace){ if (this.isReplace) {
this.replaceAtIndex(this.list, this.crIndex, obj); this.replaceAtIndex(this.list, this.crIndex, obj);
}else{ } else {
this.insertAfterIndex(this.list, this.crIndex, obj); this.insertAfterIndex(this.list, this.crIndex, obj);
} }
} else { } else {
@ -227,10 +232,10 @@
value: ``, value: ``,
type: 1, type: 1,
}; };
if (this.crIndex!=null) { if (this.crIndex != null) {
if(this.isReplace){ if (this.isReplace) {
this.replaceAtIndex(this.list, this.crIndex, obj); this.replaceAtIndex(this.list, this.crIndex, obj);
}else{ } else {
this.insertAfterIndex(this.list, this.crIndex, obj); this.insertAfterIndex(this.list, this.crIndex, obj);
} }
@ -254,7 +259,7 @@
async chooseAvatar(type) { async chooseAvatar(type) {
try { try {
const systemInfo = uni.getSystemInfoSync() const systemInfo = uni.getSystemInfoSync()
if(systemInfo.platform === 'ios') { if (systemInfo.platform === 'ios') {
this.openCamera(type); this.openCamera(type);
return return
} }
@ -334,10 +339,10 @@
value: result, value: result,
type: type, type: type,
}; };
if (this.crIndex!=null) { if (this.crIndex != null) {
if(this.isReplace){ if (this.isReplace) {
this.replaceAtIndex(this.list, this.crIndex, obj); this.replaceAtIndex(this.list, this.crIndex, obj);
}else{ } else {
this.insertAfterIndex(this.list, this.crIndex, obj); this.insertAfterIndex(this.list, this.crIndex, obj);
} }
} else { } else {
@ -347,12 +352,13 @@
}, },
}, },
}; };
</script> </script>
<style lang="less"> <style lang="less">
.addTextAndImg { .addTextAndImg {
padding-bottom: 110rpx; padding-bottom: 110rpx;
.firstAdd { .firstAdd {
position: fixed; position: fixed;
left: 50%; left: 50%;
@ -368,7 +374,8 @@
border: 3px solid #000; border: 3px solid #000;
border-radius: 50%; border-radius: 50%;
margin-bottom: 100rpx; margin-bottom: 100rpx;
.firstAdd-garden-img{
.firstAdd-garden-img {
width: 200rpx; width: 200rpx;
} }
} }
@ -444,15 +451,15 @@
border-radius: 20rpx; border-radius: 20rpx;
padding: 20rpx; padding: 20rpx;
} }
} }
.permission.transform { .permission.transform {
top: calc(var(--status-bar-height) + 88rpx + 20rpx); top: calc(var(--status-bar-height) + 88rpx + 20rpx);
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
} }
.bottom-buttons { .bottom-buttons {
position: fixed; position: fixed;
left: 0; left: 0;
right: 0; right: 0;
@ -465,6 +472,7 @@
flex-flow: row nowrap; flex-flow: row nowrap;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
.btn-submit { .btn-submit {
background-color: rgba(232, 16, 30, 1); background-color: rgba(232, 16, 30, 1);
color: #fff; color: #fff;
@ -475,9 +483,5 @@
font-size: 28rpx; font-size: 28rpx;
border-radius: 78rpx; border-radius: 78rpx;
} }
} }
</style> </style>

View File

@ -11,8 +11,50 @@
<!-- 表单区域 --> <!-- 表单区域 -->
<view class="form-container"> <view class="form-container">
<!-- 服务分类仅显示不可编辑 -->
<view class="form-item" v-if="firstClassName">
<text class="form-label">服务分类</text>
<view class="form-right">
<text class="form-value">{{ firstClassName }}</text>
</view>
</view>
<!-- 服务子类仅显示不可编辑 -->
<view class="form-item" v-if="secondClassName">
<text class="form-label">服务子类</text>
<view class="form-right">
<text class="form-value">{{ secondClassName }}</text>
</view>
</view>
<!-- 动态表单区域 --> <!-- 动态表单区域 -->
<dynamic-form :fields="formFields" v-model="formData" ref="dynamicForm" /> <dynamic-form :fields="formFields" v-model="formData" :upload-user-id="formData.publish_user_id"
ref="dynamicForm" />
<!-- 服务流程 -->
<view class="form-step">
<view class="form-item">
<text class="form-label">服务流程</text>
<text @click="addStep" style="color: #FF4767;">添加步骤</text>
</view>
<view class="step" v-for="(item, i) in formData.process" :key="i">
<text class="form-label">{{ i + 1 }}</text>
<input type="text" class="step-inp" v-model="formData.process[i].text" placeholder="请输入步骤内容" />
<image src="https://mrrplus.oss-cn-beijing.aliyuncs.com/wxstaic/images/delete_icon.png"
class="delete-icon" mode="aspectFit" @click="deleteStep(i)"></image>
</view>
</view>
<!-- 视频上传 -->
<view class="video-upload" v-if="!hasDynamicVideoField">
<view class="flex-c">
<text class="form-label required">上传视频</text>
<text class="upload-tip">(仅可上传1个视频)</text>
</view>
<ali-oss-uploader :max-count="1" :max-size="100" :type="artisanType + 1"
:user-id="formData.publish_user_id" :width="'654rpx'" :kind="2" :file-string="formData.video"
accept="video/*" button-text="上传视频" @success="uploadVideoSuc" @error="uploadVideoErr"
@delete="uploadVideoDel" @ckeckisShowPer="ckeckisShowPer" @ckecknowQer="ckecknowQer">
</ali-oss-uploader>
</view>
</view> </view>
<!-- 底部按钮 --> <!-- 底部按钮 -->
@ -57,6 +99,9 @@ export default {
data() { data() {
return { return {
formFields: [], // formFields: [], //
processFieldKey: "", // config_datakey
firstClassName: "",
secondClassName: "",
showReminderPopup: false, showReminderPopup: false,
artisanType: getApp().globalData.artisanType, artisanType: getApp().globalData.artisanType,
nowQer: "xc", nowQer: "xc",
@ -103,11 +148,11 @@ export default {
publish_user_id: null, publish_user_id: null,
isAdd: false, isAdd: false,
isDraft: false, isDraft: false,
postid: 0
}; };
}, },
onShow() { onShow() { },
}, async onLoad(options) {
onLoad(options) {
// onLoadnavigateBack // onLoadnavigateBack
uni.$on("updateGraphicDetails", (data) => { uni.$on("updateGraphicDetails", (data) => {
// formDatadynamic-formwatch.value // formDatadynamic-formwatch.value
@ -123,15 +168,17 @@ export default {
if (options.second_class_id) { if (options.second_class_id) {
this.formData.second_class_id = options.second_class_id; this.formData.second_class_id = options.second_class_id;
} }
// formFields
await this.getTemplate();
if (options.id) { if (options.id) {
this.formData.id = options.id; this.formData.id = options.id;
this.getInit(); await this.getInit();
} }
// //
this.getTemplate(); await this.loadClassNames();
// //
const notShow = uni.getStorageSync('addServiceReminderNotShow'); const notShow = uni.getStorageSync('addServiceReminderNotShow');
this.getUserInfo(); await this.getUserInfo();
}, },
onUnload() { onUnload() {
uni.$off("updateGraphicDetails"); uni.$off("updateGraphicDetails");
@ -151,6 +198,17 @@ export default {
this.ratio this.ratio
); );
}, },
hasDynamicVideoField() {
return this.formFields.some((group) => {
if (group.type === 'single' && group.field) {
return group.field.type === 'video';
}
if (group.type === 'section' && Array.isArray(group.fields)) {
return group.fields.some((field) => field.type === 'video');
}
return group.type === 'video';
});
},
}, },
methods: { methods: {
// -> dynamic-form // -> dynamic-form
@ -207,10 +265,15 @@ export default {
} }
if (preset.maxFileSize) { if (preset.maxFileSize) {
field.maxFileSize = preset.maxFileSize; field.maxFileSize = preset.maxFileSize;
field.maxSize = preset.maxFileSize;
} }
field._fieldType = preset.fieldType;
field._hasOriginalKey = !!preset.key; field._hasOriginalKey = !!preset.key;
groups.push({ type: 'single', field }); groups.push({
type: 'single',
field
});
}); });
} }
@ -220,14 +283,16 @@ export default {
const sectionFields = []; const sectionFields = [];
if (section.fields && Array.isArray(section.fields)) { if (section.fields && Array.isArray(section.fields)) {
section.fields.forEach((sectionField) => { section.fields.forEach((sectionField) => {
const fieldKey = sectionField.key || `custom_${sectionField.id || customFieldIndex++}`; const fieldKey = sectionField.key ||
`custom_${sectionField.id || customFieldIndex++}`;
const field = { const field = {
key: fieldKey, key: fieldKey,
type: this.mapFieldType(sectionField.fieldType), type: this.mapFieldType(sectionField.fieldType),
label: sectionField.title, label: sectionField.title,
is_required: sectionField.required, is_required: sectionField.required,
placeholder: sectionField.placeholder || `请输入${sectionField.title}`, placeholder: sectionField.placeholder ||
`请输入${sectionField.title}`,
}; };
if (sectionField.key === 'server_time') { if (sectionField.key === 'server_time') {
@ -235,15 +300,18 @@ export default {
field.tips = '分钟'; field.tips = '分钟';
} }
if (sectionField.options && Array.isArray(sectionField.options) && sectionField.options.length > 0) { if (sectionField.options && Array.isArray(sectionField.options) &&
sectionField.options.length > 0) {
field.options = sectionField.options; field.options = sectionField.options;
} }
if (sectionField.maxCount && (sectionField.fieldType === 'multi_select' || sectionField.fieldType === 'image_multi')) { if (sectionField.maxCount && (sectionField.fieldType === 'multi_select' ||
sectionField.fieldType === 'image_multi' || sectionField.fieldType === 'video')) {
field.maxCount = sectionField.maxCount; field.maxCount = sectionField.maxCount;
} }
if (sectionField.maxLength && (sectionField.fieldType === 'text' || sectionField.fieldType === 'textarea')) { if (sectionField.maxLength && (sectionField.fieldType === 'text' ||
sectionField.fieldType === 'textarea')) {
field.maxLength = sectionField.maxLength; field.maxLength = sectionField.maxLength;
} }
@ -253,8 +321,10 @@ export default {
if (sectionField.maxFileSize) { if (sectionField.maxFileSize) {
field.maxFileSize = sectionField.maxFileSize; field.maxFileSize = sectionField.maxFileSize;
field.maxSize = sectionField.maxFileSize;
} }
field._fieldType = sectionField.fieldType;
field._hasOriginalKey = !!sectionField.key; field._hasOriginalKey = !!sectionField.key;
field._original = { field._original = {
id: sectionField.id, id: sectionField.id,
@ -282,17 +352,26 @@ export default {
async getTemplate() { async getTemplate() {
try { try {
const res = await request.post("/sj/servers/getTemplate", { const res = await request.post("/sj/servers/getTemplate", {
first_class_id: this.formData.first_class_id, first_class: this.formData.first_class_id,
}); });
console.log("模板接口返回数据:", res); console.log("模板接口返回数据:", res);
if (res.data && res.data.config_data) { if (res.data && res.data.config_data) {
// 使config_data // 使config_data
// config_data // config_data
const configData = typeof res.data.config_data === 'string' const configData = typeof res.data.config_data === 'string' ?
? JSON.parse(res.data.config_data) JSON.parse(res.data.config_data) :
: res.data.config_data; res.data.config_data;
console.log("解析后的config_data:", configData); console.log("解析后的config_data:", configData);
console.log("presetFields类型:", typeof configData.presetFields, Array.isArray(configData.presetFields)); // service_stepkey
this.processFieldKey = "";
const allConfigFields = [
...(configData.presetFields || []),
...(configData.sections || []).reduce((acc, s) => acc.concat(s.fields || []), [])
];
const stepField = allConfigFields.find(f => f.fieldType === 'service_step');
if (stepField && stepField.key) {
this.processFieldKey = stepField.key;
}
const fields = this.transformConfigToFields(configData); const fields = this.transformConfigToFields(configData);
console.log("转换后的fields数组:", fields, "长度:", fields.length); console.log("转换后的fields数组:", fields, "长度:", fields.length);
this.formFields = fields; this.formFields = fields;
@ -305,16 +384,67 @@ export default {
} }
// //
const initialData = {}; const initialData = {};
this.formFields.forEach((field) => { const allFields = this.getDynamicFields();
allFields.forEach((field) => {
if (field.default_value !== undefined) { if (field.default_value !== undefined) {
initialData[field.key] = field.default_value; initialData[field.key] = field.default_value;
} else if (field.type === 'multi_select' || field.type === 'image_multi') { } else if (
field.type === 'multi_select' ||
field.type === 'image_multi' ||
field.type === 'service_step' ||
(field.type === 'video' && field.maxCount > 1)
) {
initialData[field.key] = []; initialData[field.key] = [];
} else if (!this.formData[field.key] && this.formData[field.key] !== 0) { } else if (!this.formData[field.key] && this.formData[field.key] !== 0) {
initialData[field.key] = ''; initialData[field.key] = '';
} }
}); });
this.formData = { ...this.formData, ...initialData }; this.formData = {
...this.formData,
...initialData
};
},
getDynamicFields() {
const fields = [];
this.formFields.forEach((group) => {
if (group.type === 'single' && group.field) {
fields.push(group.field);
} else if (group.type === 'section' && Array.isArray(group.fields)) {
fields.push(...group.fields);
} else if (group.key) {
fields.push(group);
}
});
return fields;
},
// ID
async loadClassNames() {
if (!this.formData.first_class_id) return;
try {
const firstRes = await request.post("/sj/firstclass", {});
if (firstRes.data && firstRes.data.length > 0) {
const matched = firstRes.data.find(item => item.id == this.formData.first_class_id);
if (matched) {
this.firstClassName = matched.title;
}
}
} catch (e) {
console.error("获取一级分类失败", e);
}
if (!this.formData.second_class_id) return;
try {
const secondRes = await request.post("/user/secondclass", {
id: this.formData.first_class_id,
});
if (secondRes.data && secondRes.data.length > 0) {
const matched = secondRes.data.find(item => item.id == this.formData.second_class_id);
if (matched) {
this.secondClassName = matched.title;
}
}
} catch (e) {
console.error("获取二级分类失败", e);
}
}, },
inpPrice2(e) { inpPrice2(e) {
let that = this; let that = this;
@ -475,20 +605,114 @@ export default {
...this.formData, ...this.formData,
...res.data ...res.data
}; };
// process
if (!Array.isArray(this.formData.process)) {
this.formData.process = [];
}
// detail // detail
this.formData.detail = res.data.detail || []; this.formData.detail = res.data.detail || [];
// photo
if (this.formData.photo && !Array.isArray(this.formData.photo)) {
this.formData.photo = Object.values(this.formData.photo);
}
if (Array.isArray(this.formData.photo)) {
this.formData.photo = this.formData.photo.map(function (item) {
return typeof item === 'string' ? item : (item && item.url ? item.url :
String(item));
});
}
// introJSON
if (res.data.intro) {
try {
const introArr = typeof res.data.intro === 'string' ?
JSON.parse(res.data.intro) :
res.data.intro;
if (Array.isArray(introArr)) {
// id -> field
const idToFieldMap = {};
this.formFields.forEach(function (group) {
const fields = group.field ? [group.field] : (group.fields || []);
fields.forEach(function (f) {
if (f._original && f._original.id) {
idToFieldMap[f._original.id] = f;
}
});
});
// introidformData
introArr.forEach(function (item) {
const matchedField = idToFieldMap[item.id];
if (matchedField && item.value !== undefined) {
this.$set(this.formData, matchedField.key, item.value);
}
}.bind(this));
}
} catch (e) {
console.error('解析intro字段失败:', e);
}
}
//
if (this.processFieldKey && res.data[this.processFieldKey]) {
try {
const processRaw = res.data[this.processFieldKey];
const processArr = typeof processRaw === 'string' ?
JSON.parse(processRaw) : processRaw;
if (Array.isArray(processArr)) {
this.formData.process = processArr.map(item => ({
title: item.title || '',
text: item.text || '',
photo: item.photo || '',
}));
}
} catch (e) {
console.error('解析服务步骤数据失败:', e);
}
} else if (res.data.intro) {
// keyintroservice_step
try {
const introArr = typeof res.data.intro === 'string' ?
JSON.parse(res.data.intro) : res.data.intro;
if (Array.isArray(introArr)) {
const stepItem = introArr.find(item => item.id === 'service_step');
if (stepItem && Array.isArray(stepItem.value)) {
this.formData.process = stepItem.value.map(item => ({
title: item.title || '',
text: item.text || '',
photo: item.photo || '',
}));
}
}
} catch (e) {
console.error('从intro解析服务步骤失败:', e);
}
}
// 使 // 使
this.initDetailForm(res.data.detail || []); this.initDetailForm(res.data.detail || []);
}); });
}, },
getUserInfo() { getUserInfo() {
const type = this.artisanType == 1 ? 2 : this.artisanType == 2 ? 3 : 1; return request
request .post("/sj/user/getuser", {
.post("/user/getuser", { type: 2,
type: type,
}) })
.then((result) => { .then((result) => {
this.formData.publish_user_id = result.data.id; console.log('getUserInfo返回:', result);
const userId = result.data.id || result.data.user_id || result.data.userId;
console.log('12121212');
if (userId) {
this.formData.publish_user_id = userId;
this.publish_user_id = userId;
uni.setStorageSync('userId', userId);
} else {
// idstorage
this.formData.publish_user_id = uni.getStorageSync('userId') || '';
}
console.log('publish_user_id:', this.formData.publish_user_id);
this.postid = result.data.id
})
.catch((err) => {
console.error('getUserInfo失败:', err);
// storage
this.formData.publish_user_id = uni.getStorageSync('userId') || '';
}); });
}, },
// //
@ -531,6 +755,20 @@ export default {
}; };
this.formData.process.push(arr); this.formData.process.push(arr);
}, },
//
uploadVideoSuc(file) {
this.formData.video = file;
},
uploadVideoErr() { },
uploadVideoDel() {
this.formData.video = '';
},
ckeckisShowPer(val) {
this.isShowPer = val;
},
ckecknowQer(val) {
this.nowQer = val;
},
// //
deleteVideo() { deleteVideo() {
@ -583,13 +821,15 @@ export default {
// 稿 // 稿
this.formData.state = 0; this.formData.state = 0;
const submitData = this.buildSubmitData();
submitData.state = 0;
let url = null; let url = null;
if (!this.formData.id) { if (!this.formData.id) {
url = "/user/serveradd"; url = "/user/serveradd";
} else { } else {
url = "/user/serverchange"; url = "/user/serverchange";
} }
request.post(url, this.formData).then((res) => { request.post(url, submitData).then((res) => {
if (res.state == 1) { if (res.state == 1) {
this.isDraft = true; this.isDraft = true;
const pages = getCurrentPages(); const pages = getCurrentPages();
@ -674,12 +914,22 @@ export default {
buildSubmitData() { buildSubmitData() {
const submitData = {}; const submitData = {};
const introArr = []; const introArr = [];
console.log(this.formData, '=-=-=-=-=-=');
// //
if (this.formData.id) submitData.id = this.formData.id; if (this.formData.publish_user_id) {
if (this.formData.first_class_id) submitData.first_class_id = this.formData.first_class_id; if (this.formData.id) {
if (this.formData.second_class_id) submitData.second_class_id = this.formData.second_class_id; submitData.id = this.formData.id;
}
submitData.publish_user_id = this.formData.publish_user_id || uni.getStorageSync('userId') || '';
submitData.first_class_id = this.formData.first_class_id || '';
submitData.second_class_id = this.formData.second_class_id || '';
submitData.server_kind = this.formData.server_kind || getApp().globalData.artisanType; submitData.server_kind = this.formData.server_kind || getApp().globalData.artisanType;
}
//
if (this.formData.video) {
submitData.video = this.formData.video;
}
// //
for (let i = 0; i < this.formFields.length; i++) { for (let i = 0; i < this.formFields.length; i++) {
@ -698,12 +948,20 @@ export default {
if (field._hasOriginalKey) { if (field._hasOriginalKey) {
// key // key
// URL
if (field.type === 'image_multi' && Array.isArray(value)) {
submitData[field.key] = value.map(function (item) {
return typeof item === 'string' ? item : (item && item.url ? item.url : item);
});
} else {
submitData[field.key] = value; submitData[field.key] = value;
}
} else { } else {
// keyintro // keyintro
introArr.push({ introArr.push({
id: field._original ? field._original.id : field.key, id: field._original ? field._original.id : field.key,
title: field.label, title: field.label,
type: field._fieldType || field.type,
value: value, value: value,
}); });
} }
@ -714,6 +972,31 @@ export default {
submitData.intro = JSON.stringify(introArr); submitData.intro = JSON.stringify(introArr);
} }
// keykeyintro
if (this.formData.process && this.formData.process.length > 0) {
const processArr = this.formData.process
.filter(item => item.text && item.text.trim())
.map(item => ({ text: item.text.trim() }));
if (processArr.length > 0) {
if (this.processFieldKey) {
submitData[this.processFieldKey] = JSON.stringify(processArr);
} else {
let introList = [];
try {
introList = submitData.intro ? JSON.parse(submitData.intro) : [];
} catch (e) {
introList = [];
}
introList.push({
id: 'service_step',
title: '服务流程',
value: processArr,
});
submitData.intro = JSON.stringify(introList);
}
}
}
return submitData; return submitData;
}, },
@ -742,6 +1025,14 @@ export default {
} }
console.log(this.formData, '-0-0-0-0-0-'); console.log(this.formData, '-0-0-0-0-0-');
const submitData = this.buildSubmitData(); const submitData = this.buildSubmitData();
submitData.publish_user_id = this.formData.publish_user_id;
submitData.second_class_id = this.formData.second_class_id;
submitData.photo = Object.values(this.formData.photo || {});
console.log(submitData, 'submitData');
submitData.publish_user_id = this.postid
submitData.server_kind = 2
submitData.first_class_id = this.formData.first_class_id
request.post(url, submitData).then((res) => { request.post(url, submitData).then((res) => {
if (res.state == 1) { if (res.state == 1) {
const pages = getCurrentPages(); const pages = getCurrentPages();
@ -1312,4 +1603,19 @@ export default {
color: #e8101e; color: #e8101e;
border: 1rpx #e8101e solid; border: 1rpx #e8101e solid;
} }
.video-upload {
background-color: #fff;
padding: 24rpx;
margin-bottom: 24rpx;
}
.flex-c {
display: flex;
align-items: center;
}
.flex {
display: flex;
}
</style> </style>