87d7b854 by 华明祺

feat(personal): 完善个人会员申请及支付流程

- 协议勾选区域固定在页面底部,优化用户体验
- 联系方式改为非必填,但填写时验证手机号格式
- 支付逻辑使用 await-to-js 重构,统一错误处理
- 支付流程添加 loading 状态,防止重复提交
- 支付成功后传递 orderId,跳转时获取并展示订单详情
- 支付成功页面优化:标签不换行、值支持自动换行
- 新增获取订单详情接口 /common/order/{orderId}
1 parent 7d54d38d
......@@ -195,11 +195,11 @@ export function regionsList(params) {
export function carUrl(data, type) {
return uni.uploadFile({
url: `${config.baseUrl_api}/person/info/getPersonInfoFromCert/${type}`,
header: {
'Authorization': uni.getStorageSync('token'),
'Content-Language': 'zh_CN',
'Accept-Language': 'zh-CN,zh',
},
// header: {
// 'Authorization': uni.getStorageSync('token'),
// 'Content-Language': 'zh_CN',
// 'Accept-Language': 'zh-CN,zh',
// },
name: 'pic',
filePath: data
}).then(res => {
......@@ -1415,3 +1415,11 @@ export function createMemberPayRange(data) {
data
})
}
// 获取订单详情
export function getOrderInfo(orderId) {
return request({
url: `/common/order/${orderId}`,
method: 'get'
})
}
\ No newline at end of file
......
{
"dependencies": {
"await-to-js": "^3.0.0",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.6",
"lodash": "^4.17.21",
......
......@@ -53,7 +53,7 @@
</uni-forms-item>
<uni-forms-item label="所在地区">
<!-- <uni-forms-item label="所在地区">
<uni-data-picker class="fixUniFormItemStyle" v-model="baseFormData.cityId"
:localdata="regionsList" popup-title="请选择所在地区"></uni-data-picker>
</uni-forms-item>
......@@ -65,10 +65,12 @@
<uni-file-picker v-model="photoArr" @delete="delPhoto" return-type="object" limit="1"
@select="upPhoto" :del-ico="false" :image-styles="imageStylesTx"></uni-file-picker>
<image mode="aspectFill" v-if="baseFormData.photo2" style="height:200rpx;width:200rpx;" :src="config.baseUrl_api + baseFormData.photo2"/>
</uni-forms-item>
</uni-forms-item> -->
</view>
</uni-forms>
</view>
</view>
<view class="fixed-agreeline">
<view class="agreeline">
<image @click="changeAgree(agree)" v-if="agree"
:src="config.baseUrl_api+'/fs/static/login/xz_dwn@2x.png'"></image>
......@@ -80,7 +82,8 @@
<view class="fixedBottom"><button class="btn-red" @click="goSubmit">确 定</button></view>
<!-- 会员须知 -->
<uni-popup ref="popup" type="bottom" background-color="#fff" animation :disable-scroll="true" :mask-click="false">
<uni-popup ref="popup" type="bottom" background-color="#fff" animation :disable-scroll="true"
:mask-click="false">
<view class="tt">入会须知</view>
<view class="popBody">
_{{baseFormData.name}}_欢迎您申请成为中国跆拳道协会(以下简称中国跆协)会员,请确保本次申请是经过您本人或监护人授权同意后的自愿行为,请您务必仔细阅读本入会须知。
......@@ -150,13 +153,15 @@
value: '1',
text: "来往大陆(内地)通行证"
},
// {
// value: '3',
// text: "护照"
// },
{
value: '3',
text: "护照"
}, {
value: '4',
text: '户口本'
}, {
},
{
value: '5',
text: '香港身份证'
}
......@@ -200,7 +205,7 @@
}
}
// console.log(current.value,option.tab)
getRegionsList()
// getRegionsList()
})
function getRegionsList() {
......@@ -246,33 +251,26 @@
title: '加载中'
});
baseFormData.value.card = e.tempFiles;
// console.log(e)
// const formData = new FormData()
// formData.append('pic', e.tempFiles[0].file)
api.carUrl(e.tempFilePaths[0], baseFormData.value.idcType).then(res => {
console.log(res)
uni.hideLoading()
if (res.data) {
baseFormData.value.sex = res.data.sex
baseFormData.value.birth = res.data.birth
baseFormData.value.idcCode = res.data.code
baseFormData.value.name = res.data.name
baseFormData.value.uuid = res.data.uuid
baseFormData.value.cityId = res.data.cityId
baseFormData.value.address = res.data.address
photoArr.value = {}
getExtractInfo({
idcCode: baseFormData.value.idcCode,
idcType: baseFormData.value.idcType,
perType: baseFormData.value.perType
})
// baseFormData.value.cityId = res.data.cityId
// baseFormData.value.address = res.data.address
} else {
uni.hideLoading()
uni.showModal({
content: res.msg,
success: function(modalRes) {
}
uni.showToast({
title: res.msg,
duration: 2000,
icon: 'none'
})
}
......@@ -297,7 +295,7 @@
baseFormData.value.photo = data.data.fang;
baseFormData.value.photo2 = data.data.yuan;
photoArr.value = {
url: config.baseUrl_api+baseFormData.value.photo,
url: config.baseUrl_api + baseFormData.value.photo,
name: '头像',
extname: 'jpg'
}
......@@ -352,8 +350,8 @@
baseFormData.value.birth = res.data.birth
baseFormData.value.name = res.data.name
baseFormData.value.phone = res.data.phone
baseFormData.value.cityId = res.data.cityId
baseFormData.value.address = res.data.address
// baseFormData.value.cityId = res.data.cityId
// baseFormData.value.address = res.data.address
if (res.data.photo) {
console.log(res.data.photo)
if (res.data.photo.indexOf('http') == -1) {
......@@ -398,6 +396,10 @@
function giveBirthDay() {
if (!baseFormData.value.idcCode) {
return
}
// 判断身份证正确性/赋值生日
if (baseFormData.value.idcType == 0) {
if (!(/(^\d{15}$)|(^\d{17}([0-9]|X)$)/.test(baseFormData.value.idcCode))) {
......@@ -480,21 +482,33 @@
})
return
}
console.log(baseFormData.value.photo)
if (baseFormData.value.photo == '' || baseFormData.value.photo == undefined || !baseFormData.value.photo) {
if (baseFormData.value.phone) {
const phoneReg = /^1[3-9]\d{9}$/
if (!phoneReg.test(baseFormData.value.phone)) {
uni.showToast({
title: `请上传头像`,
title: '请输入正确的联系方式',
icon: 'none'
})
return
}
}
// if (baseFormData.value.photo == '' || baseFormData.value.photo == undefined || !baseFormData.value.photo) {
// uni.showToast({
// title: `请上传头像`,
// icon: 'none'
// })
// return
// }
//信息确认弹出
uni.showModal({
content: '请确认信息正确',
success: function(res) {
if (res.confirm) {
if(baseFormData.value.idcType=='4'){
baseFormData.value.idcType='0'
if (baseFormData.value.idcType == '4') {
baseFormData.value.idcType = '0'
}
delete baseFormData.value.card
......@@ -547,18 +561,17 @@
}
});
}
function getUserInfo() {
api.getInfo(perId.value).then(res => {
baseFormData.value = res.data
if (baseFormData.areaAssName) baseFormData.ancestorNameList = baseFormData.value.ancestorNameList.join(
',').replaceAll(',',
'/')
',').replaceAll(',', '/')
})
}
</script>
<style lang="scss">
/* 字段名左对齐 */
.uni-forms-item .uni-forms-item__label {
text-align: left !important;
......@@ -587,11 +600,10 @@
/* 文本内容右对齐 */
.uni-forms-item .uni-forms-item__content text,
.uni-forms-item .uni-forms-item__content > text {
.uni-forms-item .uni-forms-item__content>text {
display: inline-block !important;
white-space: nowrap !important;
}
</style>
<style lang="scss" scoped>
......@@ -603,9 +615,11 @@
right: 0;
bottom: 0;
}
:deep(.uni-popup) {
overflow: hidden !important;
}
:deep(.segmented-control) {
height: 100rpx;
}
......@@ -636,13 +650,24 @@
}
}
.hasfixedbottom {
padding-bottom: 200rpx;
}
.fixed-agreeline {
position: fixed;
bottom: 150rpx;
left: 0;
right: 0;
z-index: 1;
}
.agreeline {
padding: 20rpx 40rpx;
box-sizing: border-box;
display: flex;
font-size: 30rpx;
text {
color: #014A9F;
}
......@@ -681,6 +706,7 @@
:deep(.item-text-overflow) {
text-align: left;
}
:deep(.fixUniFormItemStyle .uni-data-picker__input-box) {
justify-content: flex-start !important;
text-align: left !important;
......
......@@ -5,11 +5,13 @@
<view class="yearRow">
<view class="label">缴费年限</view>
<view class="control">
<image class="icon" @click="minusYear" src="/static/dd_02.png" mode="widthFix" v-if="form.payYear > 1" ></image>
<image class="icon" src="/static/dd_02_g.png" mode="widthFix" v-else ></image>
<image class="icon" @click="minusYear" src="/static/dd_02.png" mode="widthFix"
v-if="form.payYear > 1"></image>
<image class="icon" src="/static/dd_02_g.png" mode="widthFix" v-else></image>
<text class="num">{{ form.payYear }}</text>
<image class="icon" src="/static/btn_03.png" mode="widthFix" @click="plusYear" v-if="form.payYear < 5" ></image>
<image class="icon" src="/static/btn_03_g.png" mode="widthFix" v-else ></image>
<image class="icon" src="/static/btn_03.png" mode="widthFix" @click="plusYear"
v-if="form.payYear < 5"></image>
<image class="icon" src="/static/btn_03_g.png" mode="widthFix" v-else></image>
</view>
</view>
</view>
......@@ -49,25 +51,32 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app';
import * as api from '@/common/api.js'
import {
ref,
computed,
onMounted
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app';
import to from 'await-to-js'
import * as api from '@/common/api.js'
const form = ref({
const form = ref({
payYear: 1
})
})
// 支付方式
const payType = ref('1')
const isPaying = ref(false)
// 支付方式
const payType = ref('1')
const isPaying = ref(false)
// 费用与优惠
const memberFee = ref(0)
const memberTotalFee = computed(() => {
// 费用与优惠
const memberFee = ref(0)
const memberTotalFee = computed(() => {
return memberFee.value * form.value.payYear
})
onLoad((options) => {
})
onLoad((options) => {
if (options.baseFormData) {
const data = JSON.parse(decodeURIComponent(options.baseFormData))
form.value = {
......@@ -77,37 +86,45 @@ onLoad((options) => {
}
// 初始化接口
getMyMemberCertUnitFeeApi()
})
})
// 减年限
const minusYear = () => {
// 减年限
const minusYear = () => {
if (form.value.payYear > 1) {
form.value.payYear--
}
}
}
// 加年限(最大 5 年)
const plusYear = () => {
// 加年限(最大 5 年)
const plusYear = () => {
if (form.value.payYear < 5) {
form.value.payYear++
}
}
}
// 支付方式切换
const onPayTypeChange = (e) => {
// 支付方式切换
const onPayTypeChange = (e) => {
payType.value = e.detail.value
}
}
const handelPay = async () => {
const handelPay = async () => {
if (memberTotalFee.value <= 0) {
uni.showToast({ title: '支付金额异常', icon: 'none' })
uni.showToast({
title: '支付金额异常',
icon: 'none'
})
return
}
// 显示 loading
uni.showLoading({
title: '支付中...',
mask: true
})
isPaying.value = true
try {
// 拼接完整参数
const postData = {
...form.value,
......@@ -116,100 +133,126 @@ const handelPay = async () => {
totalFee: memberTotalFee.value
}
const res = await api.insertSinglePay(postData)
console.log(777,res)
if (res.data?.orderId) {
api.pcallBack2(res.data.orderId)
uni.navigateTo({
url: `/personal/sucPay`
// 创建订单
const [orderErr, orderRes] = await to(api.insertSinglePay(postData))
if (orderErr) {
uni.hideLoading()
isPaying.value = false
uni.showToast({
title: '创建订单失败',
icon: 'none'
})
return
}
if (!orderRes.data?.orderId) {
uni.hideLoading()
isPaying.value = false
uni.showToast({
title: '订单创建异常',
icon: 'none'
})
return
}
// if (data.payFlag == 0 || data.orderId) {
// data.orderId && api.callBack2(data.orderId)
// uni.navigateTo({ url: `/personal/submitPay?price=${res.data.price}` })
// }
} catch (err) {
uni.showToast({ title: '支付失败', icon: 'none' })
} finally {
// 等待支付回调
await to(api.pcallBack2(orderRes.data.orderId))
uni.hideLoading()
isPaying.value = false
// 支付成功,跳转页面
uni.navigateTo({
url: `/personal/sucPay?orderId=${orderRes.data.orderId}`
})
}
}
// 获取会员费
async function getMyMemberCertUnitFeeApi() {
// 获取会员费
async function getMyMemberCertUnitFeeApi() {
const res = await api.getZtxFeeConfig()
memberFee.value = Number(res.data.personMemberFee || 1500)
}
}
</script>
<style scoped>
.container {
.container {
min-height: 100vh;
background-color: #f7f7f7;
}
.content {
}
.content {
padding: 20rpx 20rpx 120rpx;
}
.card {
}
.card {
background: #fff;
border-radius: 8rpx;
padding: 25rpx 20rpx;
margin-bottom: 20rpx;
}
.yearRow {
}
.yearRow {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.yearRow .label {
}
.yearRow .label {
font-size: 28rpx;
color: #333;
}
.yearRow .control {
}
.yearRow .control {
display: flex;
align-items: center;
}
.control image {
}
.control image {
width: 50rpx;
height: 50rpx;
}
.yearRow .num {
}
.yearRow .num {
font-size: 28rpx;
color: #333;
min-width: 80rpx;
text-align: center;
margin: 0 10rpx;
}
.row {
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
}
.row .label {
}
.row .label {
font-size: 28rpx;
color: #333;
}
.row .value {
}
.row .value {
font-size: 30rpx;
color: #C4121B;
font-weight: 500;
}
.hintRow {
}
.hintRow {
display: flex;
align-items: flex-start;
font-size: 24rpx;
line-height: 1.4;
}
.hintRow .hintText {
}
.hintRow .hintText {
color: #FF8124;
flex: 1;
margin-top: 10rpx;
}
.deductRow {
}
.deductRow {
background: #fff;
padding: 20rpx 20rpx;
display: flex;
......@@ -217,40 +260,48 @@ async function getMyMemberCertUnitFeeApi() {
align-items: center;
margin-bottom: 10rpx;
border-radius: 8rpx;
}
.deductRow .label {
}
.deductRow .label {
font-size: 28rpx;
color: #333;
}
.deductRow .value {
}
.deductRow .value {
font-size: 30rpx;
color: #C4121B;
}
.payRow {
}
.payRow {
background: #fff;
border-radius: 8rpx;
padding: 20rpx 20rpx;
margin-bottom: 20rpx;
}
.radioItem {
}
.radioItem {
display: flex;
align-items: center;
}
.payInfo {
}
.payInfo {
display: flex;
align-items: center;
margin-left: 15rpx;
}
.payInfo .icon {
}
.payInfo .icon {
width: 40rpx;
height: 40rpx;
margin-right: 10rpx;
}
.payInfo text {
}
.payInfo text {
font-size: 28rpx;
color: #333;
}
.totalRow {
}
.totalRow {
background: #fff;
border-radius: 8rpx;
padding: 20rpx 20rpx;
......@@ -258,17 +309,20 @@ async function getMyMemberCertUnitFeeApi() {
justify-content: space-between;
align-items: center;
margin-top: 10rpx;
}
.totalRow .label {
}
.totalRow .label {
font-size: 28rpx;
color: #333;
}
.redBig {
}
.redBig {
font-size: 32rpx;
color: #C4121B;
font-weight: bold;
}
.bottomBtn {
}
.bottomBtn {
position: fixed;
bottom: 0;
left: 0;
......@@ -276,8 +330,9 @@ async function getMyMemberCertUnitFeeApi() {
padding: 20rpx 20rpx;
background: #fff;
border-top: 1rpx solid #eee;
}
.payBtn {
}
.payBtn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
......@@ -287,25 +342,30 @@ async function getMyMemberCertUnitFeeApi() {
font-size: 32rpx;
text-align: center;
border: none;
}
.payBtn[disabled] {
}
.payBtn[disabled] {
background-color: #ccc;
color: #999;
}
.red {
}
.red {
color: #C4121B;
}
.icon{
width:30px;
}
::v-deep .custom-radio .wx-radio-input {
}
.icon {
width: 30px;
}
::v-deep .custom-radio .wx-radio-input {
width: 30rpx;
height: 30rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
}
::v-deep .custom-radio .wx-radio-input.wx-radio-input-checked {
}
::v-deep .custom-radio .wx-radio-input.wx-radio-input-checked {
border-color: #C4121B !important;
background: #C4121B !important;
}
}
</style>
\ No newline at end of file
......
......@@ -14,28 +14,16 @@
<!-- 订单信息卡片(带阴影) -->
<view class="info-card">
<view class="info-item">
<text class="label">付款账户</text>
<text class="value">(5437)</text>
</view>
<view class="info-item">
<text class="label">交易流水号</text>
<text class="value">2205051351076117833</text>
<text class="value">{{ orderInfo.tradeNo }}</text>
</view>
<view class="info-item">
<text class="label">商户名称</text>
<text class="value">中国跆拳道协会</text>
<text class="value">{{ orderInfo.merchantName || '中国跆拳道协会' }}</text>
</view>
<view class="info-item">
<text class="label">订单金额</text>
<text class="value amount">1500.00元</text>
</view>
<view class="info-item">
<text class="label">会员编号</text>
<text class="value">CTA00004</text>
</view>
<view class="info-item">
<text class="label">会员有效期</text>
<text class="value">2028年1月25日</text>
<text class="value amount">{{ orderInfo.price ? orderInfo.price + '元' : '--' }}</text>
</view>
</view>
......@@ -47,20 +35,43 @@
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app'
const goBack = () => {
uni.navigateTo({
url: `/personal/home`
import {
ref
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import to from 'await-to-js'
import * as api from '@/common/api.js'
const orderInfo = ref({
id: '',
tradeNo: '',
merchantName: '中国跆拳道协会',
price: ''
})
}
onLoad((option) => {
})
const goBack = () => {
uni.reLaunch({
url: '/login/login'
})
}
onLoad(async (option) => {
if (option.orderId) {
const [err, res] = await to(api.getOrderInfo(option.orderId))
if (!err && res.data) {
orderInfo.value = res.data
} else {
orderInfo.value.id = option.orderId
}
}
})
</script>
<style scoped>
/* 全局容器 */
.success-container {
/* 全局容器 */
.success-container {
display: flex;
flex-direction: column;
align-items: center;
......@@ -68,16 +79,16 @@ onLoad((option) => {
min-height: 100vh;
background-color: #f8f9fa;
box-sizing: border-box;
}
}
/* 成功图标容器 */
.success-icon {
/* 成功图标容器 */
.success-icon {
margin-bottom: 40rpx;
animation: fadeIn 0.6s ease-out;
}
}
/* 渐变圆形背景 */
.icon-circle {
/* 渐变圆形背景 */
.icon-circle {
width: 180rpx;
height: 180rpx;
border-radius: 50%;
......@@ -89,34 +100,34 @@ onLoad((option) => {
box-shadow: 0 8rpx 30rpx rgba(6, 193, 174, 0.3);
/* 轻微上浮动效 */
animation: scaleIn 0.8s ease-out;
}
}
/* 对勾图标 */
.check-icon {
/* 对勾图标 */
.check-icon {
font-size: 90rpx;
color: #ffffff;
font-weight: bold;
}
}
/* 支付成功标题 */
.success-title {
/* 支付成功标题 */
.success-title {
font-size: 48rpx;
font-weight: 700;
color: #333333;
margin-bottom: 12rpx;
animation: slideUp 0.6s ease-out;
}
}
/* 副标题 */
.success-subtitle {
/* 副标题 */
.success-subtitle {
font-size: 28rpx;
color: #666666;
margin-bottom: 60rpx;
animation: slideUp 0.8s ease-out;
}
}
/* 订单信息卡片 */
.info-card {
/* 订单信息卡片 */
.info-card {
width: 100%;
background: #ffffff;
border-radius: 20rpx;
......@@ -124,48 +135,55 @@ onLoad((option) => {
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.05);
margin-bottom: 80rpx;
animation: fadeIn 1s ease-out;
}
}
/* 单个信息项 */
.info-item {
/* 单个信息项 */
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
/* 最后一项去掉下划线 */
.info-item:last-child {
}
/* 最后一项去掉下划线 */
.info-item:last-child {
border-bottom: none;
}
}
/* 标签样式 */
.label {
/* 标签样式 */
.label {
font-size: 32rpx;
color: #666666;
}
white-space: nowrap;
margin-right: 20rpx;
flex-shrink: 0;
}
/* 值样式 */
.value {
/* 值样式 */
.value {
font-size: 32rpx;
color: #333333;
text-align: right;
}
/* 金额特殊样式 */
.amount {
word-break: break-all;
word-wrap: break-word;
}
/* 金额特殊样式 */
.amount {
color: #cd1e27;
font-weight: 600;
}
}
/* 确定按钮区域 */
.confirm-btn-area {
/* 确定按钮区域 */
.confirm-btn-area {
width: 100%;
padding: 0 20rpx;
box-sizing: border-box;
}
}
/* 确定按钮(渐变+动效) */
.confirm-btn {
/* 确定按钮(渐变+动效) */
.confirm-btn {
width: 100%;
height: 90rpx;
line-height: 90rpx;
......@@ -180,28 +198,52 @@ onLoad((option) => {
/* 禁止默认样式 */
position: relative;
overflow: hidden;
}
/* 按钮点击反馈 */
.confirm-btn::after {
}
/* 按钮点击反馈 */
.confirm-btn::after {
border: none;
}
.confirm-btn:active {
}
.confirm-btn:active {
transform: scale(0.98);
box-shadow: 0 4rpx 10rpx rgba(6, 193, 174, 0.2);
}
/* 动画定义 */
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes scaleIn {
0% { transform: scale(0); }
70% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@keyframes slideUp {
0% { opacity: 0; transform: translateY(30rpx); }
100% { opacity: 1; transform: translateY(0); }
}
}
/* 动画定义 */
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes scaleIn {
0% {
transform: scale(0);
}
70% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
@keyframes slideUp {
0% {
opacity: 0;
transform: translateY(30rpx);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
</style>
\ No newline at end of file
......
## 2.1.6(2023-04-16)
* 修复 组件使用 v-show 指令会导致选择图片后初始位置严重偏位的问题
## 2.1.5(2023-04-16)
* 新增 兼容APP平台
## 2.1.4(2023-03-13)
* 新增 fileType 属性,用于指定生成文件的类型,只支持 'jpg' 或 'png',默认为 'png'
* 新增 delay 属性,微信小程序平台使用 `Canvas 2D` 绘制时控制图片从绘制到生成所需时间
* 优化 当生成图片的尺寸宽/高超过 Canvas 2D 最大限制(1365*1365)则将画布尺寸缩放在限制范围内绘制完成后输出目标尺寸
* 优化 旋转图标指示方向与实际旋转方向不符
## 2.1.3(2023-02-06)
* 优化 vue3支持
## 2.1.2(2023-02-03)
* 新增 navigation 属性,H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差
* 修复 H5平台部分设备(已知iPhone11以下机型)拍照的图片缩放时会闪动的问题
## 2.1.1(2022-12-06)
* 修复 横屏适配问题
## 2.1.0(2022-12-06)
* 新增 兼容H5平台,使用 renderjs 响应手势事件
## 2.0.0(2022-12-05)
* 重构 插件,使用 WXS 响应手势事件
* 新增 图片翻转
* 新增 拉伸裁剪框放大图片
* 新增 监听PC鼠标滚轮触发缩放
* 新增 圆形、圆角矩形的图片裁剪
* 优化 图片缩放,移动端以双指触摸中心点为缩放中心点,PC端以鼠标所在点为缩放中心点
* 优化 裁剪框样式
* 优化 图片位置拖动 支持边界回弹效果(滑动时可滑出边界,释放时回弹到边界)
* 优化 生成图片使用新版 Canvas 2D 接口
{
"id": "qf-image-cropper",
"displayName": "图片裁剪插件",
"version": "2.1.6",
"description": "图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。",
"keywords": [
"qf-image-cropper",
"图片裁剪",
"图片编辑",
"头像裁剪",
"小程序"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "n"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "u",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "n",
"联盟": "n"
}
}
}
}
}
\ No newline at end of file
# qf-image-cropper
## 图片裁剪插件
uniapp微信小程序图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。
### 平台支持:
1. 支持微信小程序:移动端、PC端、开发者工具
2. 支持H5平台(2.1.0版本起)
3. 支持APP平台(2.1.5版本起):Android、IOS
4. 其他平台暂未测试兼容性未知
### 支持功能:
1. 自定义裁剪尺寸
2. 定点等比例缩放:移动端以双指触摸中心点为缩放中心点,PC端以鼠标所在点为缩放中心点
3. 自由拖动:支持限制滑出边界,也支持回弹效果(滑动时可滑出边界,释放时回弹到边界)
4. 图片翻转:在裁剪尺寸非 1:1 的情况下,翻转时宽高无法铺满裁剪区域时,图片会自动放大到合适尺寸
5. 裁剪生成新图片
6. 本地选择图片
7. 可定制样式:可自由选择是否渲染裁剪边框、可伸缩裁剪顶角、参考线
8. 裁剪圆角图片:圆形、圆角矩形
### 属性说明
| 属性名 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| src | String | | 图片资源地址 |
| width | Number | 300 | 裁剪宽度 |
| height | Number | 300 | 裁剪高度 |
| showBorder | Boolean | true | 是否绘制裁剪区域边框 |
| showGrid | Boolean | true | 是否绘制裁剪区域网格参考线 |
| showAngle | Boolean | true | 是否展示四个支持伸缩的角 |
| areaScale | Number | 0.3 | 裁剪区域最小缩放倍数 |
| maxScale | Number | 5 | 图片最大缩放倍数 |
| bounce | Boolean | true | 是否有回弹效果:拖动时可以拖出边界,释放时会弹回边界 |
| rotatable | Boolean | true | 是否支持翻转 |
| choosable | Boolean | true | 是否支持从本地选择素材 |
| angleSize | Number | 20 | 四个角尺寸,单位px |
| angleBorderWidth | Number | 2 | 四个角边框宽度,单位px |
| radius | Number | | 裁剪图片圆角半径,单位px |
| fileType | String | png | 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' |
| delay | Number | 1000 | 图片从绘制到生成所需时间,单位ms<br>微信小程序平台使用 `Canvas 2D` 绘制时有效<br>如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间 |
| navigation | Boolean | true | 页面是否是原生标题栏:<br>H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 `"navigationStyle": "custom"` 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。<br>注:因H5平台的窗口高度是包含标题栏的,而屏幕触摸点的坐标是不包含的 |
| @crop | EventHandle | | 剪裁完成后触发,event = { tempFilePath }。在H5平台下,tempFilePath 为 base64 |
### 基本用法
```
<template>
<div>
<qf-image-cropper :width="500" :height="500" :radius="30" @crop="handleCrop"></qf-image-cropper>
</div>
</template>
<script>
import QfImageCropper from '@/components/qf-image-cropper/qf-image-cropper.vue';
export default {
components: {
QfImageCropper
},
methods: {
handleCrop(e) {
uni.previewImage({
urls: [e.tempFilePath],
current: 0
});
}
}
}
</script>
```
### 使用说明
1.建议在`pages.json`中将引用插件的页面添加一下配置禁止下拉刷新和禁止页面滑动,防止出现性能或页面抖动等问题。
```
{
"enablePullDownRefresh": false,
"disableScroll": true
}
```
2.建议使用本插件不要设置过大宽高的目标图片尺寸,建议1365x1365以内,否则可能会导致如下问题:
```
1.界面卡顿,内存占用过高
2.生成图片失真(模糊)
3.确定裁剪后一直显示 `裁剪中...`,该问题是由 `uni.canvasToTempFilePath` 无法回调导致,不同平台不同设备限制可能有所不同。
```
3.如裁剪后的图片存在偏移的问题,请检查是否受自己项目中父组件或全局样式影响。
4.src属性设置网络图片时,图片资源必须是能触发 `getImageInfo` API 的 success 回调才可用于插件裁剪。因此小程序平台获取网络图片信息需先配置download域名白名单才能生效。
\ No newline at end of file
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!