Compare commits

...

23 Commits

Author SHA1 Message Date
丁杰 573aef3210 服务发布 2026-06-23 09:55:51 +08:00
丁杰 3e39ff1140 Merge branch 'master' into mrr_sj_develop_丁杰_20260609_自定义服务发布 2026-06-22 14:43:11 +08:00
丁杰 ee20019de3 9800活动上线,营业资质非必填 2026-06-22 14:35:50 +08:00
丁杰 fd5923a4dc 修改request结构 2026-06-18 14:58:10 +08:00
丁杰 cd4373f91f 缺省图+非必填 2026-06-18 14:38:29 +08:00
BAKEYi 93133dc460 Merge branch 'master' of https://gitee.com/qtvbidt/mrr.sj.front 2026-06-18 14:06:04 +08:00
BAKEYi 466d1e4d2e 门店信息地址回显 2026-06-18 14:05:59 +08:00
丁杰 122c19909c 新增成长礼包活动页 2026-06-18 14:02:08 +08:00
丁杰 9ded6affa3 1 2026-06-16 14:25:52 +08:00
丁杰 c47c653bf6 Merge branch '改版' into master 2026-06-15 22:36:28 +08:00
丁杰 79ee295c18 视频上传按钮修复 2026-06-12 13:59:52 +08:00
丁杰 0e9bca8d97 修复bug,优化流程 2026-06-11 20:48:30 +08:00
丁杰 6b659a6878 备份 2026-06-10 16:23:47 +08:00
丁杰 6aa66ce311 弹窗样式修改,组件预定义 2026-06-10 10:15:10 +08:00
丁杰 a7f9544bc5 预定义组件备份 2026-06-09 17:49:15 +08:00
丁杰 d4a6a63b65 更换正式域名 2026-06-04 16:21:51 +08:00
丁杰 90edc49e80 换地址 2026-06-01 16:42:22 +08:00
BAKEYi 126e236cc2 客服电话换接口获取 2026-04-17 14:27:31 +08:00
cjl520cy e89470df68 Merge remote-tracking branch 'origin/mrr_sj_develop_丁杰_20260413_pricer' 2026-04-16 18:20:43 +08:00
cjl520cy a7654b4071 bug修改 2026-04-16 18:19:07 +08:00
cjl520cy dd731c584a bug修改 2026-04-16 11:20:53 +08:00
cjl520cy a4df9a6269 Merge remote-tracking branch 'origin/mrr_sj_develop_丁杰_20260413_pricer' 2026-04-16 10:34:54 +08:00
cjl520cy f7e8388e67 bug修改 2026-04-16 10:33:00 +08:00
53 changed files with 20850 additions and 16801 deletions

32
App.vue
View File

