服务发布
This commit is contained in:
parent
3e39ff1140
commit
573aef3210
|
|
@ -15,7 +15,6 @@
|
|||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script>
|
||||
// 引入 spark-md5 计算文件哈希
|
||||
import SparkMD5 from 'spark-md5'
|
||||
|
|
@ -42,7 +41,7 @@ export default {
|
|||
maxSize: {
|
||||
type: Number,
|
||||
default: 500
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -53,7 +52,7 @@ export default {
|
|||
fileMd5: '',
|
||||
cancelToken: null
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 选择视频文件
|
||||
async chooseVideo() {
|
||||
|
|
@ -319,7 +318,7 @@ export default {
|
|||
chunkIndex,
|
||||
fileMd5: this.fileMd5,
|
||||
chunkSize: this.chunkSize,
|
||||
totalChunks: Math.ceil(this.file.size / this.chunkSize))
|
||||
totalChunks: Math.ceil(this.file.size / this.chunkSize)
|
||||
},
|
||||
header: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
|
|
@ -411,4 +410,4 @@ button {
|
|||
color: #666;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
v-for="(file, index) in videoList"
|
||||
:key="file || index"
|
||||
class="preview-item"
|
||||
@click="previewVideo(file)"
|
||||
>
|
||||
<!-- <video
|
||||
:src="file"
|
||||
|
|
@ -57,6 +58,11 @@
|
|||
@click="handlePreview(index)"
|
||||
/> -->
|
||||
<image class="preview-image" :src="getVideoPoster(file)" mode="aspectFill"></image>
|
||||
<view class="video-play-mask">
|
||||
<view class="video-play-circle">
|
||||
<view class="video-play-icon"></view>
|
||||
</view>
|
||||
</view>
|
||||
<image
|
||||
v-if="deletable"
|
||||
class="delete-icon"
|
||||
|
|
@ -81,6 +87,14 @@
|
|||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="showVideoPlayer" class="video-player-mask" @click="closeVideoPlayer">
|
||||
<view class="video-player-box" @click.stop>
|
||||
<video :src="previewVideoUrl" class="video-player" controls autoplay show-fullscreen-btn></video>
|
||||
<view class="video-player-close" @click="closeVideoPlayer">
|
||||
<text class="video-player-close-text">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 提示文字 -->
|
||||
<!-- <text v-if="tips" class="tips-text">{{ tips }}</text> -->
|
||||
|
||||
|
|
@ -97,12 +111,12 @@ export default {
|
|||
props: {
|
||||
// 已上传文件列表
|
||||
value: {
|
||||
type: Array,
|
||||
type: [Array, String, Object],
|
||||
default: () => []
|
||||
},
|
||||
// 单个上传文件
|
||||
fileString: {
|
||||
type: String,
|
||||
type: [String, Object, Array],
|
||||
default: ''
|
||||
},
|
||||
// 按钮文字
|
||||
|
|
@ -173,10 +187,12 @@ export default {
|
|||
fileList: [],
|
||||
oneFile: '',
|
||||
videoList: [],
|
||||
loading: false,
|
||||
loading: false,
|
||||
dragIndex: -1,
|
||||
isDragging: false,
|
||||
suppressPreview: false
|
||||
suppressPreview: false,
|
||||
showVideoPlayer: false,
|
||||
previewVideoUrl: ''
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -190,19 +206,57 @@ export default {
|
|||
value: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
this.fileList = [...newVal]
|
||||
if (this.accept === 'video/*') {
|
||||
const videoValue = this.normalizeVideoValue(newVal)
|
||||
this.oneFile = videoValue
|
||||
this.videoList = videoValue ? [videoValue] : []
|
||||
return
|
||||
}
|
||||
this.fileList = Array.isArray(newVal) ? [...newVal] : []
|
||||
}
|
||||
},
|
||||
fileString: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
this.oneFile = newVal
|
||||
this.videoList = newVal ? [newVal] : []
|
||||
const videoValue = this.normalizeVideoValue(newVal)
|
||||
this.oneFile = videoValue
|
||||
this.videoList = videoValue ? [videoValue] : []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
normalizeVideoValue(value) {
|
||||
if (!value) return ''
|
||||
if (typeof value === 'string') {
|
||||
const str = value.trim()
|
||||
if (!str) return ''
|
||||
if ((str.startsWith('{') && str.endsWith('}')) || (str.startsWith('[') && str.endsWith(']'))) {
|
||||
try {
|
||||
return this.normalizeVideoValue(JSON.parse(str))
|
||||
} catch (error) {
|
||||
return str
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
const url = this.normalizeVideoValue(item)
|
||||
if (url) return url
|
||||
}
|
||||
return ''
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const candidates = [value.url, value.path, value.filePath, value.tempFilePath, value.src, value.value]
|
||||
for (const item of candidates) {
|
||||
const url = this.normalizeVideoValue(item)
|
||||
if (url) return url
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return String(value)
|
||||
},
|
||||
|
||||
// 显示权限说明弹窗
|
||||
showPermissionDialog(title, content) {
|
||||
|
|
@ -319,8 +373,9 @@ export default {
|
|||
},
|
||||
// 上传文件到OSS
|
||||
async uploadFile(file) {
|
||||
const uploadFile = await this.prepareUploadFile(file)
|
||||
// 检查文件大小
|
||||
if (file.size > this.maxSize * 1024 * 1024) {
|
||||
if (uploadFile.size > this.maxSize * 1024 * 1024) {
|
||||
uni.showToast({
|
||||
title: `文件大小不能超过${this.maxSize}MB`,
|
||||
icon: 'none'
|
||||
|
|
@ -328,7 +383,7 @@ export default {
|
|||
return Promise.reject(new Error('文件大小超出限制'))
|
||||
}
|
||||
let date = `${new Date().getTime()}_${Math.floor(Math.random() * 100000)}`
|
||||
const fileExt = this.getFileExt(file)
|
||||
const fileExt = this.getFileExt(uploadFile)
|
||||
|
||||
let artisan = this.type==1 ? 'yh' : this.type==2 ? 'syr' : this.type==3 ? 'sj' : '';
|
||||
let name = `${this.userId}_${artisan}_${date}.${fileExt}`
|
||||
|
|
@ -337,7 +392,7 @@ export default {
|
|||
// 1. 获取OSS上传凭证
|
||||
const ossConfig = await this.getOssConfig()
|
||||
// 2. 上传文件
|
||||
const fileUrl = await this.uploadToOss(file, ossConfig, name)
|
||||
const fileUrl = await this.uploadToOss(uploadFile, ossConfig, name)
|
||||
// 3. 添加到文件列表
|
||||
if(this.accept == 'image/*') {
|
||||
const newFile = [`${fileUrl}`]
|
||||
|
|
@ -379,10 +434,90 @@ 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
|
||||
const sources = this.accept === 'video/*'
|
||||
? [file && file.tempFilePath, file && file.path, file && file.url, file && file.name]
|
||||
: [file && file.path, file && file.tempFilePath, file && file.url, file && file.name]
|
||||
for (const source of sources) {
|
||||
const cleanPath = String(source || '').split('?')[0].split('#')[0]
|
||||
const ext = cleanPath.includes('.') ? cleanPath.split('.').pop().toLowerCase() : ''
|
||||
if (/^[a-z0-9]+$/.test(ext)) return ext
|
||||
}
|
||||
return defaultExt
|
||||
},
|
||||
|
||||
getFilePath(file) {
|
||||
if (!file) return ''
|
||||
return file.tempFilePath || file.path || file.url || file.name || ''
|
||||
},
|
||||
|
||||
getMimeType(file, name) {
|
||||
const ext = this.getFileExt({ tempFilePath: name, path: name, name })
|
||||
const mimeTypes = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
mp4: 'video/mp4',
|
||||
m4v: 'video/x-m4v',
|
||||
mov: 'video/quicktime',
|
||||
qt: 'video/quicktime',
|
||||
webm: 'video/webm',
|
||||
mpeg: 'video/mpeg',
|
||||
mpg: 'video/mpeg',
|
||||
avi: 'video/x-msvideo',
|
||||
'3gp': 'video/3gpp',
|
||||
mkv: 'video/x-matroska',
|
||||
}
|
||||
return mimeTypes[ext] || (file && file.type) || ''
|
||||
},
|
||||
|
||||
buildOssFormData(ossConfig, objectKey, mimeType) {
|
||||
const formData = {
|
||||
key: objectKey,
|
||||
policy: ossConfig.policy,
|
||||
OSSAccessKeyId: ossConfig.ossAccessKeyId,
|
||||
success_action_status: '200',
|
||||
signature: ossConfig.signature,
|
||||
}
|
||||
if (mimeType) {
|
||||
formData['Content-Type'] = mimeType
|
||||
formData['x-oss-meta-content-type'] = mimeType
|
||||
}
|
||||
return formData
|
||||
},
|
||||
|
||||
async prepareUploadFile(file) {
|
||||
if (this.accept !== 'video/*') return file
|
||||
const filePath = this.getFilePath(file)
|
||||
const fileExt = this.getFileExt(file)
|
||||
if (!filePath || fileExt === 'mp4' || typeof uni.compressVideo !== 'function') {
|
||||
return file
|
||||
}
|
||||
try {
|
||||
const res = await uni.compressVideo({
|
||||
src: filePath,
|
||||
quality: 'medium'
|
||||
})
|
||||
const tempFilePath = res.tempFilePath || res.path
|
||||
if (!tempFilePath) return file
|
||||
return {
|
||||
...file,
|
||||
tempFilePath,
|
||||
path: tempFilePath,
|
||||
size: res.size || file.size,
|
||||
name: this.replaceFileExt(file.name, 'mp4'),
|
||||
type: 'video/mp4'
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('视频压缩失败,使用原文件上传:', error)
|
||||
return file
|
||||
}
|
||||
},
|
||||
|
||||
replaceFileExt(name, ext) {
|
||||
if (!name) return ''
|
||||
return String(name).replace(/\.[^.]+$/, `.${ext}`)
|
||||
},
|
||||
|
||||
buildFileUrl(ossConfig, name) {
|
||||
|
|
@ -403,6 +538,21 @@ export default {
|
|||
},
|
||||
|
||||
chooseVideoFiles(sourceType, count) {
|
||||
if (count <= 1 && typeof uni.chooseVideo === 'function') {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.chooseVideo({
|
||||
sourceType: [sourceType],
|
||||
compressed: true,
|
||||
maxDuration: 60,
|
||||
success: (res) => resolve([{
|
||||
...res,
|
||||
tempFilePath: res.tempFilePath || res.path,
|
||||
path: res.tempFilePath || res.path,
|
||||
}]),
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
if (uni.chooseMedia) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.chooseMedia({
|
||||
|
|
@ -438,14 +588,20 @@ export default {
|
|||
return new Promise((resolve, reject) => {
|
||||
const objectKey = this.buildObjectKey(ossConfig, name)
|
||||
const fileUrl = this.buildFileUrl(ossConfig, name)
|
||||
// #ifdef H5
|
||||
const formData = new FormData()
|
||||
const mimeType = this.getMimeType(file, name)
|
||||
let formData;
|
||||
// #ifdef H5
|
||||
formData = new FormData()
|
||||
formData.append('name',name);
|
||||
formData.append('policy', ossConfig.policy);
|
||||
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
|
||||
formData.append('success_action_status', '200');
|
||||
formData.append('signature', ossConfig.signature);
|
||||
formData.append('key', objectKey);
|
||||
if (mimeType) {
|
||||
formData.append('Content-Type', mimeType);
|
||||
formData.append('x-oss-meta-content-type', mimeType);
|
||||
}
|
||||
// file必须为最后一个表单域,除file以外的其他表单域无顺序要求。
|
||||
formData.append('file', file.file || file.tempFile || file);
|
||||
fetch(ossConfig.host, { method: 'POST', body: formData},).then((res) => {
|
||||
|
|
@ -459,24 +615,18 @@ export default {
|
|||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
let formData = {};
|
||||
formData.name = name;
|
||||
formData.policy = ossConfig.policy;
|
||||
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;
|
||||
formData.success_action_status = '200';
|
||||
formData.signature = ossConfig.signature;
|
||||
formData.key = objectKey;
|
||||
formData.file = file;
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
formData = this.buildOssFormData(ossConfig, objectKey, mimeType);
|
||||
formData.name = name;
|
||||
let path;
|
||||
if(this.accept == 'image/*') {
|
||||
path = file.path
|
||||
path = this.getFilePath(file)
|
||||
}else if(this.accept == 'video/*') {
|
||||
// #ifdef H5
|
||||
path = file.name
|
||||
path = this.getFilePath(file)
|
||||
// #endif
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
path = file.tempFilePath
|
||||
path = this.getFilePath(file)
|
||||
// #endif
|
||||
}
|
||||
// 使用 uni.uploadFile 上传文件(支持多平台)
|
||||
|
|
@ -484,13 +634,7 @@ export default {
|
|||
url: ossConfig.host,
|
||||
filePath: path, // 手机端文件路径
|
||||
name: 'file',
|
||||
formData: {
|
||||
key: objectKey, // 文件名
|
||||
policy: ossConfig.policy, // 后台获取超时时间
|
||||
OSSAccessKeyId: ossConfig.ossAccessKeyId, // 后台获取临时ID
|
||||
success_action_status: 200, // 让服务端返回200,不然,默认会返回204
|
||||
signature: ossConfig.signature // 后台获取签名
|
||||
},
|
||||
formData,
|
||||
success: (res) => {
|
||||
const statusCode = Number(res.statusCode)
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
|
|
@ -517,6 +661,17 @@ export default {
|
|||
})
|
||||
},
|
||||
|
||||
previewVideo(url) {
|
||||
if (!url) return
|
||||
this.previewVideoUrl = url
|
||||
this.showVideoPlayer = true
|
||||
},
|
||||
|
||||
closeVideoPlayer() {
|
||||
this.showVideoPlayer = false
|
||||
this.previewVideoUrl = ''
|
||||
},
|
||||
|
||||
startDrag(index) {
|
||||
if (!this.sortable || this.accept !== 'image/*' || this.fileList.length < 2) return
|
||||
this.dragIndex = index
|
||||
|
|
@ -662,6 +817,38 @@ export default {
|
|||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.video-play-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.18);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.video-play-circle {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.video-play-icon {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 14rpx solid transparent;
|
||||
border-bottom: 14rpx solid transparent;
|
||||
border-left: 22rpx solid #fff;
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
|
|
@ -670,4 +857,50 @@ export default {
|
|||
height: 42rpx;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.video-player-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.video-player-box {
|
||||
position: relative;
|
||||
width: 690rpx;
|
||||
background-color: #000;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
width: 690rpx;
|
||||
height: 388rpx;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.video-player-close {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
right: 12rpx;
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.video-player-close-text {
|
||||
color: #fff;
|
||||
font-size: 42rpx;
|
||||
line-height: 52rpx;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<view class="form-checkbox">
|
||||
<view v-for="(item, index) in options" :key="index" class="checkbox-item" @click="onToggle(item)">
|
||||
<view class="checkbox-dot" :class="{ active: isChecked(item) }"></view>
|
||||
<text class="checkbox-label">{{ item }}</text>
|
||||
<text class="checkbox-label">{{ getOptionLabel(item) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
|
@ -16,12 +16,37 @@ export default {
|
|||
maxCount: { type: Number, default: 0 }
|
||||
},
|
||||
methods: {
|
||||
isChecked: function (val) {
|
||||
return this.value && this.value.indexOf(val) !== -1;
|
||||
getOptionValue: function (item) {
|
||||
if (item && typeof item === 'object') {
|
||||
if (item.value !== undefined) return item.value;
|
||||
if (item.id !== undefined) return item.id;
|
||||
if (item.key !== undefined) return item.key;
|
||||
if (item.code !== undefined) return item.code;
|
||||
return item.label;
|
||||
}
|
||||
return item;
|
||||
},
|
||||
onToggle: function (val) {
|
||||
getOptionLabel: function (item) {
|
||||
if (item && typeof item === 'object') {
|
||||
var label = item.label || item.title || item.name || item.text;
|
||||
if (label !== undefined && label !== null) return label;
|
||||
return String(this.getOptionValue(item));
|
||||
}
|
||||
return item;
|
||||
},
|
||||
findValueIndex: function (list, value) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i] == value) return i;
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
isChecked: function (item) {
|
||||
return this.findValueIndex(this.value || [], this.getOptionValue(item)) !== -1;
|
||||
},
|
||||
onToggle: function (item) {
|
||||
var val = this.getOptionValue(item);
|
||||
var newValue = (this.value || []).slice();
|
||||
var index = newValue.indexOf(val);
|
||||
var index = this.findValueIndex(newValue, val);
|
||||
if (index === -1) {
|
||||
if (this.maxCount > 0 && newValue.length >= this.maxCount) {
|
||||
uni.showToast({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<ali-oss-uploader v-model="currentValue" :max-count="maxCount" :max-size="maxSize" accept="image/*" :userId="userId"
|
||||
:type="ossType" width="100%" :sortable="true" @input="onInput" @change="onChange" />
|
||||
<ali-oss-uploader v-model="currentValue" :max-count="maxCount" :max-size="maxSize" accept="image/*" :userId="computedUserId"
|
||||
:type="computedOssType" width="100%" :sortable="true" @input="onInput" @change="onChange" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -9,25 +9,29 @@ import AliOssUploader from '../ali-oss-uploader/ali-oss-uploader.vue'
|
|||
export default {
|
||||
name: 'FormItemImage',
|
||||
components: { AliOssUploader },
|
||||
props: {
|
||||
value: { type: Array, default: function () { return []; } },
|
||||
maxCount: { type: Number, default: 9 },
|
||||
maxSize: { type: Number, default: 5 }
|
||||
},
|
||||
props: {
|
||||
value: { type: Array, default: function () { return []; } },
|
||||
maxCount: { type: Number, default: 9 },
|
||||
maxSize: { type: Number, default: 5 },
|
||||
userId: { type: [Number, String], default: null },
|
||||
ossType: { type: [Number, String], default: null }
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
currentValue: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
userId: function () {
|
||||
return this.$store && this.$store.state && this.$store.state.sjInfo
|
||||
? this.$store.state.sjInfo.id : null;
|
||||
},
|
||||
ossType: function () {
|
||||
var artisanType = getApp().globalData ? getApp().globalData.artisanType : 0;
|
||||
return (artisanType || 0) + 1;
|
||||
}
|
||||
computedUserId: function () {
|
||||
if (this.userId) return this.userId;
|
||||
return this.$store && this.$store.state && this.$store.state.sjInfo
|
||||
? this.$store.state.sjInfo.id : null;
|
||||
},
|
||||
computedOssType: function () {
|
||||
if (this.ossType) return this.ossType;
|
||||
var artisanType = getApp().globalData ? getApp().globalData.artisanType : 0;
|
||||
return (artisanType || 0) + 1;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<view class="form-radio">
|
||||
<view v-for="(item, index) in options" :key="index" class="radio-item" @click="onSelect(item)">
|
||||
<view class="radio-dot" :class="{ active: value === item }"></view>
|
||||
<text class="radio-label">{{ item }}</text>
|
||||
<view class="radio-dot" :class="{ active: isActive(item) }"></view>
|
||||
<text class="radio-label">{{ getOptionLabel(item) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
|
@ -15,8 +15,29 @@ export default {
|
|||
options: { type: Array, default: function () { return []; } }
|
||||
},
|
||||
methods: {
|
||||
onSelect: function (val) {
|
||||
this.$emit('input', val);
|
||||
getOptionValue: function (item) {
|
||||
if (item && typeof item === 'object') {
|
||||
if (item.value !== undefined) return item.value;
|
||||
if (item.id !== undefined) return item.id;
|
||||
if (item.key !== undefined) return item.key;
|
||||
if (item.code !== undefined) return item.code;
|
||||
return item.label;
|
||||
}
|
||||
return item;
|
||||
},
|
||||
getOptionLabel: function (item) {
|
||||
if (item && typeof item === 'object') {
|
||||
var label = item.label || item.title || item.name || item.text;
|
||||
if (label !== undefined && label !== null) return label;
|
||||
return String(this.getOptionValue(item));
|
||||
}
|
||||
return item;
|
||||
},
|
||||
isActive: function (item) {
|
||||
return this.value == this.getOptionValue(item);
|
||||
},
|
||||
onSelect: function (item) {
|
||||
this.$emit('input', this.getOptionValue(item));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<picker :range="options" @change="onChange">
|
||||
<picker :range="displayOptions" @change="onChange">
|
||||
<view class="form-select">
|
||||
<text :class="['select-text', value ? '' : 'placeholder']">
|
||||
{{ value || placeholder }}
|
||||
<text :class="['select-text', hasValue ? '' : 'placeholder']">
|
||||
{{ hasValue ? selectedLabel : placeholder }}
|
||||
</text>
|
||||
<image src="https://mrrplus.oss-cn-beijing.aliyuncs.com/wxstaic/images/arrow_right.png" class="arrow-right"
|
||||
mode="aspectFit" />
|
||||
|
|
@ -18,12 +18,48 @@ export default {
|
|||
options: { type: Array, default: function () { return []; } },
|
||||
placeholder: { type: String, default: '请选择' }
|
||||
},
|
||||
computed: {
|
||||
displayOptions: function () {
|
||||
var self = this;
|
||||
return this.options.map(function (item) {
|
||||
return self.getOptionLabel(item);
|
||||
});
|
||||
},
|
||||
hasValue: function () {
|
||||
return this.value !== undefined && this.value !== null && this.value !== '';
|
||||
},
|
||||
selectedLabel: function () {
|
||||
var self = this;
|
||||
var selected = this.options.find(function (item) {
|
||||
return self.getOptionValue(item) == self.value;
|
||||
});
|
||||
return selected !== undefined ? this.getOptionLabel(selected) : this.value;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getOptionValue: function (item) {
|
||||
if (item && typeof item === 'object') {
|
||||
if (item.value !== undefined) return item.value;
|
||||
if (item.id !== undefined) return item.id;
|
||||
if (item.key !== undefined) return item.key;
|
||||
if (item.code !== undefined) return item.code;
|
||||
return item.label;
|
||||
}
|
||||
return item;
|
||||
},
|
||||
getOptionLabel: function (item) {
|
||||
if (item && typeof item === 'object') {
|
||||
var label = item.label || item.title || item.name || item.text;
|
||||
if (label !== undefined && label !== null) return label;
|
||||
return String(this.getOptionValue(item));
|
||||
}
|
||||
return item;
|
||||
},
|
||||
onChange: function (e) {
|
||||
var index = e.detail.value;
|
||||
var selected = this.options[index];
|
||||
if (selected !== undefined) {
|
||||
this.$emit('input', selected);
|
||||
this.$emit('input', this.getOptionValue(selected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,16 +20,47 @@
|
|||
<text class="price-tip-card">{{ group.field.tip }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="group.field.type === 'textarea'" class="detail-row">
|
||||
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
|
||||
}}</text>
|
||||
<view class="detail-textarea-wrap">
|
||||
<textarea class="detail-textarea" :value="formData[group.field.key]" :placeholder="group.field.placeholder"
|
||||
placeholder-class="placeholder" maxlength="200" @input="onInput(group.field.key, $event)" />
|
||||
<text class="detail-count">{{ (formData[group.field.key] || "").length }}/200</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="group.field.type === 'select'" class="form-item">
|
||||
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
|
||||
}}</text>
|
||||
<form-item-select :value="formData[group.field.key]" :options="group.field.options || []"
|
||||
:placeholder="group.field.placeholder" @input="onInput(group.field.key, $event)" />
|
||||
</view>
|
||||
<view v-else-if="group.field.type === 'single_select'" class="form-item" style="flex-wrap: wrap;">
|
||||
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
|
||||
}}</text>
|
||||
<view class="select-options">
|
||||
<form-item-radio :value="formData[group.field.key]" :options="group.field.options || []"
|
||||
@input="onInput(group.field.key, $event)" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="group.field.type === 'multi_select'" class="form-item" style="flex-wrap: wrap;">
|
||||
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
|
||||
}}</text>
|
||||
<view class="select-options">
|
||||
<form-item-checkbox :value="formData[group.field.key]" :options="group.field.options || []"
|
||||
:maxCount="group.field.maxCount || 0" @input="onInput(group.field.key, $event)" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="group.field.type === 'image_multi'" class="form-item-up upload-item">
|
||||
<view>
|
||||
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
|
||||
}}</text>
|
||||
<text class="upload-tip">(仅可上传{{ group.field.maxCount || 9 }}个图片)</text>
|
||||
</view>
|
||||
<view class="upload-content">
|
||||
<form-item-image :value="formData[group.field.key]" :maxCount="group.field.maxCount || 9"
|
||||
@input="onInput(group.field.key, $event)" />
|
||||
</view>
|
||||
<view class="upload-content">
|
||||
<form-item-image :value="formData[group.field.key]" :maxCount="group.field.maxCount || 9"
|
||||
:userId="uploadUserId" @input="onInput(group.field.key, $event)" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="group.field.type === 'video'" class="form-item-up upload-item">
|
||||
<view>
|
||||
|
|
@ -107,10 +138,10 @@
|
|||
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
|
||||
<text class="upload-tip">(仅可上传{{ field.maxCount || 9 }}个图片)</text>
|
||||
</view>
|
||||
<view class="upload-content">
|
||||
<form-item-image :value="formData[field.key]" :maxCount="field.maxCount || 9"
|
||||
@input="onInput(field.key, $event)" />
|
||||
</view>
|
||||
<view class="upload-content">
|
||||
<form-item-image :value="formData[field.key]" :maxCount="field.maxCount || 9"
|
||||
:userId="uploadUserId" @input="onInput(field.key, $event)" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="field.type === 'video'" class="form-item-up upload-item no-bg">
|
||||
<view>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script src="https://gosspublic.alicdn.com/aliyun-oss-sdk-6.18.0.min.js"></script>
|
||||
<script>
|
||||
import request from '../../utils/request';
|
||||
export default {
|
||||
|
|
@ -235,4 +234,4 @@ import request from '../../utils/request';
|
|||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -200,12 +200,13 @@ import sparkMD5 from 'spark-md5'; // 引入MD5计算库
|
|||
const loadNext = () => {
|
||||
const start = currentChunk * chunkSize;
|
||||
const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
|
||||
// #ifdef H5
|
||||
const reader = new FileReader();
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
const reader = uni.getfilesystem;
|
||||
// #endif
|
||||
let reader;
|
||||
// #ifdef H5
|
||||
reader = new FileReader();
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
reader = uni.getfilesystem;
|
||||
// #endif
|
||||
reader.onload = (e) => {
|
||||
spark.append(e.target.result);
|
||||
currentChunk++;
|
||||
|
|
@ -226,12 +227,13 @@ import sparkMD5 from 'spark-md5'; // 引入MD5计算库
|
|||
try {
|
||||
// 获取ArrayBuffer
|
||||
const arrayBuffer = await new Promise((resolve) => {
|
||||
// #ifdef H5
|
||||
const reader = new FileReader();
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
const reader = uni.getfilesystem;
|
||||
// #endif
|
||||
let reader;
|
||||
// #ifdef H5
|
||||
reader = new FileReader();
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
reader = uni.getfilesystem;
|
||||
// #endif
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.readAsArrayBuffer(chunk);
|
||||
});
|
||||
|
|
@ -418,4 +420,4 @@ import sparkMD5 from 'spark-md5'; // 引入MD5计算库
|
|||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -270,14 +270,50 @@ export default {
|
|||
});
|
||||
},
|
||||
viewEdit(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/shop/add-service?id=${item.id}&state=${item.state}&first_class_id=${item.first_class_id}`,
|
||||
});
|
||||
let url = `/pages/shop/add-service?id=${item.id}&state=${item.state}`;
|
||||
if (item.first_class_id) {
|
||||
url += `&first_class_id=${item.first_class_id}`;
|
||||
}
|
||||
if (item.second_class_id) {
|
||||
url += `&second_class_id=${item.second_class_id}`;
|
||||
}
|
||||
uni.navigateTo({ url });
|
||||
|
||||
},
|
||||
addService() {
|
||||
const userId = uni.getStorageSync('userId') || '';
|
||||
request.post("/sj/firstclass", { id: userId }).then((res) => {
|
||||
async getCurrentSjId() {
|
||||
const sjInfo = this.$store && this.$store.state && this.$store.state.sjInfo;
|
||||
if (sjInfo && sjInfo.id) {
|
||||
return sjInfo.id;
|
||||
}
|
||||
|
||||
const storageSjId = uni.getStorageSync("sjId");
|
||||
if (storageSjId) {
|
||||
return storageSjId;
|
||||
}
|
||||
|
||||
const res = await request.post("/sj/user/getUser");
|
||||
if (res && res.data && res.data.id) {
|
||||
if (this.$store && this.$store.commit) {
|
||||
this.$store.commit("setSjInfo", res.data);
|
||||
}
|
||||
uni.setStorageSync("sjId", res.data.id);
|
||||
return res.data.id;
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
async addService() {
|
||||
try {
|
||||
const sjId = await this.getCurrentSjId();
|
||||
if (!sjId) {
|
||||
uni.showToast({
|
||||
title: "未获取到商家信息",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await request.post("/sj/firstclass", { id: sjId });
|
||||
if (res.data && res.data.length > 0) {
|
||||
this.firstClassList = res.data;
|
||||
this.selectedFirstIndex = 0;
|
||||
|
|
@ -291,7 +327,13 @@ export default {
|
|||
icon: "none",
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取商家服务分类失败:", error);
|
||||
uni.showToast({
|
||||
title: "获取分类失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
},
|
||||
loadSecondClass(firstId) {
|
||||
request.post("/user/secondclass", {
|
||||
|
|
@ -979,4 +1021,4 @@ export default {
|
|||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -285,13 +285,14 @@ export default {
|
|||
sourceType: ['album', 'camera'],
|
||||
compressed: false
|
||||
})
|
||||
// #ifdef H5
|
||||
const file = res.tempFile;
|
||||
// #endif
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
const file = res;
|
||||
// #endif
|
||||
await this.uploadFile(file)
|
||||
let selectedFile;
|
||||
// #ifdef H5
|
||||
selectedFile = res.tempFile;
|
||||
// #endif
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
selectedFile = res;
|
||||
// #endif
|
||||
await this.uploadFile(selectedFile)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传出错:', error)
|
||||
|
|
@ -369,8 +370,9 @@ export default {
|
|||
// 上传到OSS
|
||||
uploadToOss(file, ossConfig, name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef H5
|
||||
const formData = new FormData()
|
||||
let formData;
|
||||
// #ifdef H5
|
||||
formData = new FormData()
|
||||
formData.append('name',name);
|
||||
formData.append('policy', ossConfig.policy);
|
||||
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
|
||||
|
|
@ -386,15 +388,15 @@ export default {
|
|||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
let formData = {};
|
||||
formData.name = name;
|
||||
formData.policy = ossConfig.policy;
|
||||
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;
|
||||
formData.success_action_status = '200';
|
||||
formData.signature = ossConfig.signature;
|
||||
formData.key = ossConfig.dir + name;
|
||||
formData.file = file;
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
formData = {};
|
||||
formData.name = name;
|
||||
formData.policy = ossConfig.policy;
|
||||
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;
|
||||
formData.success_action_status = '200';
|
||||
formData.signature = ossConfig.signature;
|
||||
formData.key = ossConfig.dir + name;
|
||||
formData.file = file;
|
||||
let path;
|
||||
if(this.accept == 'image/*') {
|
||||
path = file.path
|
||||
|
|
@ -540,4 +542,4 @@ export default {
|
|||
font-size: 32rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -146,9 +146,6 @@
|
|||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -827,4 +824,4 @@ page {
|
|||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
/* #endif */
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -67,12 +67,10 @@
|
|||
|
||||
<view class="form-upimg">
|
||||
<view class="upload-box" @click="chooseImage('idcard_negative')">
|
||||
</image>
|
||||
<view class="upload-camera img2">
|
||||
<image v-if="formData.idcard_negative" :src="formData.idcard_negative" mode="aspectFill"
|
||||
class="preview-image">
|
||||
<image v-else src="/static/images/camera.png" mode="aspectFit" class="camera-icon">
|
||||
</image>
|
||||
class="preview-image"></image>
|
||||
<image v-else src="/static/images/camera.png" mode="aspectFit" class="camera-icon"></image>
|
||||
</view>
|
||||
</view>
|
||||
<text class="label required">请上传身份证反面(国徽面)</text>
|
||||
|
|
@ -126,12 +124,10 @@
|
|||
|
||||
<view class="form-upimg">
|
||||
<view class="upload-box" @click="chooseImage('legal_idcard_negative')">
|
||||
</image>
|
||||
<view class="upload-camera img2">
|
||||
<image v-if="formData.legal_idcard_negative" :src="formData.legal_idcard_negative"
|
||||
mode="aspectFill" class="preview-image">
|
||||
<image v-else src="/static/images/camera.png" mode="aspectFit" class="camera-icon">
|
||||
</image>
|
||||
mode="aspectFill" class="preview-image"></image>
|
||||
<image v-else src="/static/images/camera.png" mode="aspectFit" class="camera-icon"></image>
|
||||
</view>
|
||||
</view>
|
||||
<text class="label required">请上传法人身份证反面(国徽面)</text>
|
||||
|
|
@ -2002,4 +1998,4 @@
|
|||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@
|
|||
<span class="detail-info-user__company__name__line"> | </span>
|
||||
<view class="detail-info-user__company__name__text2">
|
||||
{{ item.for_shop }}
|
||||
</image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -811,4 +810,4 @@
|
|||
padding: 0rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -166,39 +166,61 @@
|
|||
</view> -->
|
||||
|
||||
<!-- 项目说明 -->
|
||||
<view class="notice-section">
|
||||
<view class="section-title">
|
||||
<text class="section-tit">项目说明</text>
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<view class="notice-cont">
|
||||
<text class="notice-title">
|
||||
服务时长:
|
||||
</text>
|
||||
<text class="notice-text">
|
||||
{{serviceInfo.server_time}}分钟
|
||||
<view class="notice-section" v-if="hasProjectInfo">
|
||||
<view class="section-title">
|
||||
<text class="section-tit">项目说明</text>
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<view class="notice-cont" v-if="serviceInfo.server_time">
|
||||
<text class="notice-title">
|
||||
服务时长:
|
||||
</text>
|
||||
<text class="notice-text">
|
||||
{{serviceInfo.server_time}}分钟
|
||||
</text>
|
||||
</view>
|
||||
<view class="notice-cont" v-for="(item,i) in visibleDetailList" :key="'detail-' + i">
|
||||
<text class="notice-title">
|
||||
{{item.title}}:
|
||||
</text>
|
||||
<text class="notice-text">
|
||||
{{item.text}}
|
||||
</text>
|
||||
</view>
|
||||
<view class="notice-cont" v-for="(item,i) in serviceInfo.detail">
|
||||
<text class="notice-title">
|
||||
{{item.title}}:
|
||||
</text>
|
||||
<text class="notice-text">
|
||||
{{item.text}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 图文详情 -->
|
||||
<imgAndText v-if="serviceInfo.graphic_details" :info="serviceInfo.graphic_details"></imgAndText>
|
||||
<view class="notice-section" v-for="(section, sectionIndex) in dynamicIntroSections"
|
||||
:key="'dynamic-section-' + sectionIndex">
|
||||
<view class="section-title">
|
||||
<text class="section-tit">{{section.title}}</text>
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<view class="notice-cont dynamic-notice-cont" v-for="(item, itemIndex) in section.fields"
|
||||
:key="'dynamic-field-' + sectionIndex + '-' + itemIndex">
|
||||
<text class="notice-title">{{item.title}}:</text>
|
||||
<view class="dynamic-media-list" v-if="item.images.length">
|
||||
<image class="dynamic-image" v-for="(img, imgIndex) in item.images"
|
||||
:key="'dynamic-img-' + imgIndex" :src="img" mode="aspectFill"
|
||||
@click="previewImage(item.images, imgIndex)"></image>
|
||||
</view>
|
||||
<view class="dynamic-media-list" v-else-if="item.videos.length">
|
||||
<video class="dynamic-video" v-for="(video, videoIndex) in item.videos"
|
||||
:key="'dynamic-video-' + videoIndex" :src="video" controls></video>
|
||||
</view>
|
||||
<text class="notice-text" v-else>{{item.displayValue}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 图文详情 -->
|
||||
<imgAndText v-if="serviceInfo.graphic_details" :info="serviceInfo.graphic_details"></imgAndText>
|
||||
|
||||
<!-- 步骤 -->
|
||||
<view class="notice-section">
|
||||
<view class="section-title">
|
||||
<text class="section-tit">项目步骤</text>
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<view class="notice-cont" v-for="(item,i) in serviceInfo.process">
|
||||
<!-- 步骤 -->
|
||||
<view class="notice-section" v-if="visibleProcessList.length">
|
||||
<view class="section-title">
|
||||
<text class="section-tit">项目步骤</text>
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<view class="notice-cont" v-for="(item,i) in visibleProcessList" :key="'process-' + i">
|
||||
<text class="notice-title">
|
||||
{{item.title}}:
|
||||
</text>
|
||||
|
|
@ -207,9 +229,9 @@
|
|||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 订购须知 -->
|
||||
<view class="notice-section">
|
||||
</view>
|
||||
<!-- 订购须知 -->
|
||||
<view class="notice-section" v-if="purchaseNotesText">
|
||||
<view class="section-title">
|
||||
<text class="section-tit">订购须知</text>
|
||||
<!-- <view class="section-tit-right">
|
||||
|
|
@ -218,9 +240,9 @@
|
|||
</view> -->
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<text class="notice-text">
|
||||
{{serviceInfo.purchasenotes}}
|
||||
</text>
|
||||
<text class="notice-text">
|
||||
{{purchaseNotesText}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -324,11 +346,30 @@
|
|||
}]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userAdrees() {
|
||||
return uni.getStorageSync("userAdrees").addressRes || getApp().globalData.addressRes;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userAdrees() {
|
||||
return uni.getStorageSync("userAdrees").addressRes || getApp().globalData.addressRes;
|
||||
},
|
||||
visibleDetailList() {
|
||||
return (this.serviceInfo.detail || []).filter((item) => {
|
||||
return item && item.title && item.text;
|
||||
});
|
||||
},
|
||||
visibleProcessList() {
|
||||
return (this.serviceInfo.process || []).filter((item) => {
|
||||
return item && item.text;
|
||||
});
|
||||
},
|
||||
hasProjectInfo() {
|
||||
return !!this.serviceInfo.server_time || this.visibleDetailList.length > 0;
|
||||
},
|
||||
purchaseNotesText() {
|
||||
return this.serviceInfo.purchasenotes || this.serviceInfo.purchase_notes || '';
|
||||
},
|
||||
dynamicIntroSections() {
|
||||
return this.buildDynamicIntroSections(this.serviceInfo.intro);
|
||||
}
|
||||
},
|
||||
onLoad(option) {
|
||||
//清除选择的信息
|
||||
this.$store.commit("clearServiceState")
|
||||
|
|
@ -367,8 +408,88 @@
|
|||
onHide() {
|
||||
this.showVideo = false;
|
||||
},
|
||||
methods: {
|
||||
async goInvite() {
|
||||
methods: {
|
||||
parseMaybeJson(raw) {
|
||||
if (!raw) return [];
|
||||
if (typeof raw !== 'string') return raw;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
getDynamicPlainValue(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value !== 'object') return String(value);
|
||||
return value.label || value.title || value.name || value.text || value.value || '';
|
||||
},
|
||||
normalizeDynamicIntroField(item) {
|
||||
if (!item) return null;
|
||||
const type = item.type || '';
|
||||
const value = item.value;
|
||||
const images = [];
|
||||
const videos = [];
|
||||
let displayValue = '';
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
if (Array.isArray(value) && value.length === 0) return null;
|
||||
|
||||
if (type === 'image_multi') {
|
||||
(value || []).forEach((img) => {
|
||||
const url = typeof img === 'string' ? img : (img && (img.url || img.path));
|
||||
if (url) images.push(url);
|
||||
});
|
||||
} else if (type === 'video') {
|
||||
const list = Array.isArray(value) ? value : [value];
|
||||
list.forEach((video) => {
|
||||
const url = typeof video === 'string' ? video : (video && (video.url || video.path));
|
||||
if (url) videos.push(url);
|
||||
});
|
||||
} else if (type === 'service_step' && Array.isArray(value)) {
|
||||
displayValue = value.map((step, index) => {
|
||||
const text = this.getDynamicPlainValue(step);
|
||||
return text ? `${index + 1}. ${text}` : '';
|
||||
}).filter(Boolean).join(';');
|
||||
} else if (Array.isArray(value)) {
|
||||
displayValue = value.map((val) => this.getDynamicPlainValue(val)).filter(Boolean).join('、');
|
||||
} else if (typeof value === 'object') {
|
||||
displayValue = this.getDynamicPlainValue(value);
|
||||
} else {
|
||||
displayValue = String(value);
|
||||
}
|
||||
|
||||
if (!displayValue && images.length === 0 && videos.length === 0) return null;
|
||||
return {
|
||||
title: item.title || '补充信息',
|
||||
type,
|
||||
displayValue,
|
||||
images,
|
||||
videos,
|
||||
sectionTitle: item.sectionTitle || '',
|
||||
sectionId: item.sectionId || '',
|
||||
};
|
||||
},
|
||||
buildDynamicIntroSections(rawIntro) {
|
||||
const intro = this.parseMaybeJson(rawIntro);
|
||||
if (!Array.isArray(intro)) return [];
|
||||
const groups = [];
|
||||
const groupMap = {};
|
||||
intro.forEach((item) => {
|
||||
const field = this.normalizeDynamicIntroField(item);
|
||||
if (!field) return;
|
||||
const title = field.sectionTitle || (field.type === 'service_step' ? '项目步骤' : '更多说明');
|
||||
const key = field.sectionId || title;
|
||||
if (!groupMap[key]) {
|
||||
groupMap[key] = {
|
||||
title,
|
||||
fields: [],
|
||||
};
|
||||
groups.push(groupMap[key]);
|
||||
}
|
||||
groupMap[key].fields.push(field);
|
||||
});
|
||||
return groups.filter((section) => section.fields.length > 0);
|
||||
},
|
||||
async goInvite() {
|
||||
let imageUrl =
|
||||
'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/yh/1756713709528-717.png?x-oss-process=image/resize,p_40'
|
||||
if (this.serviceInfo.photo[0]) {
|
||||
|
|
@ -1136,13 +1257,33 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.notice-text {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
.notice-text {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.dynamic-notice-cont {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dynamic-media-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dynamic-image,
|
||||
.dynamic-video {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin-right: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
border-radius: 8rpx;
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
|
|
@ -1346,4 +1487,4 @@
|
|||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
ref="dynamicForm" />
|
||||
|
||||
<!-- 服务流程 -->
|
||||
<view class="form-step">
|
||||
<view class="form-step" v-if="!hasDynamicServiceStepField">
|
||||
<view class="form-item">
|
||||
<text class="form-label">服务流程</text>
|
||||
<text @click="addStep" style="color: #FF4767;">添加步骤</text>
|
||||
|
|
@ -42,8 +42,9 @@
|
|||
</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">
|
||||
accept="video/*" button-text="上传视频" @input="setVideoValue" @change="setVideoValue"
|
||||
@success="uploadVideoSuc" @error="uploadVideoErr" @delete="uploadVideoDel"
|
||||
@ckeckisShowPer="ckeckisShowPer" @ckecknowQer="ckecknowQer">
|
||||
</ali-oss-uploader>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -189,37 +190,80 @@ export default {
|
|||
this.ratio
|
||||
);
|
||||
},
|
||||
hasDynamicVideoField() {
|
||||
return this.formFields.some((group) => {
|
||||
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';
|
||||
});
|
||||
return group.type === 'video';
|
||||
});
|
||||
},
|
||||
hasDynamicServiceStepField() {
|
||||
return this.formFields.some((group) => {
|
||||
if (group.type === 'single' && group.field) {
|
||||
return group.field.type === 'service_step';
|
||||
}
|
||||
if (group.type === 'section' && Array.isArray(group.fields)) {
|
||||
return group.fields.some((field) => field.type === 'service_step');
|
||||
}
|
||||
return group.type === 'service_step';
|
||||
});
|
||||
},
|
||||
serviceClassName() {
|
||||
return [this.firstClassName, this.secondClassName].filter(Boolean).join('/');
|
||||
},
|
||||
},
|
||||
serviceClassName() {
|
||||
return [this.firstClassName, this.secondClassName].filter(Boolean).join('/');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 字段类型映射:接口类型 -> dynamic-form类型
|
||||
mapFieldType(fieldType) {
|
||||
const typeMap = {
|
||||
'text': 'text',
|
||||
'number': 'number',
|
||||
'image_multi': 'image_multi',
|
||||
'video': 'video',
|
||||
'richtext_editor': 'richtext_editor',
|
||||
'select': 'select',
|
||||
'single_select': 'single_select',
|
||||
'multi_select': 'multi_select',
|
||||
'textarea': 'textarea',
|
||||
};
|
||||
return typeMap[fieldType] || 'text';
|
||||
},
|
||||
'number': 'number',
|
||||
'image_multi': 'image_multi',
|
||||
'video': 'video',
|
||||
'service_step': 'service_step',
|
||||
'richtext_editor': 'richtext_editor',
|
||||
'select': 'select',
|
||||
'single_select': 'single_select',
|
||||
'radio': 'single_select',
|
||||
'switch': 'single_select',
|
||||
'boolean': 'single_select',
|
||||
'multi_select': 'multi_select',
|
||||
'textarea': 'textarea',
|
||||
};
|
||||
return typeMap[fieldType] || 'text';
|
||||
},
|
||||
|
||||
normalizeFieldOptions(options) {
|
||||
if (!Array.isArray(options)) return [];
|
||||
return options.map((item) => {
|
||||
if (item && typeof item === 'object') {
|
||||
const value = item.value !== undefined ? item.value :
|
||||
(item.id !== undefined ? item.id :
|
||||
(item.key !== undefined ? item.key :
|
||||
(item.code !== undefined ? item.code : item.label)));
|
||||
const label = item.label || item.title || item.name || item.text || String(value);
|
||||
return {
|
||||
...item,
|
||||
label,
|
||||
value,
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: String(item),
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
applyFieldHint(field, rawConfig) {
|
||||
if (!rawConfig || !rawConfig.hint) return;
|
||||
if (rawConfig.showHint === false || rawConfig.showHint === 0 || rawConfig.showHint === '0') return;
|
||||
field.tip = rawConfig.hint;
|
||||
},
|
||||
|
||||
// 将接口config_data转换为dynamic-form需要的分组格式
|
||||
transformConfigToFields(configData) {
|
||||
|
|
@ -239,13 +283,22 @@ export default {
|
|||
placeholder: preset.placeholder || `请输入${preset.title}`,
|
||||
};
|
||||
|
||||
if (preset.showHint && preset.hint) {
|
||||
field.tip = preset.hint;
|
||||
}
|
||||
if (preset.options && Array.isArray(preset.options) && preset.options.length > 0) {
|
||||
field.options = this.normalizeFieldOptions(preset.options);
|
||||
}
|
||||
if (preset.default_value !== undefined) {
|
||||
field.default_value = preset.default_value;
|
||||
} else if (preset.defaultValue !== undefined) {
|
||||
field.default_value = preset.defaultValue;
|
||||
}
|
||||
|
||||
if (preset.key === 'line_price' || preset.key === 'server_price') {
|
||||
field.tips = '(元)';
|
||||
field.tip = preset.key === 'line_price' ? '服务的基础定价,未参与任何优惠的价格' : '服务参与单品优惠后的价格';
|
||||
this.applyFieldHint(field, preset);
|
||||
|
||||
if (preset.key === 'line_price' || preset.key === 'server_price') {
|
||||
field.tips = '(元)';
|
||||
if (!field.tip) {
|
||||
field.tip = preset.key === 'line_price' ? '服务的基础定价,未参与任何优惠的价格' : '服务参与单品优惠后的价格';
|
||||
}
|
||||
} else if (preset.key === 'server_time') {
|
||||
field.placeholder = '请输入服务时长';
|
||||
field.tips = '分钟';
|
||||
|
|
@ -294,10 +347,17 @@ export default {
|
|||
field.tips = '分钟';
|
||||
}
|
||||
|
||||
if (sectionField.options && Array.isArray(sectionField.options) &&
|
||||
sectionField.options.length > 0) {
|
||||
field.options = sectionField.options;
|
||||
}
|
||||
this.applyFieldHint(field, sectionField);
|
||||
|
||||
if (sectionField.options && Array.isArray(sectionField.options) &&
|
||||
sectionField.options.length > 0) {
|
||||
field.options = this.normalizeFieldOptions(sectionField.options);
|
||||
}
|
||||
if (sectionField.default_value !== undefined) {
|
||||
field.default_value = sectionField.default_value;
|
||||
} else if (sectionField.defaultValue !== undefined) {
|
||||
field.default_value = sectionField.defaultValue;
|
||||
}
|
||||
|
||||
if (sectionField.maxCount && (sectionField.fieldType === 'multi_select' ||
|
||||
sectionField.fieldType === 'image_multi' || sectionField.fieldType === 'video')) {
|
||||
|
|
@ -320,12 +380,15 @@ export default {
|
|||
|
||||
field._fieldType = sectionField.fieldType;
|
||||
field._hasOriginalKey = !!sectionField.key;
|
||||
field._original = {
|
||||
id: sectionField.id,
|
||||
sectionId: section.id,
|
||||
sectionType: section.sectionType,
|
||||
userDefined: sectionField.userDefined,
|
||||
};
|
||||
field._original = {
|
||||
id: sectionField.id,
|
||||
sectionId: section.id,
|
||||
sectionType: section.sectionType,
|
||||
userDefined: sectionField.userDefined,
|
||||
};
|
||||
field._sectionTitle = section.sectionTitle || '';
|
||||
field._sectionId = section.id;
|
||||
field._sectionType = section.sectionType;
|
||||
|
||||
sectionFields.push(field);
|
||||
});
|
||||
|
|
@ -444,16 +507,9 @@ export default {
|
|||
let that = this;
|
||||
let value = e.detail.value;
|
||||
|
||||
// 1. 移除非数字和小数点
|
||||
value = value.replace(/[^\d.]/g, '');
|
||||
// 1. 移除非数字和小数点
|
||||
value = value.replace(/[^\d.]/g, '');
|
||||
|
||||
// 2. 确保只有一个小数点
|
||||
const parts = value.split('.');
|
||||
if (parts.length > 2) {
|
||||
value = parts[0] + '.' + parts.slice(1).join('');
|
||||
}
|
||||
// 2. 确保只有一个小数点
|
||||
const parts = value.split('.');
|
||||
if (parts.length > 2) {
|
||||
|
|
@ -470,29 +526,8 @@ export default {
|
|||
}
|
||||
that.$set(that.formData, 'line_price', value);
|
||||
},
|
||||
// 服务简介部分
|
||||
initDetailForm(detail = []) {
|
||||
// 先重置 detailForm 为空
|
||||
this.detailForm = {
|
||||
effect: "",
|
||||
crowd: "",
|
||||
product: "",
|
||||
scope: "",
|
||||
tips: "",
|
||||
};
|
||||
this.detailOtherList = [];
|
||||
// 3. 限制小数点后最多一位
|
||||
if (parts.length > 1) {
|
||||
value = parts[0] + '.' + parts[1].slice(0, 2);
|
||||
}
|
||||
// 4. 如果以小数点开头,前面补0
|
||||
if (value.startsWith('.')) {
|
||||
value = '0' + value;
|
||||
}
|
||||
that.$set(that.formData, 'line_price', value);
|
||||
},
|
||||
// 服务简介部分
|
||||
initDetailForm(detail = []) {
|
||||
// 服务简介部分
|
||||
initDetailForm(detail = []) {
|
||||
// 先重置 detailForm 为空
|
||||
this.detailForm = {
|
||||
effect: "",
|
||||
|
|
@ -593,9 +628,43 @@ export default {
|
|||
ckecknowQer(value) {
|
||||
this.nowQer = value;
|
||||
},
|
||||
addVideo(value) {
|
||||
this.formData.video = value;
|
||||
},
|
||||
addVideo(value) {
|
||||
this.setVideoValue(value);
|
||||
},
|
||||
normalizeVideoValue(value) {
|
||||
if (!value) return "";
|
||||
if (typeof value === "string") {
|
||||
const str = value.trim();
|
||||
if (!str) return "";
|
||||
if ((str.startsWith("{") && str.endsWith("}")) || (str.startsWith("[") && str.endsWith("]"))) {
|
||||
try {
|
||||
return this.normalizeVideoValue(JSON.parse(str));
|
||||
} catch (error) {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const url = this.normalizeVideoValue(value[i]);
|
||||
if (url) return url;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
const candidates = [value.url, value.path, value.filePath, value.tempFilePath, value.src, value.value];
|
||||
for (let i = 0; i < candidates.length; i++) {
|
||||
const url = this.normalizeVideoValue(candidates[i]);
|
||||
if (url) return url;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return String(value);
|
||||
},
|
||||
setVideoValue(value) {
|
||||
this.formData.video = this.normalizeVideoValue(value);
|
||||
},
|
||||
// addImg(arr) {
|
||||
// this.formData.photo = this.formData.photo.concat(arr);
|
||||
// },
|
||||
|
|
@ -615,25 +684,29 @@ export default {
|
|||
onFileDelete(file, index) {
|
||||
console.log("删除文件:", this.formData.photo, index);
|
||||
},
|
||||
uploadVideoSuc(file) {
|
||||
this.formData.video = file;
|
||||
},
|
||||
uploadVideoSuc(file) {
|
||||
this.setVideoValue(file);
|
||||
},
|
||||
uploadVideoErr() { },
|
||||
uploadVideoDel() {
|
||||
this.formData.video = null;
|
||||
},
|
||||
async getInit() {
|
||||
await request
|
||||
.post("/sj/servers/detail", {
|
||||
id: this.formData.id
|
||||
})
|
||||
.then((res) => {
|
||||
// 直接合并所有字段,避免遗漏
|
||||
this.formData = {
|
||||
...this.formData,
|
||||
...res.data
|
||||
};
|
||||
// 确保 process 保持为数组(后端可能返回字符串)
|
||||
await request
|
||||
.post("/sj/servers/detail", {
|
||||
id: this.formData.id
|
||||
})
|
||||
.then(async (res) => {
|
||||
// 直接合并所有字段,避免遗漏
|
||||
this.formData = {
|
||||
...this.formData,
|
||||
...res.data
|
||||
};
|
||||
this.setVideoValue(this.formData.video);
|
||||
if (this.formData.first_class_id && this.formFields.length === 0) {
|
||||
await this.getTemplate();
|
||||
}
|
||||
// 确保 process 保持为数组(后端可能返回字符串)
|
||||
if (!Array.isArray(this.formData.process)) {
|
||||
this.formData.process = [];
|
||||
}
|
||||
|
|
@ -752,16 +825,9 @@ export default {
|
|||
let that = this;
|
||||
let value = e.detail.value;
|
||||
|
||||
// 1. 移除非数字和小数点
|
||||
value = value.replace(/[^\d.]/g, "");
|
||||
// 1. 移除非数字和小数点
|
||||
value = value.replace(/[^\d.]/g, "");
|
||||
|
||||
// 2. 确保只有一个小数点
|
||||
const parts = value.split(".");
|
||||
if (parts.length > 2) {
|
||||
value = parts[0] + "." + parts.slice(1).join("");
|
||||
}
|
||||
// 2. 确保只有一个小数点
|
||||
const parts = value.split(".");
|
||||
if (parts.length > 2) {
|
||||
|
|
@ -792,7 +858,7 @@ export default {
|
|||
},
|
||||
// 视频上传相关方法
|
||||
uploadVideoSuc(file) {
|
||||
this.formData.video = file;
|
||||
this.setVideoValue(file);
|
||||
},
|
||||
uploadVideoErr() { },
|
||||
uploadVideoDel() {
|
||||
|
|
@ -848,50 +914,7 @@ export default {
|
|||
return value.length === 0;
|
||||
}
|
||||
return !value;
|
||||
});
|
||||
// 保存草稿
|
||||
saveDraft: debounce(function () {
|
||||
if (this.isDraft) return;
|
||||
this.syncDetailForm();
|
||||
const {
|
||||
title,
|
||||
server_price,
|
||||
line_price,
|
||||
server_time,
|
||||
first_class_id,
|
||||
second_class_id,
|
||||
detail,
|
||||
process,
|
||||
video,
|
||||
photo,
|
||||
} = this.formData;
|
||||
let item = {
|
||||
title,
|
||||
server_price,
|
||||
line_price,
|
||||
server_time,
|
||||
first_class_id,
|
||||
second_class_id,
|
||||
detail,
|
||||
process,
|
||||
video,
|
||||
photo,
|
||||
};
|
||||
// 检查是否所有字段都为空
|
||||
const isEmpty = Object.values(item).every((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length === 0;
|
||||
}
|
||||
return !value;
|
||||
});
|
||||
|
||||
if (isEmpty) {
|
||||
uni.showToast({
|
||||
title: "请至少填写一项内容",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isEmpty) {
|
||||
uni.showToast({
|
||||
title: "请至少填写一项内容",
|
||||
|
|
@ -942,8 +965,8 @@ export default {
|
|||
|
||||
// 验证动态表单字段
|
||||
validateDynamicForm() {
|
||||
// 遍历 formGroups(分组结构)按顺序验证必填项
|
||||
for (let i = 0; i < this.formFields.length; i++) {
|
||||
// 遍历 formGroups(分组结构)按顺序验证必填项
|
||||
for (let i = 0; i < this.formFields.length; i++) {
|
||||
const group = this.formFields[i];
|
||||
const fieldsToCheck = [];
|
||||
if (group.type === 'single' && group.field) {
|
||||
|
|
@ -960,17 +983,30 @@ export default {
|
|||
const isEmpty = value === undefined || value === null || value === '' ||
|
||||
(Array.isArray(value) && value.length === 0);
|
||||
|
||||
if (isEmpty) {
|
||||
// 根据字段类型显示不同的提示语
|
||||
const prefix = (field.type === 'image_multi' || field.type === 'video') ? '请上传' : '请输入';
|
||||
uni.showToast({
|
||||
title: `${prefix}${field.label}`,
|
||||
icon: "none",
|
||||
if (isEmpty) {
|
||||
// 根据字段类型显示不同的提示语
|
||||
const prefix = (field.type === 'image_multi' || field.type === 'video') ? '请上传' :
|
||||
(field.type === 'select' || field.type === 'single_select' || field.type === 'multi_select') ?
|
||||
'请选择' : '请输入';
|
||||
uni.showToast({
|
||||
title: `${prefix}${field.label}`,
|
||||
icon: "none",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 服务时长特殊验证
|
||||
if (field.type === 'service_step' && Array.isArray(value)) {
|
||||
const hasEmptyStep = value.some((item) => !item || !item.text || !item.text.trim());
|
||||
if (hasEmptyStep) {
|
||||
uni.showToast({
|
||||
title: "请完善服务流程内容",
|
||||
icon: "none",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 服务时长特殊验证
|
||||
if (field.key === 'server_time') {
|
||||
if (value < 10) {
|
||||
uni.showToast({
|
||||
|
|
@ -1008,8 +1044,9 @@ export default {
|
|||
}
|
||||
|
||||
// 视频字段(独立上传组件)
|
||||
if (this.formData.video) {
|
||||
submitData.video = this.formData.video;
|
||||
const videoUrl = this.normalizeVideoValue(this.formData.video);
|
||||
if (videoUrl) {
|
||||
submitData.video = videoUrl;
|
||||
}
|
||||
|
||||
// 遍历分组,按顺序处理字段
|
||||
|
|
@ -1021,15 +1058,36 @@ export default {
|
|||
} else if (group.type === 'section' && group.fields) {
|
||||
fieldsToProcess = group.fields;
|
||||
}
|
||||
for (let j = 0; j < fieldsToProcess.length; j++) {
|
||||
const field = fieldsToProcess[j];
|
||||
const value = this.formData[field.key];
|
||||
if (value === undefined || value === null || value === '') continue;
|
||||
if (Array.isArray(value) && value.length === 0) continue;
|
||||
for (let j = 0; j < fieldsToProcess.length; j++) {
|
||||
const field = fieldsToProcess[j];
|
||||
const value = this.formData[field.key];
|
||||
if (value === undefined || value === null || value === '') continue;
|
||||
if (Array.isArray(value) && value.length === 0) continue;
|
||||
|
||||
if (field._hasOriginalKey) {
|
||||
// 有key的字段直接作为请求参数
|
||||
// 图片字段确保为纯URL字符串数组
|
||||
if (field.type === 'service_step' && Array.isArray(value)) {
|
||||
const processArr = value
|
||||
.filter(item => item && item.text && item.text.trim())
|
||||
.map(item => ({ text: item.text.trim() }));
|
||||
if (processArr.length === 0) continue;
|
||||
if (field._hasOriginalKey) {
|
||||
submitData[field.key] = JSON.stringify(processArr);
|
||||
} else {
|
||||
introArr.push({
|
||||
id: field._original ? field._original.id : field.key,
|
||||
title: field.label,
|
||||
type: field._fieldType || field.type,
|
||||
value: processArr,
|
||||
sectionId: field._sectionId || (field._original && field._original.sectionId),
|
||||
sectionTitle: field._sectionTitle || '',
|
||||
sectionType: field._sectionType || (field._original && field._original.sectionType),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field._hasOriginalKey) {
|
||||
// 有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);
|
||||
|
|
@ -1039,13 +1097,16 @@ export default {
|
|||
}
|
||||
} else {
|
||||
// 没有key的字段放入intro数组
|
||||
introArr.push({
|
||||
id: field._original ? field._original.id : field.key,
|
||||
title: field.label,
|
||||
type: field._fieldType || field.type,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
introArr.push({
|
||||
id: field._original ? field._original.id : field.key,
|
||||
title: field.label,
|
||||
type: field._fieldType || field.type,
|
||||
value: value,
|
||||
sectionId: field._sectionId || (field._original && field._original.sectionId),
|
||||
sectionTitle: field._sectionTitle || '',
|
||||
sectionType: field._sectionType || (field._original && field._original.sectionType),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1054,9 +1115,9 @@ export default {
|
|||
}
|
||||
|
||||
// 服务步骤:有key时作为独立参数,无key时放入intro数组
|
||||
if (this.formData.process && this.formData.process.length > 0) {
|
||||
const processArr = this.formData.process
|
||||
.filter(item => item.text && item.text.trim())
|
||||
if (!this.hasDynamicServiceStepField && 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) {
|
||||
|
|
@ -1068,11 +1129,12 @@ export default {
|
|||
} catch (e) {
|
||||
introList = [];
|
||||
}
|
||||
introList.push({
|
||||
id: 'service_step',
|
||||
title: '服务流程',
|
||||
value: processArr,
|
||||
});
|
||||
introList.push({
|
||||
id: 'service_step',
|
||||
title: '服务流程',
|
||||
type: 'service_step',
|
||||
value: processArr,
|
||||
});
|
||||
submitData.intro = JSON.stringify(introList);
|
||||
}
|
||||
}
|
||||
|
|
@ -1085,11 +1147,19 @@ export default {
|
|||
submitForm: debounce(function () {
|
||||
if (this.isAdd) return;
|
||||
|
||||
// 先同步服务简介数据到formData
|
||||
this.syncDetailForm();
|
||||
// 先同步服务简介数据到formData
|
||||
this.syncDetailForm();
|
||||
|
||||
// 验证动态表单字段
|
||||
if (!this.validateDynamicForm()) {
|
||||
if (!this.formData.first_class_id) {
|
||||
uni.showToast({
|
||||
title: "请选择服务分类",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证动态表单字段
|
||||
if (!this.validateDynamicForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1603,17 +1673,8 @@ export default {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
height: 100rpx;
|
||||
padding: 40rpx 0 64rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-draft,
|
||||
.btn-submit {
|
||||
.btn-draft,
|
||||
.btn-submit {
|
||||
width: 334rpx;
|
||||
height: 76rpx;
|
||||
line-height: 76rpx;
|
||||
|
|
@ -1942,14 +2003,6 @@ align-items: center;
|
|||
|
||||
/* 平台特定样式 */
|
||||
/* #ifdef H5 */
|
||||
/* 平台特定样式 */
|
||||
/* #ifdef H5 */
|
||||
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
outline: none;
|
||||
|
|
@ -1958,18 +2011,7 @@ align-items: center;
|
|||
.upload-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef APP-PLUS */
|
||||
.form-input {
|
||||
height: 90rpx;
|
||||
}
|
||||
|
||||
/* #ifdef APP-PLUS */
|
||||
.form-input {
|
||||
|
|
@ -1981,20 +2023,7 @@ align-items: center;
|
|||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.permission.transform {
|
||||
top: calc(var(--status-bar-height) + 88rpx + 30rpx);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef MP-WEIXIN */
|
||||
.form-input {
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
/* #ifdef MP-WEIXIN */
|
||||
.form-input {
|
||||
|
|
@ -2004,12 +2033,8 @@ align-items: center;
|
|||
.bottom-buttons {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.bottom-buttons {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
.tip {
|
||||
padding: 20rpx 30rpx;
|
||||
background: #feefec;
|
||||
|
|
@ -2017,31 +2042,9 @@ align-items: center;
|
|||
font-size: 30rpx;
|
||||
color: #f31d2f;
|
||||
line-height: 42rpx;
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
|
||||
/* #endif */
|
||||
.tip {
|
||||
padding: 20rpx 30rpx;
|
||||
background: #feefec;
|
||||
font-weight: 400;
|
||||
font-size: 30rpx;
|
||||
color: #f31d2f;
|
||||
line-height: 42rpx;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
|
||||
.tip-icon {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
margin-right: 10rpx;
|
||||
transform: translateY(6rpx);
|
||||
}
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
|
|
|
|||
|
|
@ -152,20 +152,58 @@
|
|||
</view>
|
||||
|
||||
<!-- 项目说明 -->
|
||||
<view class="notice-section">
|
||||
<view class="section-title">
|
||||
<text class="section-tit">项目说明</text>
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<view class="notice-cont">
|
||||
<text class="notice-title">
|
||||
服务时长:
|
||||
</text>
|
||||
<text class="notice-text">
|
||||
{{serviceInfo.server_time}}分钟
|
||||
<view class="notice-section" v-if="hasProjectInfo">
|
||||
<view class="section-title">
|
||||
<text class="section-tit">项目说明</text>
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<view class="notice-cont" v-if="serviceInfo.server_time">
|
||||
<text class="notice-title">
|
||||
服务时长:
|
||||
</text>
|
||||
<text class="notice-text">
|
||||
{{serviceInfo.server_time}}分钟
|
||||
</text>
|
||||
</view>
|
||||
<view class="notice-cont" v-for="(item,i) in visibleDetailList" :key="'detail-' + i">
|
||||
<text class="notice-title">
|
||||
{{item.title}}:
|
||||
</text>
|
||||
<text class="notice-text">
|
||||
{{item.text}}
|
||||
</text>
|
||||
</view>
|
||||
<view class="notice-cont" v-for="(item,i) in serviceInfo.detail">
|
||||
</view>
|
||||
</view>
|
||||
<view class="notice-section" v-for="(section, sectionIndex) in dynamicIntroSections"
|
||||
:key="'dynamic-section-' + sectionIndex">
|
||||
<view class="section-title">
|
||||
<text class="section-tit">{{section.title}}</text>
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<view class="notice-cont dynamic-notice-cont" v-for="(item, itemIndex) in section.fields"
|
||||
:key="'dynamic-field-' + sectionIndex + '-' + itemIndex">
|
||||
<text class="notice-title">{{item.title}}:</text>
|
||||
<view class="dynamic-media-list" v-if="item.images.length">
|
||||
<image class="dynamic-image" v-for="(img, imgIndex) in item.images"
|
||||
:key="'dynamic-img-' + imgIndex" :src="img" mode="aspectFill"
|
||||
@click="previewImage(item.images, imgIndex)"></image>
|
||||
</view>
|
||||
<view class="dynamic-media-list" v-else-if="item.videos.length">
|
||||
<video class="dynamic-video" v-for="(video, videoIndex) in item.videos"
|
||||
:key="'dynamic-video-' + videoIndex" :src="video" controls></video>
|
||||
</view>
|
||||
<text class="notice-text" v-else>{{item.displayValue}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 步骤 -->
|
||||
<view class="notice-section" v-if="visibleProcessList.length">
|
||||
<view class="section-title">
|
||||
<text class="section-tit">项目步骤</text>
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<view class="notice-cont" v-for="(item,i) in visibleProcessList" :key="'process-' + i">
|
||||
<text class="notice-title">
|
||||
{{item.title}}:
|
||||
</text>
|
||||
|
|
@ -174,25 +212,9 @@
|
|||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 步骤 -->
|
||||
<view class="notice-section">
|
||||
<view class="section-title">
|
||||
<text class="section-tit">项目步骤</text>
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<view class="notice-cont" v-for="(item,i) in serviceInfo.process">
|
||||
<text class="notice-title">
|
||||
{{item.title}}:
|
||||
</text>
|
||||
<text class="notice-text">
|
||||
{{item.text}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 订购须知 -->
|
||||
<view class="notice-section">
|
||||
<!-- 订购须知 -->
|
||||
<view class="notice-section" v-if="purchaseNotesText">
|
||||
<view class="section-title">
|
||||
<text class="section-tit">订购须知</text>
|
||||
<!-- <view class="section-tit-right">
|
||||
|
|
@ -201,9 +223,9 @@
|
|||
</view> -->
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<text class="notice-text">
|
||||
{{serviceInfo.purchasenotes}}
|
||||
</text>
|
||||
<text class="notice-text">
|
||||
{{purchaseNotesText}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -283,9 +305,30 @@
|
|||
tab: '超出期待',
|
||||
type: '双色法式美甲'
|
||||
}]
|
||||
}
|
||||
},
|
||||
onLoad(option) {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
visibleDetailList() {
|
||||
return (this.serviceInfo.detail || []).filter((item) => {
|
||||
return item && item.title && item.text;
|
||||
});
|
||||
},
|
||||
visibleProcessList() {
|
||||
return (this.serviceInfo.process || []).filter((item) => {
|
||||
return item && item.text;
|
||||
});
|
||||
},
|
||||
hasProjectInfo() {
|
||||
return !!this.serviceInfo.server_time || this.visibleDetailList.length > 0;
|
||||
},
|
||||
purchaseNotesText() {
|
||||
return this.serviceInfo.purchasenotes || this.serviceInfo.purchase_notes || '';
|
||||
},
|
||||
dynamicIntroSections() {
|
||||
return this.buildDynamicIntroSections(this.serviceInfo.intro);
|
||||
}
|
||||
},
|
||||
onLoad(option) {
|
||||
// Android可能需要延迟加载
|
||||
request.post('/sj/serverdetail', {
|
||||
id: option.id
|
||||
|
|
@ -315,9 +358,89 @@
|
|||
onHide() {
|
||||
this.showVideo = false;
|
||||
},
|
||||
methods: {
|
||||
// 获取手艺人详情
|
||||
getSyrDetail(id) {
|
||||
methods: {
|
||||
parseMaybeJson(raw) {
|
||||
if (!raw) return [];
|
||||
if (typeof raw !== 'string') return raw;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
getDynamicPlainValue(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value !== 'object') return String(value);
|
||||
return value.label || value.title || value.name || value.text || value.value || '';
|
||||
},
|
||||
normalizeDynamicIntroField(item) {
|
||||
if (!item) return null;
|
||||
const type = item.type || '';
|
||||
const value = item.value;
|
||||
const images = [];
|
||||
const videos = [];
|
||||
let displayValue = '';
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
if (Array.isArray(value) && value.length === 0) return null;
|
||||
|
||||
if (type === 'image_multi') {
|
||||
(value || []).forEach((img) => {
|
||||
const url = typeof img === 'string' ? img : (img && (img.url || img.path));
|
||||
if (url) images.push(url);
|
||||
});
|
||||
} else if (type === 'video') {
|
||||
const list = Array.isArray(value) ? value : [value];
|
||||
list.forEach((video) => {
|
||||
const url = typeof video === 'string' ? video : (video && (video.url || video.path));
|
||||
if (url) videos.push(url);
|
||||
});
|
||||
} else if (type === 'service_step' && Array.isArray(value)) {
|
||||
displayValue = value.map((step, index) => {
|
||||
const text = this.getDynamicPlainValue(step);
|
||||
return text ? `${index + 1}. ${text}` : '';
|
||||
}).filter(Boolean).join(';');
|
||||
} else if (Array.isArray(value)) {
|
||||
displayValue = value.map((val) => this.getDynamicPlainValue(val)).filter(Boolean).join('、');
|
||||
} else if (typeof value === 'object') {
|
||||
displayValue = this.getDynamicPlainValue(value);
|
||||
} else {
|
||||
displayValue = String(value);
|
||||
}
|
||||
|
||||
if (!displayValue && images.length === 0 && videos.length === 0) return null;
|
||||
return {
|
||||
title: item.title || '补充信息',
|
||||
type,
|
||||
displayValue,
|
||||
images,
|
||||
videos,
|
||||
sectionTitle: item.sectionTitle || '',
|
||||
sectionId: item.sectionId || '',
|
||||
};
|
||||
},
|
||||
buildDynamicIntroSections(rawIntro) {
|
||||
const intro = this.parseMaybeJson(rawIntro);
|
||||
if (!Array.isArray(intro)) return [];
|
||||
const groups = [];
|
||||
const groupMap = {};
|
||||
intro.forEach((item) => {
|
||||
const field = this.normalizeDynamicIntroField(item);
|
||||
if (!field) return;
|
||||
const title = field.sectionTitle || (field.type === 'service_step' ? '项目步骤' : '更多说明');
|
||||
const key = field.sectionId || title;
|
||||
if (!groupMap[key]) {
|
||||
groupMap[key] = {
|
||||
title,
|
||||
fields: [],
|
||||
};
|
||||
groups.push(groupMap[key]);
|
||||
}
|
||||
groupMap[key].fields.push(field);
|
||||
});
|
||||
return groups.filter((section) => section.fields.length > 0);
|
||||
},
|
||||
// 获取手艺人详情
|
||||
getSyrDetail(id) {
|
||||
request.post('/sj/syrdetail', {
|
||||
id
|
||||
}).then(res => {
|
||||
|
|
@ -808,13 +931,33 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.notice-text {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
.notice-text {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.dynamic-notice-cont {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dynamic-media-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dynamic-image,
|
||||
.dynamic-video {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin-right: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
border-radius: 8rpx;
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
|
|
@ -1012,4 +1155,4 @@
|
|||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -228,8 +228,9 @@ const getOssConfig = async (type) => {
|
|||
const uploadToOss = (file, ossConfig, name, uploadType) => {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef H5
|
||||
const formData = new FormData()
|
||||
let formData;
|
||||
// #ifdef H5
|
||||
formData = new FormData()
|
||||
formData.append('name', name);
|
||||
formData.append('policy', ossConfig.policy);
|
||||
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
|
||||
|
|
@ -247,15 +248,15 @@ const uploadToOss = (file, ossConfig, name, uploadType) => {
|
|||
reject(new Error(`上传失败: ${res.statusCode}`))
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
let formData = {};
|
||||
formData.name = name;
|
||||
formData.policy = ossConfig.policy;
|
||||
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;
|
||||
formData.success_action_status = '200';
|
||||
formData.signature = ossConfig.signature;
|
||||
formData.key = ossConfig.dir + name;
|
||||
formData.file = file;
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
formData = {};
|
||||
formData.name = name;
|
||||
formData.policy = ossConfig.policy;
|
||||
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;
|
||||
formData.success_action_status = '200';
|
||||
formData.signature = ossConfig.signature;
|
||||
formData.key = ossConfig.dir + name;
|
||||
formData.file = file;
|
||||
|
||||
let path;
|
||||
if (uploadType == 'image') {
|
||||
|
|
@ -314,4 +315,4 @@ const uploadToOss = (file, ossConfig, name, uploadType) => {
|
|||
}
|
||||
export default {
|
||||
handleUpload
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,8 +148,9 @@ const getOssConfig = async (type)=> {
|
|||
const uploadToOss = (file, ossConfig, name)=> {
|
||||
console.log(ossConfig)
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef H5
|
||||
const formData = new FormData()
|
||||
let formData;
|
||||
// #ifdef H5
|
||||
formData = new FormData()
|
||||
formData.append('name',name);
|
||||
formData.append('policy', ossConfig.policy);
|
||||
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
|
||||
|
|
@ -165,7 +166,7 @@ const uploadToOss = (file, ossConfig, name)=> {
|
|||
})
|
||||
// #endif
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
let formData = {};
|
||||
formData = {};
|
||||
formData.name = name;
|
||||
formData.policy = ossConfig.policy;
|
||||
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;
|
||||
|
|
@ -277,4 +278,4 @@ const uploadToOss = (file, ossConfig, name)=> {
|
|||
}
|
||||
export default {
|
||||
handleUpload
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue