Compare commits
5 Commits
d4a6a63b65
...
79ee295c18
| Author | SHA1 | Date |
|---|---|---|
|
|
79ee295c18 | |
|
|
0e9bca8d97 | |
|
|
6b659a6878 | |
|
|
6aa66ce311 | |
|
|
a7f9544bc5 |
|
|
@ -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 模板编译校验。
|
||||||
|
|
@ -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,34 @@
|
||||||
|
|
||||||
<!-- 视频预览区域 -->
|
<!-- 视频预览区域 -->
|
||||||
<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"
|
|
||||||
>
|
>
|
||||||
<!-- <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()">
|
<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>
|
||||||
|
</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>
|
||||||
|
|
@ -145,6 +161,10 @@ export default {
|
||||||
kind: {
|
kind: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1
|
default: 1
|
||||||
|
},
|
||||||
|
sortable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -152,7 +172,17 @@ export default {
|
||||||
return {
|
return {
|
||||||
fileList: [],
|
fileList: [],
|
||||||
oneFile: '',
|
oneFile: '',
|
||||||
loading: false
|
videoList: [],
|
||||||
|
loading: false,
|
||||||
|
dragIndex: -1,
|
||||||
|
isDragging: false,
|
||||||
|
suppressPreview: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
videoCount() {
|
||||||
|
return this.showPreview ? this.videoList.length : 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -167,6 +197,7 @@ export default {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
this.oneFile = newVal
|
this.oneFile = newVal
|
||||||
|
this.videoList = newVal ? [newVal] : []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -272,12 +303,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);
|
||||||
|
|
@ -293,30 +327,18 @@ export default {
|
||||||
})
|
})
|
||||||
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(file)
|
||||||
if(this.accept == 'image/*') {
|
|
||||||
imageType = file.path.split('.')
|
|
||||||
}else if(this.accept == 'video/*') {
|
|
||||||
// #ifdef H5
|
|
||||||
imageType = file.name.split('.')
|
|
||||||
// #endif
|
|
||||||
// #ifdef APP-PLUS || MP-WEIXIN
|
|
||||||
imageType = file.tempFilePath.split('.')
|
|
||||||
// #endif
|
|
||||||
}
|
|
||||||
console.log('imageType',imageType)
|
|
||||||
|
|
||||||
let artisan = this.type==1 ? 'yh' : this.type==2 ? 'syr' : this.type==3 ? 'sj' : '';
|
let artisan = this.type==1 ? 'yh' : this.type==2 ? 'syr' : this.type==3 ? 'sj' : '';
|
||||||
let name = `${this.userId}_${artisan}_${date}.${imageType[imageType.length - 1]}`
|
let name = `${this.userId}_${artisan}_${date}.${fileExt}`
|
||||||
console.log('name',name)
|
console.log('name',name)
|
||||||
try {
|
try {
|
||||||
// 1. 获取OSS上传凭证
|
// 1. 获取OSS上传凭证
|
||||||
const ossConfig = await this.getOssConfig()
|
const ossConfig = await this.getOssConfig()
|
||||||
// 2. 上传文件
|
// 2. 上传文件
|
||||||
const uploadRes = await this.uploadToOss(file, ossConfig, name)
|
const fileUrl = await this.uploadToOss(file, ossConfig, name)
|
||||||
// 3. 添加到文件列表
|
// 3. 添加到文件列表
|
||||||
const fileUrl = `${ossConfig.host}/${ossConfig.dir}${name}`
|
|
||||||
if(this.accept == 'image/*') {
|
if(this.accept == 'image/*') {
|
||||||
const newFile = [`${fileUrl}`]
|
const newFile = [`${fileUrl}`]
|
||||||
this.fileList = this.fileList.concat(newFile)
|
this.fileList = this.fileList.concat(newFile)
|
||||||
|
|
@ -327,8 +349,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,9 +377,67 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getFileExt(file) {
|
||||||
|
const defaultExt = this.accept === 'video/*' ? 'mp4' : 'jpg'
|
||||||
|
const source = file && (file.name || file.tempFilePath || file.path || file.url || '')
|
||||||
|
const cleanPath = String(source).split('?')[0].split('#')[0]
|
||||||
|
const ext = cleanPath.includes('.') ? cleanPath.split('.').pop().toLowerCase() : ''
|
||||||
|
return /^[a-z0-9]+$/.test(ext) ? ext : defaultExt
|
||||||
|
},
|
||||||
|
|
||||||
|
buildFileUrl(ossConfig, name) {
|
||||||
|
const host = String(ossConfig.host || '').replace(/\/+$/, '')
|
||||||
|
return `${host}/${this.buildObjectKey(ossConfig, name)}`
|
||||||
|
},
|
||||||
|
|
||||||
|
buildObjectKey(ossConfig, name) {
|
||||||
|
const dir = String(ossConfig.dir || '').replace(/^\/+/, '').replace(/\/+$/, '')
|
||||||
|
return dir ? `${dir}/${name}` : name
|
||||||
|
},
|
||||||
|
|
||||||
|
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 (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)
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('name',name);
|
formData.append('name',name);
|
||||||
|
|
@ -361,13 +445,17 @@ export default {
|
||||||
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
|
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
|
||||||
formData.append('success_action_status', '200');
|
formData.append('success_action_status', '200');
|
||||||
formData.append('signature', ossConfig.signature);
|
formData.append('signature', ossConfig.signature);
|
||||||
formData.append('key', ossConfig.dir + name);
|
formData.append('key', objectKey);
|
||||||
// file必须为最后一个表单域,除file以外的其他表单域无顺序要求。
|
// file必须为最后一个表单域,除file以外的其他表单域无顺序要求。
|
||||||
formData.append('file', file);
|
formData.append('file', file.file || file.tempFile || file);
|
||||||
fetch(ossConfig.host, { method: 'POST', body: formData},).then((res) => {
|
fetch(ossConfig.host, { method: 'POST', body: formData},).then((res) => {
|
||||||
resolve(res)
|
if (res.ok) {
|
||||||
}).catch(()=>{
|
resolve(fileUrl)
|
||||||
reject(new Error(`上传失败: ${res.statusCode}`))
|
} else {
|
||||||
|
reject(new Error(`上传失败: ${res.status}`))
|
||||||
|
}
|
||||||
|
}).catch((error)=>{
|
||||||
|
reject(error)
|
||||||
})
|
})
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
|
|
@ -378,7 +466,7 @@ export default {
|
||||||
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;
|
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;
|
||||||
formData.success_action_status = '200';
|
formData.success_action_status = '200';
|
||||||
formData.signature = ossConfig.signature;
|
formData.signature = ossConfig.signature;
|
||||||
formData.key = ossConfig.dir + name;
|
formData.key = objectKey;
|
||||||
formData.file = file;
|
formData.file = file;
|
||||||
let path;
|
let path;
|
||||||
if(this.accept == 'image/*') {
|
if(this.accept == 'image/*') {
|
||||||
|
|
@ -397,14 +485,22 @@ export default {
|
||||||
filePath: path, // 手机端文件路径
|
filePath: path, // 手机端文件路径
|
||||||
name: 'file',
|
name: 'file',
|
||||||
formData: {
|
formData: {
|
||||||
key: ossConfig.dir + name, // 文件名
|
key: objectKey, // 文件名
|
||||||
policy: ossConfig.policy, // 后台获取超时时间
|
policy: ossConfig.policy, // 后台获取超时时间
|
||||||
OSSAccessKeyId: ossConfig.ossAccessKeyId, // 后台获取临时ID
|
OSSAccessKeyId: ossConfig.ossAccessKeyId, // 后台获取临时ID
|
||||||
success_action_status: 200, // 让服务端返回200,不然,默认会返回204
|
success_action_status: 200, // 让服务端返回200,不然,默认会返回204
|
||||||
signature: ossConfig.signature // 后台获取签名
|
signature: ossConfig.signature // 后台获取签名
|
||||||
},
|
},
|
||||||
success(res) {
|
success: (res) => {
|
||||||
resolve(`${ossConfig.host}/${ossConfig.dir}${name}`)
|
const statusCode = Number(res.statusCode)
|
||||||
|
if (statusCode >= 200 && statusCode < 300) {
|
||||||
|
resolve(fileUrl)
|
||||||
|
} else {
|
||||||
|
reject(new Error(`上传失败: ${statusCode} ${res.data || ''}`))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail(error) {
|
||||||
|
reject(error)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// #endif
|
// #endif
|
||||||
|
|
@ -414,12 +510,58 @@ export default {
|
||||||
|
|
||||||
// 预览图片
|
// 预览图片
|
||||||
handlePreview(index) {
|
handlePreview(index) {
|
||||||
|
if (this.isDragging || this.suppressPreview) return
|
||||||
uni.previewImage({
|
uni.previewImage({
|
||||||
current: index,
|
current: index,
|
||||||
urls: this.fileList.map(item => item.url)
|
urls: this.fileList
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
startDrag(index) {
|
||||||
|
if (!this.sortable || this.accept !== 'image/*' || this.fileList.length < 2) return
|
||||||
|
this.dragIndex = index
|
||||||
|
this.isDragging = true
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDragMove(event) {
|
||||||
|
if (!this.isDragging || this.dragIndex < 0) return
|
||||||
|
if (event.preventDefault) event.preventDefault()
|
||||||
|
if (event.stopPropagation) event.stopPropagation()
|
||||||
|
const touch = event.touches && event.touches[0]
|
||||||
|
if (!touch) return
|
||||||
|
uni.createSelectorQuery()
|
||||||
|
.in(this)
|
||||||
|
.selectAll('.image-preview-item')
|
||||||
|
.boundingClientRect((rects) => {
|
||||||
|
if (!Array.isArray(rects) || !rects.length) return
|
||||||
|
const targetIndex = rects.findIndex((rect) => {
|
||||||
|
return touch.clientX >= rect.left &&
|
||||||
|
touch.clientX <= rect.right &&
|
||||||
|
touch.clientY >= rect.top &&
|
||||||
|
touch.clientY <= rect.bottom
|
||||||
|
})
|
||||||
|
if (targetIndex < 0 || targetIndex === this.dragIndex || targetIndex >= this.fileList.length) return
|
||||||
|
const nextList = this.fileList.slice()
|
||||||
|
const moved = nextList.splice(this.dragIndex, 1)[0]
|
||||||
|
nextList.splice(targetIndex, 0, moved)
|
||||||
|
this.fileList = nextList
|
||||||
|
this.dragIndex = targetIndex
|
||||||
|
this.$emit('input', this.fileList)
|
||||||
|
this.$emit('change', this.fileList)
|
||||||
|
})
|
||||||
|
.exec()
|
||||||
|
},
|
||||||
|
|
||||||
|
endDrag() {
|
||||||
|
if (!this.isDragging) return
|
||||||
|
this.dragIndex = -1
|
||||||
|
this.suppressPreview = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isDragging = false
|
||||||
|
this.suppressPreview = false
|
||||||
|
}, 200)
|
||||||
|
},
|
||||||
|
|
||||||
// 删除文件
|
// 删除文件
|
||||||
handleDelete(index) {
|
handleDelete(index) {
|
||||||
// uni.showModal({
|
// uni.showModal({
|
||||||
|
|
@ -434,11 +576,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 +606,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 +649,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 {
|
||||||
|
|
@ -514,15 +666,8 @@ export default {
|
||||||
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%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 32rpx;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
<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">{{ 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: {
|
||||||
|
isChecked: function (val) {
|
||||||
|
return this.value && this.value.indexOf(val) !== -1;
|
||||||
|
},
|
||||||
|
onToggle: function (val) {
|
||||||
|
var newValue = (this.value || []).slice();
|
||||||
|
var index = newValue.indexOf(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>
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<template>
|
||||||
|
<ali-oss-uploader v-model="currentValue" :max-count="maxCount" :max-size="maxSize" accept="image/*" :userId="userId"
|
||||||
|
:type="ossType" width="100%" :sortable="true" @input="onInput" @change="onChange" />
|
||||||
|
</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 }
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
currentValue: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
userId: function () {
|
||||||
|
return this.$store && this.$store.state && this.$store.state.sjInfo
|
||||||
|
? this.$store.state.sjInfo.id : null;
|
||||||
|
},
|
||||||
|
ossType: function () {
|
||||||
|
var artisanType = getApp().globalData ? getApp().globalData.artisanType : 0;
|
||||||
|
return (artisanType || 0) + 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<view class="form-radio">
|
||||||
|
<view v-for="(item, index) in options" :key="index" class="radio-item" @click="onSelect(item)">
|
||||||
|
<view class="radio-dot" :class="{ active: value === item }"></view>
|
||||||
|
<text class="radio-label">{{ item }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'FormItemRadio',
|
||||||
|
props: {
|
||||||
|
value: { type: [String, Number], default: '' },
|
||||||
|
options: { type: Array, default: function () { return []; } }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSelect: function (val) {
|
||||||
|
this.$emit('input', val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<picker :range="options" @change="onChange">
|
||||||
|
<view class="form-select">
|
||||||
|
<text :class="['select-text', value ? '' : 'placeholder']">
|
||||||
|
{{ value || 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: '请选择' }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onChange: function (e) {
|
||||||
|
var index = e.detail.value;
|
||||||
|
var selected = this.options[index];
|
||||||
|
if (selected !== undefined) {
|
||||||
|
this.$emit('input', 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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">×</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>
|
||||||
|
|
@ -0,0 +1,440 @@
|
||||||
|
<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 === 'image_multi'" class="form-item-up upload-item">
|
||||||
|
<view>
|
||||||
|
<text :class="['form-label', group.field.is_required === true ? 'required' : '']">{{ group.field.label
|
||||||
|
}}</text>
|
||||||
|
<text class="upload-tip">(仅可上传{{ group.field.maxCount || 9 }}个图片)</text>
|
||||||
|
</view>
|
||||||
|
<view class="upload-content">
|
||||||
|
<form-item-image :value="formData[group.field.key]" :maxCount="group.field.maxCount || 9"
|
||||||
|
@input="onInput(group.field.key, $event)" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<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"
|
||||||
|
@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>
|
||||||
|
|
@ -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">×</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,
|
||||||
|
|
@ -243,14 +271,73 @@
|
||||||
},
|
},
|
||||||
viewEdit(item) {
|
viewEdit(item) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/shop/add-service?id=${item.id}&state=${item.state}`,
|
url: `/pages/shop/add-service?id=${item.id}&state=${item.state}&first_class_id=${item.first_class_id}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
addService() {
|
addService() {
|
||||||
uni.navigateTo({
|
const userId = uni.getStorageSync('userId') || '';
|
||||||
url: `/pages/shop/add-service`,
|
request.post("/sj/firstclass", { id: userId }).then((res) => {
|
||||||
|
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",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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 +353,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 +367,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 +377,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;
|
||||||
|
|
@ -467,7 +524,7 @@
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
width: 32rpx;
|
width: 32rpx;
|
||||||
height: 6rpx;
|
height: 6rpx;
|
||||||
background-color: #E8101E;
|
background-color: #FF4767;
|
||||||
border-radius: 3rpx;
|
border-radius: 3rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -512,7 +569,7 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 160rpx;
|
width: 160rpx;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #E8101E;
|
background-color: #FF4767;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -558,20 +615,20 @@
|
||||||
|
|
||||||
.price-symbol {
|
.price-symbol {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #E8101E;
|
color: #FF4767;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-value {
|
.price-value {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #E8101E;
|
color: #FF4767;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin: 0 4rpx;
|
margin: 0 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-unit {
|
.price-unit {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #E8101E;
|
color: #FF4767;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-tag {
|
.status-tag {
|
||||||
|
|
@ -589,11 +646,11 @@
|
||||||
|
|
||||||
/* 已上架 */
|
/* 已上架 */
|
||||||
.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 {
|
||||||
color: #E8101E;
|
color: #FF4767;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 审核中 */
|
/* 审核中 */
|
||||||
|
|
@ -609,13 +666,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 +762,6 @@
|
||||||
|
|
||||||
/* #ifdef MP-WEIXIN */
|
/* #ifdef MP-WEIXIN */
|
||||||
.search-box input {
|
.search-box input {
|
||||||
/* 小程序搜索框样式调整 */
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -714,7 +770,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -735,16 +790,193 @@
|
||||||
height: 88rpx;
|
height: 88rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 88rpx;
|
line-height: 88rpx;
|
||||||
background-color: #E8101E;
|
background-color: #FF4767;
|
||||||
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>
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
<!-- <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,8 +121,12 @@
|
||||||
},
|
},
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
console.log(options)
|
console.log(options)
|
||||||
if(options.list){
|
if (options.list && options.list !== 'null' && options.list !== 'undefined') {
|
||||||
this.list = JSON.parse(options.list)
|
try {
|
||||||
|
this.list = JSON.parse(options.list) || [];
|
||||||
|
} catch (e) {
|
||||||
|
this.list = [];
|
||||||
|
}
|
||||||
console.log(this.list)
|
console.log(this.list)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -353,6 +358,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%;
|
||||||
|
|
@ -368,6 +374,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;
|
||||||
}
|
}
|
||||||
|
|
@ -465,6 +472,7 @@
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.btn-submit {
|
.btn-submit {
|
||||||
background-color: rgba(232, 16, 30, 1);
|
background-color: rgba(232, 16, 30, 1);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
@ -476,8 +484,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
|
|
@ -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://116.63.163.121:93"; // 发布到生产环境时,此处代码会被摇树移除掉。
|
baseURL = "http://116.63.163.121:93"; // 发布到生产环境时,此处代码会被摇树移除掉。
|
||||||
//baseURL = 'http://60.247.146.5:96'; // 发布到生产环境时,此处代码会被摇树移除掉。
|
// 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://116.63.163.121:93"; // 发布到生产环境时,此处代码会被摇树移除掉。
|
baseURL = "http://116.63.163.121:93"; // 发布到生产环境时,此处代码会被摇树移除掉。
|
||||||
//baseURL = 'http://60.247.146.5:96'; // 发布到生产环境时,此处代码会被摇树移除掉。
|
// baseURL = "http://116.63.163.121:96"; // 发布到生产环境时,此处代码会被摇树移除掉。
|
||||||
baseURL = "https://app.mrrweb.com.cn";
|
// baseURL = "https://app.mrrweb.com.cn";
|
||||||
|
|
||||||
console.log("生产环境");
|
console.log("生产环境");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue