This commit is contained in:
2026-03-06 16:54:32 +08:00
parent be96b28828
commit 47594ed095
46 changed files with 3745 additions and 462 deletions

View File

@ -7,9 +7,10 @@ import { LOGIN, GET_PHONE, USER_SHARE,USER_WALLET,RECHARGE_WALLET,USER_WXPAY,USE
// 微信登陆 // 微信登陆
export const getCodeByWxLogin = () => { export const getCodeByWxLogin = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
uni.login({ tt.login({
provider: "weixin", provider: "toutiao",
success: function (loginRes) { success: function (loginRes) {
// console.log(loginRes,'?1?')
resolve(loginRes.code); resolve(loginRes.code);
}, },
fail: function (err) { fail: function (err) {
@ -22,7 +23,7 @@ export const getCodeByWxLogin = () => {
import Store from "../store"; import Store from "../store";
// 登录鉴权 // 登录鉴权
export const login = (phone) => { export const login = (nickName, avatarUrl) => {
return getCodeByWxLogin().then((code) => { return getCodeByWxLogin().then((code) => {
// 从 vuex 中获取 referrerID如果通过二维码扫描进入 // 从 vuex 中获取 referrerID如果通过二维码扫描进入
const referrerID = Store.state.user?.referrerID || 0; const referrerID = Store.state.user?.referrerID || 0;
@ -31,12 +32,17 @@ export const login = (phone) => {
url: LOGIN, url: LOGIN,
method: "POST", method: "POST",
data: { data: {
nickName:nickName,
avatarUrl:avatarUrl,
code: code, code: code,
// yaoqing_code: inviteCode || null, source:'douyin',
phone: phone || null, referrerID:0,
source: "wechat", referrerType:'douyin'
referrerID: Number(referrerID) || 0, // // yaoqing_code: inviteCode || null,
referrerType: "wechat" // phone: phone || null,
// source: "wechat",
// referrerID: Number(referrerID) || 0,
// referrerType: "wechat"
}, },
}).then((res) => { }).then((res) => {
// 登录接口使用完 referrerID 后,清除一次,避免重复使用 // 登录接口使用完 referrerID 后,清除一次,避免重复使用

View File

@ -26,7 +26,7 @@ export const RECHARGE_WALLET = '/wallet/details'
export const USER_WALLET = '/wallet/info' export const USER_WALLET = '/wallet/info'
//钱包充值接口 //钱包充值接口
export const USER_WXPAY = '/wxpay/jsapi' export const USER_WXPAY = '/ttpay/jsapi'
//钱包充值金额 //钱包充值金额
export const USER_RECHARGE = '/recharge/bonus-list' export const USER_RECHARGE = '/recharge/bonus-list'
//积分充值列表 //积分充值列表
@ -186,7 +186,7 @@ export const HOME_ORDERS_CANCEL = "/home/orders/cancel";
// 校验节假日费用 // 校验节假日费用
export const CHECK_HOLIDAY_FEE = "/check_holiday_fee"; export const CHECK_HOLIDAY_FEE = "/check_holiday_fee";
//支付订单 //支付订单
export const PAY_ORDER = "/wxpay/jsapi"; export const PAY_ORDER = "/ttpay/jsapi";
//取消订单 //取消订单
export const CANCEL_ORDER = "/app/chongwu_order/order_quxiao"; export const CANCEL_ORDER = "/app/chongwu_order/order_quxiao";
@ -235,7 +235,7 @@ export const CREATE_ORDER_NEW = "/product/order/create";
// 创建购物车订单 // 创建购物车订单
export const CREATE_CART_ORDER = "/product/order/create"; export const CREATE_CART_ORDER = "/product/order/create";
// 订单支付 // 订单支付
export const PAY_ORDER_NEW = "/wxpay/jsapi"; export const PAY_ORDER_NEW = "/ttpay/jsapi";
// 订单列表 // 订单列表
export const SHOP_ORDER_LIST = "/product/order/list"; export const SHOP_ORDER_LIST = "/product/order/list";
// 订单详情 // 订单详情
@ -304,3 +304,5 @@ export const HOMETRAINING_ORDERS_CANCEL = '/hometraining/orders/cancel'
// 撤销申请(领养申请) // 撤销申请(领养申请)
export const ADOPTIONS_PET_APPLY_CANCEL = '/adoptions/pet/apply/cancel' export const ADOPTIONS_PET_APPLY_CANCEL = '/adoptions/pet/apply/cancel'

View File

@ -1,6 +1,6 @@
<template> <template>
<view class="nav-container"> <view class="nav-container">
<view :style="{ height: ststuaBarHeight + 'px' }"></view> <!-- <view :style="{ height: ststuaBarHeight + 'px' }"></view> -->
<view <view
class="flex-row-center nav-title ali-puhui-bold" class="flex-row-center nav-title ali-puhui-bold"
:style="{ height: menuButtonHeight + 'px' }" :style="{ height: menuButtonHeight + 'px' }"

View File

@ -551,13 +551,12 @@ export default {
} }
payOrder(this.orderId, chaJiaType).then((res) => { payOrder(this.orderId, chaJiaType).then((res) => {
const payData = res?.info?.pay_data || {}; const payData = res?.info?.pay_data || {};
uni.requestPayment({ tt.pay({
provider: 'wxpay', orderInfo: {
timeStamp: payData.timeStamp, order_id:res.data.orderInfo.order_id,
nonceStr: payData.nonceStr, order_token:res.data.orderInfo.order_token,
package: payData.package, },
signType: payData.signType, service:5,
paySign: payData.sign,
success: (res) => { success: (res) => {
console.log('success:' + JSON.stringify(res)); console.log('success:' + JSON.stringify(res));
uni.hideLoading(); uni.hideLoading();

View File

@ -272,14 +272,12 @@ export default {
}, },
weixinPay(orderId) { weixinPay(orderId) {
payOrder(orderId).then((res) => { payOrder(orderId).then((res) => {
const payData = res?.info?.pay_data || {}; tt.pay({
uni.requestPayment({ orderInfo: {
provider: 'wxpay', order_id:res.data.orderInfo.order_id,
timeStamp: payData.timeStamp, order_token:res.data.orderInfo.order_token,
nonceStr: payData.nonceStr, },
package: payData.package, service:5,
signType: payData.signType,
paySign: payData.sign,
success: (res) => { success: (res) => {
uni.hideLoading(); uni.hideLoading();
const eventChannel = this.getOpenerEventChannel(); const eventChannel = this.getOpenerEventChannel();

View File

@ -60,7 +60,8 @@
<text v-if="item.item_cut_price" class="price-original">{{ item.item_price }}</text> <text v-if="item.item_cut_price" class="price-original">{{ item.item_price }}</text>
</view> </view>
<image class="cart-icon" :src="item.selected ? require('@/static/images/xz.png') : ''" /> <image v-show="item.selected" class="cart-icon" src="/static/images/xz.png"></image>
<!-- <image class="cart-icon" :src="item.selected ? require('@/static/images/xz.png') : ''" /> -->
</view> </view>
</view> </view>
</view> </view>
@ -109,8 +110,8 @@
<text class="zf"> 请选择支付方式 </text> <text class="zf"> 请选择支付方式 </text>
<view class="wechat" @click.stop="selectOption1('1')"> <view class="wechat" @click.stop="selectOption1('1')">
<view class="select"> <view class="select">
<image class="w" src="@/static/images/wx.png" mode="widthFix" /> <image class="w" src="@/static/images/douy.png" mode="widthFix" />
<text class="x">微信</text> <text class="x">抖音</text>
</view> </view>
<image v-if="selected1" class="not-selected" src="@/static/images/w.png" mode="widthFix" /> <image v-if="selected1" class="not-selected" src="@/static/images/w.png" mode="widthFix" />
<image v-if="selected2" class="not-selected" src="@/static/images/y.png" mode="widthFix" /> <image v-if="selected2" class="not-selected" src="@/static/images/y.png" mode="widthFix" />
@ -533,12 +534,12 @@ export default {
}; };
walletWxpay(data).then((res) => { walletWxpay(data).then((res) => {
// 使用获取的支付参数进行支付 // 使用获取的支付参数进行支付
uni.requestPayment({ tt.pay({
timeStamp: res.data.timeStamp, // 确保这些字段都正确 orderInfo: {
nonceStr: res.data.nonceStr, order_id:res.data.orderInfo.order_id,
package: res.data.package, order_token:res.data.orderInfo.order_token,
signType: res.data.signType, },
paySign: res.data.paySign, service:5,
success: (res) => { success: (res) => {
// 关闭支付弹窗 // 关闭支付弹窗
this.additionalBom = false; this.additionalBom = false;

View File

@ -192,8 +192,8 @@
</view> </view>
<view class="wechat" @click.stop="selectOption1('1')"> <view class="wechat" @click.stop="selectOption1('1')">
<view class="select"> <view class="select">
<image class="w" src="@/static/images/wx.png" mode="widthFix" /> <image class="w" src="@/static/images/douy.png" mode="widthFix" />
<text class="x">微信</text> <text class="x">抖音</text>
</view> </view>
<image v-if="selected1" class="not-selected" src="@/static/images/w.png" mode="widthFix" /> <image v-if="selected1" class="not-selected" src="@/static/images/w.png" mode="widthFix" />
<image v-if="selected2" class="not-selected" src="@/static/images/y.png" mode="widthFix" /> <image v-if="selected2" class="not-selected" src="@/static/images/y.png" mode="widthFix" />
@ -702,12 +702,12 @@ export default {
}; };
payOrder(params) payOrder(params)
.then((res) => { .then((res) => {
uni.requestPayment({ tt.pay({
timeStamp: res.data.timeStamp, orderInfo: {
nonceStr: res.data.nonceStr, order_id:res.data.orderInfo.order_id,
package: res.data.package, order_token:res.data.orderInfo.order_token,
signType: res.data.signType, },
paySign: res.data.paySign, service:5,
success: (res) => { success: (res) => {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({

View File

@ -297,8 +297,8 @@
<view class="recharge-method"> <view class="recharge-method">
<view class="wechat" @click.stop="selectOption1('1')"> <view class="wechat" @click.stop="selectOption1('1')">
<view class="select"> <view class="select">
<image class="w" src="@/static/images/wx.png" mode="widthFix" /> <image class="w" src="@/static/images/douy.png" mode="widthFix" />
<text class="x">微信</text> <text class="x">抖音</text>
</view> </view>
<image v-if="selected1" class="not-selected" src="@/static/images/w.png" mode="widthFix" /> <image v-if="selected1" class="not-selected" src="@/static/images/w.png" mode="widthFix" />
<image v-if="selected2" class="not-selected" src="@/static/images/y.png" mode="widthFix" /> <image v-if="selected2" class="not-selected" src="@/static/images/y.png" mode="widthFix" />
@ -369,8 +369,8 @@
</view> </view>
<view class="wechat"> <view class="wechat">
<view class="select"> <view class="select">
<image class="w" src="@/static/images/wx.png" mode="widthFix" /> <image class="w" src="@/static/images/douy.png" mode="widthFix" />
<text class="x">微信</text> <text class="x">抖音</text>
</view> </view>
<image v-if="select1" class="not-selected" @click.stop="selectOpt1('1')" src="@/static/images/w.png" <image v-if="select1" class="not-selected" @click.stop="selectOpt1('1')" src="@/static/images/w.png"
mode="widthFix" /> mode="widthFix" />
@ -792,12 +792,12 @@ export default {
}; };
memberWXPAY(data).then((res) => { memberWXPAY(data).then((res) => {
// 使用获取的支付参数进行支付 // 使用获取的支付参数进行支付
uni.requestPayment({ tt.pay({
timeStamp: res.data.timeStamp, // 确保这些字段都正确 orderInfo: {
nonceStr: res.data.nonceStr, order_id:res.data.orderInfo.order_id,
package: res.data.package, order_token:res.data.orderInfo.order_token,
signType: res.data.signType, },
paySign: res.data.paySign, service:5,
success: (res) => { success: (res) => {
uni.showToast({ uni.showToast({
title: "支付成功", title: "支付成功",
@ -1202,12 +1202,12 @@ export default {
} }
payOrder(params) payOrder(params)
.then((res) => { .then((res) => {
uni.requestPayment({ tt.pay({
timeStamp: res.data.timeStamp, // 确保这些字段都正确 orderInfo: {
nonceStr: res.data.nonceStr, order_id:res.data.orderInfo.order_id,
package: res.data.package, order_token:res.data.orderInfo.order_token,
signType: res.data.signType, },
paySign: res.data.paySign, service:5,
success: (res) => { success: (res) => {
uni.hideLoading(); uni.hideLoading();
const eventChannel = this.getOpenerEventChannel(); const eventChannel = this.getOpenerEventChannel();

View File

@ -115,8 +115,8 @@
<!-- 支付方式 --> <!-- 支付方式 -->
<view class="info-cell payment-method-cell"> <view class="info-cell payment-method-cell">
<view class="payment-option" @click="selectPaymentMethod('wechat')"> <view class="payment-option" @click="selectPaymentMethod('wechat')">
<image src="@/static/images/wx.png" mode="aspectFit" class="payment-icon" /> <image src="@/static/images/douy.png" mode="aspectFit" class="payment-icon" />
<text class="payment-name">微信支付</text> <text class="payment-name">抖音支付</text>
<!-- 未选中状态 --> <!-- 未选中状态 -->
<image v-if="paymentMethod !== 'wechat'" src="@/static/images/w.png" mode="widthFix" class="not-selected" /> <image v-if="paymentMethod !== 'wechat'" src="@/static/images/w.png" mode="widthFix" class="not-selected" />
<!-- 选中状态 --> <!-- 选中状态 -->
@ -436,16 +436,12 @@ export default {
return; return;
} }
const payData = res.data || res.info?.pay_data || {}; tt.pay({
orderInfo: {
// 调用微信支付 order_id:res.data.orderInfo.order_id,
uni.requestPayment({ order_token:res.data.orderInfo.order_token,
provider: 'wxpay', },
timeStamp: payData.timeStamp, service:5,
nonceStr: payData.nonceStr,
package: payData.package,
signType: payData.signType,
paySign: payData.paySign || payData.sign,
success: (payRes) => { success: (payRes) => {
console.log('支付成功:', payRes); console.log('支付成功:', payRes);
uni.showToast({ uni.showToast({

View File

@ -435,13 +435,12 @@ export default {
order_no: orderNo, order_no: orderNo,
total_fee: 200 total_fee: 200
}).then((res) => { }).then((res) => {
uni.requestPayment({ tt.pay({
provider: "wxpay", orderInfo: {
timeStamp: res.data.timeStamp, order_id:res.data.orderInfo.order_id,
nonceStr: res.data.nonceStr, order_token:res.data.orderInfo.order_token,
package: res.data.package, },
signType: res.data.signType, service:5,
paySign: res.data.paySign,
complete: (res) => { complete: (res) => {
// 支付成功后跳转到订单页面 // 支付成功后跳转到订单页面
setTimeout(() => { setTimeout(() => {

View File

@ -47,8 +47,8 @@
<!-- 支付方式 --> <!-- 支付方式 -->
<view class="info-cell payment-method-cell"> <view class="info-cell payment-method-cell">
<view class="payment-option" @click="selectPaymentMethod('wechat')"> <view class="payment-option" @click="selectPaymentMethod('wechat')">
<image src="@/static/images/wx.png" mode="aspectFit" class="payment-icon" /> <image src="@/static/images/douy.png" mode="aspectFit" class="payment-icon" />
<text class="payment-name">微信支付</text> <text class="payment-name">抖音支付</text>
<!-- 未选中状态 --> <!-- 未选中状态 -->
<image v-if="paymentMethod !== 'wechat'" src="@/static/images/w.png" mode="widthFix" class="not-selected" /> <image v-if="paymentMethod !== 'wechat'" src="@/static/images/w.png" mode="widthFix" class="not-selected" />
<!-- 选中状态 --> <!-- 选中状态 -->
@ -262,16 +262,12 @@ export default {
return; return;
} }
const payData = res.data || res.info?.pay_data || {}; tt.pay({
orderInfo: {
// 调用微信支付 order_id:res.data.orderInfo.order_id,
uni.requestPayment({ order_token:res.data.orderInfo.order_token,
provider: 'wxpay', },
timeStamp: payData.timeStamp, service:5,
nonceStr: payData.nonceStr,
package: payData.package,
signType: payData.signType,
paySign: payData.paySign || payData.sign,
success: (payRes) => { success: (payRes) => {
console.log('支付成功:', payRes); console.log('支付成功:', payRes);
uni.showToast({ uni.showToast({

View File

@ -71,8 +71,15 @@
</view> </view>
</view> </view>
<canvas
id="myCanvas"
type="2d"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
style="position: fixed; left: -9999px; top: -9999px;"
></canvas>
<!-- 隐藏的 wxml-to-canvas 组件用于生成证书图片 --> <!-- 隐藏的 wxml-to-canvas 组件用于生成证书图片 -->
<wxml-to-canvas class="widget" :width="canvasWidth" :height="canvasHeight" style="position: fixed; left: -9999px; top: -9999px;"></wxml-to-canvas> <!-- <wxml-to-canvas class="widget" :width="canvasWidth" :height="canvasHeight" style="position: fixed; left: -9999px; top: -9999px;"></wxml-to-canvas> -->
</view> </view>
</template> </template>
@ -107,7 +114,7 @@ export default {
}, },
onReady() { onReady() {
// 获取 wxml-to-canvas 组件实例 // 获取 wxml-to-canvas 组件实例
this.widget = this.$refs.widget || this.selectComponent('.widget'); // this.widget = this.$refs.widget || this.selectComponent('.widget');
}, },
methods: { methods: {
getCertificates(data) { getCertificates(data) {
@ -124,6 +131,27 @@ export default {
}, },
closeModal() { closeModal() {
this.showModal = false; this.showModal = false;
},
wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const words = text.split('');
let line = '';
let currentY = y;
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i];
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && i > 0) {
ctx.fillText(line, x, currentY);
line = words[i];
currentY += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line, x, currentY);
}, },
onScroll(e) { onScroll(e) {
// 获取滚动位置单位px // 获取滚动位置单位px
@ -141,10 +169,32 @@ export default {
this.currentPage = newPage; this.currentPage = newPage;
} }
}, },
async saveCurrentImage() { async loadImage(src) {
// 保存当前图片 - 使用 wxml-to-canvas 生成 console.log('正在加载图片:', src);
return new Promise((resolve, reject) => {
tt.downloadFile({
url: src,
success: (res) => {
if (res.statusCode === 200) {
console.log('图片加载成功:', res.tempFilePath);
resolve(res.tempFilePath);
} else {
console.error('图片加载失败,状态码:', res.statusCode);
reject(new Error(`图片加载失败,状态码: ${res.statusCode}`));
}
},
fail: (err) => {
console.error('图片加载失败:', err.errMsg);
reject(new Error(`图片加载失败: ${err.errMsg}`));
}
});
});
},
async saveCurrentImage() {
const currentIndex = this.currentPage - 1; const currentIndex = this.currentPage - 1;
const currentItem = this.certificateList[currentIndex]; const currentItem = this.certificateList[currentIndex];
if (!currentItem) { if (!currentItem) {
uni.showToast({ uni.showToast({
title: '证书信息加载中,请稍候', title: '证书信息加载中,请稍候',
@ -153,82 +203,109 @@ export default {
return; return;
} }
// 存储当前证书信息到 data供模板使用
this.currentCertificateItem = currentItem; this.currentCertificateItem = currentItem;
// 获取组件实例 - 多种方式尝试
if (!this.widget) {
this.widget = this.$refs.widget;
}
if (!this.widget) {
this.widget = this.selectComponent('.widget');
}
if (!this.widget) {
// 延迟重试一次
await new Promise(resolve => setTimeout(resolve, 100));
this.widget = this.$refs.widget || this.selectComponent('.widget');
}
if (!this.widget) {
uni.hideLoading();
uni.showToast({
title: '组件初始化失败,请重试',
icon: 'none',
duration: 3000
});
console.error('wxml-to-canvas 组件获取失败');
return;
}
// 检查组件方法是否可用
if (typeof this.widget.renderToCanvas !== 'function') {
uni.hideLoading();
uni.showToast({
title: '组件方法不可用,请重试',
icon: 'none',
duration: 3000
});
console.error('renderToCanvas 方法不存在');
return;
}
uni.showLoading({ uni.showLoading({
title: '生成图片中...', title: '生成图片中...',
mask: true mask: true
}); });
try { try {
// 固定画布尺寸 // 获取 Canvas 节点
const canvasWidth = 317; const query = tt.createSelectorQuery();
const canvasHeight = 224; const canvasNode = await new Promise((resolve, reject) => {
query
// 更新 canvas 尺寸 .select('#myCanvas')
this.canvasWidth = canvasWidth; .fields({ node: true, size: true })
this.canvasHeight = canvasHeight; .exec((res) => {
if (res[0]) {
await this.$nextTick(); resolve(res[0].node);
} else {
// 生成证书的 wxml 和 style使用固定尺寸 reject(new Error('Canvas 节点获取失败'));
const { wxml, style } = this.generateCertificateTemplate(); }
});
// 渲染到 canvas
const container = await this.widget.renderToCanvas({ wxml, style });
// 转换为图片
const res = await this.widget.canvasToTempFilePath({
fileType: 'png',
quality: 1,
width: this.canvasWidth,
height: this.canvasHeight
}); });
if (!res || !res.tempFilePath) { // 获取 2D 上下文
throw new Error('生成图片路径失败'); const ctx = canvasNode.getContext('2d');
} const dpr = tt.getSystemInfoSync().pixelRatio;
canvasNode.width = this.canvasWidth * dpr;
canvasNode.height = this.canvasHeight * dpr;
ctx.scale(dpr, dpr);
// 清空画布
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 加载并绘制背景图
const backgroundImgPath = await this.loadImage(`${this.imgPrefix}certificateGround.png`);
const backgroundImg = canvasNode.createImage();
backgroundImg.src = backgroundImgPath;
await new Promise((resolve) => {
backgroundImg.onload = () => {
ctx.drawImage(backgroundImg, 0, 0, this.canvasWidth, this.canvasHeight);
resolve();
};
});
// 设置字体样式
ctx.font = '20px sans-serif';
ctx.fillStyle = '#FF19A0';
ctx.textAlign = 'center';
// 绘制证书标题
const title = currentItem.title || '为爱续航证书';
ctx.fillText(title, this.canvasWidth / 2, 50);
// 绘制英文标题
ctx.font = '12px sans-serif';
const titleEn = currentItem.title_en || 'Certificate of Love Endurance';
ctx.fillText(titleEn, this.canvasWidth / 2, 70);
// 绘制用户名
ctx.font = '16px sans-serif';
const userName = this.userName || '周佳佳';
ctx.fillText(userName, this.canvasWidth / 2, 100);
// 绘制描述文本
ctx.font = '12px sans-serif';
ctx.fillStyle = '#333333';
ctx.textAlign = 'left';
const description = `您累计捐赠${currentItem.source_value || 0}克粮,为毛孩子奉献爱心,点燃希望,感谢您的捐赠让世界变得更温暖。`;
this.wrapText(ctx, description, 20, 130, this.canvasWidth - 40, 16);
// 绘制结尾文字
ctx.fillText('特发此证,以表谢忱!', 20, 200);
// 绘制证书编号
ctx.font = '10px sans-serif';
ctx.fillStyle = '#FF19A0';
ctx.textAlign = 'right';
const certificateNo = `证书编号: ${currentItem.certificate_no || currentItem.id || '234546678896666788'}`;
ctx.fillText(certificateNo, this.canvasWidth - 20, this.canvasHeight - 20);
// 绘制证书图标
const badgeImgPath = await this.loadImage(currentItem.certificate_url);
const badgeImg = canvasNode.createImage();
badgeImg.src = badgeImgPath;
await new Promise((resolve) => {
badgeImg.onload = () => {
ctx.drawImage(badgeImg, this.canvasWidth - 60, this.canvasHeight - 80, 40, 40);
resolve();
};
});
// 导出为 base64 数据
const dataURL = canvasNode.toDataURL('image/png');
const base64Data = dataURL.replace(/^data:image\/\w+;base64,/, '');
// 将 base64 数据写入临时文件
const tempFilePath = `${tt.env.USER_DATA_PATH}/temp_certificate.png`;
const fs = tt.getFileSystemManager();
fs.writeFileSync(tempFilePath, base64Data, 'base64');
// 保存到相册 // 保存到相册
uni.saveImageToPhotosAlbum({ uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath, filePath: tempFilePath,
success: () => { success: () => {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({
@ -250,29 +327,14 @@ export default {
} catch (error) { } catch (error) {
uni.hideLoading(); uni.hideLoading();
const errorMsg = error?.message || error?.toString() || '未知错误'; const errorMsg = error?.message || error?.toString() || '未知错误';
const errorInfo = `生成图片失败: ${errorMsg}`;
console.log(errorInfo, '--=')
uni.showToast({ uni.showToast({
title: errorInfo.length > 20 ? '生成图片失败,请重试' : errorInfo, title: errorMsg.length > 20 ? '生成图片失败,请重试' : errorMsg,
icon: 'none', icon: 'none',
duration: 3000 duration: 3000
}); });
// 使用多种方式记录错误,确保线上环境能捕获
console.error('生成证书图片失败:', error); console.error('生成证书图片失败:', error);
console.error('错误详情:', {
message: error?.message,
stack: error?.stack,
error: error
});
// 如果 console 不可用,尝试通过其他方式记录
if (typeof uni.reportError === 'function') {
uni.reportError({
error: errorInfo,
errorInfo: JSON.stringify(error)
});
} }
} },
},
generateCertificateTemplate() { generateCertificateTemplate() {
// 使用存储的证书信息 // 使用存储的证书信息
const item = this.currentCertificateItem; const item = this.currentCertificateItem;

View File

@ -1,5 +1,5 @@
<template> <template>
<view v-if="show" :class="modalClassList" @click.stop="handleCancel"> <view v-if="show" class="donation-confirm-modal" :class="[type === 'dog' ? 'modal-dog' : 'modal-cat']" @click.stop="handleCancel">
<view class="modal-content" @click.stop=""> <view class="modal-content" @click.stop="">
<!-- 顶部图标 --> <!-- 顶部图标 -->
<image class="bowl-icon" :src="bowlIcon" mode="aspectFit" /> <image class="bowl-icon" :src="bowlIcon" mode="aspectFit" />
@ -64,11 +64,6 @@ export default {
? `${imgPrefix}welfare-dogFood.png` ? `${imgPrefix}welfare-dogFood.png`
: `${imgPrefix}welfare-catFood.png`; : `${imgPrefix}welfare-catFood.png`;
}, },
modalClassList() {
const baseClass = 'donation-confirm-modal';
const typeClass = this.type === 'dog' ? 'modal-dog' : 'modal-cat';
return [baseClass, typeClass];
}
}, },
methods: { methods: {
handleCancel() { handleCancel() {

View File

@ -223,6 +223,7 @@ export default {
}); });
}, },
handleDogCardClick() { handleDogCardClick() {
console.log(this)
this.donationType = 'dog'; this.donationType = 'dog';
this.showDonationModal = true; this.showDonationModal = true;
}, },

View File

@ -157,10 +157,7 @@
{ {
"path": "welfare/certificate-list", "path": "welfare/certificate-list",
"style": { "style": {
"navigationBarTitleText": "我的证书", "navigationBarTitleText": "我的证书"
"usingComponents": {
"wxml-to-canvas": "/wxcomponents/wxml-to-canvas/index"
}
} }
}, },
{ {

View File

@ -0,0 +1,235 @@
<template>
<scroll-view class="list-template-wrapper" :scroll-y="!disableScroll" :refresher-enabled="true"
:refresher-triggered="refreshTriggered" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
<slot name="top" />
<view class="list-content" v-if="list.length > 0">
<view v-for="(item, index) in list" :key="item[idKey]" :class="{ left: index % 2 === 0 }"
class="flex-column-start news-item" @click="clickCell(item)">
<slot style="width: 100%" name="item" :data="{
...item,
...listExtraFields,
deleteSelect: !!item.deleteSelect,
}" />
</view>
<uni-load-more v-if="isLoading || (!isLoading && total && total === list.length)"
:status="isLoading ? 'loading' : 'nomore'"></uni-load-more>
</view>
<view v-else class="empty-container">
<image class="home-ration" mode="widthFix" :src="emptyImage || defaultEmptyImage" />
<text v-if="emptyText" class="empty-text">{{ emptyText }}</text>
</view>
<slot name="bottom" />
</scroll-view>
</template>
<script>
export default {
name: "ListPageTemp",
props: {
getDataPromise: {
type: Function,
},
// 格式化请求结果的方法
resultFormatFunc: {
type: Function,
},
requestData: {
defult: () => { },
type: Object,
},
// 列表元素额外字段
listExtraFields: {
type: Object,
default: () => { },
},
reloadFlag: {
default: 0,
type: Number,
},
// 是否分页
isPagiantion: {
type: Boolean,
default: true,
},
// 分页数量, 不分页时有效
pageSize: {
type: Number,
default: 999,
},
defaultList: {
type: Array,
default: () => [],
},
disableScroll: {
type: Boolean,
default: false,
},
idKey: {
type: String,
default: "id",
},
// 占位图片地址
emptyImage: {
type: String,
default: "",
},
// 空状态文本(传入时才显示)
emptyText: {
type: String,
default: "",
}
},
data() {
return {
isLoading: false,
total: 0,
list: [],
refreshTriggered: false,
p: 1,
num: 10,
defaultEmptyImage: 'https://activity.wagoo.live/empty.png', // 默认占位图片
};
},
watch: {
reloadFlag: {
handler(value) {
if (value) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
immediate: true,
},
requestData: {
handler(data) {
if (data) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
deep: true,
},
defaultList: {
handler(list) {
this.list = list;
this.$forceUpdate();
},
immediate: true,
},
},
methods: {
onRefresh() {
if (this.refreshTriggered) return;
this.refreshTriggered = true;
this.p = 1;
this.num = 10;
// this.list = [];
this.total = 0;
this.getList();
},
onLoadMore() {
if (!this.isPagiantion) {
return;
}
if (!this.isLoading && this.total > this.list.length) {
this.p++;
this.getList();
}
},
clickCell(item) {
this.$emit("clickCell", item);
},
getList() {
if (this.isLoading) return;
this.isLoading = true;
// TODO: 删除测试数据
if (!this.getDataPromise) {
this.list = [0, 1, 2, 3, 4, 5, 6].map((v, k) => {
return {
title: "消息名称0000sssss撒大苏打大苏打" + v,
id: k,
len: 10,
};
});
this.total = 9;
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
return;
}
const data = Object.assign(
{},
{ p: this.p, num: !this.isPagiantion ? this.pageSize : this.num },
this.requestData
);
this.getDataPromise(data)
.then((res) => {
const list =
this.p === 1
? res?.data || []
: [...this.list, ...(res?.data || [])];
this.list = this.resultFormatFunc
? this.resultFormatFunc(list)
: list;
this.total = res?.count || 0;
this.$emit("getList", this.list, res);
// console.log(this.list,'???')
})
.finally(() => {
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
});
},
},
};
</script>
<style lang="scss" scoped>
.list-template-wrapper {
height: 100%;
width: 100%;
.list-content {
width: 100%;
}
.news-item {
width: 100%;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 400rpx auto 0;
padding: 0 40rpx;
}
.home-ration {
width: 160px;
height: 174px;
display: block;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
text-align: center;
}
}
</style>

View File

@ -38,11 +38,13 @@
<script> <script>
import AddressItem from "./components/AddressItem.vue"; import AddressItem from "./components/AddressItem.vue";
import PopUpModal from "../../../components/PopUpModal.vue"; import PopUpModal from "../../../components/PopUpModal.vue";
import ListPageTemp from "./components/ListPageTemp.vue";
import { delAddress, getAddressList } from "../../../api/address"; import { delAddress, getAddressList } from "../../../api/address";
export default { export default {
components: { components: {
AddressItem, AddressItem,
ListPageTemp,
PopUpModal, PopUpModal,
}, },
data() { data() {

View File

@ -4,10 +4,10 @@
<image class="login-ground-img" :src="`${imgPrefix}loginGroundImg.png`" mode="widthFix" /> <image class="login-ground-img" :src="`${imgPrefix}loginGroundImg.png`" mode="widthFix" />
<view class="btnConent"> <view class="btnConent">
<button v-show="!checked" class="loginBtn" @click="unCheckAndGetPhoneNumber"> <button v-show="!checked" class="loginBtn" @click="unCheckAndGetPhoneNumber">
手机快捷登录 抖音用户信息授权登录
</button> </button>
<button v-show="checked" class="loginBtn" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber"> <button v-show="checked" class="loginBtn" @tap="getPhoneNumber" >
手机快捷登录 抖音用户信息授权登录
</button> </button>
<view class="notLoginBtn" @click="goBack"> <view class="notLoginBtn" @click="goBack">
暂不登录 暂不登录
@ -49,8 +49,12 @@ export default {
imgPrefix, imgPrefix,
checked: false, checked: false,
uncheckMessageDialog: false, uncheckMessageDialog: false,
yaoqing_code: ''
}; };
}, },
onLoad(options) {
this.yaoqing_code = options.yaoqing_code
},
mounted() { }, mounted() { },
methods: { methods: {
ptfwxy() { ptfwxy() {
@ -68,35 +72,6 @@ export default {
}); });
}, },
changeChecked() { changeChecked() {
uni.requestSubscribeMessage({
tmplIds: ['QoTeQwj4xw2UQMK5jI67MzAVOo6og76oqZ7BDIJW7cE', 'GPWlTkaNbi7JqvxltLKuZZMtKedSZfEKlirV7yOUu-0'],// TEMPLATE_ID替换为选用的模版id
success(res) {
uni.hideLoading()
if (res['QoTeQwj4xw2UQMK5jI67MzAVOo6og76oqZ7BDIJW7cE'] === 'accept') {
// setSubscribeStatus(true, '已订阅')
uni.showToast({
title: '订阅成功',
icon: 'success',
})
}
if (res['GPWlTkaNbi7JqvxltLKuZZMtKedSZfEKlirV7yOUu-0'] === 'accept') {
// setSubscribeStatus(true, '已订阅')
uni.showToast({
title: '订阅成功',
icon: 'success',
})
}
},
fail() {
uni.showToast({
title: '订阅失败',
icon: 'error',
})
},
complete: () => {
// isSubscribing = false
},
})
this.checked = !this.checked; this.checked = !this.checked;
}, },
unCheckAndGetPhoneNumber() { unCheckAndGetPhoneNumber() {
@ -106,63 +81,25 @@ export default {
duration: 2000 duration: 2000
}); });
}, },
async getPhoneNumber(e) { getmembeInfo(nickName,avatarUrl) {
// console.log(e,'?') const inviteCode = this.yaoqing_code ? this.yaoqing_code : getApp().globalData.inviteCode
// return
// 检查是否有 code
if (!e.detail.code) {
uni.showToast({
title: "获取手机号失败,请重试",
icon: "none",
});
return;
}
uni.showLoading({
title: "获取手机号中...",
icon: "none",
mask: true,
});
try {
// 登录前先记录是否有邀请码(referrerID),用于登录后导航判断
const hasReferrer =
this.$store.state?.user && this.$store.state.user.referrerID;
// 拿着code获取手机号
const codeRes = await getPhone(e.detail.code);
const phone = codeRes?.data?.phoneNumber || "";
if (!phone) {
uni.hideLoading();
uni.showToast({
title: "获取手机号失败,请重试",
icon: "none",
});
return;
}
uni.showLoading({ uni.showLoading({
title: "登录中...", title: "登录中...",
icon: "none", icon: "none",
mask: true, mask: true,
}); });
login(nickName,avatarUrl).then((res) => {
// 手机号登录 uni.hideLoading()
login(phone)
.then((res) => {
uni.hideLoading();
this.$store.dispatch("user/setToken", res?.data?.token || ""); this.$store.dispatch("user/setToken", res?.data?.token || "");
this.$store.dispatch("user/setUserInfo", res?.data || {}); this.$store.dispatch("user/setUserInfo", res?.data || {});
var pages = getCurrentPages();
// 如果是带邀请码(referrerID)的登录,统一跳转到首页 console.log(pages,'sssss')
if (hasReferrer) { if (inviteCode) {
uni.reLaunch({ uni.reLaunch({
url: "/pages/client/index/index", url: "/pages/client/index/index",
}); });
return; return
} }
var pages = getCurrentPages();
if (pages.length === 1) { if (pages.length === 1) {
uni.reLaunch({ uni.reLaunch({
url: "/pages/client/index/index", url: "/pages/client/index/index",
@ -170,14 +107,95 @@ export default {
return; return;
} }
uni.navigateBack(); uni.navigateBack();
}) }).catch((err) => {
.catch((err) => {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({
title: err || "登录失败,请重试", title: err || "登录失败,请重试",
icon: "none", icon: "none",
}); });
}); });
},
// login( this.nickName, this.avatarUrl).then((res) => {
// uni.hideLoading()
// this.$store.dispatch("user/setToken", res?.data?.token || "");
// this.$store.dispatch("user/setUserInfo", res?.data || {});
// var pages = getCurrentPages();
// if (inviteCode) {
// uni.reLaunch({
// url: "/pages/client/index/index",
// });
// return
// }
// if (pages.length === 1) {
// uni.reLaunch({
// url: "/pages/client/index/index",
// });
// return;
// }
// uni.navigateBack();
// }).catch((err) => {
// uni.hideLoading();
// uni.showToast({
// title: err || "登录失败,请重试",
// icon: "none",
// });
// }),
async getPhoneNumber(e) {
// console.log(e,'?')
// return
// 检查是否有 code
// if (!e.detail.code) {
// uni.showToast({
// title: "获取手机号失败,请重试",
// icon: "none",
// });
// return;
// }
// uni.showLoading({
// title: "获取手机号中...",
// icon: "none",
// mask: true,
// });
try {
let that = this
tt.getUserProfile({
success(res) {
console.log(res,'???')
// this.nickName = res.res.userInfo.nickName
// this.avatarUrl = res.userInfo.avatarUrl
that.getmembeInfo(res.userInfo.nickName,res.userInfo.avatarUrl)
// console.log(res,'--=')
},
fail(res) {
console.log("getUserProfile 调用失败", res);
},
});
// const inviteCode = this.yaoqing_code ? this.yaoqing_code : getApp().globalData.inviteCode
// console.log('获取到邀请码----->', inviteCode)
// 手机号登录
// 拿着code获取手机号
// const codeRes = await getPhone(e.detail.code);
// const phone = codeRes?.data?.phoneNumber || "";
// if (!phone) {
// uni.hideLoading();
// uni.showToast({
// title: "获取手机号失败,请重试",
// icon: "none",
// });
// return;
// }
} catch (error) { } catch (error) {
uni.hideLoading(); uni.hideLoading();
console.error("获取手机号失败:", error); console.error("获取手机号失败:", error);
@ -215,10 +233,6 @@ export default {
}); });
return; return;
} }
// 登录前先记录是否有邀请码(referrerID),用于登录后导航判断
const hasReferrer =
this.$store.state?.user && this.$store.state.user.referrerID;
uni.showLoading({ uni.showLoading({
title: "加载中...", title: "加载中...",
icon: "none", icon: "none",
@ -228,15 +242,6 @@ export default {
.then((res) => { .then((res) => {
this.$store.dispatch("user/setToken", res?.data?.token || ""); this.$store.dispatch("user/setToken", res?.data?.token || "");
this.$store.dispatch("user/setUserInfo", res?.data || {}); this.$store.dispatch("user/setUserInfo", res?.data || {});
// 如果是带邀请码(referrerID)的登录,统一跳转到首页
if (hasReferrer) {
uni.reLaunch({
url: "/pages/client/index/index",
});
return;
}
var pages = getCurrentPages(); var pages = getCurrentPages();
if (pages.length === 1) { if (pages.length === 1) {
uni.reLaunch({ uni.reLaunch({

View File

@ -0,0 +1,235 @@
<template>
<scroll-view class="list-template-wrapper" :scroll-y="!disableScroll" :refresher-enabled="true"
:refresher-triggered="refreshTriggered" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
<slot name="top" />
<view class="list-content" v-if="list.length > 0">
<view v-for="(item, index) in list" :key="item[idKey]" :class="{ left: index % 2 === 0 }"
class="flex-column-start news-item" @click="clickCell(item)">
<slot style="width: 100%" name="item" :data="{
...item,
...listExtraFields,
deleteSelect: !!item.deleteSelect,
}" />
</view>
<uni-load-more v-if="isLoading || (!isLoading && total && total === list.length)"
:status="isLoading ? 'loading' : 'nomore'"></uni-load-more>
</view>
<view v-else class="empty-container">
<image class="home-ration" mode="widthFix" :src="emptyImage || defaultEmptyImage" />
<text v-if="emptyText" class="empty-text">{{ emptyText }}</text>
</view>
<slot name="bottom" />
</scroll-view>
</template>
<script>
export default {
name: "ListPageTemp",
props: {
getDataPromise: {
type: Function,
},
// 格式化请求结果的方法
resultFormatFunc: {
type: Function,
},
requestData: {
defult: () => { },
type: Object,
},
// 列表元素额外字段
listExtraFields: {
type: Object,
default: () => { },
},
reloadFlag: {
default: 0,
type: Number,
},
// 是否分页
isPagiantion: {
type: Boolean,
default: true,
},
// 分页数量, 不分页时有效
pageSize: {
type: Number,
default: 999,
},
defaultList: {
type: Array,
default: () => [],
},
disableScroll: {
type: Boolean,
default: false,
},
idKey: {
type: String,
default: "id",
},
// 占位图片地址
emptyImage: {
type: String,
default: "",
},
// 空状态文本(传入时才显示)
emptyText: {
type: String,
default: "",
}
},
data() {
return {
isLoading: false,
total: 0,
list: [],
refreshTriggered: false,
p: 1,
num: 10,
defaultEmptyImage: 'https://activity.wagoo.live/empty.png', // 默认占位图片
};
},
watch: {
reloadFlag: {
handler(value) {
if (value) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
immediate: true,
},
requestData: {
handler(data) {
if (data) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
deep: true,
},
defaultList: {
handler(list) {
this.list = list;
this.$forceUpdate();
},
immediate: true,
},
},
methods: {
onRefresh() {
if (this.refreshTriggered) return;
this.refreshTriggered = true;
this.p = 1;
this.num = 10;
// this.list = [];
this.total = 0;
this.getList();
},
onLoadMore() {
if (!this.isPagiantion) {
return;
}
if (!this.isLoading && this.total > this.list.length) {
this.p++;
this.getList();
}
},
clickCell(item) {
this.$emit("clickCell", item);
},
getList() {
if (this.isLoading) return;
this.isLoading = true;
// TODO: 删除测试数据
if (!this.getDataPromise) {
this.list = [0, 1, 2, 3, 4, 5, 6].map((v, k) => {
return {
title: "消息名称0000sssss撒大苏打大苏打" + v,
id: k,
len: 10,
};
});
this.total = 9;
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
return;
}
const data = Object.assign(
{},
{ p: this.p, num: !this.isPagiantion ? this.pageSize : this.num },
this.requestData
);
this.getDataPromise(data)
.then((res) => {
const list =
this.p === 1
? res?.data || []
: [...this.list, ...(res?.data || [])];
this.list = this.resultFormatFunc
? this.resultFormatFunc(list)
: list;
this.total = res?.count || 0;
this.$emit("getList", this.list, res);
// console.log(this.list,'???')
})
.finally(() => {
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
});
},
},
};
</script>
<style lang="scss" scoped>
.list-template-wrapper {
height: 100%;
width: 100%;
.list-content {
width: 100%;
}
.news-item {
width: 100%;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 400rpx auto 0;
padding: 0 40rpx;
}
.home-ration {
width: 160px;
height: 174px;
display: block;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
text-align: center;
}
}
</style>

View File

@ -51,7 +51,7 @@
<script> <script>
import CartItem from "./components/CartItem.vue"; import CartItem from "./components/CartItem.vue";
import ListPageTemp from "@/components/ListPageTemp.vue"; import ListPageTemp from "./components/ListPageTemp.vue";
import { import {
getCartList, getCartList,

View File

@ -0,0 +1,235 @@
<template>
<scroll-view class="list-template-wrapper" :scroll-y="!disableScroll" :refresher-enabled="true"
:refresher-triggered="refreshTriggered" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
<slot name="top" />
<view class="list-content" v-if="list.length > 0">
<view v-for="(item, index) in list" :key="item[idKey]" :class="{ left: index % 2 === 0 }"
class="flex-column-start news-item" @click="clickCell(item)">
<slot style="width: 100%" name="item" :data="{
...item,
...listExtraFields,
deleteSelect: !!item.deleteSelect,
}" />
</view>
<uni-load-more v-if="isLoading || (!isLoading && total && total === list.length)"
:status="isLoading ? 'loading' : 'nomore'"></uni-load-more>
</view>
<view v-else class="empty-container">
<image class="home-ration" mode="widthFix" :src="emptyImage || defaultEmptyImage" />
<text v-if="emptyText" class="empty-text">{{ emptyText }}</text>
</view>
<slot name="bottom" />
</scroll-view>
</template>
<script>
export default {
name: "ListPageTemp",
props: {
getDataPromise: {
type: Function,
},
// 格式化请求结果的方法
resultFormatFunc: {
type: Function,
},
requestData: {
defult: () => { },
type: Object,
},
// 列表元素额外字段
listExtraFields: {
type: Object,
default: () => { },
},
reloadFlag: {
default: 0,
type: Number,
},
// 是否分页
isPagiantion: {
type: Boolean,
default: true,
},
// 分页数量, 不分页时有效
pageSize: {
type: Number,
default: 999,
},
defaultList: {
type: Array,
default: () => [],
},
disableScroll: {
type: Boolean,
default: false,
},
idKey: {
type: String,
default: "id",
},
// 占位图片地址
emptyImage: {
type: String,
default: "",
},
// 空状态文本(传入时才显示)
emptyText: {
type: String,
default: "",
}
},
data() {
return {
isLoading: false,
total: 0,
list: [],
refreshTriggered: false,
p: 1,
num: 10,
defaultEmptyImage: 'https://activity.wagoo.live/empty.png', // 默认占位图片
};
},
watch: {
reloadFlag: {
handler(value) {
if (value) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
immediate: true,
},
requestData: {
handler(data) {
if (data) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
deep: true,
},
defaultList: {
handler(list) {
this.list = list;
this.$forceUpdate();
},
immediate: true,
},
},
methods: {
onRefresh() {
if (this.refreshTriggered) return;
this.refreshTriggered = true;
this.p = 1;
this.num = 10;
// this.list = [];
this.total = 0;
this.getList();
},
onLoadMore() {
if (!this.isPagiantion) {
return;
}
if (!this.isLoading && this.total > this.list.length) {
this.p++;
this.getList();
}
},
clickCell(item) {
this.$emit("clickCell", item);
},
getList() {
if (this.isLoading) return;
this.isLoading = true;
// TODO: 删除测试数据
if (!this.getDataPromise) {
this.list = [0, 1, 2, 3, 4, 5, 6].map((v, k) => {
return {
title: "消息名称0000sssss撒大苏打大苏打" + v,
id: k,
len: 10,
};
});
this.total = 9;
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
return;
}
const data = Object.assign(
{},
{ p: this.p, num: !this.isPagiantion ? this.pageSize : this.num },
this.requestData
);
this.getDataPromise(data)
.then((res) => {
const list =
this.p === 1
? res?.data || []
: [...this.list, ...(res?.data || [])];
this.list = this.resultFormatFunc
? this.resultFormatFunc(list)
: list;
this.total = res?.count || 0;
this.$emit("getList", this.list, res);
// console.log(this.list,'???')
})
.finally(() => {
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
});
},
},
};
</script>
<style lang="scss" scoped>
.list-template-wrapper {
height: 100%;
width: 100%;
.list-content {
width: 100%;
}
.news-item {
width: 100%;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 400rpx auto 0;
padding: 0 40rpx;
}
.home-ration {
width: 160px;
height: 174px;
display: block;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
text-align: center;
}
}
</style>

View File

@ -18,7 +18,7 @@
</template> </template>
<script> <script>
import ListPageTemp from "@/components/ListPageTemp"; import ListPageTemp from "./ListPageTemp";
import CouponItem from "@/components/coupon/CouponItem"; import CouponItem from "@/components/coupon/CouponItem";
import { getCouponData, receiveCoupon } from "@/api/coupon"; import { getCouponData, receiveCoupon } from "@/api/coupon";

View File

@ -36,7 +36,7 @@
<script> <script>
import TabsList from "@/components/TabsList.vue"; import TabsList from "@/components/TabsList.vue";
import ListPageTemp from "@/components/ListPageTemp.vue"; import ListPageTemp from "./ListPageTemp.vue";
import ServiceCouponItem from "@/components/coupon/ServiceCouponItem"; import ServiceCouponItem from "@/components/coupon/ServiceCouponItem";
import { getMyServiceCouponList } from "@/api/coupon"; import { getMyServiceCouponList } from "@/api/coupon";

View File

@ -62,21 +62,21 @@
</view> </view>
</view> </view>
<view class="line" /> <view class="line" />
<view class="itemWrapper" @click="toDogTraining"> <view class="itemWrapper" @click="toPublicBenefit">
<text class="titlWrapper">狗狗训练</text> <text class="titlWrapper">关爱宠物,传递温情</text>
<view class="content">上门服务</view> <view class="content">公益助力</view>
<view class="tips">上门训犬寄养喂猫遛狗</view> <view class="tips">帮我找个家</view>
<view class="itemImg"> <view class="itemImg">
<image class="menu-img-sm" :src="`${imgPrefix}home-dogTraining.png`" mode="aspectFill" /> <image class="menu-img-sm" :src="`${imgPrefix}h-public.png`" mode="aspectFill" />
</view> </view>
</view> </view>
</view> </view>
<view class="secondMenu"> <view class="secondMenu">
<scroll-view class="scrollWrapper" :scroll-x="secondMenuItemList.length > 4" enable-flex <scroll-view class="scrollWrapper" scroll-x enable-flex
:scroll-with-animation="false" :scroll-with-animation="false"
@scroll="onSecondMenuScroll"> @scroll="onSecondMenuScroll">
<view class="secondMenuInner" :class="{ 'no-scroll': secondMenuItemList.length <= 4 }"> <view class="secondMenuInner">
<view class="itemWrapper" v-for="(item, i) in secondMenuItemList" :key="i" <view class="itemWrapper" v-for="(item, i) in secondMenuItemList" :key="i"
@click="handleNav(item)"> @click="handleNav(item)">
<view class="imgWrapper"> <view class="imgWrapper">
@ -92,19 +92,19 @@
</view> </view>
</scroll-view> </scroll-view>
<view class="custom-indicator" v-if="secondMenuItemList.length > 4"> <view class="custom-indicator">
<view class="itemLine" :class="{ active: secondMenuIndicatorIndex === 0 }"></view> <view class="itemLine" :class="{ active: secondMenuIndicatorIndex === 0 }"></view>
<view class="itemLine" :class="{ active: secondMenuIndicatorIndex === 1 }"></view> <view class="itemLine" :class="{ active: secondMenuIndicatorIndex === 1 }"></view>
</view> </view>
</view> </view>
<view class="thirdMenu"> <view class="thirdMenu">
<view class="itemWrapper" @click="toPublicBenefit"> <view class="itemWrapper" @click="toDogTraining">
<view> <view>
<view class="title">公益助力</view> <view class="title">上门服务</view>
<view class="tips">帮我找个家</view> <view class="tips">训犬寄养喂猫遛狗</view>
</view> </view>
<image class="third-icon" :src="`${imgPrefix}home-publicBenefit.png`" mode="aspectFill" /> <image class="third-icon1" :src="`${imgPrefix}h-door.png`" mode="aspectFill" />
</view> </view>
<view class="itemWrapper" @click="toJoin"> <view class="itemWrapper" @click="toJoin">
<view> <view>
@ -188,6 +188,11 @@ export default {
img: `${imgPrefix}home-openVip.png`, img: `${imgPrefix}home-openVip.png`,
naviUrl: "/pages/richText/member-interests" naviUrl: "/pages/richText/member-interests"
}, },
{
title: "健康顾问",
tips: "宠物健康智能助手",
img: `${imgPrefix}healthConsultant.png`,
},
], ],
walletInfo: {}, // 钱包信息 walletInfo: {}, // 钱包信息
secondMenuIndicatorIndex: 0, // 第二排菜单指示器当前页 0=第一页 1=第二页 secondMenuIndicatorIndex: 0, // 第二排菜单指示器当前页 0=第一页 1=第二页
@ -245,9 +250,14 @@ export default {
}); });
}, },
toDogTraining() { toDogTraining() {
uni.navigateTo({ uni.showToast({
url: '/pageHome/service/index' title: '敬请期待',
icon: 'none',
duration: 2000
}); });
// uni.navigateTo({
// url: '/pageHome/service/index'
// });
}, },
// 我的钱包 // 我的钱包
buyService() { buyService() {
@ -375,8 +385,13 @@ export default {
} }
.menu-img-sm { .menu-img-sm {
width: 120rpx; width: 160rpx;
height: 164rpx; height: 160rpx;
}
.third-icon1{
width: 96rpx;
height: 120rpx;
} }
.second-icon, .second-icon,
@ -601,13 +616,9 @@ export default {
.scrollWrapper { .scrollWrapper {
width: 100%; width: 100%;
height: auto; /* 由内容撑开;小程序 scroll-view 横向滚动时默认可能不随内容计算高度 */
min-height: 220rpx; /* 兜底:约等于 padding-top + 图标 + 标题 + 副标题 */
display: flex; display: flex;
align-items: flex-start; /* 不拉伸子项,高度由内容决定 */
white-space: nowrap; white-space: nowrap;
padding-top: 24rpx; padding: 32rpx 0 20rpx 0;
padding-bottom: 24rpx;
box-sizing: border-box; box-sizing: border-box;
.secondMenuInner { .secondMenuInner {
@ -615,16 +626,6 @@ export default {
flex-wrap: nowrap; flex-wrap: nowrap;
flex-shrink: 0; flex-shrink: 0;
width: 937.5rpx; /* 5 * 187.5 一屏4个共5项 */ width: 937.5rpx; /* 5 * 187.5 一屏4个共5项 */
&.no-scroll {
width: 100%; /* 四项时一屏排满,不滚动 */
.itemWrapper {
flex: 1;
width: 0; /* 均分宽度,避免被 menuBody padding 裁切 */
min-width: 0;
}
}
} }
.itemWrapper { .itemWrapper {
@ -653,7 +654,7 @@ export default {
.custom-indicator { .custom-indicator {
display: flex; display: flex;
margin-top: 12rpx; margin-top:-150rpx;
justify-content: center; justify-content: center;
margin-bottom: 20rpx; margin-bottom: 20rpx;
gap: 12rpx; gap: 12rpx;

View File

@ -0,0 +1,235 @@
<template>
<scroll-view class="list-template-wrapper" :scroll-y="!disableScroll" :refresher-enabled="true"
:refresher-triggered="refreshTriggered" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
<slot name="top" />
<view class="list-content" v-if="list.length > 0">
<view v-for="(item, index) in list" :key="item[idKey]" :class="{ left: index % 2 === 0 }"
class="flex-column-start news-item" @click="clickCell(item)">
<slot style="width: 100%" name="item" :data="{
...item,
...listExtraFields,
deleteSelect: !!item.deleteSelect,
}" />
</view>
<uni-load-more v-if="isLoading || (!isLoading && total && total === list.length)"
:status="isLoading ? 'loading' : 'nomore'"></uni-load-more>
</view>
<view v-else class="empty-container">
<image class="home-ration" mode="widthFix" :src="emptyImage || defaultEmptyImage" />
<text v-if="emptyText" class="empty-text">{{ emptyText }}</text>
</view>
<slot name="bottom" />
</scroll-view>
</template>
<script>
export default {
name: "ListPageTemp",
props: {
getDataPromise: {
type: Function,
},
// 格式化请求结果的方法
resultFormatFunc: {
type: Function,
},
requestData: {
defult: () => { },
type: Object,
},
// 列表元素额外字段
listExtraFields: {
type: Object,
default: () => { },
},
reloadFlag: {
default: 0,
type: Number,
},
// 是否分页
isPagiantion: {
type: Boolean,
default: true,
},
// 分页数量, 不分页时有效
pageSize: {
type: Number,
default: 999,
},
defaultList: {
type: Array,
default: () => [],
},
disableScroll: {
type: Boolean,
default: false,
},
idKey: {
type: String,
default: "id",
},
// 占位图片地址
emptyImage: {
type: String,
default: "",
},
// 空状态文本(传入时才显示)
emptyText: {
type: String,
default: "",
}
},
data() {
return {
isLoading: false,
total: 0,
list: [],
refreshTriggered: false,
p: 1,
num: 10,
defaultEmptyImage: 'https://activity.wagoo.live/empty.png', // 默认占位图片
};
},
watch: {
reloadFlag: {
handler(value) {
if (value) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
immediate: true,
},
requestData: {
handler(data) {
if (data) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
deep: true,
},
defaultList: {
handler(list) {
this.list = list;
this.$forceUpdate();
},
immediate: true,
},
},
methods: {
onRefresh() {
if (this.refreshTriggered) return;
this.refreshTriggered = true;
this.p = 1;
this.num = 10;
// this.list = [];
this.total = 0;
this.getList();
},
onLoadMore() {
if (!this.isPagiantion) {
return;
}
if (!this.isLoading && this.total > this.list.length) {
this.p++;
this.getList();
}
},
clickCell(item) {
this.$emit("clickCell", item);
},
getList() {
if (this.isLoading) return;
this.isLoading = true;
// TODO: 删除测试数据
if (!this.getDataPromise) {
this.list = [0, 1, 2, 3, 4, 5, 6].map((v, k) => {
return {
title: "消息名称0000sssss撒大苏打大苏打" + v,
id: k,
len: 10,
};
});
this.total = 9;
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
return;
}
const data = Object.assign(
{},
{ p: this.p, num: !this.isPagiantion ? this.pageSize : this.num },
this.requestData
);
this.getDataPromise(data)
.then((res) => {
const list =
this.p === 1
? res?.data || []
: [...this.list, ...(res?.data || [])];
this.list = this.resultFormatFunc
? this.resultFormatFunc(list)
: list;
this.total = res?.count || 0;
this.$emit("getList", this.list, res);
// console.log(this.list,'???')
})
.finally(() => {
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
});
},
},
};
</script>
<style lang="scss" scoped>
.list-template-wrapper {
height: 100%;
width: 100%;
.list-content {
width: 100%;
}
.news-item {
width: 100%;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 400rpx auto 0;
padding: 0 40rpx;
}
.home-ration {
width: 160px;
height: 174px;
display: block;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
text-align: center;
}
}
</style>

View File

@ -14,10 +14,12 @@
getNoticeList getNoticeList
} from "../../../api/notice"; } from "../../../api/notice";
import NewsItem from "./components/NewsItem.vue"; import NewsItem from "./components/NewsItem.vue";
import ListPageTemp from "./components/ListPageTemp.vue";
import { imgPrefix } from "@/utils/common"; import { imgPrefix } from "@/utils/common";
export default { export default {
components: { components: {
NewsItem, NewsItem,
ListPageTemp
}, },
data() { data() {
return { return {

View File

@ -24,7 +24,7 @@
<script> <script>
import TabsList from "@/components/TabsList.vue"; import TabsList from "@/components/TabsList.vue";
import ListPageTemp from "@/components/ListPageTemp.vue"; import ListPageTemp from "./components/ListPageTemp.vue";
import AfterSaleOrderItem from "./components/AfterSaleOrderItem.vue"; import AfterSaleOrderItem from "./components/AfterSaleOrderItem.vue";
import { getShopOrderList } from '@/api/shop' import { getShopOrderList } from '@/api/shop'

View File

@ -0,0 +1,235 @@
<template>
<scroll-view class="list-template-wrapper" :scroll-y="!disableScroll" :refresher-enabled="true"
:refresher-triggered="refreshTriggered" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
<slot name="top" />
<view class="list-content" v-if="list.length > 0">
<view v-for="(item, index) in list" :key="item[idKey]" :class="{ left: index % 2 === 0 }"
class="flex-column-start news-item" @click="clickCell(item)">
<slot style="width: 100%" name="item" :data="{
...item,
...listExtraFields,
deleteSelect: !!item.deleteSelect,
}" />
</view>
<uni-load-more v-if="isLoading || (!isLoading && total && total === list.length)"
:status="isLoading ? 'loading' : 'nomore'"></uni-load-more>
</view>
<view v-else class="empty-container">
<image class="home-ration" mode="widthFix" :src="emptyImage || defaultEmptyImage" />
<text v-if="emptyText" class="empty-text">{{ emptyText }}</text>
</view>
<slot name="bottom" />
</scroll-view>
</template>
<script>
export default {
name: "ListPageTemp",
props: {
getDataPromise: {
type: Function,
},
// 格式化请求结果的方法
resultFormatFunc: {
type: Function,
},
requestData: {
defult: () => { },
type: Object,
},
// 列表元素额外字段
listExtraFields: {
type: Object,
default: () => { },
},
reloadFlag: {
default: 0,
type: Number,
},
// 是否分页
isPagiantion: {
type: Boolean,
default: true,
},
// 分页数量, 不分页时有效
pageSize: {
type: Number,
default: 999,
},
defaultList: {
type: Array,
default: () => [],
},
disableScroll: {
type: Boolean,
default: false,
},
idKey: {
type: String,
default: "id",
},
// 占位图片地址
emptyImage: {
type: String,
default: "",
},
// 空状态文本(传入时才显示)
emptyText: {
type: String,
default: "",
}
},
data() {
return {
isLoading: false,
total: 0,
list: [],
refreshTriggered: false,
p: 1,
num: 10,
defaultEmptyImage: 'https://activity.wagoo.live/empty.png', // 默认占位图片
};
},
watch: {
reloadFlag: {
handler(value) {
if (value) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
immediate: true,
},
requestData: {
handler(data) {
if (data) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
deep: true,
},
defaultList: {
handler(list) {
this.list = list;
this.$forceUpdate();
},
immediate: true,
},
},
methods: {
onRefresh() {
if (this.refreshTriggered) return;
this.refreshTriggered = true;
this.p = 1;
this.num = 10;
// this.list = [];
this.total = 0;
this.getList();
},
onLoadMore() {
if (!this.isPagiantion) {
return;
}
if (!this.isLoading && this.total > this.list.length) {
this.p++;
this.getList();
}
},
clickCell(item) {
this.$emit("clickCell", item);
},
getList() {
if (this.isLoading) return;
this.isLoading = true;
// TODO: 删除测试数据
if (!this.getDataPromise) {
this.list = [0, 1, 2, 3, 4, 5, 6].map((v, k) => {
return {
title: "消息名称0000sssss撒大苏打大苏打" + v,
id: k,
len: 10,
};
});
this.total = 9;
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
return;
}
const data = Object.assign(
{},
{ p: this.p, num: !this.isPagiantion ? this.pageSize : this.num },
this.requestData
);
this.getDataPromise(data)
.then((res) => {
const list =
this.p === 1
? res?.data || []
: [...this.list, ...(res?.data || [])];
this.list = this.resultFormatFunc
? this.resultFormatFunc(list)
: list;
this.total = res?.count || 0;
this.$emit("getList", this.list, res);
// console.log(this.list,'???')
})
.finally(() => {
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
});
},
},
};
</script>
<style lang="scss" scoped>
.list-template-wrapper {
height: 100%;
width: 100%;
.list-content {
width: 100%;
}
.news-item {
width: 100%;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 400rpx auto 0;
padding: 0 40rpx;
}
.home-ration {
width: 160px;
height: 174px;
display: block;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
text-align: center;
}
}
</style>

View File

@ -93,8 +93,8 @@
<view class="info-cell payment-method-cell"> <view class="info-cell payment-method-cell">
<view class="payment-item" @click.stop="selectOption1('1')"> <view class="payment-item" @click.stop="selectOption1('1')">
<view class="payment-left"> <view class="payment-left">
<image class="payment-icon" src="@/static/images/wx.png" mode="widthFix" /> <image class="payment-icon" src="@/static/images/douy.png" mode="widthFix" />
<text class="payment-text">微信支付</text> <text class="payment-text">抖音支付</text>
</view> </view>
<image class="payment-check" <image class="payment-check"
:src="selected2 ? require('@/static/images/cart_checked.png') : require('@/static/images/unchecked.png')" :src="selected2 ? require('@/static/images/cart_checked.png') : require('@/static/images/unchecked.png')"
@ -573,15 +573,12 @@
order_no:orderId.order_no order_no:orderId.order_no
}) })
.then((res) => { .then((res) => {
const payData = res?.data || {}; tt.pay({
console.log(payData,'--=') orderInfo: {
uni.requestPayment({ order_id:res.data.orderInfo.order_id,
provider: "wxpay", order_token:res.data.orderInfo.order_token,
timeStamp: payData.timeStamp, },
nonceStr: payData.nonceStr, service:5,
package: payData.package,
signType: payData.signType,
paySign: payData.paySign,
success: (res) => { success: (res) => {
console.log(res,'--==') console.log(res,'--==')
uni.hideLoading(); uni.hideLoading();

View File

@ -227,8 +227,8 @@
<view class="info-cell payment-method-cell" v-if="orderData.status === SHOP_ORDER_UNPAY"> <view class="info-cell payment-method-cell" v-if="orderData.status === SHOP_ORDER_UNPAY">
<view class="payment-item" @click.stop="selectOption1('1')"> <view class="payment-item" @click.stop="selectOption1('1')">
<view class="payment-left"> <view class="payment-left">
<image class="payment-icon" src="@/static/images/wx.png" mode="widthFix" /> <image class="payment-icon" src="@/static/images/douy.png" mode="widthFix" />
<text class="payment-text">微信支付</text> <text class="payment-text">抖音支付</text>
</view> </view>
<image class="payment-check" <image class="payment-check"
:src="selected2 ? require('@/static/images/cart_checked.png') : require('@/static/images/unchecked.png')" :src="selected2 ? require('@/static/images/cart_checked.png') : require('@/static/images/unchecked.png')"
@ -619,14 +619,12 @@
order_id:this.orderData.order_id, order_id:this.orderData.order_id,
order_no:this.orderData.order_no order_no:this.orderData.order_no
}).then((res) => { }).then((res) => {
const payData = res?.data || {}; tt.pay({
uni.requestPayment({ orderInfo: {
provider: "wxpay", order_id:res.data.orderInfo.order_id,
timeStamp: payData.timeStamp, order_token:res.data.orderInfo.order_token,
nonceStr: payData.nonceStr, },
package: payData.package, service:5,
signType: payData.signType,
paySign: payData.paySign,
success: (res) => { success: (res) => {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({

View File

@ -61,8 +61,8 @@
</view> </view>
<view class="wechat" @click.stop="selectOption1('1')"> <view class="wechat" @click.stop="selectOption1('1')">
<view class="select"> <view class="select">
<image class="w" src="@/static/images/wx.png" mode="widthFix" /> <image class="w" src="@/static/images/douy.png" mode="widthFix" />
<text class="x">微信</text> <text class="x">抖音</text>
</view> </view>
<image v-if="selected1" class="not-selected" src="@/static/images/w.png" mode="widthFix" /> <image v-if="selected1" class="not-selected" src="@/static/images/w.png" mode="widthFix" />
<image v-if="selected2" class="not-selected" src="@/static/images/y.png" mode="widthFix" /> <image v-if="selected2" class="not-selected" src="@/static/images/y.png" mode="widthFix" />
@ -99,7 +99,7 @@
<script> <script>
import TabsList from "@/components/TabsList.vue"; import TabsList from "@/components/TabsList.vue";
import ListPageTemp from "@/components/ListPageTemp.vue"; import ListPageTemp from "./components/ListPageTemp.vue";
import OrderItem from "./components/OrderItem.vue"; import OrderItem from "./components/OrderItem.vue";
import PopUpModal from "@/components/PopUpModal.vue"; import PopUpModal from "@/components/PopUpModal.vue";
import SuccessModal from "@/components/SuccessModal.vue"; import SuccessModal from "@/components/SuccessModal.vue";
@ -340,14 +340,12 @@ export default {
order_id: this.dataList.order_id, order_id: this.dataList.order_id,
order_no: this.dataList.order_no order_no: this.dataList.order_no
}).then((res) => { }).then((res) => {
const payData = res?.data || {}; tt.pay({
uni.requestPayment({ orderInfo: {
provider: "wxpay", order_id:res.data.orderInfo.order_id,
timeStamp: payData.timeStamp, order_token:res.data.orderInfo.order_token,
nonceStr: payData.nonceStr, },
package: payData.package, service:5,
signType: payData.signType,
paySign: payData.paySign,
success: (res) => { success: (res) => {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({

View File

@ -21,8 +21,8 @@
<view class="recharge-method"> <view class="recharge-method">
<view class="wechat"> <view class="wechat">
<view class="select"> <view class="select">
<image class="w" src="@/static/images/wx.png" mode="widthFix" /> <image class="w" src="@/static/images/douy.png" mode="widthFix" />
<text class="x">微信</text> <text class="x">抖音</text>
</view> </view>
<image v-if="selected1" class="not-selected" @click.stop="selectOption1('1')" src="@/static/images/w.png" <image v-if="selected1" class="not-selected" @click.stop="selectOption1('1')" src="@/static/images/w.png"
mode="widthFix" /> mode="widthFix" />
@ -212,14 +212,12 @@
if (this.WeChat == 1) { if (this.WeChat == 1) {
payOrder(this.dataList.order_id, PRICE_DIFF_TYPE_SERVICE) payOrder(this.dataList.order_id, PRICE_DIFF_TYPE_SERVICE)
.then((res) => { .then((res) => {
const payData = res?.info?.pay_data || {}; tt.pay({
uni.requestPayment({ orderInfo: {
provider: "wxpay", order_id:res.data.orderInfo.order_id,
timeStamp: payData.timeStamp, order_token:res.data.orderInfo.order_token,
nonceStr: payData.nonceStr, },
package: payData.package, service:5,
signType: payData.signType,
paySign: payData.sign,
success: (res) => { success: (res) => {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({

View File

@ -0,0 +1,235 @@
<template>
<scroll-view class="list-template-wrapper" :scroll-y="!disableScroll" :refresher-enabled="true"
:refresher-triggered="refreshTriggered" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
<slot name="top" />
<view class="list-content" v-if="list.length > 0">
<view v-for="(item, index) in list" :key="item[idKey]" :class="{ left: index % 2 === 0 }"
class="flex-column-start news-item" @click="clickCell(item)">
<slot style="width: 100%" name="item" :data="{
...item,
...listExtraFields,
deleteSelect: !!item.deleteSelect,
}" />
</view>
<uni-load-more v-if="isLoading || (!isLoading && total && total === list.length)"
:status="isLoading ? 'loading' : 'nomore'"></uni-load-more>
</view>
<view v-else class="empty-container">
<image class="home-ration" mode="widthFix" :src="emptyImage || defaultEmptyImage" />
<text v-if="emptyText" class="empty-text">{{ emptyText }}</text>
</view>
<slot name="bottom" />
</scroll-view>
</template>
<script>
export default {
name: "ListPageTemp",
props: {
getDataPromise: {
type: Function,
},
// 格式化请求结果的方法
resultFormatFunc: {
type: Function,
},
requestData: {
defult: () => { },
type: Object,
},
// 列表元素额外字段
listExtraFields: {
type: Object,
default: () => { },
},
reloadFlag: {
default: 0,
type: Number,
},
// 是否分页
isPagiantion: {
type: Boolean,
default: true,
},
// 分页数量, 不分页时有效
pageSize: {
type: Number,
default: 999,
},
defaultList: {
type: Array,
default: () => [],
},
disableScroll: {
type: Boolean,
default: false,
},
idKey: {
type: String,
default: "id",
},
// 占位图片地址
emptyImage: {
type: String,
default: "",
},
// 空状态文本(传入时才显示)
emptyText: {
type: String,
default: "",
}
},
data() {
return {
isLoading: false,
total: 0,
list: [],
refreshTriggered: false,
p: 1,
num: 10,
defaultEmptyImage: 'https://activity.wagoo.live/empty.png', // 默认占位图片
};
},
watch: {
reloadFlag: {
handler(value) {
if (value) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
immediate: true,
},
requestData: {
handler(data) {
if (data) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
deep: true,
},
defaultList: {
handler(list) {
this.list = list;
this.$forceUpdate();
},
immediate: true,
},
},
methods: {
onRefresh() {
if (this.refreshTriggered) return;
this.refreshTriggered = true;
this.p = 1;
this.num = 10;
// this.list = [];
this.total = 0;
this.getList();
},
onLoadMore() {
if (!this.isPagiantion) {
return;
}
if (!this.isLoading && this.total > this.list.length) {
this.p++;
this.getList();
}
},
clickCell(item) {
this.$emit("clickCell", item);
},
getList() {
if (this.isLoading) return;
this.isLoading = true;
// TODO: 删除测试数据
if (!this.getDataPromise) {
this.list = [0, 1, 2, 3, 4, 5, 6].map((v, k) => {
return {
title: "消息名称0000sssss撒大苏打大苏打" + v,
id: k,
len: 10,
};
});
this.total = 9;
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
return;
}
const data = Object.assign(
{},
{ p: this.p, num: !this.isPagiantion ? this.pageSize : this.num },
this.requestData
);
this.getDataPromise(data)
.then((res) => {
const list =
this.p === 1
? res?.data || []
: [...this.list, ...(res?.data || [])];
this.list = this.resultFormatFunc
? this.resultFormatFunc(list)
: list;
this.total = res?.count || 0;
this.$emit("getList", this.list, res);
// console.log(this.list,'???')
})
.finally(() => {
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
});
},
},
};
</script>
<style lang="scss" scoped>
.list-template-wrapper {
height: 100%;
width: 100%;
.list-content {
width: 100%;
}
.news-item {
width: 100%;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 400rpx auto 0;
padding: 0 40rpx;
}
.home-ration {
width: 160px;
height: 174px;
display: block;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
text-align: center;
}
}
</style>

View File

@ -32,8 +32,8 @@
<view class="recharge-method"> <view class="recharge-method">
<view class="wechat" @click.stop="selectOption1"> <view class="wechat" @click.stop="selectOption1">
<view class="select"> <view class="select">
<image class="w" src="@/static/images/wx.png" mode="widthFix" /> <image class="w" src="@/static/images/douy.png" mode="widthFix" />
<text class="x">微信</text> <text class="x">抖音</text>
</view> </view>
<image v-if="selected1" class="not-selected" <image v-if="selected1" class="not-selected"
src="@/static/images/w.png" mode="widthFix" /> src="@/static/images/w.png" mode="widthFix" />
@ -141,13 +141,12 @@
walletWxpay(data).then((res) => { walletWxpay(data).then((res) => {
uni.hideLoading(); uni.hideLoading();
// 使用获取的支付参数进行支付 // 使用获取的支付参数进行支付
uni.requestPayment({ tt.pay({
provider: 'wxpay', orderInfo: {
timeStamp: res.data.timeStamp, order_id:res.data.orderInfo.order_id,
nonceStr: res.data.nonceStr, order_token:res.data.orderInfo.order_token,
package: res.data.package, },
signType: res.data.signType, service:5,
paySign: res.data.paySign,
success: (payRes) => { success: (payRes) => {
uni.showToast({ uni.showToast({
title: '支付成功', title: '支付成功',

View File

@ -63,8 +63,8 @@
<view class="recharge-method"> <view class="recharge-method">
<view class="wechat" @click.stop="selectOption1"> <view class="wechat" @click.stop="selectOption1">
<view class="select"> <view class="select">
<image class="w" src="@/static/images/wx.png" mode="widthFix" /> <image class="w" src="@/static/images/douy.png" mode="widthFix" />
<text class="x">微信</text> <text class="x">抖音</text>
</view> </view>
<image v-if="selected1" class="not-selected" <image v-if="selected1" class="not-selected"
src="@/static/images/w.png" mode="widthFix" /> src="@/static/images/w.png" mode="widthFix" />
@ -188,12 +188,12 @@
}; };
walletWxpay(data).then((res) => { walletWxpay(data).then((res) => {
// 使用获取的支付参数进行支付 // 使用获取的支付参数进行支付
uni.requestPayment({ tt.pay({
timeStamp: res.data.timeStamp, // 确保这些字段都正确 orderInfo: {
nonceStr: res.data.nonceStr, order_id:res.data.orderInfo.order_id,
package: res.data.package, order_token:res.data.orderInfo.order_token,
signType: res.data.signType, },
paySign: res.data.paySign, service:5,
success: (res) => { success: (res) => {
this.buyService(this.user_id); this.buyService(this.user_id);
// console.log('支付成功:', res); // console.log('支付成功:', res);

View File

@ -37,7 +37,7 @@
<script> <script>
import TabsList from "@/components/TabsList.vue"; import TabsList from "@/components/TabsList.vue";
import ListPageTemp from "@/components/ListPageTemp.vue"; import ListPageTemp from "./components/ListPageTemp.vue";
import CouponItem from "@/components/coupon/CouponItem"; import CouponItem from "@/components/coupon/CouponItem";
import { getOwnCouponData } from "@/api/coupon"; import { getOwnCouponData } from "@/api/coupon";

View File

@ -34,7 +34,7 @@
</view> </view>
<view v-if="objNew.third_party_sn " class="rech-row"> <view v-if="objNew.third_party_sn " class="rech-row">
<text class="c">充值方式</text> <text class="c">充值方式</text>
<text v-if="objNew.payment_method == 1" class="m">微信</text> <text v-if="objNew.payment_method == 1" class="m">抖音</text>
<text v-else class="m">支付宝</text> <text v-else class="m">支付宝</text>
</view> </view>
<view class="rech-row"> <view class="rech-row">

View File

@ -0,0 +1,235 @@
<template>
<scroll-view class="list-template-wrapper" :scroll-y="!disableScroll" :refresher-enabled="true"
:refresher-triggered="refreshTriggered" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
<slot name="top" />
<view class="list-content" v-if="list.length > 0">
<view v-for="(item, index) in list" :key="item[idKey]" :class="{ left: index % 2 === 0 }"
class="flex-column-start news-item" @click="clickCell(item)">
<slot style="width: 100%" name="item" :data="{
...item,
...listExtraFields,
deleteSelect: !!item.deleteSelect,
}" />
</view>
<uni-load-more v-if="isLoading || (!isLoading && total && total === list.length)"
:status="isLoading ? 'loading' : 'nomore'"></uni-load-more>
</view>
<view v-else class="empty-container">
<image class="home-ration" mode="widthFix" :src="emptyImage || defaultEmptyImage" />
<text v-if="emptyText" class="empty-text">{{ emptyText }}</text>
</view>
<slot name="bottom" />
</scroll-view>
</template>
<script>
export default {
name: "ListPageTemp",
props: {
getDataPromise: {
type: Function,
},
// 格式化请求结果的方法
resultFormatFunc: {
type: Function,
},
requestData: {
defult: () => { },
type: Object,
},
// 列表元素额外字段
listExtraFields: {
type: Object,
default: () => { },
},
reloadFlag: {
default: 0,
type: Number,
},
// 是否分页
isPagiantion: {
type: Boolean,
default: true,
},
// 分页数量, 不分页时有效
pageSize: {
type: Number,
default: 999,
},
defaultList: {
type: Array,
default: () => [],
},
disableScroll: {
type: Boolean,
default: false,
},
idKey: {
type: String,
default: "id",
},
// 占位图片地址
emptyImage: {
type: String,
default: "",
},
// 空状态文本(传入时才显示)
emptyText: {
type: String,
default: "",
}
},
data() {
return {
isLoading: false,
total: 0,
list: [],
refreshTriggered: false,
p: 1,
num: 10,
defaultEmptyImage: 'https://activity.wagoo.live/empty.png', // 默认占位图片
};
},
watch: {
reloadFlag: {
handler(value) {
if (value) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
immediate: true,
},
requestData: {
handler(data) {
if (data) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
deep: true,
},
defaultList: {
handler(list) {
this.list = list;
this.$forceUpdate();
},
immediate: true,
},
},
methods: {
onRefresh() {
if (this.refreshTriggered) return;
this.refreshTriggered = true;
this.p = 1;
this.num = 10;
// this.list = [];
this.total = 0;
this.getList();
},
onLoadMore() {
if (!this.isPagiantion) {
return;
}
if (!this.isLoading && this.total > this.list.length) {
this.p++;
this.getList();
}
},
clickCell(item) {
this.$emit("clickCell", item);
},
getList() {
if (this.isLoading) return;
this.isLoading = true;
// TODO: 删除测试数据
if (!this.getDataPromise) {
this.list = [0, 1, 2, 3, 4, 5, 6].map((v, k) => {
return {
title: "消息名称0000sssss撒大苏打大苏打" + v,
id: k,
len: 10,
};
});
this.total = 9;
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
return;
}
const data = Object.assign(
{},
{ p: this.p, num: !this.isPagiantion ? this.pageSize : this.num },
this.requestData
);
this.getDataPromise(data)
.then((res) => {
const list =
this.p === 1
? res?.data || []
: [...this.list, ...(res?.data || [])];
this.list = this.resultFormatFunc
? this.resultFormatFunc(list)
: list;
this.total = res?.count || 0;
this.$emit("getList", this.list, res);
// console.log(this.list,'???')
})
.finally(() => {
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
});
},
},
};
</script>
<style lang="scss" scoped>
.list-template-wrapper {
height: 100%;
width: 100%;
.list-content {
width: 100%;
}
.news-item {
width: 100%;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 400rpx auto 0;
padding: 0 40rpx;
}
.home-ration {
width: 160px;
height: 174px;
display: block;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
text-align: center;
}
}
</style>

View File

@ -16,7 +16,7 @@
<script> <script>
import RemarkItem from "@/components/goods/RemarkItem.vue"; import RemarkItem from "@/components/goods/RemarkItem.vue";
import ListPageTemp from "@/components/ListPageTemp.vue"; import ListPageTemp from "./ListPageTemp.vue";
import { getRemarkList } from "@/api/shop"; import { getRemarkList } from "@/api/shop";
export default { export default {

View File

@ -220,14 +220,13 @@ export default {
// 支付 // 支付
pay(orderId) { pay(orderId) {
serviceCouponOrderPay(orderId).then((res) => { serviceCouponOrderPay(orderId).then((res) => {
const payData = res?.info?.pay_data || {};
uni.requestPayment({ tt.pay({
provider: "wxpay", orderInfo: {
timeStamp: payData.timeStamp, order_id:res.data.orderInfo.order_id,
nonceStr: payData.nonceStr, order_token:res.data.orderInfo.order_token,
package: payData.package, },
signType: payData.signType, service:5,
paySign: payData.sign,
success: (res) => { success: (res) => {
uni.hideLoading(); uni.hideLoading();
this.showSuccessModal = true; this.showSuccessModal = true;

View File

@ -47,8 +47,8 @@
<text class="zf"> 请选择支付方式 </text> <text class="zf"> 请选择支付方式 </text>
<view class="wechat"> <view class="wechat">
<view class="select"> <view class="select">
<image class="w" src="@/static/images/wx.png" mode="widthFix" /> <image class="w" src="@/static/images/douy.png" mode="widthFix" />
<text class="x">微信</text> <text class="x">抖音</text>
</view> </view>
<image <image
v-if="selected1" v-if="selected1"

View File

@ -202,8 +202,8 @@
<text class="zf"> 请选择支付方式 </text> <text class="zf"> 请选择支付方式 </text>
<view class="wechat" @click.stop="selectOption1('1')"> <view class="wechat" @click.stop="selectOption1('1')">
<view class="select"> <view class="select">
<image class="w" src="@/static/images/wx.png" mode="widthFix" /> <image class="w" src="@/static/images/douy.png" mode="widthFix" />
<text class="x">微信</text> <text class="x">抖音</text>
</view> </view>
<image v-if="selected1" class="not-selected" src="@/static/images/w.png" <image v-if="selected1" class="not-selected" src="@/static/images/w.png"
mode="widthFix" /> mode="widthFix" />
@ -471,12 +471,12 @@ export default {
}; };
memberWXPAY(data).then((res) => { memberWXPAY(data).then((res) => {
// 使用获取的支付参数进行支付 // 使用获取的支付参数进行支付
uni.requestPayment({ tt.pay({
timeStamp: res.data.timeStamp, // 确保这些字段都正确 orderInfo: {
nonceStr: res.data.nonceStr, order_id:res.data.orderInfo.order_id,
package: res.data.package, order_token:res.data.orderInfo.order_token,
signType: res.data.signType, },
paySign: res.data.paySign, service:5,
success: (res) => { success: (res) => {
uni.showToast({ uni.showToast({
title: "支付成功", title: "支付成功",

View File

@ -275,14 +275,12 @@ export default {
// 支付 // 支付
pay(orderId) { pay(orderId) {
serviceCouponOrderPay(orderId).then((res) => { serviceCouponOrderPay(orderId).then((res) => {
const payData = res?.info?.pay_data || {}; tt.pay({
uni.requestPayment({ orderInfo: {
provider: "wxpay", order_id:res.data.orderInfo.order_id,
timeStamp: payData.timeStamp, order_token:res.data.orderInfo.order_token,
nonceStr: payData.nonceStr, },
package: payData.package, service:5,
signType: payData.signType,
paySign: payData.sign,
success: (res) => { success: (res) => {
uni.hideLoading(); uni.hideLoading();
this.showSuccessModal = true this.showSuccessModal = true

BIN
src/static/images/douy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,795 @@
<template>
<uni-shadow-root class="wxml-to-canvas-index"><canvas v-if="use2dCanvas" id="weui-canvas" type="2d" :style="'width: '+(width)+'px; height: '+(height)+'px;'"></canvas>
<canvas v-else canvas-id="weui-canvas" :style="'width: '+(width)+'px; height: '+(height)+'px;'"></canvas></uni-shadow-root>
</template>
<script>
global['__wxVueOptions'] = {components:{}}
global['__wxRoute'] = 'wxml-to-canvas/index'
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else {
var a = factory();
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
}
})(window, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {
const hex = (color) => {
let result = null
if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
return color
// eslint-disable-next-line no-cond-assign
} else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
return '#' + result[2].split(',').map((part, index) => {
part = part.trim()
part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10)
part = part.toString(16)
if (part.length === 1) {
part = '0' + part
}
return part
}).join('')
} else {
return '#00000000'
}
}
const splitLineToCamelCase = (str) => str.split('-').map((part, index) => {
if (index === 0) {
return part
}
return part[0].toUpperCase() + part.slice(1)
}).join('')
const compareVersion = (v1, v2) => {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
module.exports = {
hex,
splitLineToCamelCase,
compareVersion
}
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
const xmlParse = __webpack_require__(2)
const {Widget} = __webpack_require__(3)
const {Draw} = __webpack_require__(5)
const {compareVersion} = __webpack_require__(0)
const canvasId = 'weui-canvas'
Component({
properties: {
width: {
type: Number,
value: 400
},
height: {
type: Number,
value: 300
}
},
data: {
use2dCanvas: false, // 2.9.2 后可用canvas 2d 接口
},
lifetimes: {
attached() {
const {SDKVersion, pixelRatio: dpr} = wx.getSystemInfoSync()
const use2dCanvas = compareVersion(SDKVersion, '2.9.2') >= 0
this.dpr = dpr
this.setData({use2dCanvas}, () => {
if (use2dCanvas) {
const query = this.createSelectorQuery()
query.select(`#${canvasId}`)
.fields({node: true, size: true})
.exec(res => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
this.ctx = ctx
this.canvas = canvas
})
} else {
this.ctx = wx.createCanvasContext(canvasId, this)
}
})
}
},
methods: {
async renderToCanvas(args) {
const {wxml, style} = args
const ctx = this.ctx
const canvas = this.canvas
const use2dCanvas = this.data.use2dCanvas
if (use2dCanvas && !canvas) {
return Promise.reject(new Error('renderToCanvas: fail canvas has not been created'))
}
ctx.clearRect(0, 0, this.data.width, this.data.height)
const {root: xom} = xmlParse(wxml)
const widget = new Widget(xom, style)
const container = widget.init()
this.boundary = {
top: container.layoutBox.top,
left: container.layoutBox.left,
width: container.computedStyle.width,
height: container.computedStyle.height,
}
const draw = new Draw(ctx, canvas, use2dCanvas)
await draw.drawNode(container)
if (!use2dCanvas) {
await this.canvasDraw(ctx)
}
return Promise.resolve(container)
},
canvasDraw(ctx, reserve) {
return new Promise(resolve => {
ctx.draw(reserve, () => {
resolve()
})
})
},
canvasToTempFilePath(args = {}) {
const use2dCanvas = this.data.use2dCanvas
return new Promise((resolve, reject) => {
const {
top, left, width, height
} = this.boundary
const copyArgs = {
x: left,
y: top,
width,
height,
destWidth: width * this.dpr,
destHeight: height * this.dpr,
canvasId,
fileType: args.fileType || 'png',
quality: args.quality || 1,
success: resolve,
fail: reject
}
if (use2dCanvas) {
delete copyArgs.canvasId
copyArgs.canvas = this.canvas
}
wx.canvasToTempFilePath(copyArgs, this)
})
}
}
})
/***/ }),
/* 2 */
/***/ (function(module, exports) {
/**
* Module dependencies.
*/
/**
* Expose `parse`.
*/
/**
* Parse the given string of `xml`.
*
* @param {String} xml
* @return {Object}
* @api public
*/
function parse(xml) {
xml = xml.trim()
// strip comments
xml = xml.replace(/<!--[\s\S]*?-->/g, '')
return document()
/**
* XML document.
*/
function document() {
return {
declaration: declaration(),
root: tag()
}
}
/**
* Declaration.
*/
function declaration() {
const m = match(/^<\?xml\s*/)
if (!m) return
// tag
const node = {
attributes: {}
}
// attributes
while (!(eos() || is('?>'))) {
const attr = attribute()
if (!attr) return node
node.attributes[attr.name] = attr.value
}
match(/\?>\s*/)
return node
}
/**
* Tag.
*/
function tag() {
const m = match(/^<([\w-:.]+)\s*/)
if (!m) return
// name
const node = {
name: m[1],
attributes: {},
children: []
}
// attributes
while (!(eos() || is('>') || is('?>') || is('/>'))) {
const attr = attribute()
if (!attr) return node
node.attributes[attr.name] = attr.value
}
// self closing tag
if (match(/^\s*\/>\s*/)) {
return node
}
match(/\??>\s*/)
// content
node.content = content()
// children
let child
while (child = tag()) {
node.children.push(child)
}
// closing
match(/^<\/[\w-:.]+>\s*/)
return node
}
/**
* Text content.
*/
function content() {
const m = match(/^([^<]*)/)
if (m) return m[1]
return ''
}
/**
* Attribute.
*/
function attribute() {
const m = match(/([\w:-]+)\s*=\s*("[^"]*"|'[^']*'|\w+)\s*/)
if (!m) return
return {name: m[1], value: strip(m[2])}
}
/**
* Strip quotes from `val`.
*/
function strip(val) {
return val.replace(/^['"]|['"]$/g, '')
}
/**
* Match `re` and advance the string.
*/
function match(re) {
const m = xml.match(re)
if (!m) return
xml = xml.slice(m[0].length)
return m
}
/**
* End-of-source.
*/
function eos() {
return xml.length == 0
}
/**
* Check for `prefix`.
*/
function is(prefix) {
return xml.indexOf(prefix) == 0
}
}
module.exports = parse
/***/ }),
/* 3 */
/***/ (function(module, exports, __webpack_require__) {
const Block = __webpack_require__(4)
const {splitLineToCamelCase} = __webpack_require__(0)
class Element extends Block {
constructor(prop) {
super(prop.style)
this.name = prop.name
this.attributes = prop.attributes
}
}
class Widget {
constructor(xom, style) {
this.xom = xom
this.style = style
this.inheritProps = ['fontSize', 'lineHeight', 'textAlign', 'verticalAlign', 'color']
}
init() {
this.container = this.create(this.xom)
this.container.layout()
this.inheritStyle(this.container)
return this.container
}
// 继承父节点的样式
inheritStyle(node) {
const parent = node.parent || null
const children = node.children || {}
const computedStyle = node.computedStyle
if (parent) {
this.inheritProps.forEach(prop => {
computedStyle[prop] = computedStyle[prop] || parent.computedStyle[prop]
})
}
Object.values(children).forEach(child => {
this.inheritStyle(child)
})
}
create(node) {
let classNames = (node.attributes.class || '').split(' ')
classNames = classNames.map(item => splitLineToCamelCase(item.trim()))
const style = {}
classNames.forEach(item => {
Object.assign(style, this.style[item] || {})
})
const args = {name: node.name, style}
const attrs = Object.keys(node.attributes)
const attributes = {}
for (const attr of attrs) {
const value = node.attributes[attr]
const CamelAttr = splitLineToCamelCase(attr)
if (value === '' || value === 'true') {
attributes[CamelAttr] = true
} else if (value === 'false') {
attributes[CamelAttr] = false
} else {
attributes[CamelAttr] = value
}
}
attributes.text = node.content
args.attributes = attributes
const element = new Element(args)
node.children.forEach(childNode => {
const childElement = this.create(childNode)
element.add(childElement)
})
return element
}
}
module.exports = {Widget}
/***/ }),
/* 4 */
/***/ (function(module, exports) {
module.exports = require("../widget-ui/index.js")
/***/ }),
/* 5 */
/***/ (function(module, exports) {
class Draw {
constructor(context, canvas, use2dCanvas = false) {
this.ctx = context
this.canvas = canvas || null
this.use2dCanvas = use2dCanvas
}
roundRect(x, y, w, h, r, fill = true, stroke = false) {
if (r < 0) return
const ctx = this.ctx
ctx.beginPath()
ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2)
ctx.arc(x + w - r, y + r, r, Math.PI * 3 / 2, 0)
ctx.arc(x + w - r, y + h - r, r, 0, Math.PI / 2)
ctx.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI)
ctx.lineTo(x, y + r)
if (stroke) ctx.stroke()
if (fill) ctx.fill()
}
drawView(box, style) {
const ctx = this.ctx
const {
left: x, top: y, width: w, height: h
} = box
const {
borderRadius = 0,
borderWidth = 0,
borderColor,
color = '#000',
backgroundColor = 'transparent',
} = style
ctx.save()
// 外环
if (borderWidth > 0) {
ctx.fillStyle = borderColor || color
this.roundRect(x, y, w, h, borderRadius)
}
// 内环
ctx.fillStyle = backgroundColor
const innerWidth = w - 2 * borderWidth
const innerHeight = h - 2 * borderWidth
const innerRadius = borderRadius - borderWidth >= 0 ? borderRadius - borderWidth : 0
this.roundRect(x + borderWidth, y + borderWidth, innerWidth, innerHeight, innerRadius)
ctx.restore()
}
async drawImage(img, box, style) {
await new Promise((resolve, reject) => {
const ctx = this.ctx
const canvas = this.canvas
const {
borderRadius = 0
} = style
const {
left: x, top: y, width: w, height: h
} = box
ctx.save()
this.roundRect(x, y, w, h, borderRadius, false, false)
ctx.clip()
const _drawImage = (img) => {
if (this.use2dCanvas) {
const Image = canvas.createImage()
Image.onload = () => {
ctx.drawImage(Image, x, y, w, h)
ctx.restore()
resolve()
}
Image.onerror = () => { reject(new Error(`createImage fail: ${img}`)) }
Image.src = img
} else {
ctx.drawImage(img, x, y, w, h)
ctx.restore()
resolve()
}
}
const isTempFile = /^wxfile:\/\//.test(img)
const isNetworkFile = /^https?:\/\//.test(img)
if (isTempFile) {
_drawImage(img)
} else if (isNetworkFile) {
wx.downloadFile({
url: img,
success(res) {
if (res.statusCode === 200) {
_drawImage(res.tempFilePath)
} else {
reject(new Error(`downloadFile:fail ${img}`))
}
},
fail() {
reject(new Error(`downloadFile:fail ${img}`))
}
})
} else {
reject(new Error(`image format error: ${img}`))
}
})
}
// eslint-disable-next-line complexity
drawText(text, box, style) {
const ctx = this.ctx
let {
left: x, top: y, width: w, height: h
} = box
let {
color = '#000',
lineHeight = '1.4em',
fontSize = 14,
textAlign = 'left',
verticalAlign = 'top',
backgroundColor = 'transparent'
} = style
if (typeof lineHeight === 'string') { // 2em
lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize)
}
if (!text || (lineHeight > h)) return
ctx.save()
ctx.textBaseline = 'top'
ctx.font = `${fontSize}px sans-serif`
ctx.textAlign = textAlign
// 背景色
ctx.fillStyle = backgroundColor
this.roundRect(x, y, w, h, 0)
// 文字颜色
ctx.fillStyle = color
// 水平布局
switch (textAlign) {
case 'left':
break
case 'center':
x += 0.5 * w
break
case 'right':
x += w
break
default: break
}
const textWidth = ctx.measureText(text).width
const actualHeight = Math.ceil(textWidth / w) * lineHeight
let paddingTop = Math.ceil((h - actualHeight) / 2)
if (paddingTop < 0) paddingTop = 0
// 垂直布局
switch (verticalAlign) {
case 'top':
break
case 'middle':
y += paddingTop
break
case 'bottom':
y += 2 * paddingTop
break
default: break
}
const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)
// 不超过一行
if (textWidth <= w) {
ctx.fillText(text, x, y + inlinePaddingTop)
return
}
// 多行文本
const chars = text.split('')
const _y = y
// 逐行绘制
let line = ''
for (const ch of chars) {
const testLine = line + ch
const testWidth = ctx.measureText(testLine).width
if (testWidth > w) {
ctx.fillText(line, x, y + inlinePaddingTop)
y += lineHeight
line = ch
if ((y + lineHeight) > (_y + h)) break
} else {
line = testLine
}
}
// 避免溢出
if ((y + lineHeight) <= (_y + h)) {
ctx.fillText(line, x, y + inlinePaddingTop)
}
ctx.restore()
}
async drawNode(element) {
const {layoutBox, computedStyle, name} = element
const {src, text} = element.attributes
if (name === 'view') {
this.drawView(layoutBox, computedStyle)
} else if (name === 'image') {
await this.drawImage(src, layoutBox, computedStyle)
} else if (name === 'text') {
this.drawText(text, layoutBox, computedStyle)
}
const childs = Object.values(element.children)
for (const child of childs) {
await this.drawNode(child)
}
}
}
module.exports = {
Draw
}
/***/ })
/******/ ]);
});
export default global['__wxComponents']['wxml-to-canvas/index']
</script>
<style platform="mp-weixin">
</style>

View File

@ -0,0 +1,794 @@
<template>
<uni-shadow-root class="wxml-to-canvas-miniprogram_dist-index"><canvas v-if="use2dCanvas" id="weui-canvas" type="2d" :style="'width: '+(width)+'px; height: '+(height)+'px;'"></canvas>
<canvas v-else canvas-id="weui-canvas" :style="'width: '+(width)+'px; height: '+(height)+'px;'"></canvas></uni-shadow-root>
</template>
<script>
global['__wxVueOptions'] = {components:{}}
global['__wxRoute'] = 'wxml-to-canvas/miniprogram_dist/index'
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else {
var a = factory();
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
}
})(window, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {
const hex = (color) => {
let result = null
if (/^#/.test(color) && (color.length === 7 || color.length === 9)) {
return color
// eslint-disable-next-line no-cond-assign
} else if ((result = /^(rgb|rgba)\((.+)\)/.exec(color)) !== null) {
return '#' + result[2].split(',').map((part, index) => {
part = part.trim()
part = index === 3 ? Math.floor(parseFloat(part) * 255) : parseInt(part, 10)
part = part.toString(16)
if (part.length === 1) {
part = '0' + part
}
return part
}).join('')
} else {
return '#00000000'
}
}
const splitLineToCamelCase = (str) => str.split('-').map((part, index) => {
if (index === 0) {
return part
}
return part[0].toUpperCase() + part.slice(1)
}).join('')
const compareVersion = (v1, v2) => {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
module.exports = {
hex,
splitLineToCamelCase,
compareVersion
}
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
const xmlParse = __webpack_require__(2)
const {Widget} = __webpack_require__(3)
const {Draw} = __webpack_require__(5)
const {compareVersion} = __webpack_require__(0)
const canvasId = 'weui-canvas'
Component({
properties: {
width: {
type: Number,
value: 400
},
height: {
type: Number,
value: 300
}
},
data: {
use2dCanvas: false, // 2.9.2 后可用canvas 2d 接口
},
lifetimes: {
attached() {
const {SDKVersion, pixelRatio: dpr} = wx.getSystemInfoSync()
const use2dCanvas = compareVersion(SDKVersion, '2.9.2') >= 0
this.dpr = dpr
this.setData({use2dCanvas}, () => {
if (use2dCanvas) {
const query = this.createSelectorQuery()
query.select(`#${canvasId}`)
.fields({node: true, size: true})
.exec(res => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
this.ctx = ctx
this.canvas = canvas
})
} else {
this.ctx = wx.createCanvasContext(canvasId, this)
}
})
}
},
methods: {
async renderToCanvas(args) {
const {wxml, style} = args
const ctx = this.ctx
const canvas = this.canvas
const use2dCanvas = this.data.use2dCanvas
if (use2dCanvas && !canvas) {
return Promise.reject(new Error('renderToCanvas: fail canvas has not been created'))
}
ctx.clearRect(0, 0, this.data.width, this.data.height)
const {root: xom} = xmlParse(wxml)
const widget = new Widget(xom, style)
const container = widget.init()
this.boundary = {
top: container.layoutBox.top,
left: container.layoutBox.left,
width: container.computedStyle.width,
height: container.computedStyle.height,
}
const draw = new Draw(ctx, canvas, use2dCanvas)
await draw.drawNode(container)
if (!use2dCanvas) {
await this.canvasDraw(ctx)
}
return Promise.resolve(container)
},
canvasDraw(ctx, reserve) {
return new Promise(resolve => {
ctx.draw(reserve, () => {
resolve()
})
})
},
canvasToTempFilePath(args = {}) {
const use2dCanvas = this.data.use2dCanvas
return new Promise((resolve, reject) => {
const {
top, left, width, height
} = this.boundary
const copyArgs = {
x: left,
y: top,
width,
height,
destWidth: width * this.dpr,
destHeight: height * this.dpr,
canvasId,
fileType: args.fileType || 'png',
quality: args.quality || 1,
success: resolve,
fail: reject
}
if (use2dCanvas) {
delete copyArgs.canvasId
copyArgs.canvas = this.canvas
}
wx.canvasToTempFilePath(copyArgs, this)
})
}
}
})
/***/ }),
/* 2 */
/***/ (function(module, exports) {
/**
* Module dependencies.
*/
/**
* Expose `parse`.
*/
/**
* Parse the given string of `xml`.
*
* @param {String} xml
* @return {Object}
* @api public
*/
function parse(xml) {
xml = xml.trim()
// strip comments
xml = xml.replace(/<!--[\s\S]*?-->/g, '')
return document()
/**
* XML document.
*/
function document() {
return {
declaration: declaration(),
root: tag()
}
}
/**
* Declaration.
*/
function declaration() {
const m = match(/^<\?xml\s*/)
if (!m) return
// tag
const node = {
attributes: {}
}
// attributes
while (!(eos() || is('?>'))) {
const attr = attribute()
if (!attr) return node
node.attributes[attr.name] = attr.value
}
match(/\?>\s*/)
return node
}
/**
* Tag.
*/
function tag() {
const m = match(/^<([\w-:.]+)\s*/)
if (!m) return
// name
const node = {
name: m[1],
attributes: {},
children: []
}
// attributes
while (!(eos() || is('>') || is('?>') || is('/>'))) {
const attr = attribute()
if (!attr) return node
node.attributes[attr.name] = attr.value
}
// self closing tag
if (match(/^\s*\/>\s*/)) {
return node
}
match(/\??>\s*/)
// content
node.content = content()
// children
let child
while (child = tag()) {
node.children.push(child)
}
// closing
match(/^<\/[\w-:.]+>\s*/)
return node
}
/**
* Text content.
*/
function content() {
const m = match(/^([^<]*)/)
if (m) return m[1]
return ''
}
/**
* Attribute.
*/
function attribute() {
const m = match(/([\w:-]+)\s*=\s*("[^"]*"|'[^']*'|\w+)\s*/)
if (!m) return
return {name: m[1], value: strip(m[2])}
}
/**
* Strip quotes from `val`.
*/
function strip(val) {
return val.replace(/^['"]|['"]$/g, '')
}
/**
* Match `re` and advance the string.
*/
function match(re) {
const m = xml.match(re)
if (!m) return
xml = xml.slice(m[0].length)
return m
}
/**
* End-of-source.
*/
function eos() {
return xml.length == 0
}
/**
* Check for `prefix`.
*/
function is(prefix) {
return xml.indexOf(prefix) == 0
}
}
module.exports = parse
/***/ }),
/* 3 */
/***/ (function(module, exports, __webpack_require__) {
const Block = __webpack_require__(4)
const {splitLineToCamelCase} = __webpack_require__(0)
class Element extends Block {
constructor(prop) {
super(prop.style)
this.name = prop.name
this.attributes = prop.attributes
}
}
class Widget {
constructor(xom, style) {
this.xom = xom
this.style = style
this.inheritProps = ['fontSize', 'lineHeight', 'textAlign', 'verticalAlign', 'color']
}
init() {
this.container = this.create(this.xom)
this.container.layout()
this.inheritStyle(this.container)
return this.container
}
// 继承父节点的样式
inheritStyle(node) {
const parent = node.parent || null
const children = node.children || {}
const computedStyle = node.computedStyle
if (parent) {
this.inheritProps.forEach(prop => {
computedStyle[prop] = computedStyle[prop] || parent.computedStyle[prop]
})
}
Object.values(children).forEach(child => {
this.inheritStyle(child)
})
}
create(node) {
let classNames = (node.attributes.class || '').split(' ')
classNames = classNames.map(item => splitLineToCamelCase(item.trim()))
const style = {}
classNames.forEach(item => {
Object.assign(style, this.style[item] || {})
})
const args = {name: node.name, style}
const attrs = Object.keys(node.attributes)
const attributes = {}
for (const attr of attrs) {
const value = node.attributes[attr]
const CamelAttr = splitLineToCamelCase(attr)
if (value === '' || value === 'true') {
attributes[CamelAttr] = true
} else if (value === 'false') {
attributes[CamelAttr] = false
} else {
attributes[CamelAttr] = value
}
}
attributes.text = node.content
args.attributes = attributes
const element = new Element(args)
node.children.forEach(childNode => {
const childElement = this.create(childNode)
element.add(childElement)
})
return element
}
}
module.exports = {Widget}
/***/ }),
/* 4 */
/***/ (function(module, exports) {
module.exports = require("widget-ui");
/***/ }),
/* 5 */
/***/ (function(module, exports) {
class Draw {
constructor(context, canvas, use2dCanvas = false) {
this.ctx = context
this.canvas = canvas || null
this.use2dCanvas = use2dCanvas
}
roundRect(x, y, w, h, r, fill = true, stroke = false) {
if (r < 0) return
const ctx = this.ctx
ctx.beginPath()
ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2)
ctx.arc(x + w - r, y + r, r, Math.PI * 3 / 2, 0)
ctx.arc(x + w - r, y + h - r, r, 0, Math.PI / 2)
ctx.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI)
ctx.lineTo(x, y + r)
if (stroke) ctx.stroke()
if (fill) ctx.fill()
}
drawView(box, style) {
const ctx = this.ctx
const {
left: x, top: y, width: w, height: h
} = box
const {
borderRadius = 0,
borderWidth = 0,
borderColor,
color = '#000',
backgroundColor = 'transparent',
} = style
ctx.save()
// 外环
if (borderWidth > 0) {
ctx.fillStyle = borderColor || color
this.roundRect(x, y, w, h, borderRadius)
}
// 内环
ctx.fillStyle = backgroundColor
const innerWidth = w - 2 * borderWidth
const innerHeight = h - 2 * borderWidth
const innerRadius = borderRadius - borderWidth >= 0 ? borderRadius - borderWidth : 0
this.roundRect(x + borderWidth, y + borderWidth, innerWidth, innerHeight, innerRadius)
ctx.restore()
}
async drawImage(img, box, style) {
await new Promise((resolve, reject) => {
const ctx = this.ctx
const canvas = this.canvas
const {
borderRadius = 0
} = style
const {
left: x, top: y, width: w, height: h
} = box
ctx.save()
this.roundRect(x, y, w, h, borderRadius, false, false)
ctx.clip()
const _drawImage = (img) => {
if (this.use2dCanvas) {
const Image = canvas.createImage()
Image.onload = () => {
ctx.drawImage(Image, x, y, w, h)
ctx.restore()
resolve()
}
Image.onerror = () => { reject(new Error(`createImage fail: ${img}`)) }
Image.src = img
} else {
ctx.drawImage(img, x, y, w, h)
ctx.restore()
resolve()
}
}
const isTempFile = /^wxfile:\/\//.test(img)
const isNetworkFile = /^https?:\/\//.test(img)
if (isTempFile) {
_drawImage(img)
} else if (isNetworkFile) {
wx.downloadFile({
url: img,
success(res) {
if (res.statusCode === 200) {
_drawImage(res.tempFilePath)
} else {
reject(new Error(`downloadFile:fail ${img}`))
}
},
fail() {
reject(new Error(`downloadFile:fail ${img}`))
}
})
} else {
reject(new Error(`image format error: ${img}`))
}
})
}
// eslint-disable-next-line complexity
drawText(text, box, style) {
const ctx = this.ctx
let {
left: x, top: y, width: w, height: h
} = box
let {
color = '#000',
lineHeight = '1.4em',
fontSize = 14,
textAlign = 'left',
verticalAlign = 'top',
backgroundColor = 'transparent'
} = style
if (typeof lineHeight === 'string') { // 2em
lineHeight = Math.ceil(parseFloat(lineHeight.replace('em')) * fontSize)
}
if (!text || (lineHeight > h)) return
ctx.save()
ctx.textBaseline = 'top'
ctx.font = `${fontSize}px sans-serif`
ctx.textAlign = textAlign
// 背景色
ctx.fillStyle = backgroundColor
this.roundRect(x, y, w, h, 0)
// 文字颜色
ctx.fillStyle = color
// 水平布局
switch (textAlign) {
case 'left':
break
case 'center':
x += 0.5 * w
break
case 'right':
x += w
break
default: break
}
const textWidth = ctx.measureText(text).width
const actualHeight = Math.ceil(textWidth / w) * lineHeight
let paddingTop = Math.ceil((h - actualHeight) / 2)
if (paddingTop < 0) paddingTop = 0
// 垂直布局
switch (verticalAlign) {
case 'top':
break
case 'middle':
y += paddingTop
break
case 'bottom':
y += 2 * paddingTop
break
default: break
}
const inlinePaddingTop = Math.ceil((lineHeight - fontSize) / 2)
// 不超过一行
if (textWidth <= w) {
ctx.fillText(text, x, y + inlinePaddingTop)
return
}
// 多行文本
const chars = text.split('')
const _y = y
// 逐行绘制
let line = ''
for (const ch of chars) {
const testLine = line + ch
const testWidth = ctx.measureText(testLine).width
if (testWidth > w) {
ctx.fillText(line, x, y + inlinePaddingTop)
y += lineHeight
line = ch
if ((y + lineHeight) > (_y + h)) break
} else {
line = testLine
}
}
// 避免溢出
if ((y + lineHeight) <= (_y + h)) {
ctx.fillText(line, x, y + inlinePaddingTop)
}
ctx.restore()
}
async drawNode(element) {
const {layoutBox, computedStyle, name} = element
const {src, text} = element.attributes
if (name === 'view') {
this.drawView(layoutBox, computedStyle)
} else if (name === 'image') {
await this.drawImage(src, layoutBox, computedStyle)
} else if (name === 'text') {
this.drawText(text, layoutBox, computedStyle)
}
const childs = Object.values(element.children)
for (const child of childs) {
await this.drawNode(child)
}
}
}
module.exports = {
Draw
}
/***/ })
/******/ ]);
});
export default global['__wxComponents']['wxml-to-canvas/miniprogram_dist/index']
</script>
<style platform="mp-weixin">
</style>