Author | SHA1 | Message | Date |
---|---|---|---|
|
fefdfebf47 | feat: bug修复; | 2 weeks ago |
|
7bf9c02b10 |
feat: 添加H5环境兼容性修复和功能优化
refactor(router): 重构H5路由处理逻辑 fix(share): 修复微信分享URL问题 feat(payment): 添加H5环境微信支付支持 style: 统一页面标题显示 docs: 更新页面配置文档 |
3 weeks ago |
|
922517dc3b |
feat(分享): 添加微信分享功能及相关配置
refactor(支付): 移除wxPay中冗余的JSSDK配置代码 fix(优惠券): 修复优惠券列表获取时机问题 style(表单): 优化textarea组件样式代码结构 feat(提现): 增加H5端微信商户转账功能 docs(配置): 更新重定向URL配置 |
1 month ago |
|
3acefe0832 |
feat: 添加H5环境支持及功能优化
- 新增H5环境专属页面promotionH5.vue用于邀请好友功能 - 优化支付逻辑,区分微信小程序和H5环境的支付方式 - 重构wxPay方法返回Promise以支持async/await - 修复表单组件样式问题及地址选择逻辑 - 添加H5环境URL参数解析功能 - 优化canvas绘制逻辑和图片保存体验 |
1 month ago |
|
964031adb9 |
feat(登录): 实现H5和微信小程序的双平台登录逻辑
- 添加H5平台的微信授权登录流程 - 分离H5和小程序的登录逻辑到不同commit方法 - 更新登录API接口配置支持多平台 - 添加微信验证文件和配置参数 - 调整导航栏组件在微信平台的显示 - 修复登录状态检查逻辑 |
1 month ago |
|
c118865ba5 |
fix: 将环境配置更改为生产环境并修复提现错误提示
修改config.js中的环境变量从'dev'到'prod'以切换到生产环境 在withdraw.vue中更新提现失败时的错误提示,使用后端返回的message |
1 month ago |
|
80040f273b |
feat(提现): 实现微信商户转账提现功能
- 添加商户号配置和提现相关API接口 - 修改佣金显示字段为userInfo.recommendAmount - 实现微信requestMerchantTransfer提现流程 - 添加提现记录页面状态显示和领取功能 - 调整提现金额验证规则为大于1的整数 |
1 month ago |
|
b0fab06d88 |
Merge pull request 'feat: 接口对接;' (#8) from fox into master
Reviewed-on: http://175.178.51.79:3000/Augcl/massage-front/pulls/8 |
2 months ago |
|
028acfe3ff |
Merge pull request 'feat: 接口对接;' (#7) from fox into master
Reviewed-on: http://175.178.51.79:3000/Augcl/massage-front/pulls/7 |
2 months ago |
|
258f91ced7 |
Merge pull request 'feat: 签到接口对接;' (#6) from fox into master
Reviewed-on: http://175.178.51.79:3000/Augcl/massage-front/pulls/6 |
2 months ago |
|
69e6085eb3 |
Merge pull request 'feat: 申请退款接口对接;' (#5) from fox into master
Reviewed-on: http://175.178.51.79:3000/Augcl/massage-front/pulls/5 |
2 months ago |
|
637be6ca65 |
Merge pull request 'feat: 接口对接;' (#4) from fox into master
Reviewed-on: http://175.178.51.79:3000/Augcl/massage-front/pulls/4 |
2 months ago |
@ -0,0 +1 @@ | |||
wfZF8R25IxLLUUXi |
@ -1,40 +1,32 @@ | |||
<template> | |||
<uv-textarea | |||
:value="value" | |||
@input="$emit('input', $event)" | |||
:placeholder="placeholder" | |||
height="175rpx" | |||
border="none" | |||
:customStyle="{ | |||
<uv-textarea :value="value" @input="$emit('input', $event)" :placeholder="placeholder" height="175rpx" border="none" | |||
:customStyle="{ | |||
backgroundColor: '#F5F5F5', | |||
borderRadius: '6rpx', | |||
}" | |||
:placeholderStyle="{ | |||
color: '#999999', | |||
fontSize: '28rpx', | |||
}" | |||
></uv-textarea> | |||
borderRadius: '6rpx',}" | |||
></uv-textarea> | |||
<!-- | |||
:placeholderStyle="{color: '#999999', | |||
fontSize: '28rpx'}" --> | |||
</template> | |||
<script> | |||
export default { | |||
props: { | |||
props: { | |||
value: { | |||
default: null | |||
}, | |||
placeholder: { | |||
type: String, | |||
placeholder: { | |||
type: String, | |||
default: '请输入' | |||
}, | |||
}, | |||
}, | |||
data() { | |||
return { | |||
} | |||
}, | |||
methods: { | |||
}, | |||
} | |||
return {} | |||
}, | |||
methods: {}, | |||
} | |||
</script> | |||
<style scoped lang="scss"> |
@ -1,405 +1,396 @@ | |||
<template> | |||
<view class="page"> | |||
<view class="page"> | |||
<!-- 导航栏 --> | |||
<navbar title="商家合作" leftClick @leftClick="$utils.navigateBack" color="#fff" /> | |||
<view v-if="['0', '2'].includes(status) && statusDesc" class="flex tips"> | |||
<uv-icon name="info-circle" color="#86A941" size="28rpx"></uv-icon> | |||
<text style="margin-left: 3rpx;">{{ statusDesc }}</text> | |||
</view> | |||
<view class="content"> | |||
<view class="form"> | |||
<view class="form-title">门头照片</view> | |||
<view class="card upload"> | |||
<formUpload v-model="form.image"> | |||
<template v-slot="{ value }"> | |||
<view class="flex"> | |||
<image v-if="value" | |||
class="upload-img" | |||
:src="value" | |||
mode="aspectFill" | |||
/> | |||
<image v-else | |||
class="upload-img" | |||
src="../static/cooperation/icon-upload.png" | |||
mode="aspectFill" | |||
/> | |||
</view> | |||
</template> | |||
</formUpload> | |||
</view> | |||
</view> | |||
<view class="form"> | |||
<view class="form-title">店铺信息</view> | |||
<view class="card info"> | |||
<uv-form | |||
ref="form" | |||
:model="form" | |||
:rules="rules" | |||
labelPosition="left" | |||
labelWidth="150rpx" | |||
:labelStyle="{ | |||
<view v-if="['0', '2'].includes(status) && statusDesc" class="flex tips"> | |||
<uv-icon name="info-circle" color="#86A941" size="28rpx"></uv-icon> | |||
<text style="margin-left: 3rpx;">{{ statusDesc }}</text> | |||
</view> | |||
<view class="content"> | |||
<view class="form"> | |||
<view class="form-title">门头照片</view> | |||
<view class="card upload"> | |||
<formUpload v-model="form.image"> | |||
<view class="flex"> | |||
<image v-if="form.image" class="upload-img" :src="form.image" mode="aspectFill" /> | |||
<image v-else class="upload-img" src="../static/cooperation/icon-upload.png" mode="aspectFill" /> | |||
</view> | |||
</formUpload> | |||
</view> | |||
</view> | |||
<view class="form"> | |||
<view class="form-title">店铺信息</view> | |||
<view class="card info"> | |||
<uv-form ref="form" :model="form" :rules="rules" labelPosition="left" labelWidth="150rpx" | |||
:labelStyle="{ | |||
color: '#000000', | |||
fontSize: '28rpx', | |||
}" | |||
> | |||
<view class="form-item"> | |||
<uv-form-item label="店铺名称" prop="shop"> | |||
<view class="form-item-content"> | |||
<formInput v-model="form.shop" placeholder="请输入店铺名称"></formInput> | |||
</view> | |||
</uv-form-item> | |||
</view> | |||
<view class="form-item"> | |||
<uv-form-item label="您的姓名" prop="name"> | |||
<view class="form-item-content"> | |||
<formInput v-model="form.name" placeholder="请输入您的姓名"></formInput> | |||
</view> | |||
</uv-form-item> | |||
</view> | |||
<view class="form-item"> | |||
<uv-form-item label="联系手机号" prop="phone"> | |||
<view class="form-item-content"> | |||
<formInput v-model="form.phone" placeholder="请输入您的手机号"></formInput> | |||
</view> | |||
</uv-form-item> | |||
</view> | |||
<view class="form-item"> | |||
<uv-form-item label="所在地区" prop="area"> | |||
<view class="form-item-content flex area"> | |||
<text>{{ form.area ? form.area : '请选择' }}</text> | |||
<button plain class="btn area-btn" @click="selectAddr"> | |||
<image class="area-btn-icon" src="../static/cooperation/icon-arrow.png" mode="widthFix"></image> | |||
</button> | |||
</view> | |||
</uv-form-item> | |||
</view> | |||
<view class="form-item address"> | |||
<uv-form-item label="详细地址" prop="address" labelPosition="top" > | |||
<view style="margin-top: 22rpx;"> | |||
<formTextarea | |||
v-model="form.address" | |||
placeholder="请输入详细地址" | |||
></formTextarea> | |||
</view> | |||
</uv-form-item> | |||
</view> | |||
</uv-form> | |||
</view> | |||
</view> | |||
<view class="tools" v-if="status != '1'"> | |||
<button plain class="btn btn-submit" @click="onSubmit">提交</button> | |||
</view> | |||
</view> | |||
</view> | |||
}"> | |||
<view class="form-item"> | |||
<uv-form-item label="店铺名称" prop="shop"> | |||
<view class="form-item-content"> | |||
<formInput v-model="form.shop" placeholder="请输入店铺名称"></formInput> | |||
</view> | |||
</uv-form-item> | |||
</view> | |||
<view class="form-item"> | |||
<uv-form-item label="您的姓名" prop="name"> | |||
<view class="form-item-content"> | |||
<formInput v-model="form.name" placeholder="请输入您的姓名"></formInput> | |||
</view> | |||
</uv-form-item> | |||
</view> | |||
<view class="form-item"> | |||
<uv-form-item label="联系手机号" prop="phone"> | |||
<view class="form-item-content"> | |||
<formInput v-model="form.phone" placeholder="请输入您的手机号"></formInput> | |||
</view> | |||
</uv-form-item> | |||
</view> | |||
<view class="form-item"> | |||
<uv-form-item label="所在地区" prop="area"> | |||
<view class="form-item-content flex area"> | |||
<view style="width: 450rpx;">{{ form.area ? form.area : '请选择' }}</view> | |||
<button plain class="btn area-btn" @click="selectAddr"> | |||
<image class="area-btn-icon" src="../static/cooperation/icon-arrow.png" | |||
mode="widthFix"></image> | |||
</button> | |||
</view> | |||
</uv-form-item> | |||
</view> | |||
<view class="form-item address"> | |||
<uv-form-item label="详细地址" prop="address" labelPosition="top"> | |||
<view style="margin-top: 22rpx;width: 100%;"> | |||
<formTextarea v-model="form.address" placeholder="请输入详细地址"></formTextarea> | |||
</view> | |||
</uv-form-item> | |||
</view> | |||
</uv-form> | |||
</view> | |||
</view> | |||
<view class="tools" v-if="status != '1'"> | |||
<button plain class="btn btn-submit" @click="onSubmit">提交</button> | |||
</view> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
import { mapState } from 'vuex' | |||
import Position from '@/utils/position.js' | |||
import formInput from '../components/formInput.vue' | |||
import formUpload from '../components/formUpload.vue' | |||
import formTextarea from '../components/formTextarea.vue' | |||
export default { | |||
components: { | |||
formInput, | |||
formUpload, | |||
formTextarea, | |||
}, | |||
data() { | |||
return { | |||
id: null, | |||
status: null, | |||
statusDesc: null, | |||
form: { | |||
image: null, | |||
shop: null, | |||
name: null, | |||
phone: null, | |||
area: null, | |||
latitude: null, | |||
longitude: null, | |||
address: null, | |||
}, | |||
rules: { | |||
'image': { | |||
type: 'string', | |||
required: true, | |||
message: '请选择门头照片', | |||
}, | |||
'shop': { | |||
type: 'string', | |||
required: true, | |||
message: '请输入店铺名称', | |||
}, | |||
'name': { | |||
type: 'string', | |||
required: true, | |||
message: '请输入您的姓名', | |||
}, | |||
'phone': { | |||
type: 'string', | |||
required: true, | |||
message: '请输入您的手机号', | |||
}, | |||
'area': { | |||
type: 'string', | |||
required: true, | |||
message: '请选择所在地区', | |||
}, | |||
'address': { | |||
type: 'string', | |||
required: true, | |||
message: '请输入详细地址', | |||
}, | |||
}, | |||
} | |||
}, | |||
computed: { | |||
...mapState(['userInfo']), | |||
}, | |||
onLoad() { | |||
this.initData() | |||
}, | |||
onPullDownRefresh() { | |||
this.updateStatus() | |||
}, | |||
methods: { | |||
//地图上选择地址 | |||
selectAddr() { | |||
// Position.getLocation(res => { | |||
Position.selectAddress(0, 0, success => { | |||
this.setAddress(success) | |||
}) | |||
// }) | |||
}, | |||
//提取用户选择的地址信息复制给表单数据 | |||
setAddress(res) { | |||
//经纬度信息 | |||
this.form.latitude = res.latitude | |||
this.form.longitude = res.longitude | |||
if (!res.address && res.name) { //用户直接选择城市的逻辑 | |||
return this.form.area = res.name | |||
} | |||
if (res.address || res.name) { | |||
return this.form.area = res.address + res.name | |||
} | |||
this.form.area = '' //用户啥都没选就点击勾选 | |||
}, | |||
async initData() { | |||
try { | |||
const shopDetails = (await this.$fetch('queryShopById')) | |||
console.log("shopDetails======") | |||
console.log(shopDetails) | |||
if (!shopDetails) { | |||
return | |||
} | |||
const { | |||
id, | |||
status, | |||
status_dictText, | |||
remark, | |||
image, | |||
shop, | |||
name, | |||
phone, | |||
area, | |||
latitude, | |||
longitude, | |||
address, | |||
} = shopDetails | |||
this.form = { | |||
image, | |||
shop, | |||
name, | |||
phone, | |||
area, | |||
latitude, | |||
longitude, | |||
address, | |||
} | |||
this.id = id | |||
this.status = status | |||
this.statusDesc = status == '2' ? `${status_dictText}:${remark}` : status_dictText | |||
} catch (err) { | |||
} | |||
}, | |||
async updateStatus() { | |||
if (!this.id) { | |||
return | |||
} | |||
try { | |||
const { status, status_dictText, remark } = await this.$fetch('queryShopById', { id: this.id }) | |||
this.status = status | |||
this.statusDesc = status == '2' ? `${status_dictText}:${remark}` : status_dictText | |||
} catch (err) { | |||
console.log('--err', err) | |||
} | |||
uni.stopPullDownRefresh(); | |||
}, | |||
async onSubmit() { | |||
try { | |||
await this.$refs.form.validate() | |||
const { | |||
image, | |||
shop, | |||
name, | |||
phone, | |||
area, | |||
latitude, | |||
longitude, | |||
address, | |||
} = this.form | |||
const params = { | |||
image, | |||
shop, | |||
name, | |||
phone, | |||
area, | |||
latitude, | |||
longitude, | |||
address, | |||
} | |||
let api = this.id ? 'updateShop' : 'addShop' | |||
await this.$fetch(api, params) | |||
uni.showToast({ | |||
title: '提交成功', | |||
icon: 'none' | |||
}) | |||
setTimeout(uni.navigateBack, 1000, -1) | |||
} catch (err) { | |||
} | |||
}, | |||
}, | |||
} | |||
import { | |||
mapState | |||
} from 'vuex' | |||
import Position from '@/utils/position.js' | |||
import formInput from '../components/formInput.vue' | |||
import formUpload from '../components/formUpload.vue' | |||
import formTextarea from '../components/formTextarea.vue' | |||
export default { | |||
components: { | |||
formInput, | |||
formUpload, | |||
formTextarea, | |||
}, | |||
data() { | |||
return { | |||
id: null, | |||
status: null, | |||
statusDesc: null, | |||
form: { | |||
image: null, | |||
shop: null, | |||
name: null, | |||
phone: null, | |||
area: null, | |||
latitude: null, | |||
longitude: null, | |||
address: null, | |||
}, | |||
rules: { | |||
'image': { | |||
type: 'string', | |||
required: true, | |||
message: '请选择门头照片', | |||
}, | |||
'shop': { | |||
type: 'string', | |||
required: true, | |||
message: '请输入店铺名称', | |||
}, | |||
'name': { | |||
type: 'string', | |||
required: true, | |||
message: '请输入您的姓名', | |||
}, | |||
'phone': { | |||
type: 'string', | |||
required: true, | |||
message: '请输入您的手机号', | |||
}, | |||
'area': { | |||
type: 'string', | |||
required: true, | |||
message: '请选择所在地区', | |||
}, | |||
'address': { | |||
type: 'string', | |||
required: true, | |||
message: '请输入详细地址', | |||
}, | |||
}, | |||
} | |||
}, | |||
computed: { | |||
...mapState(['userInfo']), | |||
}, | |||
onLoad() { | |||
this.initData() | |||
}, | |||
onPullDownRefresh() { | |||
this.updateStatus() | |||
}, | |||
methods: { | |||
//地图上选择地址 | |||
selectAddr() { | |||
// Position.getLocation(res => { | |||
Position.selectAddress(0, 0, success => { | |||
this.setAddress(success) | |||
}) | |||
// }) | |||
}, | |||
//提取用户选择的地址信息复制给表单数据 | |||
setAddress(res) { | |||
//经纬度信息 | |||
this.form.latitude = res.latitude | |||
this.form.longitude = res.longitude | |||
if (!res.address && res.name) { //用户直接选择城市的逻辑 | |||
return this.form.area = res.name | |||
} | |||
if (res.address || res.name) { | |||
return this.form.area = res.address + res.name | |||
} | |||
this.form.area = '' //用户啥都没选就点击勾选 | |||
}, | |||
async initData() { | |||
try { | |||
const shopDetails = (await this.$fetch('queryShopById')) | |||
console.log("shopDetails======") | |||
console.log(shopDetails) | |||
if (!shopDetails) { | |||
return | |||
} | |||
const { | |||
id, | |||
status, | |||
status_dictText, | |||
remark, | |||
image, | |||
shop, | |||
name, | |||
phone, | |||
area, | |||
latitude, | |||
longitude, | |||
address, | |||
} = shopDetails | |||
this.form = { | |||
image, | |||
shop, | |||
name, | |||
phone, | |||
area, | |||
latitude, | |||
longitude, | |||
address, | |||
} | |||
this.id = id | |||
this.status = status | |||
this.statusDesc = status == '2' ? `${status_dictText}:${remark}` : status_dictText | |||
} catch (err) { | |||
} | |||
}, | |||
async updateStatus() { | |||
if (!this.id) { | |||
return | |||
} | |||
try { | |||
const { | |||
status, | |||
status_dictText, | |||
remark | |||
} = await this.$fetch('queryShopById', { | |||
id: this.id | |||
}) | |||
this.status = status | |||
this.statusDesc = status == '2' ? `${status_dictText}:${remark}` : status_dictText | |||
} catch (err) { | |||
console.log('--err', err) | |||
} | |||
uni.stopPullDownRefresh(); | |||
}, | |||
async onSubmit() { | |||
try { | |||
await this.$refs.form.validate() | |||
const { | |||
image, | |||
shop, | |||
name, | |||
phone, | |||
area, | |||
latitude, | |||
longitude, | |||
address, | |||
} = this.form | |||
const params = { | |||
image, | |||
shop, | |||
name, | |||
phone, | |||
area, | |||
latitude, | |||
longitude, | |||
address, | |||
} | |||
let api = this.id ? 'updateShop' : 'addShop' | |||
await this.$fetch(api, params) | |||
uni.showToast({ | |||
title: '提交成功', | |||
icon: 'none' | |||
}) | |||
setTimeout(uni.navigateBack, 1000, -1) | |||
} catch (err) { | |||
} | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.page { | |||
background-color: $uni-bg-color; | |||
min-height: 100vh; | |||
/deep/ .nav-bar__view { | |||
background-image: linear-gradient(#84A73F, #D8FF8F); | |||
} | |||
} | |||
.tips { | |||
padding: 5rpx 0; | |||
font-weight: bold; | |||
font-size: 28rpx; | |||
color: $uni-color; | |||
background-color: rgba($color: #D8FF8F, $alpha: 0.3); | |||
} | |||
.content { | |||
padding: 28rpx 30rpx; | |||
} | |||
.form { | |||
& + & { | |||
margin-top: 44rpx; | |||
} | |||
&-title { | |||
color: #000000; | |||
font-size: 28rpx; | |||
margin-bottom: 15rpx; | |||
} | |||
&-item { | |||
padding-left: 8rpx; | |||
& + & { | |||
// margin-top: 20rpx; | |||
border-top: 1rpx solid rgba($color: #C7C7C7, $alpha: 0.69); | |||
} | |||
&-content { | |||
min-height: 60rpx; | |||
display: flex; | |||
align-items: center; | |||
justify-content: flex-end; | |||
font-size: 28rpx;; | |||
color: #999999; | |||
} | |||
} | |||
} | |||
.upload { | |||
padding: 37rpx 22rpx; | |||
&-img { | |||
width: 131rpx; height: 131rpx; | |||
} | |||
} | |||
.area { | |||
color: #000000; | |||
font-size: 28rpx; | |||
line-height: 40rpx; | |||
justify-content: flex-end; | |||
&-btn { | |||
border: none; | |||
padding: 7rpx 20rpx 7rpx 7rpx; | |||
&-icon { | |||
width: 30rpx; | |||
height: auto; | |||
} | |||
} | |||
} | |||
.address { | |||
padding: 0; | |||
/deep/ .uv-form-item__body__left__content { | |||
margin-top: 10rpx; | |||
padding-left: 8rpx; | |||
} | |||
} | |||
.tools { | |||
padding: 0 56rpx; | |||
margin-top: 126rpx; | |||
} | |||
.btn-submit { | |||
padding: 29rpx 0; | |||
border: none; | |||
font-size: 36rpx; | |||
border-radius: 45rpx; | |||
color: $uni-text-color-inverse; | |||
background-image: linear-gradient(to right, #84A73F, #D8FF8F); | |||
} | |||
.tips { | |||
padding: 5rpx 0; | |||
font-weight: bold; | |||
font-size: 28rpx; | |||
color: $uni-color; | |||
background-color: rgba($color: #D8FF8F, $alpha: 0.3); | |||
} | |||
.content { | |||
padding: 28rpx 30rpx; | |||
} | |||
.form { | |||
&+& { | |||
margin-top: 44rpx; | |||
} | |||
&-title { | |||
color: #000000; | |||
font-size: 28rpx; | |||
margin-bottom: 15rpx; | |||
} | |||
&-item { | |||
padding-left: 8rpx; | |||
&+& { | |||
// margin-top: 20rpx; | |||
border-top: 1rpx solid rgba($color: #C7C7C7, $alpha: 0.69); | |||
} | |||
&-content { | |||
min-height: 60rpx; | |||
display: flex; | |||
align-items: center; | |||
justify-content: flex-end; | |||
font-size: 28rpx; | |||
; | |||
color: #999999; | |||
} | |||
} | |||
} | |||
.upload { | |||
padding: 37rpx 22rpx; | |||
&-img { | |||
width: 131rpx; | |||
height: 131rpx; | |||
} | |||
} | |||
.area { | |||
color: #000000; | |||
font-size: 28rpx; | |||
line-height: 40rpx; | |||
justify-content: flex-end; | |||
&-btn { | |||
border: none; | |||
padding: 7rpx 20rpx 7rpx 7rpx; | |||
&-icon { | |||
width: 30rpx; | |||
height: auto; | |||
} | |||
} | |||
} | |||
.address { | |||
padding: 0; | |||
/deep/ .uv-form-item__body__left__content { | |||
margin-top: 10rpx; | |||
padding-left: 8rpx; | |||
} | |||
} | |||
.tools { | |||
padding: 0 56rpx; | |||
margin-top: 126rpx; | |||
} | |||
.btn-submit { | |||
padding: 29rpx 0; | |||
border: none; | |||
font-size: 36rpx; | |||
border-radius: 45rpx; | |||
color: $uni-text-color-inverse; | |||
background-image: linear-gradient(to right, #84A73F, #D8FF8F); | |||
} | |||
</style> |
@ -0,0 +1,602 @@ | |||
<template> | |||
<view class="placard"> | |||
<view class="placard-content"> | |||
<!-- H5环境显示最终图片 --> | |||
<view v-if="tempFilePath" class="img-box" :style="{ width: canvasW + 'px', height: canvasH + 'px' }"> | |||
<img | |||
:src="tempFilePath" | |||
:style="{ width: canvasW + 'px', height: canvasH + 'px' }" | |||
@contextmenu.prevent | |||
style="display: block; border-radius: 10px;" | |||
/> | |||
</view> | |||
<!-- 生成中的提示 --> | |||
<view v-else class="loading-box" :style="{ width: canvasW + 'px', height: canvasH + 'px' }"> | |||
<view class="loading-content"> | |||
<view class="loading-spinner"></view> | |||
<text class="loading-text"> | |||
{{ !userId ? '获取用户信息中...' : '海报生成中...' }} | |||
</text> | |||
</view> | |||
</view> | |||
<!-- 二维码组件 - 隐藏 --> | |||
<view v-if="userId" class="qrcode" ref="qrcode"> | |||
<uv-qrcode | |||
:value="qrCodeValue" | |||
:size="qrCodeSize" | |||
type="image/png" | |||
ref="qrCodeComponent" | |||
@complete="onQRCodeComplete"> | |||
</uv-qrcode> | |||
</view> | |||
<!-- 画布 - 隐藏 --> | |||
<canvas | |||
:style="{ width: canvasW + 'px', height: canvasH + 'px' }" | |||
canvas-id="myCanvas" | |||
id="myCanvas" | |||
:width="canvasW" | |||
:height="canvasH" | |||
class="hidden-canvas"> | |||
</canvas> | |||
<view class="add-btn"> | |||
<view class="btn" @click="handleSaveImage"> | |||
长按图片保存到手机 | |||
</view> | |||
</view> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
import Vue from 'vue' | |||
import { mapState } from 'vuex' | |||
export default { | |||
name: 'Placard', | |||
computed: { | |||
...mapState(['userInfo']), | |||
// 获取用户ID | |||
userId() { | |||
return this.userInfo?.id || '' | |||
}, | |||
// 动态生成二维码内容 | |||
qrCodeContent() { | |||
if (this.userId) { | |||
return Vue.prototype.$config.redirect + `?vid=${this.userId}` | |||
} | |||
return Vue.prototype.$config.redirect | |||
} | |||
}, | |||
data() { | |||
return { | |||
qrCodeValue: '', // 初始为空,等用户信息加载后再设置 | |||
qrCodeSize: 180, | |||
qrCodeDarkColor: '#000', | |||
qrCodeLightColor: '#fff', | |||
margin: 0, | |||
//画布信息 | |||
canvasW: 299, | |||
canvasH: 403, | |||
//设备信息 | |||
systemInfo: {}, | |||
//图片路径 | |||
tempFilePath: '', | |||
_rpx: 1, // H5环境使用固定比例 | |||
_center: 0, | |||
// 二维码是否生成完成 | |||
qrCodeReady: false, | |||
// 重试次数 | |||
retryCount: 0, | |||
maxRetry: 1, | |||
// 用户信息是否已加载 | |||
userInfoLoaded: false | |||
} | |||
}, | |||
watch: { | |||
// 监听用户信息变化 | |||
userInfo: { | |||
handler(newVal) { | |||
if (newVal && newVal.id) { | |||
this.userInfoLoaded = true | |||
this.qrCodeValue = this.qrCodeContent | |||
// 如果二维码还未生成,现在生成 | |||
if (!this.qrCodeReady) { | |||
this.$nextTick(() => { | |||
this.generateQRCode() | |||
}) | |||
} | |||
} | |||
}, | |||
deep: true, | |||
immediate: true | |||
} | |||
}, | |||
mounted() { | |||
this.initCanvas() | |||
}, | |||
onShow() { | |||
// 页面显示时确保获取用户信息 | |||
this.getUserInfo() | |||
}, | |||
methods: { | |||
// 初始化画布 | |||
initCanvas() { | |||
// H5环境使用固定尺寸,便于显示 | |||
this.canvasW = 349 | |||
this.canvasH = 389 | |||
this.qrCodeSize = 180 | |||
this._center = this.canvasW / 2 | |||
// 检查用户信息是否已加载 | |||
if (this.userInfoLoaded && this.qrCodeValue) { | |||
this.generateQRCode() | |||
} else { | |||
// 获取用户信息 | |||
this.getUserInfo() | |||
} | |||
}, | |||
// 获取用户信息 | |||
getUserInfo() { | |||
// 如果store中没有用户信息,尝试获取 | |||
if (!this.userInfo?.id) { | |||
this.$store.commit('getUserInfo') | |||
} | |||
}, | |||
// 生成二维码 | |||
generateQRCode() { | |||
// 确保有用户ID才生成二维码 | |||
if (!this.userId) { | |||
console.log('等待用户信息加载...') | |||
return | |||
} | |||
// 确保二维码内容已设置 | |||
if (!this.qrCodeValue) { | |||
this.qrCodeValue = this.qrCodeContent | |||
} | |||
console.log('生成二维码,内容:', this.qrCodeValue) | |||
this.$nextTick(() => { | |||
if (this.$refs.qrCodeComponent) { | |||
this.$refs.qrCodeComponent.make() | |||
} | |||
}) | |||
}, | |||
// 二维码生成完成回调 | |||
onQRCodeComplete(res) { | |||
if (res.success) { | |||
this.qrCodeReady = true | |||
this.draw() | |||
} | |||
}, | |||
// 绘制海报 | |||
async draw() { | |||
if (!this.qrCodeReady) { | |||
console.log('二维码未准备好') | |||
return | |||
} | |||
const ctx = uni.createCanvasContext('myCanvas', this) | |||
// 设置背景色 | |||
ctx.setFillStyle('#ffffff') | |||
ctx.fillRect(0, 0, this.canvasW, this.canvasH) | |||
ctx.setFillStyle('#000000') | |||
ctx.setFontSize(16) | |||
let text = '邀请好友' | |||
// 计算文本的宽度和高度 | |||
let metrics = ctx.measureText(text); | |||
ctx.fillText(text, this._center - metrics.width / 2, 53); | |||
text = `邀请码:${this.userId}` | |||
// 计算文本的宽度和高度 | |||
metrics = ctx.measureText(text); | |||
// todo: check code | |||
ctx.fillText(text, this._center - metrics.width / 2, 342); | |||
// 获取二维码图片并绘制 | |||
try { | |||
const qrCodeImagePath = await this.getQRCodeImage() | |||
if (qrCodeImagePath) { | |||
const qrSize = 192 | |||
const qrX = (this.canvasW - qrSize) / 2 | |||
const qrY = 94 | |||
ctx.drawImage(qrCodeImagePath, qrX, qrY, qrSize, qrSize) | |||
} | |||
} catch (e) { | |||
console.log('二维码绘制失败', e) | |||
} | |||
// 执行绘制 | |||
ctx.draw(false, () => { | |||
// 导出图片 | |||
setTimeout(() => { | |||
this.canvasToTempFilePath() | |||
}, 1000) | |||
}) | |||
return | |||
// 绘制标题 | |||
ctx.setFillStyle('#333333') | |||
ctx.setFontSize(18) | |||
ctx.setTextAlign('center') | |||
ctx.fillText('邀请好友一起享受优质服务', this.canvasW / 2, 30) | |||
// 绘制用户头像(如果有) | |||
if (this.userInfo.headImage) { | |||
try { | |||
// 先下载图片到本地 | |||
const avatarPath = await this.downloadImage(this.userInfo.headImage) | |||
const avatarSize = 60 | |||
const avatarX = (this.canvasW - avatarSize) / 2 | |||
const avatarY = 50 | |||
// 绘制圆形头像背景 | |||
ctx.setFillStyle('#f0f0f0') | |||
ctx.beginPath() | |||
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI) | |||
ctx.fill() | |||
// 绘制圆形头像 | |||
ctx.save() | |||
ctx.beginPath() | |||
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI) | |||
ctx.clip() | |||
ctx.drawImage(avatarPath, avatarX, avatarY, avatarSize, avatarSize) | |||
ctx.restore() | |||
} catch (e) { | |||
console.log('头像绘制失败', e) | |||
// 绘制默认头像圆圈 | |||
const avatarSize = 60 | |||
const avatarX = (this.canvasW - avatarSize) / 2 | |||
const avatarY = 50 | |||
ctx.setFillStyle('#e0e0e0') | |||
ctx.beginPath() | |||
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI) | |||
ctx.fill() | |||
} | |||
} else { | |||
// 没有头像时绘制默认头像圆圈 | |||
const avatarSize = 60 | |||
const avatarX = (this.canvasW - avatarSize) / 2 | |||
const avatarY = 50 | |||
ctx.setFillStyle('#e0e0e0') | |||
ctx.beginPath() | |||
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI) | |||
ctx.fill() | |||
} | |||
// 绘制用户昵称 | |||
if (this.userInfo.nickName) { | |||
ctx.setFillStyle('#333333') | |||
ctx.setFontSize(16) | |||
ctx.setTextAlign('center') | |||
ctx.fillText(this.userInfo.nickName || '用户', this.canvasW / 2, 140) | |||
} | |||
// 获取二维码图片并绘制 | |||
try { | |||
const qrCodeImagePath = await this.getQRCodeImage() | |||
if (qrCodeImagePath) { | |||
const qrSize = 120 | |||
const qrX = (this.canvasW - qrSize) / 2 | |||
const qrY = 160 | |||
ctx.drawImage(qrCodeImagePath, qrX, qrY, qrSize, qrSize) | |||
} | |||
} catch (e) { | |||
console.log('二维码绘制失败', e) | |||
} | |||
// 绘制底部文案 | |||
ctx.setFillStyle('#666666') | |||
ctx.setFontSize(14) | |||
ctx.setTextAlign('center') | |||
ctx.fillText('扫码关注,享受专业服务', this.canvasW / 2, 310) | |||
// 执行绘制 | |||
ctx.draw(false, () => { | |||
// 导出图片 | |||
setTimeout(() => { | |||
this.canvasToTempFilePath() | |||
}, 1000) | |||
}) | |||
}, | |||
// 下载网络图片到本地 | |||
downloadImage(url) { | |||
return new Promise((resolve, reject) => { | |||
// H5环境下直接使用网络图片URL,避免跨域问题 | |||
// #ifdef H5 | |||
resolve(url) | |||
// #endif | |||
// #ifndef H5 | |||
uni.downloadFile({ | |||
url: url, | |||
success: (res) => { | |||
if (res.statusCode === 200) { | |||
resolve(res.tempFilePath) | |||
} else { | |||
reject('下载失败') | |||
} | |||
}, | |||
fail: reject | |||
}) | |||
// #endif | |||
}) | |||
}, | |||
// 获取二维码图片 | |||
getQRCodeImage() { | |||
return new Promise((resolve, reject) => { | |||
if (this.$refs.qrCodeComponent) { | |||
this.$refs.qrCodeComponent.toTempFilePath({ | |||
success: (res) => { | |||
resolve(res.tempFilePath) | |||
}, | |||
fail: (err) => { | |||
reject(err) | |||
} | |||
}) | |||
} else { | |||
reject('二维码组件不存在') | |||
} | |||
}) | |||
}, | |||
// 画布导出为图片 | |||
canvasToTempFilePath() { | |||
// #ifdef H5 | |||
// H5环境下直接转换canvas | |||
setTimeout(() => { | |||
this.convertCanvasToBase64() | |||
}, 500) | |||
// #endif | |||
// #ifndef H5 | |||
uni.canvasToTempFilePath({ | |||
canvasId: 'myCanvas', | |||
success: (res) => { | |||
this.tempFilePath = res.tempFilePath | |||
console.log('海报生成成功', res.tempFilePath) | |||
}, | |||
fail: (err) => { | |||
console.log('海报生成失败', err) | |||
uni.showToast({ | |||
title: '海报生成失败', | |||
icon: 'none' | |||
}) | |||
} | |||
}, this) | |||
// #endif | |||
}, | |||
// 将canvas转换为base64图片 | |||
convertCanvasToBase64() { | |||
// #ifdef H5 | |||
try { | |||
// 获取canvas元素 | |||
const canvasElement = document.querySelector('#myCanvas') | |||
if (!canvasElement) { | |||
console.error('找不到canvas元素') | |||
this.retryConvert() | |||
return | |||
} | |||
// 检查是否是canvas元素 | |||
if (canvasElement.tagName.toLowerCase() !== 'canvas') { | |||
console.error('元素不是canvas') | |||
this.retryConvert() | |||
return | |||
} | |||
// 等待canvas绘制完成 | |||
setTimeout(() => { | |||
try { | |||
// 转换为base64 | |||
const dataURL = canvasElement.toDataURL('image/png', 1.0) | |||
if (dataURL && dataURL.startsWith('data:image')) { | |||
this.tempFilePath = dataURL | |||
console.log('海报生成成功') | |||
this.retryCount = 0 // 重置重试次数 | |||
} else { | |||
throw new Error('生成的图片数据无效') | |||
} | |||
} catch (e) { | |||
console.error('转换图片失败', e) | |||
this.retryConvert() | |||
} | |||
}, 200) | |||
} catch (e) { | |||
console.error('转换图片失败', e) | |||
this.retryConvert() | |||
} | |||
// #endif | |||
}, | |||
// 重试转换 | |||
retryConvert() { | |||
if (this.retryCount < this.maxRetry) { | |||
this.retryCount++ | |||
console.log(`重试转换图片,第${this.retryCount}次`) | |||
setTimeout(() => { | |||
this.convertCanvasToBase64() | |||
}, 500) | |||
} else { | |||
console.log('重试次数已达上限,使用备用方法') | |||
this.fallbackCanvasConvert() | |||
} | |||
}, | |||
// 备用的canvas转换方法 | |||
fallbackCanvasConvert() { | |||
uni.canvasToTempFilePath({ | |||
canvasId: 'myCanvas', | |||
success: (res) => { | |||
this.tempFilePath = res.tempFilePath | |||
console.log('海报生成成功(备用方法)', res.tempFilePath) | |||
}, | |||
fail: (err) => { | |||
console.log('海报生成失败', err) | |||
uni.showToast({ | |||
title: '海报生成失败', | |||
icon: 'none' | |||
}) | |||
} | |||
}, this) | |||
}, | |||
// 保存图片 - H5环境提示用户长按 | |||
handleSaveImage() { | |||
if (!this.tempFilePath) { | |||
uni.showToast({ | |||
title: '图片还未生成完成', | |||
icon: 'none' | |||
}) | |||
return | |||
} | |||
// H5环境下提示用户长按保存 | |||
uni.showModal({ | |||
title: '保存图片', | |||
content: '请长按上方图片,选择"保存图片"即可保存到手机', | |||
showCancel: false, | |||
confirmText: '知道了' | |||
}) | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.placard { | |||
display: flex; | |||
align-items: start; | |||
justify-content: center; | |||
min-height: 100vh; | |||
background-color: #f5f5f5; | |||
padding: 48rpx 26rpx; | |||
.placard-content { | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
.img-box { | |||
background-color: #fff; | |||
border-radius: 10px; | |||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); | |||
overflow: hidden; | |||
margin-bottom: 20px; | |||
img { | |||
user-select: none; | |||
-webkit-user-select: none; | |||
-webkit-touch-callout: default; | |||
} | |||
} | |||
.loading-box { | |||
background-color: #fff; | |||
border-radius: 10px; | |||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); | |||
margin-bottom: 20px; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
.loading-content { | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
.loading-spinner { | |||
width: 30px; | |||
height: 30px; | |||
border: 3px solid #f3f3f3; | |||
border-top: 3px solid #84A73F; | |||
border-radius: 50%; | |||
animation: spin 1s linear infinite; | |||
margin-bottom: 10px; | |||
} | |||
.loading-text { | |||
color: #666; | |||
font-size: 14px; | |||
} | |||
} | |||
} | |||
.add-btn { | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
width: 100%; | |||
.btn { | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
width: 80%; | |||
height: 40px; | |||
border-radius: 20px; | |||
color: white; | |||
font-size: 16px; | |||
background: linear-gradient(to right, #84A73F, #D8FF8F); | |||
margin-top: 20px; | |||
box-shadow: 0 4px 12px rgba(132, 167, 63, 0.3); | |||
cursor: pointer; | |||
&:active { | |||
transform: scale(0.98); | |||
transition: transform 0.1s; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
.hidden-canvas { | |||
opacity: 0; | |||
position: fixed; | |||
top: -9999px; | |||
left: -9999px; | |||
pointer-events: none; | |||
} | |||
.qrcode { | |||
position: fixed; | |||
top: -9999px; | |||
left: -9999px; | |||
opacity: 0; | |||
pointer-events: none; | |||
} | |||
@keyframes spin { | |||
0% { transform: rotate(0deg); } | |||
100% { transform: rotate(360deg); } | |||
} | |||
</style> |
@ -1,302 +1,343 @@ | |||
<template> | |||
<view class="page"> | |||
<!-- 导航栏 --> | |||
<navbar title="商品详情" leftClick @leftClick="$utils.navigateBack" color="#fff" /> | |||
<uv-swiper :list="bannerList" indicator indicatorMode="dot" height="475rpx"></uv-swiper> | |||
<view class="overview"> | |||
<view class="flex sale"> | |||
<view class="flex price" :class="[role]"> | |||
<view> | |||
<text class="price-unit">¥</text> | |||
<text>{{ productDetail.price }}</text> | |||
</view> | |||
<view class="flex tag" v-if="role"> | |||
<image class="icon" :src="vipInfo.massageVipCombo.imagePrice"></image> | |||
<text>{{ vipInfo.massageVipCombo.title }}</text> | |||
</view> | |||
</view> | |||
<view> | |||
<text>{{ `已售出:${productDetail.sales}单` }}</text> | |||
</view> | |||
</view> | |||
<view class="title"> | |||
<text>{{ productDetail.title }}</text> | |||
</view> | |||
<view class="flex desc"> | |||
<view v-for="tag in tags" :key="tag" class="flex tag"> | |||
<view class="dot"></view> | |||
<text>{{ tag }}</text> | |||
</view> | |||
</view> | |||
</view> | |||
<!-- 商品详情 --> | |||
<view class="detail"> | |||
<view class="header"> | |||
<cardTitle>商品详情</cardTitle> | |||
</view> | |||
<uv-parse :content="productDetail.details"></uv-parse> | |||
</view> | |||
<!-- 分享和购买按钮 --> | |||
<view class="flex bar"> | |||
<button plain class="flex flex-column btn btn-share" open-type="share"> | |||
<image class="btn-share-icon" src="../static/productDetail/icon-share.png"></image> | |||
<text>分享</text> | |||
</button> | |||
<view class="flex count"> | |||
<text>合计:</text> | |||
<view class="price"> | |||
<text class="price-unit">¥</text> | |||
<!-- todo: check --> | |||
<text>{{ productDetail.price }}</text> | |||
</view> | |||
</view> | |||
<button plain class="btn btn-pay" @click="submit">立即支付</button> | |||
</view> | |||
</view> | |||
<view class="page"> | |||
<!-- 导航栏 --> | |||
<navbar title="商品详情" leftClick @leftClick="$utils.navigateBack" color="#fff" /> | |||
<uv-swiper :list="bannerList" indicator indicatorMode="dot" height="475rpx"></uv-swiper> | |||
<view class="overview"> | |||
<view class="flex sale"> | |||
<view class="flex price" :class="[role]"> | |||
<view> | |||
<text class="price-unit">¥</text> | |||
<text>{{ productDetail.price }}</text> | |||
</view> | |||
<view class="flex tag" v-if="role"> | |||
<image class="icon" :src="vipInfo.massageVipCombo.imagePrice"></image> | |||
<text>{{ vipInfo.massageVipCombo.title }}</text> | |||
</view> | |||
</view> | |||
<view> | |||
<text>{{ `已售出:${productDetail.sales}单` }}</text> | |||
</view> | |||
</view> | |||
<view class="title"> | |||
<text>{{ productDetail.title }}</text> | |||
</view> | |||
<view class="flex desc"> | |||
<view v-for="tag in tags" :key="tag" class="flex tag"> | |||
<view class="dot"></view> | |||
<text>{{ tag }}</text> | |||
</view> | |||
</view> | |||
</view> | |||
<!-- 商品详情 --> | |||
<view class="detail"> | |||
<view class="header"> | |||
<cardTitle>商品详情</cardTitle> | |||
</view> | |||
<uv-parse :content="productDetail.details"></uv-parse> | |||
</view> | |||
<!-- 分享和购买按钮 --> | |||
<view class="flex bar"> | |||
<!-- #ifdef H5 --> | |||
<button plain class="flex flex-column btn btn-share" @click="handleShare"> | |||
<image class="btn-share-icon" src="../static/productDetail/icon-share.png"></image> | |||
<text>分享</text> | |||
</button> | |||
<!-- #endif --> | |||
<!-- #ifdef MP-WEIXIN --> | |||
<button plain class="flex flex-column btn btn-share" open-type="share"> | |||
<image class="btn-share-icon" src="../static/productDetail/icon-share.png"></image> | |||
<text>分享</text> | |||
</button> | |||
<!-- #endif --> | |||
<view class="flex count"> | |||
<text>合计:</text> | |||
<view class="price"> | |||
<text class="price-unit">¥</text> | |||
<!-- todo: check --> | |||
<text>{{ productDetail.price }}</text> | |||
</view> | |||
</view> | |||
<button plain class="btn btn-pay" @click="submit">立即支付</button> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
import { mapGetters, mapState } from 'vuex' | |||
import cardTitle from '@/components/base/cardTitle.vue' | |||
export default { | |||
components: { | |||
cardTitle, | |||
}, | |||
data() { | |||
return { | |||
productDetail: { | |||
image: '', | |||
details: '', | |||
}, | |||
id: 0, | |||
} | |||
}, | |||
computed: { | |||
...mapGetters(['role']), | |||
...mapState(['vipInfo']), | |||
// todo: check | |||
bannerList() { | |||
const { image } = this.productDetail | |||
if (!image) { | |||
return [] | |||
} | |||
return Array.isArray(image) ? image : image.split(',') | |||
}, | |||
tags() { | |||
const { tag } = this.productDetail | |||
return tag?.split('、') || [] | |||
} | |||
}, | |||
onLoad(args) { | |||
this.id = args.id | |||
this.fetchProductDetail(this.id) | |||
}, | |||
onShow() { | |||
}, | |||
import { mapGetters, mapState } from 'vuex' | |||
import cardTitle from '@/components/base/cardTitle.vue' | |||
export default { | |||
components: { | |||
cardTitle, | |||
}, | |||
data() { | |||
return { | |||
productDetail: { | |||
image: '', | |||
details: '', | |||
}, | |||
id: 0, | |||
} | |||
}, | |||
computed: { | |||
...mapGetters(['role']), | |||
...mapState(['vipInfo']), | |||
// todo: check | |||
bannerList() { | |||
const { image } = this.productDetail | |||
if (!image) { | |||
return [] | |||
} | |||
return Array.isArray(image) ? image : image.split(',') | |||
}, | |||
tags() { | |||
const { tag } = this.productDetail | |||
return tag?.split('、') || [] | |||
} | |||
}, | |||
onLoad(args) { | |||
this.id = args.id | |||
this.fetchProductDetail(this.id) | |||
// 处理分享ID参数 | |||
if (args.shareId) { | |||
uni.setStorageSync('shareId', args.shareId) | |||
} | |||
}, | |||
onShow() { | |||
// 页面显示时重新设置分享内容 | |||
this.updateShareContent() | |||
}, | |||
onShareAppMessage(res) { | |||
const { | |||
title, | |||
} = this.productDetail | |||
let o = { | |||
title: title, | |||
// todo: check | |||
imageUrl: bannerList[0], | |||
query: `id=${this.productDetail.id}`, | |||
} | |||
return o | |||
// 小程序分享:返回商品特定的分享内容 | |||
return { | |||
title: this.productDetail.title || '愈然工坊', | |||
imageUrl: this.bannerList[0] || '', | |||
path: `/pages_order/product/productDetail?id=${this.productDetail.id}${this.userInfo.id ? '&shareId=' + this.userInfo.id : ''}`, | |||
} | |||
}, | |||
methods: { | |||
// 获取商品 | |||
async fetchProductDetail(id) { | |||
try { | |||
this.productDetail = await this.$fetch('queryProductDetail', { id }) | |||
} catch (err) { | |||
} | |||
}, | |||
// 立即下单 | |||
submit() { | |||
this.$store.commit('setPayOrderProduct', [ | |||
this.productDetail | |||
]) | |||
this.$utils.navigateTo('/pages_order/order/createOrder') | |||
}, | |||
} | |||
} | |||
methods: { | |||
// #ifdef H5 | |||
// H5分享处理 | |||
handleShare() { | |||
uni.showToast({ | |||
title: '请点击右上角分享', | |||
icon: 'none' | |||
}) | |||
}, | |||
// #endif | |||
// 更新分享内容 | |||
updateShareContent() { | |||
if (this.productDetail.id) { | |||
// 设置商品特定的分享内容到全局分享配置 | |||
this.Gshare.title = this.productDetail.title || '愈然工坊' | |||
this.Gshare.desc = this.productDetail.title || '愈然工坊,温柔呵护每一刻!' | |||
this.Gshare.path = `/pages_order/product/productDetail?id=${this.productDetail.id}` | |||
this.Gshare.imageUrl = this.bannerList[0] || '' | |||
// #ifdef H5 | |||
// H5环境下重新设置微信分享 | |||
this.setupWeixinShare() | |||
// #endif | |||
} | |||
}, | |||
// 获取商品 | |||
async fetchProductDetail(id) { | |||
try { | |||
this.productDetail = await this.$fetch('queryProductDetail', { id }) | |||
// 商品数据加载完成后更新分享内容 | |||
this.updateShareContent() | |||
} catch (err) { | |||
} | |||
}, | |||
// 立即下单 | |||
submit() { | |||
this.$store.commit('setPayOrderProduct', [ | |||
this.productDetail | |||
]) | |||
this.$utils.navigateTo('/pages_order/order/createOrder') | |||
}, | |||
} | |||
} | |||
</script> | |||
<style scoped lang="scss"> | |||
$bar-height: 132rpx; | |||
.page { | |||
padding-bottom: calc(#{$bar-height} + env(safe-area-inset-bottom)); | |||
background-color: #F3F3F3; | |||
/deep/ .nav-bar__view { | |||
background-image: linear-gradient(#84A73F, #D8FF8F); | |||
} | |||
.overview { | |||
padding: 16rpx 31rpx 18rpx 24rpx; | |||
background-color: $uni-fg-color; | |||
.sale { | |||
justify-content: space-between; | |||
color: #949494; | |||
font-size: 20rpx; | |||
.price { | |||
color: #FF2A2A; | |||
font-size: 45rpx; | |||
&-unit { | |||
font-size: 30rpx; | |||
margin-right: 3rpx; | |||
} | |||
.tag { | |||
font-size: 18rpx; | |||
font-weight: 700; | |||
padding: 9rpx 12rpx 9rpx 19rpx; | |||
margin-left: 15rpx; | |||
.icon { | |||
width: 27rpx; | |||
height: 19rpx; | |||
margin-right: 3rpx; | |||
} | |||
} | |||
&.member-personal { | |||
color: $uni-color-light; | |||
.tag { | |||
background-color: rgba($color: #D8FF8F, $alpha: 0.72); | |||
} | |||
} | |||
&.member-business { | |||
color: #FFB465; | |||
.tag { | |||
background-color: rgba($color: #FFFBC4, $alpha: 0.72); | |||
} | |||
} | |||
} | |||
} | |||
.title { | |||
color: #3A3A3A; | |||
font-size: 28rpx; | |||
font-weight: 700; | |||
margin-top: 15rpx; | |||
} | |||
.desc { | |||
justify-content: space-between; | |||
margin-top: 20rpx; | |||
.tag { | |||
color: #949494; | |||
font-size: 22rpx; | |||
.dot { | |||
width: 8rpx; | |||
height: 8rpx; | |||
border-radius: 50%; | |||
background-color: #949494; | |||
margin-right: 16rpx; | |||
} | |||
} | |||
} | |||
} | |||
.detail { | |||
margin-top: 12rpx; | |||
padding: 18rpx 28rpx; | |||
background-color: $uni-fg-color; | |||
.header { | |||
margin-bottom: 30rpx; | |||
} | |||
} | |||
.bar { | |||
position: fixed; | |||
bottom: 0; | |||
left: 0; | |||
width: 100vw; | |||
height: $bar-height; | |||
padding-bottom: env(safe-area-inset-bottom); | |||
background-color: $uni-fg-color; | |||
.count { | |||
flex: 1; | |||
color: #000000; | |||
font-size: 28rpx; | |||
margin-left: 29rpx; | |||
padding-left: 40rpx; | |||
border-left: 1rpx solid #B3997E; | |||
justify-content: flex-start; | |||
.price { | |||
color: #FF2A2A; | |||
font-size: 30rpx; | |||
&-unit { | |||
font-size: 18rpx; | |||
} | |||
} | |||
} | |||
.btn { | |||
border: none; | |||
line-height: 1; | |||
background-color: transparent; | |||
padding: 0; | |||
width: auto; | |||
height: auto; | |||
margin: 0; | |||
&-share { | |||
margin-left: 58rpx; | |||
color: #000000; | |||
font-size: 22rpx; | |||
&-icon { | |||
width: 48rpx; | |||
height: 50rpx; | |||
margin-bottom: 11rpx; | |||
} | |||
} | |||
&-pay { | |||
margin-right: 27rpx; | |||
padding: 24rpx 137rpx; | |||
color: $uni-text-color-inverse; | |||
font-size: 28rpx; | |||
border-radius: 44rpx; | |||
background-image: linear-gradient(to right, #84A73F, #D8FF8F); | |||
} | |||
} | |||
} | |||
} | |||
$bar-height: 132rpx; | |||
.page { | |||
padding-bottom: calc(#{$bar-height} + env(safe-area-inset-bottom)); | |||
background-color: #F3F3F3; | |||
/deep/ .nav-bar__view { | |||
background-image: linear-gradient(#84A73F, #D8FF8F); | |||
} | |||
.overview { | |||
padding: 16rpx 31rpx 18rpx 24rpx; | |||
background-color: $uni-fg-color; | |||
.sale { | |||
justify-content: space-between; | |||
color: #949494; | |||
font-size: 20rpx; | |||
.price { | |||
color: #FF2A2A; | |||
font-size: 45rpx; | |||
&-unit { | |||
font-size: 30rpx; | |||
margin-right: 3rpx; | |||
} | |||
.tag { | |||
font-size: 18rpx; | |||
font-weight: 700; | |||
padding: 9rpx 12rpx 9rpx 19rpx; | |||
margin-left: 15rpx; | |||
.icon { | |||
width: 27rpx; | |||
height: 19rpx; | |||
margin-right: 3rpx; | |||
} | |||
} | |||
&.member-personal { | |||
color: $uni-color-light; | |||
.tag { | |||
background-color: rgba($color: #D8FF8F, $alpha: 0.72); | |||
} | |||
} | |||
&.member-business { | |||
color: #FFB465; | |||
.tag { | |||
background-color: rgba($color: #FFFBC4, $alpha: 0.72); | |||
} | |||
} | |||
} | |||
} | |||
.title { | |||
color: #3A3A3A; | |||
font-size: 28rpx; | |||
font-weight: 700; | |||
margin-top: 15rpx; | |||
} | |||
.desc { | |||
justify-content: space-between; | |||
margin-top: 20rpx; | |||
.tag { | |||
color: #949494; | |||
font-size: 22rpx; | |||
.dot { | |||
width: 8rpx; | |||
height: 8rpx; | |||
border-radius: 50%; | |||
background-color: #949494; | |||
margin-right: 16rpx; | |||
} | |||
} | |||
} | |||
} | |||
.detail { | |||
margin-top: 12rpx; | |||
padding: 18rpx 28rpx; | |||
background-color: $uni-fg-color; | |||
.header { | |||
margin-bottom: 30rpx; | |||
} | |||
} | |||
.bar { | |||
position: fixed; | |||
bottom: 0; | |||
left: 0; | |||
width: 100vw; | |||
height: $bar-height; | |||
padding-bottom: env(safe-area-inset-bottom); | |||
background-color: $uni-fg-color; | |||
.count { | |||
flex: 1; | |||
color: #000000; | |||
font-size: 28rpx; | |||
margin-left: 29rpx; | |||
padding-left: 40rpx; | |||
border-left: 1rpx solid #B3997E; | |||
justify-content: flex-start; | |||
.price { | |||
color: #FF2A2A; | |||
font-size: 30rpx; | |||
&-unit { | |||
font-size: 18rpx; | |||
} | |||
} | |||
} | |||
.btn { | |||
border: none; | |||
line-height: 1; | |||
background-color: transparent; | |||
padding: 0; | |||
width: auto; | |||
height: auto; | |||
margin: 0; | |||
&-share { | |||
margin-left: 58rpx; | |||
color: #000000; | |||
font-size: 22rpx; | |||
&-icon { | |||
width: 48rpx; | |||
height: 50rpx; | |||
margin-bottom: 11rpx; | |||
} | |||
} | |||
&-pay { | |||
margin-right: 27rpx; | |||
padding: 24rpx 137rpx; | |||
color: $uni-text-color-inverse; | |||
font-size: 28rpx; | |||
border-radius: 44rpx; | |||
background-image: linear-gradient(to right, #84A73F, #D8FF8F); | |||
} | |||
} | |||
} | |||
} | |||
</style> |
@ -0,0 +1,107 @@ | |||
// H5 环境修复 | |||
export function fixH5Environment() { | |||
// #ifdef H5 | |||
try { | |||
// 修复路由 meta 属性访问问题 | |||
const originalGetCurrentPages = getCurrentPages; | |||
window.getCurrentPages = function() { | |||
try { | |||
const pages = originalGetCurrentPages(); | |||
// 确保页面对象有基本的路由信息 | |||
if (pages && pages.length > 0) { | |||
pages.forEach(page => { | |||
if (page && !page.route) { | |||
page.route = page.$page?.fullPath || '/'; | |||
} | |||
if (page && page.route && !page.$page?.meta) { | |||
page.$page = page.$page || {}; | |||
page.$page.meta = page.$page.meta || {}; | |||
} | |||
}); | |||
} | |||
return pages; | |||
} catch(e) { | |||
console.warn('getCurrentPages 修复失败:', e); | |||
return []; | |||
} | |||
}; | |||
// 修复 History API | |||
const originalReplaceState = window.history.replaceState; | |||
window.history.replaceState = function(state, title, url) { | |||
try { | |||
// 确保 URL 格式正确 | |||
if (url && typeof url === 'string') { | |||
// 修复格式不正确的 URL | |||
if (url.startsWith('https:/#/')) { | |||
url = url.replace('https:/#/', window.location.origin + '/#/'); | |||
} | |||
// 如果 URL 不包含完整的域名,添加基础路径 | |||
if (url.startsWith('#/')) { | |||
url = window.location.origin + window.location.pathname + url; | |||
} | |||
} | |||
return originalReplaceState.call(this, state, title, url); | |||
} catch(e) { | |||
console.warn('History.replaceState 修复失败:', e); | |||
return null; | |||
} | |||
}; | |||
// 修复路由跳转相关问题 | |||
const originalNavigateTo = uni.navigateTo; | |||
uni.navigateTo = function(options) { | |||
try { | |||
return originalNavigateTo(options); | |||
} catch(e) { | |||
console.warn('uni.navigateTo 失败:', e); | |||
// 降级处理 | |||
window.location.href = window.location.origin + window.location.pathname + '#' + options.url; | |||
} | |||
}; | |||
const originalReLaunch = uni.reLaunch; | |||
uni.reLaunch = function(options) { | |||
try { | |||
return originalReLaunch(options); | |||
} catch(e) { | |||
console.warn('uni.reLaunch 失败:', e); | |||
// 降级处理 | |||
window.location.href = window.location.origin + window.location.pathname + '#' + options.url; | |||
} | |||
}; | |||
console.log('H5 环境修复完成'); | |||
} catch(e) { | |||
console.warn('H5 环境修复失败:', e); | |||
} | |||
// #endif | |||
} | |||
// 修复 URL 参数解析 | |||
export function fixUrlParams() { | |||
// #ifdef H5 | |||
try { | |||
function GetQueryString(name) { | |||
const url = window.location.href; | |||
try { | |||
const cs = url.split('?')[1]; | |||
if (cs) { | |||
const cs_arr = cs.split('&'); | |||
for (let i = 0; i < cs_arr.length; i++) { | |||
if (cs_arr[i].split('=')[0] === name) { | |||
sessionStorage.setItem('vid', cs_arr[i].split('=')[1]); | |||
} | |||
} | |||
} | |||
} catch(e) { | |||
console.warn('URL参数解析失败:', e); | |||
} | |||
} | |||
GetQueryString('vid'); | |||
} catch(e) { | |||
console.warn('URL参数修复失败:', e); | |||
} | |||
// #endif | |||
} |
@ -0,0 +1,146 @@ | |||
// H5 路由和环境修复 | |||
// #ifdef H5 | |||
// 存储原始方法 | |||
let originalGetCurrentPages; | |||
let originalHistoryReplaceState; | |||
let isFixed = false; | |||
// 修复H5环境路由问题的主函数 | |||
export function fixH5Router() { | |||
if (isFixed) return; | |||
try { | |||
// 1. 修复 getCurrentPages 方法 | |||
if (typeof getCurrentPages === 'function') { | |||
originalGetCurrentPages = getCurrentPages; | |||
window.getCurrentPages = function() { | |||
try { | |||
const pages = originalGetCurrentPages(); | |||
if (pages && Array.isArray(pages)) { | |||
// 确保每个页面对象有必要的属性 | |||
pages.forEach((page, index) => { | |||
if (!page.route) { | |||
page.route = page.$page?.fullPath || `page_${index}`; | |||
} | |||
if (!page.options) { | |||
page.options = {}; | |||
} | |||
// 确保页面有 meta 属性 | |||
if (page.$page && !page.$page.meta) { | |||
page.$page.meta = {}; | |||
} | |||
}); | |||
} | |||
return pages || []; | |||
} catch(e) { | |||
console.warn('getCurrentPages 执行失败:', e); | |||
return [{ | |||
route: '/', | |||
options: {}, | |||
$page: { meta: {} } | |||
}]; | |||
} | |||
}; | |||
} | |||
// 2. 修复 History API | |||
if (window.history && window.history.replaceState) { | |||
originalHistoryReplaceState = window.history.replaceState; | |||
window.history.replaceState = function(state, title, url) { | |||
try { | |||
// 修复 URL 格式问题 | |||
if (url && typeof url === 'string') { | |||
// 处理错误的 URL 格式 | |||
if (url.includes('https:/#/')) { | |||
url = url.replace(/https:\/+#\//, window.location.origin + '/#/'); | |||
} | |||
// 确保相对路径的正确性 | |||
if (url.startsWith('#/')) { | |||
url = window.location.origin + window.location.pathname + url; | |||
} | |||
// 处理双斜杠问题 | |||
url = url.replace(/([^:]\/)\/+/g, '$1'); | |||
} | |||
return originalHistoryReplaceState.call(this, state, title, url); | |||
} catch(e) { | |||
console.warn('History.replaceState 修复执行失败:', e); | |||
// 忽略错误,不执行原方法 | |||
return null; | |||
} | |||
}; | |||
} | |||
// 3. 添加全局错误处理 | |||
const originalOnError = window.onerror; | |||
window.onerror = function(message, source, lineno, colno, error) { | |||
// 过滤已知的H5兼容性错误 | |||
if (typeof message === 'string') { | |||
if (message.includes("Cannot read property 'meta'") || | |||
message.includes('replaceState') || | |||
message.includes('showTabBar') || | |||
message.includes('History')) { | |||
console.warn('H5兼容性错误已忽略:', message); | |||
return true; // 阻止默认错误处理 | |||
} | |||
} | |||
// 其他错误交给原处理器 | |||
if (originalOnError) { | |||
return originalOnError(message, source, lineno, colno, error); | |||
} | |||
return false; | |||
}; | |||
// 4. 修复 URL 哈希处理 | |||
if (window.location.hash === '' || window.location.hash === '#') { | |||
window.location.hash = '#/'; | |||
} | |||
// 5. 监听 hashchange 事件,确保路由正常 | |||
window.addEventListener('hashchange', function(e) { | |||
try { | |||
const hash = window.location.hash; | |||
if (!hash || hash === '#') { | |||
window.location.hash = '#/'; | |||
} | |||
} catch(e) { | |||
console.warn('hashchange 处理失败:', e); | |||
} | |||
}); | |||
isFixed = true; | |||
console.log('H5 路由修复完成'); | |||
} catch(e) { | |||
console.warn('H5 路由修复失败:', e); | |||
} | |||
} | |||
// 恢复原始方法(用于调试) | |||
export function restoreH5Router() { | |||
if (!isFixed) return; | |||
try { | |||
if (originalGetCurrentPages) { | |||
window.getCurrentPages = originalGetCurrentPages; | |||
} | |||
if (originalHistoryReplaceState) { | |||
window.history.replaceState = originalHistoryReplaceState; | |||
} | |||
isFixed = false; | |||
console.log('H5 路由修复已恢复'); | |||
} catch(e) { | |||
console.warn('H5 路由恢复失败:', e); | |||
} | |||
} | |||
// 页面加载完成后自动执行修复 | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', fixH5Router); | |||
} else { | |||
fixH5Router(); | |||
} | |||
// #endif |