Browse Source

feat: 添加H5环境支持及功能优化

- 新增H5环境专属页面promotionH5.vue用于邀请好友功能
- 优化支付逻辑,区分微信小程序和H5环境的支付方式
- 重构wxPay方法返回Promise以支持async/await
- 修复表单组件样式问题及地址选择逻辑
- 添加H5环境URL参数解析功能
- 优化canvas绘制逻辑和图片保存体验
master
前端-胡立永 16 hours ago
parent
commit
3acefe0832
12 changed files with 1092 additions and 498 deletions
  1. +5
    -0
      components/couponList/couponList.vue
  2. +19
    -0
      main.js
  3. +3
    -0
      pages.json
  4. +8
    -0
      pages/index/center.vue
  5. +12
    -23
      pages_order/components/formTextarea.vue
  6. +379
    -385
      pages_order/mine/cooperation.vue
  7. +1
    -1
      pages_order/mine/partner.vue
  8. +56
    -52
      pages_order/mine/promotion.vue
  9. +563
    -0
      pages_order/mine/promotionH5.vue
  10. +9
    -1
      pages_order/order/createOrder.vue
  11. +3
    -2
      store/store.js
  12. +34
    -34
      utils/pay.js

+ 5
- 0
components/couponList/couponList.vue View File

@ -53,6 +53,11 @@
this.getCouponList()
}
},
// #ifndef H5
mounted() {
this.getCouponList()
},
// #endif
methods: {
select(item) {
this.$emit('select', item)


+ 19
- 0
main.js View File

@ -25,6 +25,25 @@ import navbar from '@/components/base/navbar.vue'
Vue.component('configPopup',configPopup)
Vue.component('navbar',navbar)
// #ifdef H5
//获取url中参数的方法
function GetQueryString(name) {
var url = window.location.href;
try {
var cs = url.split('?')[1]; //获取?之后的参数字符串
var cs_arr = cs.split('&'); //参数字符串分割为数组
for (var i = 0; i < cs_arr.length; i++) { //遍历数组,拿到json对象
if (cs_arr[i].split('=')[0] == name) {
sessionStorage.setItem('vid',cs_arr[i].split('=')[1]);
}
}
}catch(e){}
}
GetQueryString('vid');
// #endif
const app = new Vue({
...App,
store,


+ 3
- 0
pages.json View File

@ -76,6 +76,9 @@
{
"path": "mine/promotion"
},
{
"path": "mine/promotionH5"
},
{
"path": "mine/coupon"
},


+ 8
- 0
pages/index/center.vue View File

@ -97,7 +97,15 @@
<image class="fun-common-icon" src="@/static/image/center/icon-team.png" mode="widthFix"></image>
<text class="fun-common-label">我的团队</text>
</view>
<!-- #ifdef MP-WEIXIN -->
<view class="flex flex-column" @click="$utils.navigateTo('/pages_order/mine/promotion')">
<!-- #endif -->
<!-- #ifdef H5 -->
<view class="flex flex-column" @click="$utils.navigateTo('/pages_order/mine/promotionH5')">
<!-- #endif -->
<image class="fun-common-icon" src="@/static/image/center/icon-invite.png" mode="widthFix"></image>
<text class="fun-common-label">邀请好友</text>
</view>


+ 12
- 23
pages_order/components/formTextarea.vue View File

@ -1,40 +1,29 @@
<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>
}" placeholderStyle="color: '#999999';
fontSize: '28rpx';"></uv-textarea>
</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">

+ 379
- 385
pages_order/mine/cooperation.vue View File

@ -1,405 +1,399 @@
<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">
<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="{
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>

+ 1
- 1
pages_order/mine/partner.vue View File

@ -25,7 +25,7 @@
<view class="list">
<view class="list-header">
<view class="list-header-title">
<text>直推用户<text class="sub">{{ `${total}人)` }}</text></text>
<text>直推用户<text class="sub">{{ `${total || 0}人)` }}</text></text>
<view class="list-header-title-line"></view>
</view>
</view>


+ 56
- 52
pages_order/mine/promotion.vue View File

@ -1,14 +1,14 @@
<template>
<view class="page">
<navbar title="邀请好友" leftClick @leftClick="$utils.navigateBack" color="#fff" />
<view class="flex flex-column content">
<view style="width: 698rpx; height: 788rpx; border-radius: 16rpx; overflow: hidden;">
<canvas id="myCanvas" canvas-id="firstCanvas1" type="2d" style="width: 100%; height: 100%;"></canvas>
</view>
<view style="width: 698rpx; height: 788rpx; border-radius: 16rpx; overflow: hidden;">
<canvas id="myCanvas" canvas-id="firstCanvas1" type="2d" style="width: 100%; height: 100%;"></canvas>
</view>
<view class="tools">
<button plain class="flex btn" @click="saveImg">
@ -17,13 +17,15 @@
</view>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'Promotion',
computed: {
@ -36,19 +38,22 @@
canvas: {},
}
},
onReady() {
this.fetchQrCode()
},
onReady() {
this.fetchQrCode()
},
methods: {
async fetchQrCode() {
try {
this.wxCodeImage = (await this.$fetch('getInviteCode'))?.url
this.draw()
} catch (err) {
}
},
async fetchQrCode() {
try {
// #ifdef MP-WEIXIN
this.wxCodeImage = (await this.$fetch('getInviteCode'))?.url
// #endif
this.draw()
} catch (err) {
}
},
draw() {
uni.showLoading({
@ -99,45 +104,45 @@
const codeY = 645 * Ratio / dpr
// todo: fetch code
ctx.fillText(`邀请码:${'YFY1688'}`, codeX, codeY);
//
const coderImage = canvas.createImage()
coderImage.src = this.wxCodeImage
coderImage.onload = () => {
const x = 158 * Ratio / dpr
const y = 188 * Ratio / dpr
const size = 382 * Ratio / dpr
ctx.drawImage(coderImage, x, y, size, size)
// coderImage.onload = () => {
// const x = 158 * Ratio / dpr
// const y = 188 * Ratio / dpr
// const size = 382 * Ratio / dpr
// ctx.drawImage(coderImage, x, y, size, size)
uni.hideLoading()
}
// }
})
},
saveImg(){
this.$authorize('scope.writePhotosAlbum').then((res) => {
this.imgApi()
})
},
saveImg() {
this.$authorize('scope.writePhotosAlbum').then((res) => {
this.imgApi()
})
},
imgApi() {
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: this.canvas.width,
height: this.canvas.height,
canvas: this.canvas,
success: (res) => {
let tempFilePath = res.tempFilePath;
this.saveImgToPhone(tempFilePath)
},
fail: (err) => {
console.log('--canvasToTempFilePath--fail', err)
}
}, this);
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: this.canvas.width,
height: this.canvas.height,
canvas: this.canvas,
success: (res) => {
let tempFilePath = res.tempFilePath;
this.saveImgToPhone(tempFilePath)
},
fail: (err) => {
console.log('--canvasToTempFilePath--fail', err)
}
}, this);
},
saveImgToPhone(image) {
saveImgToPhone(image) {
/* 获取图片的信息 */
uni.getImageInfo({
src: image,
@ -158,17 +163,16 @@
});
}
});
}
}
}
}
</script>
<style lang="scss" scoped>
.page {
background-color: $uni-bg-color;
min-height: 100vh;
/deep/ .nav-bar__view {
background-image: linear-gradient(#84A73F, #D8FF8F);
}
@ -179,7 +183,7 @@
padding: 48rpx 26rpx;
}
.image{
.image {
width: 100%;
height: 778rpx;
}
@ -189,7 +193,7 @@
width: 100%;
padding: 0 56rpx;
box-sizing: border-box;
.btn {
width: 100%;
padding: 29rpx 0;


+ 563
- 0
pages_order/mine/promotionH5.vue View File

@ -0,0 +1,563 @@
<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 = 299
this.canvasH = 403
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('#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
// H5canvas
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
},
// canvasbase64
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: center;
justify-content: center;
min-height: 100vh;
background-color: #f5f5f5;
padding: 20px;
.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>

+ 9
- 1
pages_order/order/createOrder.vue View File

@ -172,7 +172,15 @@
} else { //
console.log('--发起支付接口回调', res)
// #ifdef MP-WEIXIN
await uni.requestPaymentWxPay(res)
// #endif
// #ifdef H5
await this.$wxPay(res)
// #endif
}
uni.showToast({
@ -181,7 +189,7 @@
})
setTimeout(uni.redirectTo, 700, {
url: '/pages/index/order'
url: '/pages/index/order'
})
} catch (err) {
}


+ 3
- 2
store/store.js View File

@ -126,8 +126,9 @@ const store = new Vuex.Store({
Vue.set(state, 'userInfo', res.result)
if (!state.userInfo.nickName ||
!state.userInfo.headImage ||
!state.userInfo.phone
!state.userInfo.headImage
// ||
// !state.userInfo.phone
) {
uni.showModal({
title: '申请获取您的信息!',


+ 34
- 34
utils/pay.js View File

@ -1,4 +1,3 @@
// #ifdef H5
import jWeixin from './lib/jweixin-module.js'
// #endif
@ -6,42 +5,43 @@ import jWeixin from './lib/jweixin-module.js'
/**
* 调用微信支付
* @param {Object} res - 支付参数对象包含appIdtimeStampnonceStr等必要信息
* @param {Function} successCallback - 支付成功的回调函数
* @param {Function} failCallback - 支付失败的回调函数
* @param {Function} optionCallback - 配置失败的回调函数
* @returns {Promise} - 返回Promise对象resolve表示支付成功reject表示支付失败
*/
export function wxPay(res, successCallback, failCallback, optionCallback) {
// 配置微信JSSDK
jWeixin.config({
debug: false,
appId: res.result.appId, //必填,公众号的唯一标识
jsApiList: ['chooseWXPay'] //必填,需要使用的JS接口列表
});
export function wxPay(res) {
return new Promise((resolve, reject) => {
// 配置微信JSSDK
jWeixin.config({
debug: false,
appId: res.result.appId, //必填,公众号的唯一标识
jsApiList: ['chooseWXPay'] //必填,需要使用的JS接口列表
});
// JSSDK配置成功后的回调
jWeixin.ready(function() {
// 调用微信支付接口
jWeixin.chooseWXPay({
appId: res.result.appId,
timestamp: res.result.timeStamp, // 支付签名时间戳
nonceStr: res.result.nonceStr, // 支付签名随机串
package: res.result.packageValue, // 统一支付接口返回的prepay_id参数值
signType: res.result.signType, // 签名类型,默认为MD5
paySign: res.result.paySign, // 支付签名
success: function() {
successCallback && successCallback();
},
fail: function(error) {
failCallback && failCallback();
},
cancel : function(){
failCallback && failCallback();
}
// JSSDK配置成功后的回调
jWeixin.ready(function() {
// 调用微信支付接口
jWeixin.chooseWXPay({
appId: res.result.appId,
timestamp: res.result.timeStamp, // 支付签名时间戳
nonceStr: res.result.nonceStr, // 支付签名随机串
package: res.result.packageValue, // 统一支付接口返回的prepay_id参数值
signType: res.result.signType, // 签名类型,默认为MD5
paySign: res.result.paySign, // 支付签名
success: function(result) {
resolve(result);
},
fail: function(error) {
reject(error);
},
cancel: function(error) {
reject({ type: 'cancel', ...error });
}
});
});
});
// JSSDK配置失败处理
jWeixin.error(function(res) {
optionCallback && optionCallback()
// JSSDK配置失败处理
jWeixin.error(function(error) {
reject({ type: 'config_error', ...error });
});
});
}

Loading…
Cancel
Save