@ -16,6 +16,9 @@ import request from './utils/request'
import { import {
setVersion setVersion
} from './utils/version.js' } from './utils/version.js'
import {
APP_BACK_BASE_URL
} from './utils/appBackBase.js'
// //
const aLiSDKModule = uni.requireNativePlugin('AliCloud-NirvanaPns'); const aLiSDKModule = uni.requireNativePlugin('AliCloud-NirvanaPns');
export default { export default {
@ -66,38 +69,13 @@ export default {
// await setVersion() // await setVersion()
//访 //访
// GET
let version = uni.getStorageSync('setVersion')
let baseUrl
await uni.request({
url: 'https://api.mrrwlkj.top/api/openPuc/getSjApiUrl', //
method: 'GET', // GET
header: {
'Content-Type': 'application/json', //
'VersionCode': version.VersionCode,
'DeviceBrand': version.DeviceBrand,
'DeviceType': version.DeviceType
},
success: (res) => {
if (res.data.code == 200) {
// #ifndef MP-WEIXIN // #ifndef MP-WEIXIN
uni.setStorageSync('baseUrl', res.data.data.url) uni.setStorageSync('baseUrl', APP_BACK_BASE_URL)
// #endif // #endif
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
wx.setStorageSync('baseUrl', res.data.data.url) wx.setStorageSync('baseUrl', APP_BACK_BASE_URL)
// #endif // #endif
this.$store.commit("setLocation", true) this.$store.commit("setLocation", true)
}
console.log('请求成功', res.data);
},
fail: (err) => {
console.log('请求失败', err);
},
complete: () => {
console.log('请求完成(无论成功失败都会执行)');
}
});
// #ifdef APP-PLUS // #ifdef APP-PLUS
// //

View File

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

View File

@ -15,7 +15,6 @@
</view> </view>
</template> </template>
<script>
<script> <script>
// spark-md5 // spark-md5
import SparkMD5 from 'spark-md5' import SparkMD5 from 'spark-md5'
@ -42,7 +41,7 @@ export default {
maxSize: { maxSize: {
type: Number, type: Number,
default: 500 default: 500
} },
}, },
data() { data() {
return { return {
@ -53,7 +52,7 @@ export default {
fileMd5: '', fileMd5: '',
cancelToken: null cancelToken: null
}; };
} },
methods: { methods: {
// //
async chooseVideo() { async chooseVideo() {
@ -319,7 +318,7 @@ export default {
chunkIndex, chunkIndex,
fileMd5: this.fileMd5, fileMd5: this.fileMd5,
chunkSize: this.chunkSize, chunkSize: this.chunkSize,
totalChunks: Math.ceil(this.file.size / this.chunkSize)) totalChunks: Math.ceil(this.file.size / this.chunkSize)
}, },
header: { header: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'

View File

@ -5,8 +5,13 @@
<view v-if="accept == 'image/*'" class="preview-container" :style="'width:'+width"> <view v-if="accept == 'image/*'" class="preview-container" :style="'width:'+width">
<view <view
v-for="(file, index) in fileList" v-for="(file, index) in fileList"
:key="index" :key="file || index"
class="preview-item" class="preview-item image-preview-item"
:class="{ dragging: dragIndex === index }"
@longpress="startDrag(index)"
@touchmove="handleDragMove"
@touchend="endDrag"
@touchcancel="endDrag"
> >
<image <image
:src="file" :src="file"
@ -14,9 +19,13 @@
mode="aspectFill" mode="aspectFill"
@click="handlePreview(index)" @click="handlePreview(index)"
/> />
<view v-if="deletable" class="delete-icon" @click.stop="handleDelete(index)"> <image
× v-if="deletable"
</view> class="delete-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/6ab3fc63-3bad-403d-bbfc-9609a93c048a.png"
mode="aspectFit"
@click.stop="handleDelete(index)"
></image>
</view> </view>
<!-- 上传按钮 --> <!-- 上传按钮 -->
<view class="preview-item"> <view class="preview-item">
@ -36,27 +45,40 @@
<!-- 视频预览区域 --> <!-- 视频预览区域 -->
<view class="preview-container" :style="'width:'+width" v-if="accept == 'video/*'"> <view class="preview-container" :style="'width:'+width" v-if="accept == 'video/*'">
<block v-if="showPreview">
<view <view
v-for="(file, index) in videoList"
:key="file || index"
class="preview-item" class="preview-item"
v-if="oneFile" @click="previewVideo(file)"
> >
<!-- <video <!-- <video
:src="oneFile" :src="file"
class="preview-image" class="preview-image"
@click="handlePreview(index)" @click="handlePreview(index)"
/> --> /> -->
<image class="preview-image" :src="oneFile + '?x-oss-process=video/snapshot,t_0,f_jpg'" mode="aspectFill"></image> <image class="preview-image" :src="getVideoPoster(file)" mode="aspectFill"></image>
<view v-if="deletable" class="delete-icon" @click.stop="handleDelete()"> <view class="video-play-mask">
× <view class="video-play-circle">
<view class="video-play-icon"></view>
</view> </view>
</view> </view>
<image
v-if="deletable"
class="delete-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/6ab3fc63-3bad-403d-bbfc-9609a93c048a.png"
mode="aspectFit"
@click.stop="handleDelete(index)"
></image>
</view>
</block>
<view class="preview-item"> <view class="preview-item">
<!-- 上传按钮 --> <!-- 上传按钮 -->
<view <view
class="upload-btn" class="upload-btn"
@click="handleUpload" @click="handleUpload"
:style="btnStyle" :style="btnStyle"
v-if="!oneFile" v-if="videoCount < maxCount"
> >
<slot name="button"> <slot name="button">
<text class="plus-icon">+</text> <text class="plus-icon">+</text>
@ -65,6 +87,14 @@
</view> </view>
</view> </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> --> <!-- <text v-if="tips" class="tips-text">{{ tips }}</text> -->
@ -81,12 +111,12 @@ export default {
props: { props: {
// //
value: { value: {
type: Array, type: [Array, String, Object],
default: () => [] default: () => []
}, },
// //
fileString: { fileString: {
type: String, type: [String, Object, Array],
default: '' default: ''
}, },
// //
@ -145,6 +175,10 @@ export default {
kind: { kind: {
type: Number, type: Number,
default: 1 default: 1
},
sortable: {
type: Boolean,
default: false
} }
}, },
@ -152,7 +186,19 @@ export default {
return { return {
fileList: [], fileList: [],
oneFile: '', oneFile: '',
loading: false videoList: [],
loading: false,
dragIndex: -1,
isDragging: false,
suppressPreview: false,
showVideoPlayer: false,
previewVideoUrl: ''
}
},
computed: {
videoCount() {
return this.showPreview ? this.videoList.length : 0
} }
}, },
@ -160,18 +206,57 @@ export default {
value: { value: {
immediate: true, immediate: true,
handler(newVal) { 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: { fileString: {
immediate: true, immediate: true,
handler(newVal) { handler(newVal) {
this.oneFile = newVal const videoValue = this.normalizeVideoValue(newVal)
this.oneFile = videoValue
this.videoList = videoValue ? [videoValue] : []
} }
} }
}, },
methods: { 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) { showPermissionDialog(title, content) {
@ -272,12 +357,15 @@ export default {
await this.uploadFile(file); await this.uploadFile(file);
} }
} else if (this.accept === 'video/*') { } else if (this.accept === 'video/*') {
const res = await uni.chooseVideo({ const count = this.maxCount - this.videoCount;
sourceType: [sourceType], if (count <= 0) {
compressed: true, uni.showToast({ title: `最多上传${this.maxCount}个文件`, icon: 'none' });
maxDuration: 60 return;
}); }
await this.uploadFile(res); const files = await this.chooseVideoFiles(sourceType, count);
for (const file of files) {
await this.uploadFile(file);
}
} }
} catch (error) { } catch (error) {
console.error('选择文件失败:', error); console.error('选择文件失败:', error);
@ -285,38 +373,27 @@ export default {
}, },
// OSS // OSS
async uploadFile(file) { 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({ uni.showToast({
title: `文件大小不能超过${this.maxSize}MB`, title: `文件大小不能超过${this.maxSize}MB`,
icon: 'none' icon: 'none'
}) })
return Promise.reject(new Error('文件大小超出限制')) return Promise.reject(new Error('文件大小超出限制'))
} }
let date = new Date().getTime() let date = `${new Date().getTime()}_${Math.floor(Math.random() * 100000)}`
let imageType; const fileExt = this.getFileExt(uploadFile)
if(this.accept == 'image/*') {
imageType = file.path.split('.')
}else if(this.accept == 'video/*') {
// #ifdef H5
imageType = file.name.split('.')
// #endif
// #ifdef APP-PLUS || MP-WEIXIN
imageType = file.tempFilePath.split('.')
// #endif
}
console.log('imageType',imageType)
let artisan = this.type==1 ? 'yh' : this.type==2 ? 'syr' : this.type==3 ? 'sj' : ''; let artisan = this.type==1 ? 'yh' : this.type==2 ? 'syr' : this.type==3 ? 'sj' : '';
let name = `${this.userId}_${artisan}_${date}.${imageType[imageType.length - 1]}` let name = `${this.userId}_${artisan}_${date}.${fileExt}`
console.log('name',name) console.log('name',name)
try { try {
// 1. OSS // 1. OSS
const ossConfig = await this.getOssConfig() const ossConfig = await this.getOssConfig()
// 2. // 2.
const uploadRes = await this.uploadToOss(file, ossConfig, name) const fileUrl = await this.uploadToOss(uploadFile, ossConfig, name)
// 3. // 3.
const fileUrl = `${ossConfig.host}/${ossConfig.dir}${name}`
if(this.accept == 'image/*') { if(this.accept == 'image/*') {
const newFile = [`${fileUrl}`] const newFile = [`${fileUrl}`]
this.fileList = this.fileList.concat(newFile) this.fileList = this.fileList.concat(newFile)
@ -327,8 +404,12 @@ export default {
}else if(this.accept == 'video/*') { }else if(this.accept == 'video/*') {
const newFile = fileUrl const newFile = fileUrl
this.oneFile = fileUrl this.oneFile = fileUrl
this.$emit('input', this.oneFile) if (this.showPreview) {
this.$emit('change', this.oneFile) this.videoList = this.videoList.concat([newFile]).slice(0, this.maxCount)
}
const emitValue = this.maxCount > 1 && this.showPreview ? this.videoList : this.oneFile
this.$emit('input', emitValue)
this.$emit('change', emitValue)
this.$emit('success', newFile) this.$emit('success', newFile)
return newFile return newFile
} }
@ -351,44 +432,201 @@ export default {
} }
}, },
getFileExt(file) {
const defaultExt = this.accept === 'video/*' ? 'mp4' : 'jpg'
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) {
const host = String(ossConfig.host || '').replace(/\/+$/, '')
return `${host}/${this.buildObjectKey(ossConfig, name)}`
},
buildObjectKey(ossConfig, name) {
const dir = String(ossConfig.dir || '').replace(/^\/+/, '').replace(/\/+$/, '')
return dir ? `${dir}/${name}` : name
},
getVideoPoster(url) {
if (!url) return ''
if (String(url).indexOf('x-oss-process=') > -1) return url
const connector = String(url).indexOf('?') > -1 ? '&' : '?'
return `${url}${connector}x-oss-process=video/snapshot,t_2000,f_jpg,m_fast`
},
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({
count,
mediaType: ['video'],
sourceType: [sourceType],
maxDuration: 60,
success: (res) => {
const files = (res.tempFiles || []).map((item) => ({
...item,
tempFilePath: item.tempFilePath || item.path,
path: item.tempFilePath || item.path,
}));
resolve(files);
},
fail: reject,
});
});
}
return new Promise((resolve, reject) => {
uni.chooseVideo({
sourceType: [sourceType],
compressed: true,
maxDuration: 60,
success: (res) => resolve([res]),
fail: reject,
});
});
},
// OSS // OSS
uploadToOss(file, ossConfig, name) { uploadToOss(file, ossConfig, name) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const objectKey = this.buildObjectKey(ossConfig, name)
const fileUrl = this.buildFileUrl(ossConfig, name)
const mimeType = this.getMimeType(file, name)
let formData;
// #ifdef H5 // #ifdef H5
const formData = new FormData() formData = new FormData()
formData.append('name',name); formData.append('name',name);
formData.append('policy', ossConfig.policy); formData.append('policy', ossConfig.policy);
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId); formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
formData.append('success_action_status', '200'); formData.append('success_action_status', '200');
formData.append('signature', ossConfig.signature); formData.append('signature', ossConfig.signature);
formData.append('key', ossConfig.dir + name); formData.append('key', objectKey);
if (mimeType) {
formData.append('Content-Type', mimeType);
formData.append('x-oss-meta-content-type', mimeType);
}
// filefile // filefile
formData.append('file', file); formData.append('file', file.file || file.tempFile || file);
fetch(ossConfig.host, { method: 'POST', body: formData},).then((res) => { fetch(ossConfig.host, { method: 'POST', body: formData},).then((res) => {
resolve(res) if (res.ok) {
}).catch(()=>{ resolve(fileUrl)
reject(new Error(`上传失败: ${res.statusCode}`)) } else {
reject(new Error(`上传失败: ${res.status}`))
}
}).catch((error)=>{
reject(error)
}) })
// #endif // #endif
// #ifdef APP-PLUS || MP-WEIXIN // #ifdef APP-PLUS || MP-WEIXIN
let formData = {}; formData = this.buildOssFormData(ossConfig, objectKey, mimeType);
formData.name = name; 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; let path;
if(this.accept == 'image/*') { if(this.accept == 'image/*') {
path = file.path path = this.getFilePath(file)
}else if(this.accept == 'video/*') { }else if(this.accept == 'video/*') {
// #ifdef H5 // #ifdef H5
path = file.name path = this.getFilePath(file)
// #endif // #endif
// #ifdef APP-PLUS || MP-WEIXIN // #ifdef APP-PLUS || MP-WEIXIN
path = file.tempFilePath path = this.getFilePath(file)
// #endif // #endif
} }
// 使 uni.uploadFile // 使 uni.uploadFile
@ -396,15 +634,17 @@ export default {
url: ossConfig.host, url: ossConfig.host,
filePath: path, // filePath: path, //
name: 'file', name: 'file',
formData: { formData,
key: ossConfig.dir + name, // success: (res) => {
policy: ossConfig.policy, // const statusCode = Number(res.statusCode)
OSSAccessKeyId: ossConfig.ossAccessKeyId, // ID if (statusCode >= 200 && statusCode < 300) {
success_action_status: 200, // 200,204 resolve(fileUrl)
signature: ossConfig.signature // } else {
reject(new Error(`上传失败: ${statusCode} ${res.data || ''}`))
}
}, },
success(res) { fail(error) {
resolve(`${ossConfig.host}/${ossConfig.dir}${name}`) reject(error)
} }
}); });
// #endif // #endif
@ -414,12 +654,69 @@ export default {
// //
handlePreview(index) { handlePreview(index) {
if (this.isDragging || this.suppressPreview) return
uni.previewImage({ uni.previewImage({
current: index, current: index,
urls: this.fileList.map(item => item.url) urls: this.fileList
}) })
}, },
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
this.isDragging = true
},
handleDragMove(event) {
if (!this.isDragging || this.dragIndex < 0) return
if (event.preventDefault) event.preventDefault()
if (event.stopPropagation) event.stopPropagation()
const touch = event.touches && event.touches[0]
if (!touch) return
uni.createSelectorQuery()
.in(this)
.selectAll('.image-preview-item')
.boundingClientRect((rects) => {
if (!Array.isArray(rects) || !rects.length) return
const targetIndex = rects.findIndex((rect) => {
return touch.clientX >= rect.left &&
touch.clientX <= rect.right &&
touch.clientY >= rect.top &&
touch.clientY <= rect.bottom
})
if (targetIndex < 0 || targetIndex === this.dragIndex || targetIndex >= this.fileList.length) return
const nextList = this.fileList.slice()
const moved = nextList.splice(this.dragIndex, 1)[0]
nextList.splice(targetIndex, 0, moved)
this.fileList = nextList
this.dragIndex = targetIndex
this.$emit('input', this.fileList)
this.$emit('change', this.fileList)
})
.exec()
},
endDrag() {
if (!this.isDragging) return
this.dragIndex = -1
this.suppressPreview = true
setTimeout(() => {
this.isDragging = false
this.suppressPreview = false
}, 200)
},
// //
handleDelete(index) { handleDelete(index) {
// uni.showModal({ // uni.showModal({
@ -434,11 +731,14 @@ export default {
this.$emit('change', this.fileList) this.$emit('change', this.fileList)
this.$emit('delete', deletedFile, index) this.$emit('delete', deletedFile, index)
}else if(this.accept == 'video/*') { }else if(this.accept == 'video/*') {
const deletedFile = this.oneFile const deleteIndex = index === undefined ? 0 : index
this.oneFile = '' const deletedFile = this.videoList[deleteIndex]
this.$emit('input', this.oneFile) this.videoList.splice(deleteIndex, 1)
this.$emit('change', this.oneFile) this.oneFile = this.videoList[0] || ''
this.$emit('delete', deletedFile) const emitValue = this.maxCount > 1 ? this.videoList : this.oneFile
this.$emit('input', emitValue)
this.$emit('change', emitValue)
this.$emit('delete', deletedFile, deleteIndex)
} }
// } // }
@ -461,8 +761,11 @@ export default {
.preview-container { .preview-container {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 200rpx);
justify-items: center; column-gap: 18rpx;
row-gap: 20rpx;
justify-content: flex-start;
justify-items: stretch;
align-items: center; align-items: center;
position: relative; position: relative;
} }
@ -501,7 +804,11 @@ export default {
position: relative; position: relative;
width: 200rpx; width: 200rpx;
height: 200rpx; height: 200rpx;
margin-top: 20rpx; }
.image-preview-item.dragging {
opacity: 0.65;
transform: scale(0.96);
} }
.preview-image { .preview-image {
@ -510,19 +817,90 @@ export default {
border-radius: 8rpx; 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 { .delete-icon {
position: absolute; position: absolute;
top: -10rpx; top: -10rpx;
right: -10rpx; right: -10rpx;
width: 40rpx; width: 42rpx;
height: 40rpx; height: 42rpx;
background-color: #ff4d4f; z-index: 5;
color: white; }
border-radius: 50%;
.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; display: flex;
justify-content: center;
align-items: center; align-items: center;
font-size: 32rpx; justify-content: center;
z-index: 1; }
.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> </style>

View File

@ -0,0 +1,111 @@
<template>
<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">{{ getOptionLabel(item) }}</text>
</view>
</view>
</template>
<script>
export default {
name: 'FormItemCheckbox',
props: {
value: { type: Array, default: function () { return []; } },
options: { type: Array, default: function () { return []; } },
maxCount: { type: Number, default: 0 }
},
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;
},
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 = this.findValueIndex(newValue, val);
if (index === -1) {
if (this.maxCount > 0 && newValue.length >= this.maxCount) {
uni.showToast({
title: '最多只能选择' + this.maxCount + '项',
icon: 'none'
});
return;
}
newValue.push(val);
} else {
newValue.splice(index, 1);
}
this.$emit('input', newValue);
}
}
};
</script>
<style scoped>
.form-checkbox {
display: flex;
flex-wrap: wrap;
}
.checkbox-item {
display: flex;
align-items: center;
margin-right: 40rpx;
margin-bottom: 16rpx;
}
.checkbox-dot {
width: 36rpx;
height: 36rpx;
border-radius: 8rpx;
border: 2rpx solid #cccccc;
margin-right: 12rpx;
position: relative;
}
.checkbox-dot.active {
border-color: #e8101e;
background-color: #e8101e;
}
.checkbox-dot.active::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -60%) rotate(-45deg);
width: 20rpx;
height: 12rpx;
border-left: 4rpx solid #ffffff;
border-bottom: 4rpx solid #ffffff;
}
.checkbox-label {
font-size: 28rpx;
color: #333333;
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<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>
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 },
userId: { type: [Number, String], default: null },
ossType: { type: [Number, String], default: null }
},
data: function () {
return {
currentValue: []
};
},
computed: {
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: {
handler: function (val) {
this.currentValue = Array.isArray(val) ? val.slice() : [];
},
immediate: true
}
},
methods: {
onInput: function (list) {
this.$emit('input', list);
},
onChange: function (list) {
this.$emit('change', list);
}
}
};
</script>

View File

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

View File

@ -0,0 +1,89 @@
<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: isActive(item) }"></view>
<text class="radio-label">{{ getOptionLabel(item) }}</text>
</view>
</view>
</template>
<script>
export default {
name: 'FormItemRadio',
props: {
value: { type: [String, Number], default: '' },
options: { type: Array, default: function () { return []; } }
},
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;
},
isActive: function (item) {
return this.value == this.getOptionValue(item);
},
onSelect: function (item) {
this.$emit('input', this.getOptionValue(item));
}
}
};
</script>
<style scoped>
.form-radio {
display: flex;
flex-wrap: wrap;
}
.radio-item {
display: flex;
align-items: center;
margin-right: 40rpx;
margin-bottom: 16rpx;
}
.radio-dot {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 2rpx solid #cccccc;
margin-right: 12rpx;
position: relative;
}
.radio-dot.active {
border-color: #e8101e;
background-color: #e8101e;
}
.radio-dot.active::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background-color: #ffffff;
}
.radio-label {
font-size: 28rpx;
color: #333333;
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<view class="form-richtext">
<view class="richtext-trigger" @click="goEdit">
<text class="richtext-value">{{ value ? '已设置' : '去设置' }}</text>
</view>
</view>
</template>
<script>
export default {
name: 'FormItemRichtext',
props: {
value: { type: String, default: '' }
},
methods: {
goEdit: function () {
uni.navigateTo({
url: '/pages/shop/add-img-text?list=' + this.value
});
}
}
};
</script>
<style scoped>
.form-richtext {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
}
.richtext-trigger {
display: flex;
align-items: center;
}
.richtext-value {
font-size: 28rpx;
color: #333333;
text-align: right;
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<picker :range="displayOptions" @change="onChange">
<view class="form-select">
<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" />
</view>
</picker>
</template>
<script>
export default {
name: 'FormItemSelect',
props: {
value: { type: [String, Number], 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', this.getOptionValue(selected));
}
}
}
};
</script>
<style scoped>
.form-select {
display: flex;
align-items: center;
justify-content: space-between;
}
.select-text {
font-size: 28rpx;
color: #333333;
margin-right: 16rpx;
}
.placeholder {
font-weight: 400;
font-size: 28rpx;
color: #999999;
}
.arrow-right {
width: 32rpx;
height: 32rpx;
}
</style>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,256 @@
<template>
<view>
<view v-if="maxCount > 1" class="video-list">
<view v-for="(item, index) in currentList" :key="index" class="video-wrapper">
<image :src="getVideoPoster(item)" class="preview-video" mode="aspectFill" @click="previewVideo(item)" />
<view class="play-mask" @click="previewVideo(item)">
<view class="play-circle">
<view class="play-icon"></view>
</view>
</view>
<image
class="delete-btn"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/6ab3fc63-3bad-403d-bbfc-9609a93c048a.png"
mode="aspectFit"
@click="deleteVideo(index)"
></image>
</view>
<ali-oss-uploader v-if="currentList.length < maxCount"
:max-count="maxCount - currentList.length"
:max-size="maxSize" accept="video/*" :userId="uploadUserId" :type="ossType" :kind="2" width="200rpx"
:show-preview="false"
@input="onMultiInput" @change="onChange" />
</view>
<view v-else>
<ali-oss-uploader v-if="!currentValue" :max-count="1" :max-size="maxSize" accept="video/*"
:userId="uploadUserId" :type="ossType" :kind="2" width="100%" :file-string="currentValue" @input="onInput"
@change="onChange" @success="onInput" @delete="deleteSingleVideo" />
<view v-else class="video-wrapper">
<image :src="getVideoPoster(currentValue)" class="preview-video" mode="aspectFill" @click="previewVideo(currentValue)" />
<view class="play-mask" @click="previewVideo(currentValue)">
<view class="play-circle">
<view class="play-icon"></view>
</view>
</view>
<image
class="delete-btn"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/6ab3fc63-3bad-403d-bbfc-9609a93c048a.png"
mode="aspectFit"
@click="deleteSingleVideo"
></image>
</view>
</view>
<view v-if="showPlayer" class="player-mask" @click="closePlayer">
<view class="player-box" @click.stop>
<video :src="playingVideo" class="player-video" controls autoplay show-fullscreen-btn />
<view class="player-close" @click="closePlayer">
<text class="player-close-text">&times;</text>
</view>
</view>
</view>
</view>
</template>
<script>
import AliOssUploader from '../ali-oss-uploader/ali-oss-uploader.vue'
export default {
name: 'FormItemVideo',
components: { AliOssUploader },
props: {
value: { type: [String, Array], default: '' },
maxSize: { type: Number, default: 100 },
maxCount: { type: Number, default: 1 },
userId: { type: [Number, String], default: null }
},
data: function () {
return {
currentValue: '',
currentList: [],
showPlayer: false,
playingVideo: ''
};
},
computed: {
uploadUserId: function () {
if (this.userId) return this.userId;
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;
}
},
watch: {
value: {
handler: function (val) {
if (this.maxCount > 1) {
if (Array.isArray(val)) {
this.currentList = val.filter(function (v) { return !!v; });
} else if (val) {
this.currentList = [val];
} else {
this.currentList = [];
}
} else {
this.currentValue = Array.isArray(val) ? (val[0] || '') : (val || '');
}
},
immediate: true
}
},
methods: {
onInput: function (val) {
this.currentValue = val;
this.$emit('input', val);
},
onMultiInput: function (val) {
var arr = Array.isArray(val) ? val : [val];
var existing = this.currentList.filter(function (item) {
return arr.indexOf(item) === -1;
});
var list = existing.concat(arr).filter(function (item, index, self) {
return item && self.indexOf(item) === index;
}).slice(0, this.maxCount);
this.currentList = list;
this.$emit('input', list);
},
onChange: function (val) {
this.$emit('change', val);
},
getVideoPoster: function (url) {
if (!url) return '';
if (String(url).indexOf('x-oss-process=') > -1) return url;
var connector = String(url).indexOf('?') > -1 ? '&' : '?';
return url + connector + 'x-oss-process=video/snapshot,t_2000,f_jpg,m_fast';
},
previewVideo: function (url) {
if (!url) return;
this.playingVideo = url;
this.showPlayer = true;
},
closePlayer: function () {
this.showPlayer = false;
this.playingVideo = '';
},
deleteSingleVideo: function () {
this.currentValue = '';
this.$emit('input', '');
},
deleteVideo: function (index) {
this.currentList.splice(index, 1);
this.$emit('input', this.currentList);
}
}
};
</script>
<style scoped>
.video-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.video-wrapper {
position: relative;
width: 200rpx;
height: 200rpx;
overflow: visible;
}
.preview-video {
width: 200rpx;
height: 200rpx;
border-radius: 8rpx;
background-color: #f5f5f5;
}
.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.15);
border-radius: 8rpx;
z-index: 2;
}
.play-circle {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
}
.play-icon {
width: 0;
height: 0;
border-top: 14rpx solid transparent;
border-bottom: 14rpx solid transparent;
border-left: 22rpx solid #ffffff;
margin-left: 6rpx;
}
.delete-btn {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 42rpx;
height: 42rpx;
z-index: 5;
}
.player-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.82);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
box-sizing: border-box;
}
.player-box {
position: relative;
width: 100%;
}
.player-video {
width: 100%;
height: 420rpx;
background-color: #000000;
}
.player-close {
position: absolute;
right: -12rpx;
top: -58rpx;
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.22);
display: flex;
align-items: center;
justify-content: center;
}
.player-close-text {
color: #ffffff;
font-size: 42rpx;
line-height: 48rpx;
}
</style>

View File

@ -0,0 +1,471 @@
<template>
<view class="dynamic-form">
<view v-for="(group, gi) in fields" :key="gi" class="form-group">
<!-- single -->
<block v-if="group.type === 'single'">
<view v-if="group.field.type === 'text'" class="form-item">
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
}}</text>
<input type="text" :value="formData[group.field.key]" :placeholder="group.field.placeholder"
placeholder-class="placeholder" class="form-input" @input="onInput(group.field.key, $event)" />
</view>
<view v-else-if="group.field.type === 'number'" class="form-item"
:style="group.field.tip ? 'flex-wrap: wrap;' : ''">
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
}}</text>
<input type="digit" :value="formData[group.field.key]" :placeholder="group.field.placeholder"
placeholder-class="placeholder" class="form-input" @input="onInput(group.field.key, $event)" />
<text v-if="group.field.tips && !group.field.tip" class="form-tips">{{ group.field.tips }}</text>
<view v-if="group.field.tip" class="price-tip">
<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"
: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>
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
}}</text>
<text class="upload-tip">(仅可上传{{ group.field.maxCount || 1 }}个视频)</text>
</view>
<view class="upload-content">
<form-item-video :value="formData[group.field.key]" :maxCount="group.field.maxCount || 1"
:maxSize="group.field.maxSize || group.field.maxFileSize || 100"
:userId="uploadUserId"
@input="onInput(group.field.key, $event)" />
</view>
</view>
<view v-else-if="group.field.type === 'richtext_editor'" class="form-item">
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
}}</text>
<form-item-richtext :value="formData[group.field.key]" @input="onInput(group.field.key, $event)" />
</view>
<view v-else-if="group.field.type === 'service_step'" class="form-item-up upload-item">
<form-item-service-step :value="formData[group.field.key]" :label="group.field.label"
:required="group.field.is_required === true" @input="onInput(group.field.key, $event)" />
</view>
</block>
<!-- section -->
<view v-if="group.type === 'section'" class="section-card">
<view v-if="group.sectionTitle" class="section-title-bar">
<text class="section-title">{{ group.sectionTitle }}</text>
</view>
<view v-for="(field, fi) in group.fields" :key="field.key || fi" class="section-field">
<view v-if="field.type === 'text'" class="form-item no-bg">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<input type="text" :value="formData[field.key]" :placeholder="field.placeholder"
placeholder-class="placeholder" class="form-input" @input="onInput(field.key, $event)" />
</view>
<view v-else-if="field.type === 'textarea'" class="detail-row">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<view class="detail-textarea-wrap">
<textarea class="detail-textarea" :value="formData[field.key]" :placeholder="field.placeholder"
placeholder-class="placeholder" maxlength="200" @input="onInput(field.key, $event)" />
<text class="detail-count">{{ (formData[field.key] || "").length }}/200</text>
</view>
</view>
<view v-else-if="field.type === 'number'" class="form-item no-bg"
:style="field.tip ? 'flex-wrap: wrap;' : ''">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<input type="digit" :value="formData[field.key]" :placeholder="field.placeholder"
placeholder-class="placeholder" class="form-input" @input="onInput(field.key, $event)" />
<text v-if="field.tips && !field.tip" class="form-tips">{{ field.tips }}</text>
<view v-if="field.tip" class="price-tip">
<text class="price-tip-card">{{ field.tip }}</text>
</view>
</view>
<view v-else-if="field.type === 'select'" class="form-item no-bg">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<form-item-select :value="formData[field.key]" :options="field.options || []"
:placeholder="field.placeholder" @input="onInput(field.key, $event)" />
</view>
<view v-else-if="field.type === 'single_select'" class="form-item no-bg" style="flex-wrap: wrap;">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<view class="select-options">
<form-item-radio :value="formData[field.key]" :options="field.options || []"
@input="onInput(field.key, $event)" />
</view>
</view>
<view v-else-if="field.type === 'multi_select'" class="form-item no-bg" style="flex-wrap: wrap;">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<view class="select-options">
<form-item-checkbox :value="formData[field.key]" :options="field.options || []"
:maxCount="field.maxCount || 0" @input="onInput(field.key, $event)" />
</view>
</view>
<view v-else-if="field.type === 'image_multi'" class="form-item-up upload-item no-bg">
<view>
<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"
: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>
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<text class="upload-tip">(仅可上传{{ field.maxCount || 1 }}个视频)</text>
</view>
<view class="upload-content">
<form-item-video :value="formData[field.key]" :maxCount="field.maxCount || 1"
:maxSize="field.maxSize || field.maxFileSize || 100" :userId="uploadUserId"
@input="onInput(field.key, $event)" />
</view>
</view>
<view v-else-if="field.type === 'richtext_editor'" class="form-item no-bg">
<text :class="['form-label', field.is_required === true ? 'required' : '']">{{ field.label }}</text>
<form-item-richtext :value="formData[field.key]" @input="onInput(field.key, $event)" />
</view>
<view v-else-if="field.type === 'service_step'" class="form-item-up upload-item no-bg">
<form-item-service-step :value="formData[field.key]" :label="field.label"
:required="field.is_required === true" @input="onInput(field.key, $event)" />
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import FormItemText from './FormItemText.vue';
import FormItemTextarea from './FormItemTextarea.vue';
import FormItemNumber from './FormItemNumber.vue';
import FormItemImage from './FormItemImage.vue';
import FormItemVideo from './FormItemVideo.vue';
import FormItemSelect from './FormItemSelect.vue';
import FormItemRadio from './FormItemRadio.vue';
import FormItemCheckbox from './FormItemCheckbox.vue';
import FormItemRichtext from './FormItemRichtext.vue';
import FormItemServiceStep from './FormItemServiceStep.vue';
export default {
name: 'DynamicForm',
components: {
'form-item-text': FormItemText,
'form-item-textarea': FormItemTextarea,
'form-item-number': FormItemNumber,
'form-item-image': FormItemImage,
'form-item-video': FormItemVideo,
'form-item-select': FormItemSelect,
'form-item-radio': FormItemRadio,
'form-item-checkbox': FormItemCheckbox,
'form-item-richtext': FormItemRichtext,
'form-item-service-step': FormItemServiceStep
},
props: {
fields: {
type: Array,
default: function () { return []; }
},
value: {
type: Object,
default: function () { return {}; }
},
uploadUserId: {
type: [Number, String],
default: null
}
},
data: function () {
return {
formData: {}
};
},
watch: {
value: {
handler: function (val) {
this.formData = Object.assign({}, val);
},
immediate: true
},
fields: {
handler: function () {
var self = this;
var data = {};
this.fields.forEach(function (group) {
if (group.type === 'single' && group.field) {
self._initFieldData(data, group.field);
} else if (group.type === 'section' && group.fields) {
group.fields.forEach(function (field) {
self._initFieldData(data, field);
});
}
});
this.formData = Object.assign({}, this.formData, data);
},
immediate: true
}
},
methods: {
_initFieldData: function (data, field) {
if (this.formData[field.key] !== undefined) {
data[field.key] = this.formData[field.key];
} else if (field.default_value !== undefined) {
data[field.key] = field.default_value;
} else if (
field.type === 'multi_select' ||
field.type === 'image_multi' ||
field.type === 'service_step' ||
(field.type === 'video' && field.maxCount > 1)
) {
data[field.key] = [];
} else {
data[field.key] = '';
}
},
onInput: function (key, event) {
var value = event;
if (event && event.target && event.target.value !== undefined) {
value = event.target.value;
}
this.$set(this.formData, key, value);
this.$emit('input', this.formData);
},
getData: function () {
return Object.assign({}, this.formData);
},
validate: function () {
var self = this;
for (var i = 0; i < this.fields.length; i++) {
var group = this.fields[i];
var fieldsToCheck = [];
if (group.type === 'single' && group.field) {
fieldsToCheck.push(group.field);
} else if (group.type === 'section' && group.fields) {
fieldsToCheck = group.fields;
}
for (var j = 0; j < fieldsToCheck.length; j++) {
var field = fieldsToCheck[j];
if (field.is_required === true) {
var value = self.formData[field.key];
if (!value || (Array.isArray(value) && value.length === 0)) {
uni.showToast({
title: field.label + '不能为空',
icon: 'none'
});
return false;
}
}
}
}
return true;
}
}
};
</script>
<style scoped>
.dynamic-form {
width: 100%;
}
.form-group {
width: 100%;
}
.form-item {
background-color: #fff;
padding: 24rpx;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
border-radius: 10rpx;
margin-bottom: 24rpx;
}
.form-label {
font-size: 28rpx;
color: #333333;
}
.required::before {
content: '*';
color: #ff0000;
margin-right: 4rpx;
}
.form-tips {
font-size: 24rpx;
color: #333333;
margin-left: 0;
}
.price-tip {
width: 100%;
margin-top: 40rpx;
}
.price-tip-card {
padding: 8rpx 12rpx;
background: #FDF0F0;
border-radius: 8rpx;
font-weight: 400;
font-size: 24rpx;
color: #E8101E;
line-height: 32rpx;
}
.form-item-up {
background-color: #fff;
padding: 24rpx;
display: flex;
flex-flow: column nowrap;
justify-content: space-between;
align-items: flex-start;
box-sizing: border-box;
border-radius: 10rpx;
margin-bottom: 24rpx;
}
.upload-item {
padding-bottom: 30rpx;
}
.upload-tip {
font-size: 24rpx;
color: #999999;
margin-left: 10rpx;
}
.upload-content {
margin-top: 20rpx;
width: 100%;
}
.select-options {
width: 100%;
margin-top: 16rpx;
}
.section-card {
background: #ffffff;
border-radius: 20rpx;
padding: 0 24rpx 24rpx;
margin-bottom: 24rpx;
box-sizing: border-box;
overflow: hidden;
}
/* section内字段间距20rpx通过section-field包装器控制 */
.section-field+.section-field {
padding-top: 12rpx;
}
/* section内字段去掉独立白色背景和底部间距 */
.section-card .form-item,
.section-card .form-item-up {
background-color: transparent;
padding-left: 0;
padding-right: 0;
margin-bottom: 0;
border-radius: 0;
}
.section-card .detail-row {
padding-top: 0;
padding-bottom: 0;
}
.section-title-bar {
padding: 0;
margin: 0;
}
.section-title {
padding: 24rpx 0 20rpx;
font-size: 30rpx;
font-weight: 500;
color: #333;
line-height: 40rpx;
display: block;
}
.detail-row {
padding: 20rpx 0 0;
}
.detail-row:first-child {
padding-top: 0;
}
.detail-row .form-label {
display: block;
margin-bottom: 20rpx;
line-height: 40rpx;
}
.detail-textarea-wrap {
position: relative;
background: #f7f7f7;
border-radius: 16rpx;
padding: 24rpx 24rpx 52rpx;
box-sizing: border-box;
}
.detail-textarea {
width: 100%;
height: 108rpx;
min-height: 108rpx;
margin: 0;
padding: 0;
background: transparent;
border: 0;
font-size: 28rpx;
line-height: 40rpx;
color: #333333;
box-sizing: border-box;
}
.detail-count {
position: absolute;
right: 24rpx;
bottom: 16rpx;
font-size: 22rpx;
color: #999999;
line-height: 32rpx;
}
</style>

View File

@ -22,7 +22,6 @@
</view> </view>
</view> </view>
</template> </template>
<script src="https://gosspublic.alicdn.com/aliyun-oss-sdk-6.18.0.min.js"></script>
<script> <script>
import request from '../../utils/request'; import request from '../../utils/request';
export default { export default {

View File

@ -200,11 +200,12 @@ import sparkMD5 from 'spark-md5'; // 引入MD5计算库
const loadNext = () => { const loadNext = () => {
const start = currentChunk * chunkSize; const start = currentChunk * chunkSize;
const end = start + chunkSize >= file.size ? file.size : start + chunkSize; const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
let reader;
// #ifdef H5 // #ifdef H5
const reader = new FileReader(); reader = new FileReader();
// #endif // #endif
// #ifndef H5 // #ifndef H5
const reader = uni.getfilesystem; reader = uni.getfilesystem;
// #endif // #endif
reader.onload = (e) => { reader.onload = (e) => {
spark.append(e.target.result); spark.append(e.target.result);
@ -226,11 +227,12 @@ import sparkMD5 from 'spark-md5'; // 引入MD5计算库
try { try {
// ArrayBuffer // ArrayBuffer
const arrayBuffer = await new Promise((resolve) => { const arrayBuffer = await new Promise((resolve) => {
let reader;
// #ifdef H5 // #ifdef H5
const reader = new FileReader(); reader = new FileReader();
// #endif // #endif
// #ifndef H5 // #ifndef H5
const reader = uni.getfilesystem; reader = uni.getfilesystem;
// #endif // #endif
reader.onload = () => resolve(reader.result); reader.onload = () => resolve(reader.result);
reader.readAsArrayBuffer(chunk); reader.readAsArrayBuffer(chunk);

View File

@ -2,8 +2,8 @@
"name" : "美融融商家", "name" : "美融融商家",
"appid" : "__UNI__BBE6285", "appid" : "__UNI__BBE6285",
"description" : "同城便捷到家美业“技术外卖”服务、同城预约到店服务平台美融融为同城爱美人群提供更优质的更全面的服务。", "description" : "同城便捷到家美业“技术外卖”服务、同城预约到店服务平台美融融为同城爱美人群提供更优质的更全面的服务。",
"versionName" : "1.0.21", "versionName" : "1.0.22",
"versionCode" : 1021, "versionCode" : 1022,
"transformPx" : false, "transformPx" : false,
/* 5+App */ /* 5+App */
"app-plus" : { "app-plus" : {

View File

@ -31,6 +31,20 @@
"enablePullDownRefresh": true "enablePullDownRefresh": true
} }
}, },
{
"path": "pages/home/growth-package",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "pages/home/growth-package-result",
"style": {
"navigationBarTitleText": "支付结果",
"navigationStyle": "custom"
}
},
{ {
"path": "pages/message/message", "path": "pages/message/message",
"style": { "style": {

View File

@ -99,7 +99,54 @@
</view> </view>
</view> </view>
<!-- <view class="add-btn"></view> -->
<!-- 服务分类选择弹窗 -->
<view class="category-popup-mask" v-if="showCategoryPopup" @click="closeCategoryPopup">
<view class="category-popup" @click.stop>
<view class="category-popup-header">
<text class="category-popup-title">选择服务类型</text>
<view class="category-popup-close" @click="closeCategoryPopup">
<text class="category-popup-close-icon">&times;</text>
</view>
</view>
<view class="category-popup-body">
<!-- 左侧一级分类 -->
<scroll-view scroll-y class="category-left">
<view v-for="(item, index) in firstClassList" :key="item.id"
:class="['category-left-item', { active: selectedFirstIndex === index }]"
@click="selectFirstClass(index, item)">
<text class="category-left-text">{{ item.title }}</text>
</view>
</scroll-view>
<!-- 右侧二级分类 -->
<scroll-view scroll-y class="category-right">
<view v-if="secondClassList.length === 0" class="category-empty">
<text class="category-empty-text">暂无子分类</text>
</view>
<view v-for="item in secondClassList" :key="item.id"
:class="['category-right-item', { selected: selectedSecondId === item.id }]"
@click="selectSecondClass(item)">
<text class="category-right-text">{{ item.title }}</text>
<image class="category-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/b6fa6b5b-b5c1-45e3-9372-74ade548f76b.png"
mode="widthFix" v-if="!selectedSecondId || selectedSecondId !== item.id"></image>
<image class="category-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/f0f97cad-03f6-4363-abb5-58d3176cfa81.png"
mode="widthFix" v-else></image>
</view>
</scroll-view>
</view>
<!-- 底部按钮 -->
<view class="category-popup-footer">
<view class="category-popup-btn category-popup-btn-cancel" @click="closeCategoryPopup">
<text>取消</text>
</view>
<view class="category-popup-btn category-popup-btn-confirm" @click="confirmCategory">
<text>确认</text>
</view>
</view>
</view>
</view>
</view> </view>
</template> </template>
@ -137,33 +184,14 @@
startX: 0, startX: 0,
moveX: 0, moveX: 0,
currentIndex: -1, currentIndex: -1,
//
showCategoryPopup: false,
firstClassList: [],
secondClassList: [],
selectedFirstIndex: -1,
selectedSecondId: null,
}; };
}, },
// computed: {
// filteredServices() {
// return this.services.filter(service => {
// //
// if (this.title && !service.title.includes(this.title)) {
// return false
// }
// //
// switch (this.currentTab) {
// case 0: // 稿
// return service.state === 2
// case 1: //
// return service.state === 1
// case 2: //
// return service.state === 2
// case 3: //
// return service.state === 3
// case 4: //
// return service.state === 4
// default:
// return true
// }
// })
// }
// },
onLoad() { onLoad() {
this.getServicesList(); this.getServicesList();
}, },
@ -208,7 +236,7 @@
title: "下架成功", title: "下架成功",
icon: "none", icon: "none",
}); });
this.getServicesList(0); // this.getServicesList(0);
} else { } else {
uni.showToast({ uni.showToast({
title: res.msg, title: res.msg,
@ -232,7 +260,7 @@
title: "上架成功", title: "上架成功",
icon: "none", icon: "none",
}); });
this.getServicesList(0); // this.getServicesList(0);
} else { } else {
uni.showToast({ uni.showToast({
title: res.msg, title: res.msg,
@ -242,15 +270,116 @@
}); });
}, },
viewEdit(item) { viewEdit(item) {
uni.navigateTo({ let url = `/pages/shop/add-service?id=${item.id}&state=${item.state}`;
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() { async getCurrentSjId() {
uni.navigateTo({ const sjInfo = this.$store && this.$store.state && this.$store.state.sjInfo;
url: `/pages/shop/add-service`, 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;
this.selectedSecondId = null;
this.secondClassList = [];
this.showCategoryPopup = true;
this.loadSecondClass(res.data[0].id);
} else {
uni.showToast({
title: "暂无分类数据",
icon: "none",
});
}
} catch (error) {
console.error("获取商家服务分类失败:", error);
uni.showToast({
title: "获取分类失败",
icon: "none",
});
}
},
loadSecondClass(firstId) {
request.post("/user/secondclass", {
id: firstId,
}).then((res) => {
this.secondClassList = res.data || [];
});
},
selectFirstClass(index, item) {
if (this.selectedFirstIndex === index) return;
this.selectedFirstIndex = index;
this.selectedSecondId = null;
this.loadSecondClass(item.id);
},
selectSecondClass(item) {
this.selectedSecondId = item.id;
},
confirmCategory() {
if (this.selectedFirstIndex < 0) {
uni.showToast({
title: "请选择服务分类",
icon: "none",
});
return;
}
if (this.secondClassList.length > 0 && !this.selectedSecondId) {
uni.showToast({
title: "请选择服务子类",
icon: "none",
});
return;
}
const firstItem = this.firstClassList[this.selectedFirstIndex];
let url = `/pages/shop/add-service?first_class_id=${firstItem.id}`;
if (this.selectedSecondId) {
url += `&second_class_id=${this.selectedSecondId}`;
}
uni.navigateTo({ url });
this.closeCategoryPopup();
},
closeCategoryPopup() {
this.showCategoryPopup = false;
this.firstClassList = [];
this.secondClassList = [];
this.selectedFirstIndex = -1;
this.selectedSecondId = null;
}, },
deleteService(item) { deleteService(item) {
uni.showModal({ uni.showModal({
@ -266,7 +395,7 @@
title: "删除成功", title: "删除成功",
icon: "none", icon: "none",
}); });
this.getServicesList(0); // this.getServicesList(0);
} else { } else {
uni.showToast({ uni.showToast({
title: res.msg, title: res.msg,
@ -280,11 +409,8 @@
}, },
touchStart(e, index) { touchStart(e, index) {
if (this.currentTab != 3) return; if (this.currentTab != 3) return;
//
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
//
if (e.touches && e.touches[0]) { if (e.touches && e.touches[0]) {
this.startX = e.touches[0].clientX; this.startX = e.touches[0].clientX;
this.currentIndex = index; this.currentIndex = index;
@ -293,87 +419,60 @@
touchMove(e) { touchMove(e) {
if (this.currentTab != 3) return; if (this.currentTab != 3) return;
if (this.currentIndex === -1) return; if (this.currentIndex === -1) return;
//
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
//
if (e.touches && e.touches[0]) { if (e.touches && e.touches[0]) {
this.moveX = e.touches[0].clientX - this.startX; this.moveX = e.touches[0].clientX - this.startX;
//
if (this.moveX > 0) { if (this.moveX > 0) {
this.moveX = 0; this.moveX = 0;
} }
//
if (this.moveX < -160) { if (this.moveX < -160) {
this.moveX = -160; this.moveX = -160;
} }
//
this.$set(this.services[this.currentIndex], "slideX", this.moveX); this.$set(this.services[this.currentIndex], "slideX", this.moveX);
} }
}, },
touchEnd(e) { touchEnd(e) {
if (this.currentTab != 3) return; if (this.currentTab != 3) return;
if (this.currentIndex === -1) return; if (this.currentIndex === -1) return;
//
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
//
if (this.moveX < -80) { if (this.moveX < -80) {
this.$set(this.services[this.currentIndex], "slideX", -160); this.$set(this.services[this.currentIndex], "slideX", -160);
} else { } else {
//
this.$set(this.services[this.currentIndex], "slideX", 0); this.$set(this.services[this.currentIndex], "slideX", 0);
} }
this.currentIndex = -1; this.currentIndex = -1;
}, },
mouseDown(e, index) { mouseDown(e, index) {
if (this.currentTab != 3) return; if (this.currentTab != 3) return;
//
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.startX = e.clientX; this.startX = e.clientX;
this.currentIndex = index; this.currentIndex = index;
}, },
mouseMove(e) { mouseMove(e) {
if (this.currentTab != 3) return; if (this.currentTab != 3) return;
if (this.currentIndex === -1) return; if (this.currentIndex === -1) return;
//
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.moveX = e.clientX - this.startX; this.moveX = e.clientX - this.startX;
//
if (this.moveX > 0) { if (this.moveX > 0) {
this.moveX = 0; this.moveX = 0;
} }
//
if (this.moveX < -160) { if (this.moveX < -160) {
this.moveX = -160; this.moveX = -160;
} }
//
this.$set(this.services[this.currentIndex], "slideX", this.moveX); this.$set(this.services[this.currentIndex], "slideX", this.moveX);
}, },
mouseUp(e) { mouseUp(e) {
if (this.currentTab != 3) return; if (this.currentTab != 3) return;
if (this.currentIndex === -1) return; if (this.currentIndex === -1) return;
//
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
//
if (this.moveX < -80) { if (this.moveX < -80) {
this.$set(this.services[this.currentIndex], "slideX", -160); this.$set(this.services[this.currentIndex], "slideX", -160);
} else { } else {
//
this.$set(this.services[this.currentIndex], "slideX", 0); this.$set(this.services[this.currentIndex], "slideX", 0);
} }
this.currentIndex = -1; this.currentIndex = -1;
@ -589,7 +688,7 @@
/* 已上架 */ /* 已上架 */
.status-tag.state2 { .status-tag.state2 {
background-color: rgba(232, 16, 30, 0.2); background-color: rgba(255, 71, 103, 0.2);
} }
.status-tag.state2 .status-text { .status-tag.state2 .status-text {
@ -609,13 +708,13 @@
.status-tag.state0, .status-tag.state0,
.status-tag.state3, .status-tag.state3,
.status-tag.state4 { .status-tag.state4 {
background-color: rgba(209, 15, 19, 0.2); background-color: rgba(255, 71, 103, 0.2);
} }
.status-tag.state0 .status-text, .status-tag.state0 .status-text,
.status-tag.state3 .status-text, .status-tag.state3 .status-text,
.status-tag.state4 .status-text { .status-tag.state4 .status-text {
color: #d10f13; color: #FF4767;
} }
.btns { .btns {
@ -705,7 +804,6 @@
/* #ifdef MP-WEIXIN */ /* #ifdef MP-WEIXIN */
.search-box input { .search-box input {
/* 小程序搜索框样式调整 */
box-sizing: border-box; box-sizing: border-box;
} }
@ -714,7 +812,6 @@
} }
.service-item { .service-item {
/* 小程序阴影效果 */
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
} }
@ -739,12 +836,189 @@
border-radius: 16px; border-radius: 16px;
.serviceBtn-text { .serviceBtn-text {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #ffffff; color: #ffffff;
} }
} }
}
/* 分类选择弹窗样式 */
.category-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2000;
display: flex;
align-items: flex-end;
justify-content: center;
}
.category-popup {
width: 100%;
height: 70vh;
background-color: #ffffff;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
overflow: hidden;
.category-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
position: relative;
border-bottom: 1rpx solid #f0f0f0;
}
.category-popup-title {
font-size: 34rpx;
font-weight: 600;
color: #333333;
}
.category-popup-close {
position: absolute;
right: 32rpx;
top: 50%;
transform: translateY(-50%);
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
}
.category-popup-close-icon {
font-size: 48rpx;
color: #999999;
line-height: 1;
}
.category-popup-body {
flex: 1;
display: flex;
overflow: hidden;
}
.category-left {
width: 220rpx;
background-color: #f5f5f5;
height: 100%;
}
.category-left-item {
padding: 30rpx 24rpx;
text-align: center;
position: relative;
}
.category-left-item.active {
background-color: #ffffff;
}
.category-left-item.active::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 40rpx;
background-color: #FF4767;
border-radius: 0 3rpx 3rpx 0;
}
.category-left-text {
font-size: 28rpx;
color: #333333;
}
.category-left-item.active .category-left-text {
color: #FF4767;
font-weight: 500;
}
.category-right {
flex: 1;
background-color: #ffffff;
height: 100%;
}
.category-right-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 32rpx;
/* 移除 border-bottom: 1rpx solid #f5f5f5; */
}
.category-right-item.selected .category-right-text {
color: #FF4767;
font-weight: 500;
}
.category-right-text {
font-size: 28rpx;
color: #333333;
flex: 1;
}
.category-icon {
width: 36rpx;
height: 36rpx;
margin-left: 16rpx;
}
.category-empty {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.category-empty-text {
font-size: 28rpx;
color: #999999;
}
.category-popup-footer {
display: flex;
padding: 24rpx 32rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0;
background-color: #ffffff;
}
.category-popup-btn {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 44rpx;
font-size: 30rpx;
}
.category-popup-btn-cancel {
background-color: #f5f5f5;
margin-right: 24rpx;
}
.category-popup-btn-cancel text {
color: #666666;
}
.category-popup-btn-confirm {
background-color: #FF4767;
}
.category-popup-btn-confirm text {
color: #ffffff;
}
} }
</style> </style>

View File

@ -26,17 +26,31 @@
<script> <script>
import permissionUtils from "@/utils/per"; import permissionUtils from "@/utils/per";
import locationService from "@/utils/locationService"; import locationService from "@/utils/locationService";
import request from "@/utils/request";
export default { export default {
data() { data() {
return { return {
isShowPer: false, // isShowPer: false, //
phone: '19950079687' phone: '19950079687' // 使
} }
}, },
onLoad(options) { onLoad(options) {
this.phone = options.phone || '19950079687' //
this.getCustomerService();
}, },
methods: { methods: {
//
async getCustomerService() {
try {
const res = await request.post('/sj/poster/getCustomerService');
if (res.code === 200 && res.data && res.data.phone) {
this.phone = res.data.phone;
}
} catch (error) {
console.error('获取客服电话失败', error);
}
},
// //
async callPhone() { async callPhone() {
const systemInfo = uni.getSystemInfoSync(); const systemInfo = uni.getSystemInfoSync();

View File

@ -285,13 +285,14 @@ export default {
sourceType: ['album', 'camera'], sourceType: ['album', 'camera'],
compressed: false compressed: false
}) })
let selectedFile;
// #ifdef H5 // #ifdef H5
const file = res.tempFile; selectedFile = res.tempFile;
// #endif // #endif
// #ifdef APP-PLUS || MP-WEIXIN // #ifdef APP-PLUS || MP-WEIXIN
const file = res; selectedFile = res;
// #endif // #endif
await this.uploadFile(file) await this.uploadFile(selectedFile)
} }
} catch (error) { } catch (error) {
console.error('上传出错:', error) console.error('上传出错:', error)
@ -369,8 +370,9 @@ export default {
// OSS // OSS
uploadToOss(file, ossConfig, name) { uploadToOss(file, ossConfig, name) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let formData;
// #ifdef H5 // #ifdef H5
const formData = new FormData() formData = new FormData()
formData.append('name',name); formData.append('name',name);
formData.append('policy', ossConfig.policy); formData.append('policy', ossConfig.policy);
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId); formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
@ -387,7 +389,7 @@ export default {
// #endif // #endif
// #ifdef APP-PLUS || MP-WEIXIN // #ifdef APP-PLUS || MP-WEIXIN
let formData = {}; formData = {};
formData.name = name; formData.name = name;
formData.policy = ossConfig.policy; formData.policy = ossConfig.policy;
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId; formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;

View File

@ -0,0 +1,167 @@
<template>
<view class="growth-package-result-page">
<custom-navbar
title="支付结果"
:showBack="true"
backgroundColor="#F7F7F7"
borderBottom="none"
></custom-navbar>
<view class="result-main">
<view class="result-icon" :class="isFail ? 'is-fail' : 'is-success'">
<block v-if="isFail">
<view class="fail-line"></view>
<view class="fail-dot"></view>
</block>
<view v-else class="success-check"></view>
</view>
<view class="result-title">{{ resultInfo.title }}</view>
<view class="result-desc">{{ resultInfo.desc }}</view>
<view class="primary-btn" @tap="handlePrimaryTap">{{ isFail ? "重新支付" : "返回首页" }}</view>
</view>
</view>
</template>
<script>
import CustomNavbar from "@/components/custom-navbar/custom-navbar.vue";
export default {
components: {
CustomNavbar,
},
data() {
return {
status: "success",
};
},
computed: {
isFail() {
return this.status === "fail";
},
resultInfo() {
if (this.isFail) {
return {
title: "报名失败",
desc: "最后一步出了点问题 请重新支付",
};
}
return {
title: "报名成功",
desc: "您已完成支付 感谢你对美融融的支持",
};
},
},
onLoad(options) {
this.status = options.status === "fail" ? "fail" : "success";
},
methods: {
handlePrimaryTap() {
if (this.isFail) {
uni.redirectTo({
url: "/pages/home/growth-package",
});
return;
}
uni.switchTab({
url: "/pages/home/home",
});
},
},
};
</script>
<style lang="scss" scoped>
page {
background: #ffffff;
}
.growth-package-result-page {
min-height: 100vh;
background: #ffffff;
padding-bottom: env(safe-area-inset-bottom);
}
.result-main {
display: flex;
flex-direction: column;
align-items: center;
padding: 246rpx 40rpx 0;
}
.result-icon {
width: 104rpx;
height: 104rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.result-icon.is-success {
background: #12c76b;
}
.result-icon.is-fail {
background: #ff4652;
}
.success-check {
width: 44rpx;
height: 24rpx;
border-left: 8rpx solid #ffffff;
border-bottom: 8rpx solid #ffffff;
transform: rotate(-45deg);
margin-top: -8rpx;
}
.fail-line {
width: 8rpx;
height: 46rpx;
border-radius: 8rpx;
background: #ffffff;
}
.fail-dot {
width: 8rpx;
height: 8rpx;
margin-left: -8rpx;
margin-top: 58rpx;
border-radius: 50%;
background: #ffffff;
}
.result-title {
margin-top: 34rpx;
font-weight: 600;
font-size: 40rpx;
color: #333333;
line-height: 56rpx;
text-align: center;
}
.result-desc {
margin-top: 8rpx;
font-weight: 400;
font-size: 26rpx;
color: #b9b9b9;
line-height: 38rpx;
text-align: center;
}
.primary-btn {
width: 350rpx;
height: 80rpx;
margin-top: 144rpx;
border: 1rpx solid #ff4b67;
border-radius: 40rpx;
box-sizing: border-box;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 400;
font-size: 30rpx;
color: #ff4767;
line-height: 42rpx;
}
</style>

View File

@ -0,0 +1,348 @@
<template>
<view class="growth-package-page">
<image class="content-image" src="@/static/images/home/growth-package/intro.png" mode="widthFix"></image>
<view class="deal-carousel">
<swiper class="deal-swiper" autoplay circular :interval="3000" previous-margin="40rpx"
next-margin="40rpx" :current="dealCurrentIndex" @change="handleDealChange">
<swiper-item v-for="(item, index) in dealSlides" :key="item">
<view class="deal-slide" :class="getDealSlideClass(index)">
<image class="deal-slide-image" :src="item" mode="aspectFit"></image>
</view>
</swiper-item>
</swiper>
<view class="deal-indicator">
<view class="indicator-track" v-for="(item, index) in dealSlides" :key="item"
:class="{ active: index === dealCurrentIndex }"></view>
</view>
</view>
<image class="content-image" src="@/static/images/home/growth-package/plan.png" mode="widthFix"></image>
<view class="fixed-action" @tap="handlePayTap">
<image class="action-image" :src="actionImage" mode="widthFix"></image>
</view>
</view>
</template>
<script>
import request from "@/utils/request";
export default {
name: "GrowthPackage",
data() {
return {
dealCurrentIndex: 0,
isPaying: false,
isJoined: false,
dealSlides: [
"/static/images/home/growth-package/deal-1.png",
"/static/images/home/growth-package/deal-2.png",
"/static/images/home/growth-package/deal-3.png",
],
};
},
computed: {
actionImage() {
return this.isJoined
? "/static/images/home/growth-package/action-success.png"
: "/static/images/home/growth-package/action.png";
},
},
onShow() {
this.getGrowthPackageStatus();
},
methods: {
async getGrowthPackageStatus() {
if (!uni.getStorageSync("accessToken")) {
this.isJoined = false;
return;
}
if (this.isPaying) return;
uni.showLoading({
title: "查询报名状态...",
mask: true,
});
try {
const res = await request.post(
"/sj/growthPackage/status",
{},
{
hideLoading: true,
}
);
this.isJoined = !!(res && res.code === 200 && res.data && Number(res.data.is_joined) === 1);
} catch (error) {
console.log("growthPackageStatus error", error);
} finally {
uni.hideLoading();
}
},
handleDealChange(event) {
this.dealCurrentIndex = event.detail.current;
},
getDealSlideClass(index) {
const length = this.dealSlides.length;
const prevIndex = (this.dealCurrentIndex + length - 1) % length;
const nextIndex = (this.dealCurrentIndex + 1) % length;
if (index === this.dealCurrentIndex) {
return "is-active";
}
if (index === prevIndex) {
return "is-prev";
}
if (index === nextIndex) {
return "is-next";
}
return "";
},
handlePayTap() {
if (this.isPaying) return;
if (this.isJoined) return;
if (!uni.getStorageSync("accessToken")) {
uni.navigateTo({
url: "/pages/blogPopup/blogPopup",
});
return;
}
uni.showActionSheet({
itemList: ["微信支付", "支付宝支付"],
success: (res) => {
const payKind = res.tapIndex === 0 ? 2 : 3;
this.createGrowthPackagePay(payKind);
},
});
},
async createGrowthPackagePay(payKind) {
this.isPaying = true;
uni.showLoading({
title: "支付中...",
mask: true,
});
try {
const payRes = await request.post(
"/sj/growthPackage/pay",
{
pay_kind: payKind,
},
{
hideLoading: true,
}
);
if (!payRes || payRes.code !== 200) {
uni.hideLoading();
this.isPaying = false;
if (payRes && payRes.data && (payRes.data.id || payRes.data.number)) {
this.goToPayResult("fail", payRes.data);
return;
}
uni.showToast({
title: (payRes && payRes.msg) || "支付请求失败",
icon: "none",
});
return;
}
const orderInfo = payRes.data || {};
if (Number(orderInfo.is_joined) === 1) {
uni.hideLoading();
this.isPaying = false;
this.isJoined = true;
this.goToPayResult("success", orderInfo.order || {});
return;
}
const paymentParams = orderInfo.pay_params || orderInfo.orderInfo;
if (!paymentParams) {
uni.hideLoading();
this.isPaying = false;
if (orderInfo.id || orderInfo.number) {
this.goToPayResult("fail", orderInfo);
return;
}
uni.showToast({
title: "支付参数缺失",
icon: "none",
});
return;
}
uni.hideLoading();
this.requestGrowthPackagePayment(payKind, paymentParams, orderInfo);
} catch (error) {
uni.hideLoading();
uni.showToast({
title: "支付请求失败",
icon: "none",
});
this.isPaying = false;
console.log("growthPackagePay error", error);
}
},
requestGrowthPackagePayment(payKind, paymentParams, orderInfo) {
const provider = payKind === 2 ? "wxpay" : "alipay";
let payOrderInfo = paymentParams;
if (payKind === 2 && paymentParams && typeof paymentParams === "object") {
payOrderInfo = {
...paymentParams,
};
const systemInfo = uni.getSystemInfoSync();
if (systemInfo.platform === "ios" && payOrderInfo.appid) {
payOrderInfo.appId = payOrderInfo.appid;
delete payOrderInfo.appid;
}
}
uni.requestPayment({
provider,
orderInfo: payOrderInfo,
success: () => {
this.queryGrowthPackagePay(orderInfo);
},
fail: (err) => {
this.isPaying = false;
console.log("growthPackage requestPayment fail", err);
this.goToPayResult("fail", orderInfo);
},
});
},
async queryGrowthPackagePay(orderInfo) {
try {
const queryRes = await request.post(
"/sj/growthPackage/queryPay",
{
id: orderInfo.id,
number: orderInfo.number,
},
{
hideLoading: true,
}
);
const order = queryRes && queryRes.data && queryRes.data.order;
if (queryRes && queryRes.code === 200 && order && Number(order.state) === 2) {
this.isJoined = true;
this.goToPayResult("success", order);
return;
}
this.goToPayResult("fail", orderInfo);
} catch (error) {
console.log("growthPackageQueryPay error", error);
this.goToPayResult("fail", orderInfo);
} finally {
this.isPaying = false;
}
},
goToPayResult(status, orderInfo = {}) {
const params = [`status=${status}`];
if (orderInfo.number) {
params.push(`number=${encodeURIComponent(orderInfo.number)}`);
}
if (orderInfo.pay_money || orderInfo.order_money) {
params.push(`money=${encodeURIComponent(orderInfo.pay_money || orderInfo.order_money)}`);
}
uni.redirectTo({
url: `/pages/home/growth-package-result?${params.join("&")}`,
});
},
},
}
</script>
<style lang="scss">
page {
background: #150903;
}
.growth-package-page {
min-height: 100vh;
padding-bottom: 259rpx;
background: #150903;
.content-image {
display: block;
width: 750rpx;
}
.deal-carousel {
position: relative;
width: 750rpx;
height: 1342rpx;
background: #000000;
overflow: hidden;
}
.deal-swiper {
width: 750rpx;
height: 1280rpx;
background: #000000;
}
.deal-slide {
position: relative;
width: 670rpx;
height: 1280rpx;
background: #000000;
overflow: hidden;
}
.deal-slide-image {
display: block;
width: 670rpx;
height: 1280rpx;
}
.deal-slide.is-prev,
.deal-slide.is-next {
height: 1169rpx;
margin-top: 56rpx;
}
.deal-slide.is-prev .deal-slide-image,
.deal-slide.is-next .deal-slide-image {
position: absolute;
top: -56rpx;
}
.deal-slide.is-prev .deal-slide-image {
right: 10rpx;
border-top-right-radius: 20rpx;
border-bottom-right-radius: 20rpx;
}
.deal-slide.is-next .deal-slide-image {
left: 10rpx;
border-top-left-radius: 20rpx;
border-bottom-left-radius: 20rpx;
}
.deal-indicator {
display: flex;
align-items: center;
justify-content: center;
height: 62rpx;
column-gap: 8rpx;
background: #000000;
}
.indicator-track {
width: 146rpx;
height: 8rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.42);
}
.indicator-track.active {
background: #ffffff;
}
.fixed-action {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 20;
width: 750rpx;
height: 259rpx;
background: #150903;
}
.action-image {
display: block;
width: 750rpx;
}
}
</style>

View File

@ -20,6 +20,10 @@
</view> </view>
</view> </view>
<view class="home-banner" @tap="goToGrowthPackage">
<image class="home-banner-img" src="@/static/images/home/banner.png" mode="aspectFill"></image>
</view>
<view class="setup-modal-mask" v-if="showSetupModal" catchtouchmove="true"> <view class="setup-modal-mask" v-if="showSetupModal" catchtouchmove="true">
<view class="setup-modal-content"> <view class="setup-modal-content">
<image class="modal-bg" <image class="modal-bg"
@ -181,7 +185,7 @@
</view> </view>
</view> </view>
<view class="notice-bar card-box"> <view class="notice-bar card-box" v-if="noticeList.length > 0">
<image class="notice-icon" <image class="notice-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/75f624b8-a6ed-4787-9380-1a087062805b.png"> src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/75f624b8-a6ed-4787-9380-1a087062805b.png">
</image> </image>
@ -206,6 +210,9 @@
<brand-card class="brand-card-wrap" v-for="(item, index) in brands" :key="index" :item="item" <brand-card class="brand-card-wrap" v-for="(item, index) in brands" :key="index" :item="item"
:tagKeys="item.tagKeys"></brand-card> :tagKeys="item.tagKeys"></brand-card>
</view> </view>
<view class="home-empty" v-if="brands.length === 0">
<noData top="20rpx"></noData>
</view>
</view> </view>
<view class="skills-section card-box"> <view class="skills-section card-box">
@ -219,6 +226,9 @@
<view class="skill-list"> <view class="skill-list">
<skill-card v-for="(item, index) in skills" :key="index" :item="item"></skill-card> <skill-card v-for="(item, index) in skills" :key="index" :item="item"></skill-card>
</view> </view>
<view class="home-empty" v-if="skills.length === 0">
<noData top="20rpx"></noData>
</view>
</view> </view>
<view style="height: 120rpx;"></view> <view style="height: 120rpx;"></view>
@ -260,11 +270,7 @@ export default {
displayScore: 0, displayScore: 0,
animationTimer: null, animationTimer: null,
noticeList: [{ noticeList: [],
id: 0,
title: '',
url: ''
}],
quickActions: [{ quickActions: [{
name: '扫码验券', name: '扫码验券',
@ -472,6 +478,11 @@ export default {
}); });
} }
}, },
goToGrowthPackage() {
uni.navigateTo({
url: '/pages/home/growth-package'
});
},
// //
closeSetupModal() { closeSetupModal() {
this.showSetupModal = false; this.showSetupModal = false;
@ -544,7 +555,14 @@ export default {
uni.navigateTo({ uni.navigateTo({
url: '/pages/blogPopup/blogPopup' url: '/pages/blogPopup/blogPopup'
}); });
return return;
}
if (this.brands.length === 0) {
uni.showToast({
title: '暂无数据',
icon: 'none'
});
return;
} }
uni.navigateTo({ uni.navigateTo({
url: '/pages/jingxuan/selected-brands' url: '/pages/jingxuan/selected-brands'
@ -555,7 +573,14 @@ export default {
uni.navigateTo({ uni.navigateTo({
url: '/pages/blogPopup/blogPopup' url: '/pages/blogPopup/blogPopup'
}); });
return return;
}
if (this.skills.length === 0) {
uni.showToast({
title: '暂无数据',
icon: 'none'
});
return;
} }
uni.navigateTo({ uni.navigateTo({
url: '/pages/jingxuan/selected-skills' url: '/pages/jingxuan/selected-skills'
@ -717,7 +742,11 @@ export default {
tagKeys: tags tagKeys: tags
}; };
}); });
} else {
this.brands = [];
} }
}).catch(() => {
this.brands = [];
}); });
}, },
@ -742,7 +771,11 @@ export default {
video: item.video || '' video: item.video || ''
}; };
}); });
} else {
this.skills = [];
} }
}).catch(() => {
this.skills = [];
}); });
}, },
@ -1100,7 +1133,7 @@ page {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 40rpx; margin-bottom: 24rpx;
.user-info { .user-info {
display: flex; display: flex;
@ -1203,6 +1236,23 @@ page {
} }
} }
.home-banner {
position: relative;
z-index: 1;
width: 710rpx;
height: 266rpx;
margin-left: -4rpx;
margin-bottom: 24rpx;
border-radius: 24rpx;
overflow: hidden;
.home-banner-img {
display: block;
width: 100%;
height: 100%;
}
}
.settlement-guide { .settlement-guide {
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
@ -1488,6 +1538,12 @@ page {
} }
} }
.home-empty {
height: 360rpx;
display: flex;
justify-content: center;
}
.skills-section { .skills-section {
.skill-list { .skill-list {
display: flex; display: flex;

View File

@ -145,9 +145,6 @@
<view class="per-cont">当您需要联系商家或平台客服的时候需要获取拨打电话权限</view> <view class="per-cont">当您需要联系商家或平台客服的时候需要获取拨打电话权限</view>
</view> </view>
</view>
</view> </view>
</template> </template>

View File

@ -67,12 +67,10 @@
<view class="form-upimg"> <view class="form-upimg">
<view class="upload-box" @click="chooseImage('idcard_negative')"> <view class="upload-box" @click="chooseImage('idcard_negative')">
</image>
<view class="upload-camera img2"> <view class="upload-camera img2">
<image v-if="formData.idcard_negative" :src="formData.idcard_negative" mode="aspectFill" <image v-if="formData.idcard_negative" :src="formData.idcard_negative" mode="aspectFill"
class="preview-image"> class="preview-image"></image>
<image v-else src="/static/images/camera.png" mode="aspectFit" class="camera-icon"> <image v-else src="/static/images/camera.png" mode="aspectFit" class="camera-icon"></image>
</image>
</view> </view>
</view> </view>
<text class="label required">请上传身份证反面(国徽面)</text> <text class="label required">请上传身份证反面(国徽面)</text>
@ -126,12 +124,10 @@
<view class="form-upimg"> <view class="form-upimg">
<view class="upload-box" @click="chooseImage('legal_idcard_negative')"> <view class="upload-box" @click="chooseImage('legal_idcard_negative')">
</image>
<view class="upload-camera img2"> <view class="upload-camera img2">
<image v-if="formData.legal_idcard_negative" :src="formData.legal_idcard_negative" <image v-if="formData.legal_idcard_negative" :src="formData.legal_idcard_negative"
mode="aspectFill" class="preview-image"> mode="aspectFill" class="preview-image"></image>
<image v-else src="/static/images/camera.png" mode="aspectFit" class="camera-icon"> <image v-else src="/static/images/camera.png" mode="aspectFit" class="camera-icon"></image>
</image>
</view> </view>
</view> </view>
<text class="label required">请上传法人身份证反面(国徽面)</text> <text class="label required">请上传法人身份证反面(国徽面)</text>

View File

@ -181,7 +181,7 @@
</view> </view>
<view class="form-item-one box-cont"> <view class="form-item-one box-cont">
<text class="label required">行业资质</text> <text class="label">行业资质</text>
<view class="image-list"> <view class="image-list">
<view class="image-item" v-for="(img, index) in formData.qualifications_sector" :key="'industry-' + index"> <view class="image-item" v-for="(img, index) in formData.qualifications_sector" :key="'industry-' + index">
<image :src="img" mode="aspectFill" class="qualification-image"></image> <image :src="img" mode="aspectFill" class="qualification-image"></image>
@ -1080,13 +1080,6 @@ export default {
}); });
return false; return false;
} }
if (!this.formData.qualifications_sector || this.formData.qualifications_sector.length === 0) {
uni.showToast({
title: '请上传行业资质',
icon: 'none'
});
return false;
}
return true; return true;
} }
} }

View File

@ -45,7 +45,6 @@
<span class="detail-info-user__company__name__line"> | </span> <span class="detail-info-user__company__name__line"> | </span>
<view class="detail-info-user__company__name__text2"> <view class="detail-info-user__company__name__text2">
{{ item.for_shop }} {{ item.for_shop }}
</image>
</view> </view>
</view> </view>

View File

@ -166,12 +166,12 @@
</view> --> </view> -->
<!-- 项目说明 --> <!-- 项目说明 -->
<view class="notice-section"> <view class="notice-section" v-if="hasProjectInfo">
<view class="section-title"> <view class="section-title">
<text class="section-tit">项目说明</text> <text class="section-tit">项目说明</text>
</view> </view>
<view class="notice-content"> <view class="notice-content">
<view class="notice-cont"> <view class="notice-cont" v-if="serviceInfo.server_time">
<text class="notice-title"> <text class="notice-title">
服务时长: 服务时长:
</text> </text>
@ -179,7 +179,7 @@
{{serviceInfo.server_time}}分钟 {{serviceInfo.server_time}}分钟
</text> </text>
</view> </view>
<view class="notice-cont" v-for="(item,i) in serviceInfo.detail"> <view class="notice-cont" v-for="(item,i) in visibleDetailList" :key="'detail-' + i">
<text class="notice-title"> <text class="notice-title">
{{item.title}} {{item.title}}
</text> </text>
@ -189,16 +189,38 @@
</view> </view>
</view> </view>
</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>
<!-- 图文详情 --> <!-- 图文详情 -->
<imgAndText v-if="serviceInfo.graphic_details" :info="serviceInfo.graphic_details"></imgAndText> <imgAndText v-if="serviceInfo.graphic_details" :info="serviceInfo.graphic_details"></imgAndText>
<!-- 步骤 --> <!-- 步骤 -->
<view class="notice-section"> <view class="notice-section" v-if="visibleProcessList.length">
<view class="section-title"> <view class="section-title">
<text class="section-tit">项目步骤</text> <text class="section-tit">项目步骤</text>
</view> </view>
<view class="notice-content"> <view class="notice-content">
<view class="notice-cont" v-for="(item,i) in serviceInfo.process"> <view class="notice-cont" v-for="(item,i) in visibleProcessList" :key="'process-' + i">
<text class="notice-title"> <text class="notice-title">
{{item.title}} {{item.title}}
</text> </text>
@ -209,7 +231,7 @@
</view> </view>
</view> </view>
<!-- 订购须知 --> <!-- 订购须知 -->
<view class="notice-section"> <view class="notice-section" v-if="purchaseNotesText">
<view class="section-title"> <view class="section-title">
<text class="section-tit">订购须知</text> <text class="section-tit">订购须知</text>
<!-- <view class="section-tit-right"> <!-- <view class="section-tit-right">
@ -219,7 +241,7 @@
</view> </view>
<view class="notice-content"> <view class="notice-content">
<text class="notice-text"> <text class="notice-text">
{{serviceInfo.purchasenotes}} {{purchaseNotesText}}
</text> </text>
</view> </view>
</view> </view>
@ -327,6 +349,25 @@
computed: { computed: {
userAdrees() { userAdrees() {
return uni.getStorageSync("userAdrees").addressRes || getApp().globalData.addressRes; 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) { onLoad(option) {
@ -368,6 +409,86 @@
this.showVideo = false; this.showVideo = false;
}, },
methods: { 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() { async goInvite() {
let imageUrl = let imageUrl =
'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/yh/1756713709528-717.png?x-oss-process=image/resize,p_40' 'https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/yh/1756713709528-717.png?x-oss-process=image/resize,p_40'
@ -1142,6 +1263,26 @@
color: #666; 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 { .bottom-bar {
position: fixed; position: fixed;

View File

@ -12,7 +12,9 @@
<!-- <rich-text :nodes="item.value" v-if="item.type==1"></rich-text> --> <!-- <rich-text :nodes="item.value" v-if="item.type==1"></rich-text> -->
<uv-divider dashed v-if="item.type == 1"></uv-divider> <uv-divider dashed v-if="item.type == 1"></uv-divider>
<view class="addTextAndImg-content-card-text" v-if="item.type == 2">{{ item.value }}</view> <view class="addTextAndImg-content-card-text" v-if="item.type == 2">{{ item.value }}</view>
<image style="width: 100%;" mode="widthFix" :src="item.value" v-else-if="item.type==3|| item.type==4"></image> <image style="width: 100%;" mode="widthFix" :src="item.value"
v-else-if="item.type == 3 || item.type == 4">
</image>
<view class="addTextAndImg-content-card-btns"> <view class="addTextAndImg-content-card-btns">
<view class="addTextAndImg-content-card-btns-btn" v-for="item2 in btns" :key="item2.id" <view class="addTextAndImg-content-card-btns-btn" v-for="item2 in btns" :key="item2.id"
@click.stop="handleCard(item2.id, index)" v-if=" @click.stop="handleCard(item2.id, index)" v-if="
@ -120,9 +122,12 @@
}, },
onLoad(options) { onLoad(options) {
console.log(options) console.log(options)
if(options.list){ if (options.list && options.list !== 'null' && options.list !== 'undefined') {
const parsed = JSON.parse(options.list) try {
this.list = Array.isArray(parsed) ? parsed : [] this.list = JSON.parse(options.list) || [];
} catch (e) {
this.list = [];
}
console.log(this.list) console.log(this.list)
} }
}, },
@ -354,6 +359,7 @@
<style lang="less"> <style lang="less">
.addTextAndImg { .addTextAndImg {
padding-bottom: 110rpx; padding-bottom: 110rpx;
.firstAdd { .firstAdd {
position: fixed; position: fixed;
left: 50%; left: 50%;
@ -369,6 +375,7 @@
border: 3px solid #000; border: 3px solid #000;
border-radius: 50%; border-radius: 50%;
margin-bottom: 100rpx; margin-bottom: 100rpx;
.firstAdd-garden-img { .firstAdd-garden-img {
width: 200rpx; width: 200rpx;
} }
@ -466,6 +473,7 @@
flex-flow: row nowrap; flex-flow: row nowrap;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
.btn-submit { .btn-submit {
background-color: rgba(232, 16, 30, 1); background-color: rgba(232, 16, 30, 1);
color: #fff; color: #fff;
@ -477,8 +485,4 @@
border-radius: 78rpx; border-radius: 78rpx;
} }
} }
</style> </style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -225,7 +225,7 @@
</view> </view>
<view class="form-item-image-box box-cont"> <view class="form-item-image-box box-cont">
<text class="label required">行业资质</text> <text class="label">行业资质</text>
<view class="image-list"> <view class="image-list">
<view class="image-item" v-for="(img, index) in displayData.qualifications_sector" <view class="image-item" v-for="(img, index) in displayData.qualifications_sector"
:key="'industry-' + index"> :key="'industry-' + index">

View File

@ -152,12 +152,12 @@
</view> </view>
<!-- 项目说明 --> <!-- 项目说明 -->
<view class="notice-section"> <view class="notice-section" v-if="hasProjectInfo">
<view class="section-title"> <view class="section-title">
<text class="section-tit">项目说明</text> <text class="section-tit">项目说明</text>
</view> </view>
<view class="notice-content"> <view class="notice-content">
<view class="notice-cont"> <view class="notice-cont" v-if="serviceInfo.server_time">
<text class="notice-title"> <text class="notice-title">
服务时长: 服务时长:
</text> </text>
@ -165,7 +165,7 @@
{{serviceInfo.server_time}}分钟 {{serviceInfo.server_time}}分钟
</text> </text>
</view> </view>
<view class="notice-cont" v-for="(item,i) in serviceInfo.detail"> <view class="notice-cont" v-for="(item,i) in visibleDetailList" :key="'detail-' + i">
<text class="notice-title"> <text class="notice-title">
{{item.title}} {{item.title}}
</text> </text>
@ -175,13 +175,35 @@
</view> </view>
</view> </view>
</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"> <view class="notice-section" v-if="visibleProcessList.length">
<view class="section-title"> <view class="section-title">
<text class="section-tit">项目步骤</text> <text class="section-tit">项目步骤</text>
</view> </view>
<view class="notice-content"> <view class="notice-content">
<view class="notice-cont" v-for="(item,i) in serviceInfo.process"> <view class="notice-cont" v-for="(item,i) in visibleProcessList" :key="'process-' + i">
<text class="notice-title"> <text class="notice-title">
{{item.title}} {{item.title}}
</text> </text>
@ -192,7 +214,7 @@
</view> </view>
</view> </view>
<!-- 订购须知 --> <!-- 订购须知 -->
<view class="notice-section"> <view class="notice-section" v-if="purchaseNotesText">
<view class="section-title"> <view class="section-title">
<text class="section-tit">订购须知</text> <text class="section-tit">订购须知</text>
<!-- <view class="section-tit-right"> <!-- <view class="section-tit-right">
@ -202,7 +224,7 @@
</view> </view>
<view class="notice-content"> <view class="notice-content">
<text class="notice-text"> <text class="notice-text">
{{serviceInfo.purchasenotes}} {{purchaseNotesText}}
</text> </text>
</view> </view>
</view> </view>
@ -285,6 +307,27 @@
}] }]
} }
}, },
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) { onLoad(option) {
// Android可能需要延迟加载 // Android可能需要延迟加载
request.post('/sj/serverdetail', { request.post('/sj/serverdetail', {
@ -316,6 +359,86 @@
this.showVideo = false; this.showVideo = false;
}, },
methods: { 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) { getSyrDetail(id) {
request.post('/sj/syrdetail', { request.post('/sj/syrdetail', {
@ -814,6 +937,26 @@
color: #666; 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 { .bottom-bar {
position: fixed; position: fixed;

View File

@ -331,7 +331,47 @@
} catch (e) {} } catch (e) {}
}, },
// //
pickFirst(data, fields) {
for (const field of fields) {
const value = data && data[field];
if (value !== undefined && value !== null && String(value).trim() !== '') {
return value;
}
}
return '';
},
normalizeStoreInfo(data = {}) {
return {
head_photo: this.pickFirst(data, ['head_photo', 'shop_head_photo']),
name: this.pickFirst(data, ['name', 'shop_name']),
dependency: this.pickFirst(data, ['dependency', 'shop_dependency', 'uscc_dependency']),
address: this.pickFirst(data, ['address', 'shop_address', 'uscc_address']),
dependency_code: this.pickFirst(data, ['dependency_code', 'shop_dependency_code', 'uscc_area']),
dependency_province: this.pickFirst(data, ['dependency_province', 'shop_dependency_province', 'uscc_province']),
dependency_city: this.pickFirst(data, ['dependency_city', 'shop_dependency_city', 'uscc_city']),
longitude: this.pickFirst(data, ['longitude', 'shop_longitude']),
latitude: this.pickFirst(data, ['latitude', 'shop_latitude'])
};
},
mergeStoreInfoWithOriginal(data = {}) {
const storeInfo = this.normalizeStoreInfo(data);
return {
head_photo: storeInfo.head_photo || this.originalData.head_photo,
name: storeInfo.name || this.originalData.name,
dependency: storeInfo.dependency || this.originalData.dependency,
address: storeInfo.address || this.originalData.address,
dependency_code: storeInfo.dependency_code || this.originalData.dependency_code,
dependency_province: storeInfo.dependency_province || this.originalData.dependency_province,
dependency_city: storeInfo.dependency_city || this.originalData.dependency_city,
longitude: storeInfo.longitude || this.originalData.longitude,
latitude: storeInfo.latitude || this.originalData.latitude
};
},
//
getUserInfo() { getUserInfo() {
console.log('开始获取用户信息...'); console.log('开始获取用户信息...');
const type = 3; const type = 3;
@ -347,23 +387,13 @@
this.userId = userData.id; this.userId = userData.id;
this.originalData = { this.originalData = this.normalizeStoreInfo(userData);
head_photo: userData.head_photo || '',
name: userData.name || '',
dependency: userData.dependency || '',
address: userData.address || '',
dependency_code: userData.dependency_code || '',
dependency_province: userData.dependency_province || '',
dependency_city: userData.dependency_city || '',
longitude: userData.longitude || '',
latitude: userData.latitude || ''
};
this.dependency_code = userData.dependency_code || ''; this.dependency_code = this.originalData.dependency_code || '';
this.dependency_province = userData.dependency_province || ''; this.dependency_province = this.originalData.dependency_province || '';
this.dependency_city = userData.dependency_city || ''; this.dependency_city = this.originalData.dependency_city || '';
this.longitude = userData.longitude || ''; this.longitude = this.originalData.longitude || '';
this.latitude = userData.latitude || ''; this.latitude = this.originalData.latitude || '';
this.pendingData = { this.pendingData = {
...this.originalData ...this.originalData
@ -444,18 +474,7 @@
} }
if (this.auditStatus === 1 || this.auditStatus === 3) { if (this.auditStatus === 1 || this.auditStatus === 3) {
this.pendingData = { this.pendingData = this.mergeStoreInfoWithOriginal(detail);
head_photo: detail.head_photo || this.originalData.head_photo,
name: detail.name || this.originalData.name,
dependency: detail.dependency || this.originalData.dependency,
address: detail.address || this.originalData.address,
dependency_code: detail.dependency_code || this.originalData.dependency_code,
dependency_province: detail.dependency_province || this.originalData
.dependency_province,
dependency_city: detail.dependency_city || this.originalData.dependency_city,
longitude: detail.longitude || this.originalData.longitude,
latitude: detail.latitude || this.originalData.latitude
};
} }
if (this.auditStatus === 2) { if (this.auditStatus === 2) {

View File

@ -7,7 +7,7 @@
</serviecFirstTab> </serviecFirstTab>
<view class="service-list"> <view class="service-list">
<view class="service-item flex-row-center-between" v-for="(item, index) in getList" :key="index"> <view class="service-item flex-row-center-between" v-for="(item, index) in getList" :key="index" v-if="(item.is_choice && isBack!=0) || isBack==0">
<view class="item-left flex-row-start"> <view class="item-left flex-row-start">
<image class="item-left-photo" :src="item.photo[0]" mode="aspectFill"></image> <image class="item-left-photo" :src="item.photo[0]" mode="aspectFill"></image>
<view class="item-left-texts"> <view class="item-left-texts">

View File

@ -233,6 +233,13 @@
}, },
// //
startOrder(e, order) { startOrder(e, order) {
if (order.team_buy_order_state == 1) {
uni.showToast({
title: "订单当前处于拼团中,服务将在拼团完成后才开启~",
icon: "none",
});
return
}
this.showInput = true; this.showInput = true;
this.handelOrder = order; this.handelOrder = order;
}, },

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 MiB

View File

@ -228,8 +228,9 @@ const getOssConfig = async (type) => {
const uploadToOss = (file, ossConfig, name, uploadType) => { const uploadToOss = (file, ossConfig, name, uploadType) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let formData;
// #ifdef H5 // #ifdef H5
const formData = new FormData() formData = new FormData()
formData.append('name', name); formData.append('name', name);
formData.append('policy', ossConfig.policy); formData.append('policy', ossConfig.policy);
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId); formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
@ -248,7 +249,7 @@ const uploadToOss = (file, ossConfig, name, uploadType) => {
}) })
// #endif // #endif
// #ifdef APP-PLUS || MP-WEIXIN // #ifdef APP-PLUS || MP-WEIXIN
let formData = {}; formData = {};
formData.name = name; formData.name = name;
formData.policy = ossConfig.policy; formData.policy = ossConfig.policy;
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId; formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;

1
utils/appBackBase.js Normal file
View File

@ -0,0 +1 @@
export const APP_BACK_BASE_URL = "http://test.mrrwlkj.top";

View File

@ -120,14 +120,14 @@ const request = async (options, isUpdate) => {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
baseURL = url; // 发布到生产环境时,此处代码会被摇树移除掉。 baseURL = url; // 发布到生产环境时,此处代码会被摇树移除掉。
// baseURL = "http://test.mrrwlkj.top"; // 发布到生产环境时,此处代码会被摇树移除掉。 baseURL = "http://116.63.163.121:93"; // 发布到生产环境时,此处代码会被摇树移除掉。
//baseURL = 'http://dev.mrrwlkj.top'; // 发布到生产环境时,此处代码会被摇树移除掉。 // baseURL = "http://116.63.163.121:96"; // 发布到生产环境时,此处代码会被摇树移除掉。
baseURL = "https://app.mrrweb.com.cn"; // baseURL = "https://app.mrrweb.com.cn";
} else { } else {
baseURL = url; // 发布到生产环境时,此处代码会被摇树移除掉。 baseURL = url; // 发布到生产环境时,此处代码会被摇树移除掉。
// baseURL = "http://test.mrrwlkj.top"; // 发布到生产环境时,此处代码会被摇树移除掉。 baseURL = "http://116.63.163.121:93"; // 发布到生产环境时,此处代码会被摇树移除掉。
//baseURL = 'http://dev.mrrwlkj.top'; // 发布到生产环境时,此处代码会被摇树移除掉。 // baseURL = "http://116.63.163.121:96"; // 发布到生产环境时,此处代码会被摇树移除掉。
baseURL = "https://app.mrrweb.com.cn"; // baseURL = "https://app.mrrweb.com.cn";
console.log("生产环境"); console.log("生产环境");
} }

View File

@ -148,8 +148,9 @@ const getOssConfig = async (type)=> {
const uploadToOss = (file, ossConfig, name)=> { const uploadToOss = (file, ossConfig, name)=> {
console.log(ossConfig) console.log(ossConfig)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let formData;
// #ifdef H5 // #ifdef H5
const formData = new FormData() formData = new FormData()
formData.append('name',name); formData.append('name',name);
formData.append('policy', ossConfig.policy); formData.append('policy', ossConfig.policy);
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId); formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
@ -165,7 +166,7 @@ const uploadToOss = (file, ossConfig, name)=> {
}) })
// #endif // #endif
// #ifdef APP-PLUS || MP-WEIXIN // #ifdef APP-PLUS || MP-WEIXIN
let formData = {}; formData = {};
formData.name = name; formData.name = name;
formData.policy = ossConfig.policy; formData.policy = ossConfig.policy;
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId; formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;

View File

@ -1,4 +1,6 @@
import { APP_BACK_BASE_URL } from './appBackBase.js'
let VersionCode = ''; let VersionCode = '';
let DeviceType = ''; let DeviceType = '';
let platform = ''; let platform = '';
@ -60,30 +62,6 @@ export function setVersion() {
} }
export async function getBaseUrl() { export async function getBaseUrl() {
let version = uni.getStorageSync('setVersion') uni.setStorageSync('baseUrl', APP_BACK_BASE_URL)
await uni.request({ return APP_BACK_BASE_URL
url: 'https://api.mrrwlkj.top/api/openPuc/getSjApiUrl', // 接口地址
method: 'GET', // 请求方法(默认 GET
header: {
'Content-Type': 'application/json', // 请求头
'VersionCode': version.VersionCode,
'DeviceBrand': version.DeviceBrand,
'DeviceType': version.DeviceType
},
success: (res) => {
if (res.data.code == 200) {
uni.setStorageSync('baseUrl', res.data.data.url)
// store.commit('setBaseUrl', res.data.data.url)
}
return res.data.data.url;
console.log('请求成功', res.data);
},
fail: (err) => {
console.log('请求失败', err);
return 'http://60.247.146.5:93';
},
complete: () => {
console.log('请求完成(无论成功失败都会执行)');
}
});
} }