服务发布

This commit is contained in:
丁杰 2026-06-23 09:55:51 +08:00
parent 3e39ff1140
commit 573aef3210
19 changed files with 1165 additions and 490 deletions

View File

@ -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>

View File

@ -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">&times;</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);
}
// filefile
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>

View File

@ -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({

View File

@ -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: {

View File

@ -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));
}
}
};

View File

@ -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));
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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_datadynamic-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 {
// keyintro
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 {
}
// keykeyintro
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;

View File

@ -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>

View File

@ -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
}
}

View File

@ -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
}
}