修复bug,优化流程
This commit is contained in:
parent
6b659a6878
commit
0e9bca8d97
|
|
@ -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 模板编译校验。
|
||||||
|
|
@ -3,10 +3,15 @@
|
||||||
<view class="preview-container" :style="'width:'+width">
|
<view class="preview-container" :style="'width:'+width">
|
||||||
<!-- 预览区域 -->
|
<!-- 预览区域 -->
|
||||||
<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"
|
||||||
|
|
@ -20,7 +25,7 @@
|
||||||
</view>
|
</view>
|
||||||
<!-- 上传按钮 -->
|
<!-- 上传按钮 -->
|
||||||
<view class="preview-item">
|
<view class="preview-item">
|
||||||
<view
|
<view
|
||||||
class="upload-btn"
|
class="upload-btn"
|
||||||
@click="handleUpload"
|
@click="handleUpload"
|
||||||
:style="btnStyle"
|
:style="btnStyle"
|
||||||
|
|
@ -31,12 +36,12 @@
|
||||||
</slot>
|
</slot>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 视频预览区域 -->
|
<!-- 视频预览区域 -->
|
||||||
<view class="preview-container" :style="'width:'+width" v-if="accept == 'video/*'">
|
<view class="preview-container" :style="'width:'+width" v-if="accept == 'video/*'">
|
||||||
<view
|
<view
|
||||||
class="preview-item"
|
class="preview-item"
|
||||||
v-if="oneFile"
|
v-if="oneFile"
|
||||||
>
|
>
|
||||||
|
|
@ -52,7 +57,7 @@
|
||||||
</view>
|
</view>
|
||||||
<view class="preview-item">
|
<view class="preview-item">
|
||||||
<!-- 上传按钮 -->
|
<!-- 上传按钮 -->
|
||||||
<view
|
<view
|
||||||
class="upload-btn"
|
class="upload-btn"
|
||||||
@click="handleUpload"
|
@click="handleUpload"
|
||||||
:style="btnStyle"
|
:style="btnStyle"
|
||||||
|
|
@ -67,7 +72,7 @@
|
||||||
</view>
|
</view>
|
||||||
<!-- 提示文字 -->
|
<!-- 提示文字 -->
|
||||||
<!-- <text v-if="tips" class="tips-text">{{ tips }}</text> -->
|
<!-- <text v-if="tips" class="tips-text">{{ tips }}</text> -->
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -77,7 +82,7 @@ import permissionUtils from '../../utils/per'
|
||||||
import locationService from '../../utils/locationService';
|
import locationService from '../../utils/locationService';
|
||||||
export default {
|
export default {
|
||||||
name: 'AliOssUploader',
|
name: 'AliOssUploader',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
// 已上传文件列表
|
// 已上传文件列表
|
||||||
value: {
|
value: {
|
||||||
|
|
@ -145,17 +150,24 @@ export default {
|
||||||
kind: {
|
kind: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1
|
default: 1
|
||||||
|
},
|
||||||
|
sortable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
fileList: [],
|
fileList: [],
|
||||||
oneFile: '',
|
oneFile: '',
|
||||||
loading: false
|
loading: false,
|
||||||
|
dragIndex: -1,
|
||||||
|
isDragging: false,
|
||||||
|
suppressPreview: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
value: {
|
value: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
|
|
@ -170,9 +182,9 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
// 显示权限说明弹窗
|
// 显示权限说明弹窗
|
||||||
showPermissionDialog(title, content) {
|
showPermissionDialog(title, content) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -332,14 +332,14 @@ export default {
|
||||||
this.$emit('success', newFile)
|
this.$emit('success', newFile)
|
||||||
return newFile
|
return newFile
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('上传失败:', error)
|
console.error('上传失败:', error)
|
||||||
this.$emit('error', error)
|
this.$emit('error', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取OSS配置
|
// 获取OSS配置
|
||||||
async getOssConfig() {
|
async getOssConfig() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -350,10 +350,30 @@ export default {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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,16 +381,20 @@ 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);
|
||||||
// file必须为最后一个表单域,除file以外的其他表单域无顺序要求。
|
// file必须为最后一个表单域,除file以外的其他表单域无顺序要求。
|
||||||
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
|
||||||
|
|
||||||
// #ifdef APP-PLUS || MP-WEIXIN
|
// #ifdef APP-PLUS || MP-WEIXIN
|
||||||
let formData = {};
|
let formData = {};
|
||||||
formData.name = name;
|
formData.name = name;
|
||||||
|
|
@ -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,29 +421,83 @@ 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
|
||||||
})
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 预览图片
|
// 预览图片
|
||||||
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({
|
||||||
|
|
@ -440,7 +518,7 @@ export default {
|
||||||
this.$emit('change', this.oneFile)
|
this.$emit('change', this.oneFile)
|
||||||
this.$emit('delete', deletedFile)
|
this.$emit('delete', deletedFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -525,4 +608,4 @@ export default {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -1,11 +1,26 @@
|
||||||
<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">
|
||||||
<view v-else class="video-wrapper">
|
<video :src="item" class="preview-video" controls />
|
||||||
<video :src="currentValue" class="preview-video" controls />
|
<view class="delete-btn" @click="deleteVideo(index)">
|
||||||
<view class="delete-btn" @click="deleteVideo">
|
<text class="delete-icon">×</text>
|
||||||
<text class="delete-icon">×</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">
|
||||||
|
<video :src="currentValue" class="preview-video" controls />
|
||||||
|
<view class="delete-btn" @click="deleteSingleVideo">
|
||||||
|
<text class="delete-icon">×</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -10,25 +10,26 @@
|
||||||
<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="
|
||||||
!(index == 0 && item2.id == 2) &&
|
!(index == 0 && item2.id == 2) &&
|
||||||
!(index == list.length - 1 && item2.id == 3)
|
!(index == list.length - 1 && item2.id == 3)
|
||||||
">
|
">
|
||||||
{{ item2.text }}
|
{{ item2.text }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="bottom-buttons" v-if="list.length != 0">
|
<view class="bottom-buttons" v-if="list.length != 0">
|
||||||
<button class="btn-submit" @click="submitForm">保存</button>
|
<button class="btn-submit" @click="submitForm">保存</button>
|
||||||
</view>
|
</view>
|
||||||
<uv-overlay :show="showPop" @click="showPop = false">
|
<uv-overlay :show="showPop" @click="showPop = false">
|
||||||
<view class="pop-cards">
|
<view class="pop-cards">
|
||||||
<view class="pop-cards-card" v-for="item in addWays" :key="item.id" @click.stop="addContent(item.id)">
|
<view class="pop-cards-card" v-for="item in addWays" :key="item.id" @click.stop="addContent(item.id)">
|
||||||
|
|
@ -60,424 +61,427 @@
|
||||||
</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,
|
||||||
list: [],
|
list: [],
|
||||||
textareaValue: "",
|
textareaValue: "",
|
||||||
showPop: false,
|
showPop: false,
|
||||||
showTextarea: false,
|
showTextarea: false,
|
||||||
addWays: [{
|
addWays: [{
|
||||||
text: "分割线",
|
text: "分割线",
|
||||||
id: 1,
|
id: 1,
|
||||||
url: "/static/images/shop/imgAndText/fenge.png",
|
url: "/static/images/shop/imgAndText/fenge.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "文本",
|
text: "文本",
|
||||||
id: 2,
|
id: 2,
|
||||||
url: "/static/images/shop/imgAndText/text.png",
|
url: "/static/images/shop/imgAndText/text.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "图片",
|
text: "图片",
|
||||||
id: 3,
|
id: 3,
|
||||||
url: "/static/images/shop/imgAndText/bz.png",
|
url: "/static/images/shop/imgAndText/bz.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "相机",
|
text: "相机",
|
||||||
id: 4,
|
id: 4,
|
||||||
url: "/static/images/shop/imgAndText/camera.png",
|
url: "/static/images/shop/imgAndText/camera.png",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
btns: [{
|
btns: [{
|
||||||
text: "删除",
|
text: "删除",
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "上移",
|
text: "上移",
|
||||||
id: 2,
|
id: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "下移",
|
text: "下移",
|
||||||
id: 3,
|
id: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "替换",
|
text: "替换",
|
||||||
id: 4,
|
id: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "插入",
|
text: "插入",
|
||||||
id: 5,
|
id: 5,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
console.log(options)
|
||||||
|
if (options.list && options.list !== 'null' && options.list !== 'undefined') {
|
||||||
|
try {
|
||||||
|
this.list = JSON.parse(options.list) || [];
|
||||||
|
} catch (e) {
|
||||||
|
this.list = [];
|
||||||
|
}
|
||||||
|
console.log(this.list)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submitForm() {
|
||||||
|
uni.$emit('updateGraphicDetails', this.list);
|
||||||
|
uni.navigateBack()
|
||||||
},
|
},
|
||||||
onLoad(options) {
|
// 定义一个函数,在数组的指定索引后插入数据
|
||||||
console.log(options)
|
insertAfterIndex(arr, index, data) {
|
||||||
if(options.list){
|
// 检查索引是否有效
|
||||||
this.list = JSON.parse(options.list)
|
if (index < 0 || index >= arr.length) {
|
||||||
console.log(this.list)
|
console.error("索引无效");
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用splice方法在index + 1的位置插入数据
|
||||||
|
// 第二个参数0表示不删除任何元素,后面的参数是要插入的数据
|
||||||
|
arr.splice(index + 1, 0, data);
|
||||||
|
this.crIndex = null
|
||||||
|
return arr;
|
||||||
|
},
|
||||||
|
// 定义一个函数,替换数组指定索引处的数据
|
||||||
|
replaceAtIndex(arr, index, data) {
|
||||||
|
// 检查索引是否有效(有效索引范围:0 <= index < arr.length)
|
||||||
|
if (index < 0 || index >= arr.length) {
|
||||||
|
console.error("索引无效");
|
||||||
|
return arr; // 索引无效时返回原数组
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接替换指定索引处的数据
|
||||||
|
arr[index] = data;
|
||||||
|
|
||||||
|
// 保持与插入方法一致的业务逻辑(如果crIndex是你的业务变量)
|
||||||
|
this.crIndex = null;
|
||||||
|
this.isReplace = false
|
||||||
|
|
||||||
|
return arr; // 返回修改后的数组
|
||||||
|
},
|
||||||
|
//写入文本
|
||||||
|
setTextarea() {
|
||||||
|
let obj = {
|
||||||
|
value: this.textareaValue,
|
||||||
|
type: 2,
|
||||||
|
};
|
||||||
|
if (this.textareaValue.length == 0) {
|
||||||
|
this.showTextarea = false
|
||||||
|
this.crIndex = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.crIndex != null) {
|
||||||
|
if (this.isReplace) {
|
||||||
|
this.replaceAtIndex(this.list, this.crIndex, obj);
|
||||||
|
} else {
|
||||||
|
this.insertAfterIndex(this.list, this.crIndex, obj);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.list.push(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.textareaValue = ""
|
||||||
|
this.showTextarea = false
|
||||||
|
},
|
||||||
|
//操作卡片 删除,上移等
|
||||||
|
handleCard(id, index) {
|
||||||
|
switch (id) {
|
||||||
|
case 1:
|
||||||
|
this.list.splice(index, 1); // 从索引 index 开始,删除 1 个元素
|
||||||
|
this.showPop = false
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// 保存要交换的两个元素
|
||||||
|
let temp = this.list[index - 1];
|
||||||
|
|
||||||
|
// 用 splice 交换:先替换 index-1 位置为 index 的值,再替换 index 位置为 temp
|
||||||
|
this.list.splice(index - 1, 1, this.list[index]); // 把 index-1 位置换成 index 的值
|
||||||
|
this.list.splice(index, 1, temp); // 把 index 位置换成原来 index-1 的值
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// 保存要交换的两个元素
|
||||||
|
let temp2 = this.list[index + 1];
|
||||||
|
|
||||||
|
// 用 splice 交换:先替换 index-1 位置为 index 的值,再替换 index 位置为 temp
|
||||||
|
this.list.splice(index + 1, 1, this.list[index]); // 把 index-1 位置换成 index 的值
|
||||||
|
this.list.splice(index, 1, temp2); // 把 index 位置换成原来 index-1 的值
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
this.crIndex = index
|
||||||
|
this.isReplace = true
|
||||||
|
this.showPop = true
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
this.crIndex = index
|
||||||
|
this.showPop = true
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
addContent(id) {
|
||||||
submitForm(){
|
switch (id) {
|
||||||
uni.$emit('updateGraphicDetails', this.list);
|
case 1:
|
||||||
uni.navigateBack()
|
let obj = {
|
||||||
},
|
value: ``,
|
||||||
// 定义一个函数,在数组的指定索引后插入数据
|
type: 1,
|
||||||
insertAfterIndex(arr, index, data) {
|
};
|
||||||
// 检查索引是否有效
|
if (this.crIndex != null) {
|
||||||
if (index < 0 || index >= arr.length) {
|
if (this.isReplace) {
|
||||||
console.error("索引无效");
|
this.replaceAtIndex(this.list, this.crIndex, obj);
|
||||||
return arr;
|
} else {
|
||||||
}
|
this.insertAfterIndex(this.list, this.crIndex, obj);
|
||||||
|
}
|
||||||
|
|
||||||
// 使用splice方法在index + 1的位置插入数据
|
} else {
|
||||||
// 第二个参数0表示不删除任何元素,后面的参数是要插入的数据
|
this.list.push(obj);
|
||||||
arr.splice(index + 1, 0, data);
|
}
|
||||||
this.crIndex = null
|
this.showPop = false
|
||||||
return arr;
|
break;
|
||||||
},
|
case 2:
|
||||||
// 定义一个函数,替换数组指定索引处的数据
|
this.showTextarea = true
|
||||||
replaceAtIndex(arr, index, data) {
|
this.showPop = false
|
||||||
// 检查索引是否有效(有效索引范围:0 <= index < arr.length)
|
break;
|
||||||
if (index < 0 || index >= arr.length) {
|
case 3:
|
||||||
console.error("索引无效");
|
this.chooseAvatar(3)
|
||||||
return arr; // 索引无效时返回原数组
|
break;
|
||||||
}
|
case 4:
|
||||||
|
this.chooseAvatar(4)
|
||||||
// 直接替换指定索引处的数据
|
|
||||||
arr[index] = data;
|
}
|
||||||
|
},
|
||||||
// 保持与插入方法一致的业务逻辑(如果crIndex是你的业务变量)
|
async chooseAvatar(type) {
|
||||||
this.crIndex = null;
|
try {
|
||||||
this.isReplace = false
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
if (systemInfo.platform === 'ios') {
|
||||||
return arr; // 返回修改后的数组
|
this.openCamera(type);
|
||||||
},
|
|
||||||
//写入文本
|
|
||||||
setTextarea() {
|
|
||||||
let obj = {
|
|
||||||
value: this.textareaValue,
|
|
||||||
type: 2,
|
|
||||||
};
|
|
||||||
if (this.textareaValue.length == 0) {
|
|
||||||
this.showTextarea = false
|
|
||||||
this.crIndex = null
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.crIndex!=null) {
|
// 根据类型确定权限字符串和提示文本
|
||||||
if(this.isReplace){
|
const permissionKey = type === 3 ? 'photo_library' : 'camera';
|
||||||
this.replaceAtIndex(this.list, this.crIndex, obj);
|
const permissionAndroid = type === 3
|
||||||
}else{
|
? 'android.permission.READ_EXTERNAL_STORAGE,android.permission.WRITE_EXTERNAL_STORAGE'
|
||||||
this.insertAfterIndex(this.list, this.crIndex, obj);
|
: 'android.permission.CAMERA';
|
||||||
}
|
const dialogTitle = type === 3 ? '相册权限申请' : '相机权限申请';
|
||||||
} else {
|
const dialogContent = type === 3
|
||||||
this.list.push(obj);
|
? '我们需要访问您的相册以完成图片添加'
|
||||||
|
: '我们需要访问您的相机以完成图片拍摄';
|
||||||
|
const nowQer = type === 3 ? 'xc' : 'xj';
|
||||||
|
|
||||||
|
// 检查是否是首次请求(用于控制自定义视图显示)
|
||||||
|
const firstRequest = !plus.storage.getItem(`perm_${permissionKey}`);
|
||||||
|
if (firstRequest) {
|
||||||
|
this.isShowPer = true;
|
||||||
|
this.nowQer = nowQer;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.textareaValue = ""
|
// 1. 检查权限
|
||||||
this.showTextarea = false
|
const { granted } = await permissionUtils.checkPermission(permissionKey, dialogContent);
|
||||||
},
|
if (granted) {
|
||||||
//操作卡片 删除,上移等
|
|
||||||
handleCard(id, index) {
|
|
||||||
switch (id) {
|
|
||||||
case 1:
|
|
||||||
this.list.splice(index, 1); // 从索引 index 开始,删除 1 个元素
|
|
||||||
this.showPop = false
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
// 保存要交换的两个元素
|
|
||||||
let temp = this.list[index - 1];
|
|
||||||
|
|
||||||
// 用 splice 交换:先替换 index-1 位置为 index 的值,再替换 index 位置为 temp
|
|
||||||
this.list.splice(index - 1, 1, this.list[index]); // 把 index-1 位置换成 index 的值
|
|
||||||
this.list.splice(index, 1, temp); // 把 index 位置换成原来 index-1 的值
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
// 保存要交换的两个元素
|
|
||||||
let temp2 = this.list[index + 1];
|
|
||||||
|
|
||||||
// 用 splice 交换:先替换 index-1 位置为 index 的值,再替换 index 位置为 temp
|
|
||||||
this.list.splice(index + 1, 1, this.list[index]); // 把 index-1 位置换成 index 的值
|
|
||||||
this.list.splice(index, 1, temp2); // 把 index 位置换成原来 index-1 的值
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
this.crIndex = index
|
|
||||||
this.isReplace = true
|
|
||||||
this.showPop = true
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
this.crIndex = index
|
|
||||||
this.showPop = true
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addContent(id) {
|
|
||||||
switch (id) {
|
|
||||||
case 1:
|
|
||||||
let obj = {
|
|
||||||
value: ``,
|
|
||||||
type: 1,
|
|
||||||
};
|
|
||||||
if (this.crIndex!=null) {
|
|
||||||
if(this.isReplace){
|
|
||||||
this.replaceAtIndex(this.list, this.crIndex, obj);
|
|
||||||
}else{
|
|
||||||
this.insertAfterIndex(this.list, this.crIndex, obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.list.push(obj);
|
|
||||||
}
|
|
||||||
this.showPop = false
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
this.showTextarea = true
|
|
||||||
this.showPop = false
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
this.chooseAvatar(3)
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
this.chooseAvatar(4)
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async chooseAvatar(type) {
|
|
||||||
try {
|
|
||||||
const systemInfo = uni.getSystemInfoSync()
|
|
||||||
if(systemInfo.platform === 'ios') {
|
|
||||||
this.openCamera(type);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 根据类型确定权限字符串和提示文本
|
|
||||||
const permissionKey = type === 3 ? 'photo_library' : 'camera';
|
|
||||||
const permissionAndroid = type === 3
|
|
||||||
? 'android.permission.READ_EXTERNAL_STORAGE,android.permission.WRITE_EXTERNAL_STORAGE'
|
|
||||||
: 'android.permission.CAMERA';
|
|
||||||
const dialogTitle = type === 3 ? '相册权限申请' : '相机权限申请';
|
|
||||||
const dialogContent = type === 3
|
|
||||||
? '我们需要访问您的相册以完成图片添加'
|
|
||||||
: '我们需要访问您的相机以完成图片拍摄';
|
|
||||||
const nowQer = type === 3 ? 'xc' : 'xj';
|
|
||||||
|
|
||||||
// 检查是否是首次请求(用于控制自定义视图显示)
|
|
||||||
const firstRequest = !plus.storage.getItem(`perm_${permissionKey}`);
|
|
||||||
if (firstRequest) {
|
|
||||||
this.isShowPer = true;
|
|
||||||
this.nowQer = nowQer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 检查权限
|
|
||||||
const { granted } = await permissionUtils.checkPermission(permissionKey, dialogContent);
|
|
||||||
if (granted) {
|
|
||||||
this.isShowPer = false;
|
|
||||||
this.openCamera(type);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 隐藏自定义视图,显示系统风格弹窗
|
|
||||||
this.isShowPer = false;
|
this.isShowPer = false;
|
||||||
const confirm = await this.showPermissionDialog(dialogTitle, dialogContent);
|
this.openCamera(type);
|
||||||
if (!confirm) return;
|
return;
|
||||||
|
|
||||||
// 3. 请求权限
|
|
||||||
const result = await permissionUtils.requestPermission(permissionKey, dialogContent);
|
|
||||||
if (result) {
|
|
||||||
this.openCamera(type);
|
|
||||||
} else {
|
|
||||||
locationService.openAppSettings();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('权限处理出错:', error);
|
|
||||||
uni.showToast({
|
|
||||||
title: '权限检查失败',
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
// 显示权限说明弹窗
|
|
||||||
showPermissionDialog(title, content) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
uni.showModal({
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
confirmText: '去开启',
|
|
||||||
success(res) {
|
|
||||||
resolve(res.confirm);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// 打开相机/相册
|
|
||||||
async openCamera(type) {
|
|
||||||
this.isShowPer = false;
|
|
||||||
let result = null
|
|
||||||
if (type == 3) {
|
|
||||||
result = await uploadImage.handleUpload(this.$store.state.sjInfo.id ? this.$store.state.sjInfo.id :
|
|
||||||
Math.floor(Math.random() * 1000), 1, ['album']);
|
|
||||||
} else {
|
|
||||||
result = await uploadImage.handleUpload(this.$store.state.sjInfo.id ? this.$store.state.sjInfo.id :
|
|
||||||
Math.floor(Math.random() * 1000), 1, ['camera']);
|
|
||||||
}
|
|
||||||
uni.hideLoading();
|
|
||||||
// this.userInfo.head_photo = result;
|
|
||||||
let obj = {
|
|
||||||
value: result,
|
|
||||||
type: type,
|
|
||||||
};
|
|
||||||
if (this.crIndex!=null) {
|
|
||||||
if(this.isReplace){
|
|
||||||
this.replaceAtIndex(this.list, this.crIndex, obj);
|
|
||||||
}else{
|
|
||||||
this.insertAfterIndex(this.list, this.crIndex, obj);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.list.push(obj);
|
|
||||||
}
|
|
||||||
this.showPop = false
|
|
||||||
},
|
|
||||||
|
|
||||||
|
// 2. 隐藏自定义视图,显示系统风格弹窗
|
||||||
|
this.isShowPer = false;
|
||||||
|
const confirm = await this.showPermissionDialog(dialogTitle, dialogContent);
|
||||||
|
if (!confirm) return;
|
||||||
|
|
||||||
|
// 3. 请求权限
|
||||||
|
const result = await permissionUtils.requestPermission(permissionKey, dialogContent);
|
||||||
|
if (result) {
|
||||||
|
this.openCamera(type);
|
||||||
|
} else {
|
||||||
|
locationService.openAppSettings();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('权限处理出错:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: '权限检查失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
// 显示权限说明弹窗
|
||||||
|
showPermissionDialog(title, content) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
uni.showModal({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
confirmText: '去开启',
|
||||||
|
success(res) {
|
||||||
|
resolve(res.confirm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 打开相机/相册
|
||||||
|
async openCamera(type) {
|
||||||
|
this.isShowPer = false;
|
||||||
|
let result = null
|
||||||
|
if (type == 3) {
|
||||||
|
result = await uploadImage.handleUpload(this.$store.state.sjInfo.id ? this.$store.state.sjInfo.id :
|
||||||
|
Math.floor(Math.random() * 1000), 1, ['album']);
|
||||||
|
} else {
|
||||||
|
result = await uploadImage.handleUpload(this.$store.state.sjInfo.id ? this.$store.state.sjInfo.id :
|
||||||
|
Math.floor(Math.random() * 1000), 1, ['camera']);
|
||||||
|
}
|
||||||
|
uni.hideLoading();
|
||||||
|
// this.userInfo.head_photo = result;
|
||||||
|
let obj = {
|
||||||
|
value: result,
|
||||||
|
type: type,
|
||||||
|
};
|
||||||
|
if (this.crIndex != null) {
|
||||||
|
if (this.isReplace) {
|
||||||
|
this.replaceAtIndex(this.list, this.crIndex, obj);
|
||||||
|
} else {
|
||||||
|
this.insertAfterIndex(this.list, this.crIndex, obj);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.list.push(obj);
|
||||||
|
}
|
||||||
|
this.showPop = false
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.addTextAndImg {
|
.addTextAndImg {
|
||||||
padding-bottom: 110rpx;
|
padding-bottom: 110rpx;
|
||||||
.firstAdd {
|
|
||||||
position: fixed;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate3d(-50%, -50%, 0);
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.firstAdd-garden {
|
.firstAdd {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate3d(-50%, -50%, 0);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.firstAdd-garden {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border: 3px solid #000;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-bottom: 100rpx;
|
||||||
|
|
||||||
|
.firstAdd-garden-img {
|
||||||
width: 200rpx;
|
width: 200rpx;
|
||||||
height: 200rpx;
|
|
||||||
border: 3px solid #000;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-bottom: 100rpx;
|
|
||||||
.firstAdd-garden-img{
|
|
||||||
width: 200rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.firstAdd-text {
|
|
||||||
font-size: 32rpx;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 750rpx;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.addTextAndImg-content {
|
.firstAdd-text {
|
||||||
.addTextAndImg-content-card {
|
font-size: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 750rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.addTextAndImg-content-card-btns {
|
.addTextAndImg-content {
|
||||||
|
.addTextAndImg-content-card {
|
||||||
|
|
||||||
|
.addTextAndImg-content-card-btns {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10rpx;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding-bottom: 10rpx;
|
||||||
|
|
||||||
|
.addTextAndImg-content-card-btns-btn {
|
||||||
|
color: rgba(232, 16, 30, 1);
|
||||||
|
border: 2rpx solid rgba(232, 16, 30, 1);
|
||||||
|
width: 80rpx;
|
||||||
|
height: 50rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: center;
|
||||||
gap: 10rpx;
|
|
||||||
background: #f5f5f5;
|
|
||||||
padding-bottom: 10rpx;
|
|
||||||
|
|
||||||
.addTextAndImg-content-card-btns-btn {
|
|
||||||
color: rgba(232, 16, 30, 1);
|
|
||||||
border: 2rpx solid rgba(232, 16, 30, 1);
|
|
||||||
width: 80rpx;
|
|
||||||
height: 50rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.addTextAndImg-content-card-text {
|
|
||||||
padding: 20rpx;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.pop-cards {
|
.addTextAndImg-content-card-text {
|
||||||
z-index: 10;
|
padding: 20rpx;
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: #fff;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: 63rpx;
|
|
||||||
padding: 68rpx 40rpx;
|
|
||||||
|
|
||||||
.pop-cards-card {
|
|
||||||
.pop-cards-card-img {
|
|
||||||
width: 120rpx;
|
|
||||||
height: 120rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pop-cards-card-text {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #333333;
|
|
||||||
line-height: 40rpx;
|
|
||||||
text-align: center;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pop-textarea {
|
|
||||||
background-color: #fff;
|
|
||||||
margin: 200rpx 20rpx 20rpx 20rpx;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 20rpx;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.permission.transform {
|
.pop-cards {
|
||||||
top: calc(var(--status-bar-height) + 88rpx + 20rpx);
|
z-index: 10;
|
||||||
opacity: 1;
|
position: absolute;
|
||||||
visibility: visible;
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 63rpx;
|
||||||
|
padding: 68rpx 40rpx;
|
||||||
|
|
||||||
|
.pop-cards-card {
|
||||||
|
.pop-cards-card-img {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pop-cards-card-text {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-buttons {
|
.pop-textarea {
|
||||||
position: fixed;
|
background-color: #fff;
|
||||||
left: 0;
|
margin: 200rpx 20rpx 20rpx 20rpx;
|
||||||
right: 0;
|
border-radius: 20rpx;
|
||||||
bottom: 0;
|
padding: 20rpx;
|
||||||
background-color: #fff;
|
}
|
||||||
height: 100rpx;
|
}
|
||||||
padding: 40rpx 0 64rpx;
|
|
||||||
box-sizing: border-box;
|
.permission.transform {
|
||||||
display: flex;
|
top: calc(var(--status-bar-height) + 88rpx + 20rpx);
|
||||||
flex-flow: row nowrap;
|
opacity: 1;
|
||||||
justify-content: space-between;
|
visibility: visible;
|
||||||
align-items: center;
|
}
|
||||||
.btn-submit {
|
|
||||||
|
.bottom-buttons {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
height: 100rpx;
|
||||||
|
padding: 40rpx 0 64rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
background-color: rgba(232, 16, 30, 1);
|
background-color: rgba(232, 16, 30, 1);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
width: 334rpx;
|
width: 334rpx;
|
||||||
height: 78rpx;
|
height: 78rpx;
|
||||||
line-height: 78rpx;
|
line-height: 78rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
border-radius: 78rpx;
|
border-radius: 78rpx;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -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_data中的key,用作提交参数名
|
||||||
|
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) {
|
|
||||||
// 监听图文详情编辑回调(在onLoad注册,确保navigateBack时能收到事件)
|
// 监听图文详情编辑回调(在onLoad注册,确保navigateBack时能收到事件)
|
||||||
uni.$on("updateGraphicDetails", (data) => {
|
uni.$on("updateGraphicDetails", (data) => {
|
||||||
// 替换整个formData对象引用,确保dynamic-form的watch.value能触发
|
// 替换整个formData对象引用,确保dynamic-form的watch.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_step类型的字段,记录其key作为提交参数名
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 解析intro字段的JSON数据用于回显自定义字段
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 用intro中的id去匹配表单字段,将值回填到formData
|
||||||
|
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) {
|
||||||
|
// 无独立key时,从intro数组中查找service_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 {
|
||||||
|
// 接口没有返回id,从storage获取
|
||||||
|
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.server_kind = this.formData.server_kind || getApp().globalData.artisanType;
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 视频字段(独立上传组件)
|
||||||
|
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的字段直接作为请求参数
|
||||||
submitData[field.key] = value;
|
// 图片字段确保为纯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;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 没有key的字段放入intro数组
|
// 没有key的字段放入intro数组
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 服务步骤:有key时作为独立参数,无key时放入intro数组
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
.video-upload {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-c {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue